diff --git a/.appveyor/build.bat b/.appveyor/build.bat index e2c6b553f..fa4e48ecc 100644 --- a/.appveyor/build.bat +++ b/.appveyor/build.bat @@ -24,14 +24,31 @@ vcpkg integrate install cd %APPVEYOR_BUILD_FOLDER% REM Acquire FFmpeg -set FFMPEG_VER=ffmpeg-4.2.1-win64 +set FFMPEG_VER=ffmpeg-4.2.3-win64 curl https://ffmpeg.zeranoe.com/builds/win64/dev/%FFMPEG_VER%-dev.zip > %FFMPEG_VER%-dev.zip curl https://ffmpeg.zeranoe.com/builds/win64/shared/%FFMPEG_VER%-shared.zip > %FFMPEG_VER%-shared.zip 7z x %FFMPEG_VER%-dev.zip 7z x %FFMPEG_VER%-shared.zip -REM Add Qt and FFmpeg directory to path -set PATH=%PATH%;C:\Qt\5.13.2\msvc2017_64\bin;%APPVEYOR_BUILD_FOLDER%\%FFMPEG_VER%-dev +REM Acquire Google Crashpad +git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +set PATH=%PATH%;%APPVEYOR_BUILD_FOLDER%\depot_tools + +REM Run `fetch` through cmd /c since fetch is a batch file that seems to call exit +cmd /c fetch crashpad +cd crashpad +cmd /c gn gen out/Default + +REM Patch to build a dynamic release instead of a static release +ren out\Default\toolchain.ninja toolchain.ninja.old +sed "s/${cflags_c}/${cflags_c} \/MD/g" out\Default\toolchain.ninja.old > out\Default\toolchain.ninja + +REM Build Crashpad +ninja.exe -C out/Default +cd .. + +REM Add Qt, FFmpeg, and Crashpad to path +set PATH=%PATH%;C:\Qt\5.13.2\msvc2017_64\bin;%APPVEYOR_BUILD_FOLDER%\%FFMPEG_VER%-dev;%APPVEYOR_BUILD_FOLDER%\crashpad;%APPVEYOR_BUILD_FOLDER%\crashpad\out\Default REM Run cmake cmake -G "Ninja" . -DCMAKE_TOOLCHAIN_FILE=c:/Tools/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo @@ -42,36 +59,21 @@ ninja.exe || exit /B 1 REM If this is a pull request, no further packaging/deploying needs to be done if NOT "%APPVEYOR_PULL_REQUEST_NUMBER%" == "" goto end +REM Create Crashpad symbol file and upload it +C:\msys64\usr\bin\wget.exe https://github.com/google/breakpad/blob/master/src/tools/windows/binaries/dump_syms.exe?raw=true -O dump_syms.exe +dump_syms app\olive-editor.pdb > olive-editor.sym +curl -F symfile=@olive-editor.sym https://olivevideoeditor.org/crashpad/symbols.php + REM Start building package mkdir olive-editor cd olive-editor copy ..\app\olive-editor.exe . copy ..\app\olive-editor.pdb . copy ..\app\crashhandler.exe . +copy ..\crashpad\out\Default\crashpad_handler.exe . windeployqt olive-editor.exe copy ..\%FFMPEG_VER%-shared\bin\*.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\OpenColorIO.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\OpenImageIO.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\yaml-cpp.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\Half-2_3.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\Iex-2_3.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\IexMath-2_3.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\IlmImf-2_3.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\IlmImfUtil-2_3.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\IlmThread-2_3.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\Imath-2_3.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\*.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\*.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\*.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\*.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\libpng16.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\jpeg62.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\tiff.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\zlib1.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\lzma.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\boost_date_time-vc141-mt-x64-1_72.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\boost_filesystem-vc141-mt-x64-1_72.dll . -copy C:\Tools\vcpkg\installed\x64-windows\bin\boost_thread-vc141-mt-x64-1_72.dll . +copy ..\app\*.dll . REM Package done, begin deployment cd .. @@ -85,9 +87,6 @@ REM Create portable copy nul olive-editor\portable 7z a %PKGNAME%.zip olive-editor -REM We're ready to upload, but we only upload *sometimes* -REM set PATH=%PATH%;C:\msys64\usr\bin - REM If this was a tagged build, upload if "%APPVEYOR_REPO_TAG%"=="true" GOTO upload diff --git a/.travis/script.sh b/.travis/script.sh index defeda5b2..b4cb03e76 100644 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -6,7 +6,7 @@ export VERSION=$(git rev-parse --short=8 HEAD) if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then # Generate Makefile - cmake . + cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo # Make make -j$(sysctl -n hw.ncpu) @@ -50,7 +50,7 @@ if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then elif [[ "$TRAVIS_OS_NAME" == "linux" ]]; then # Generate Makefile - cmake . + cmake . -DCMAKE_BUILD_TYPE=RelWithDebInfo # Make make -j$(nproc) diff --git a/CMakeLists.txt b/CMakeLists.txt index 95a3ef62d..3c0f423c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,12 @@ find_package(FFMPEG 3.0 REQUIRED swresample ) +find_package(GoogleCrashpad) + +if (NOT GoogleCrashpad_FOUND) + message(" Automatic crash reporting will be disabled.") +endif() + if(EXISTS "${CMAKE_SOURCE_DIR}/.git") find_package(Git) if(GIT_FOUND) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 6050fabfe..714a5e01d 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -79,9 +79,6 @@ if(APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9") endif() -# Set compiler definitions -target_compile_definitions(${OLIVE_TARGET} PRIVATE ${OLIVE_DEFINITIONS}) - # Set compiler options if(MSVC) target_compile_options( @@ -100,13 +97,14 @@ else() target_compile_options( ${OLIVE_TARGET} PRIVATE - -O2 + "$<$:-O2>" -Werror -Wuninitialized -pedantic-errors -Wall -Wextra -Wno-unused-parameter + -Wshadow ) endif() @@ -125,7 +123,12 @@ target_include_directories( ${FFMPEG_INCLUDE_DIRS} ${OCIO_INCLUDE_DIRS} ${OIIO_INCLUDE_DIRS} - ${OPENEXR_INCLUDE_DIRS} + ${OPENEXR_INCLUDES} + + # HACK: Brew on macOS separates OpenEXR and IlmBase into two folders even though they seem to + # expect to be in one. This includes the IlmBase files as if they were in the same folders + # as the OpenEXR headers. + ${ILMBASE_INCLUDES}/OpenEXR ) # Set link libraries @@ -165,6 +168,26 @@ elseif (APPLE) ) endif() +# Enable Crashpad if found +if (GoogleCrashpad_FOUND) + set(OLIVE_DEFINITIONS ${OLIVE_DEFINITIONS} USE_CRASHPAD) + + target_include_directories( + ${OLIVE_TARGET} + PRIVATE + ${CRASHPAD_INCLUDE_DIRS} + ) + + target_link_libraries( + ${OLIVE_TARGET} + PRIVATE + ${CRASHPAD_LIBRARIES} + ) +endif() + +# Set compiler definitions +target_compile_definitions(${OLIVE_TARGET} PRIVATE ${OLIVE_DEFINITIONS}) + set(OLIVE_TS_FILES # FIXME: Empty variable ) diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 4a8ac8435..f5a1a866d 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -48,18 +48,31 @@ AudioManager *AudioManager::instance() void AudioManager::RefreshDevices() { - output_watcher_.setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioOutput)); - input_watcher_.setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioInput)); + if (!is_refreshing_outputs_) { + QFutureWatcher< QList >* output_watcher = new QFutureWatcher< QList >(); + connect(output_watcher, &QFutureWatcher< QList >::finished, this, &AudioManager::OutputDevicesRefreshed); + output_watcher->setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioOutput)); + + is_refreshing_outputs_ = true; + } + + if (!is_refreshing_inputs_) { + QFutureWatcher< QList >* input_watcher = new QFutureWatcher< QList >(); + connect(input_watcher, &QFutureWatcher< QList >::finished, this, &AudioManager::InputDevicesRefreshed); + input_watcher->setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioInput)); + + is_refreshing_inputs_ = true; + } } bool AudioManager::IsRefreshingOutputs() { - return output_watcher_.isRunning(); + return is_refreshing_outputs_; } bool AudioManager::IsRefreshingInputs() { - return input_watcher_.isRunning(); + return is_refreshing_inputs_; } void AudioManager::PushToOutput(const QByteArray &samples) @@ -200,6 +213,8 @@ void AudioManager::ReverseBuffer(char *buffer, int buffer_size, int sample_size) } AudioManager::AudioManager() : + is_refreshing_inputs_(false), + is_refreshing_outputs_(false), output_is_set_(false), input_(nullptr), input_file_(nullptr) @@ -210,9 +225,6 @@ AudioManager::AudioManager() : output_manager_.moveToThread(&output_thread_); connect(&output_manager_, &AudioOutputManager::OutputNotified, this, &AudioManager::OutputNotified); - - connect(&output_watcher_, &QFutureWatcher< QList >::finished, this, &AudioManager::OutputDevicesRefreshed); - connect(&input_watcher_, &QFutureWatcher< QList >::finished, this, &AudioManager::InputDevicesRefreshed); } AudioManager::~AudioManager() @@ -224,7 +236,11 @@ AudioManager::~AudioManager() void AudioManager::OutputDevicesRefreshed() { - output_devices_ = output_watcher_.result(); + QFutureWatcher< QList >* watcher = static_cast >*>(sender()); + + output_devices_ = watcher->result(); + watcher->deleteLater(); + is_refreshing_outputs_ = false; QString preferred_audio_output = Config::Current()["PreferredAudioOutput"].toString(); @@ -247,7 +263,11 @@ void AudioManager::OutputDevicesRefreshed() void AudioManager::InputDevicesRefreshed() { - input_devices_ = input_watcher_.result(); + QFutureWatcher< QList >* watcher = static_cast >*>(sender()); + + input_devices_ = watcher->result(); + watcher->deleteLater(); + is_refreshing_inputs_ = false; QString preferred_audio_input = Config::Current()["PreferredAudioInput"].toString(); diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 07a5fb9a1..f0c6c7db0 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -102,8 +102,8 @@ private: QList input_devices_; QList output_devices_; - QFutureWatcher< QList > input_watcher_; - QFutureWatcher< QList > output_watcher_; + bool is_refreshing_inputs_; + bool is_refreshing_outputs_; static AudioManager* instance_; diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index 3beeb2cbb..08c6c27b0 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -231,10 +231,13 @@ void AudioVisualWaveform::DrawSample(QPainter *painter, const QVector(1.0f)); + qfloat16 min = qMax(sample.at(i).min, static_cast(-1.0)); + if (Config::Current()[QStringLiteral("RectifiedWaveforms")].toBool()) { int channel_bottom = y + channel_height * (i + 1); - int diff = qRound((sample.at(i).max - sample.at(i).min) * channel_half_height); + int diff = qRound((max - min) * channel_half_height); painter->drawLine(x, channel_bottom - diff, @@ -244,9 +247,9 @@ void AudioVisualWaveform::DrawSample(QPainter *painter, const QVectordrawLine(x, - channel_mid + qRound(sample.at(i).min * static_cast(channel_half_height)), + channel_mid + qRound(min * static_cast(channel_half_height)), x, - channel_mid + qRound(sample.at(i).max * static_cast(channel_half_height))); + channel_mid + qRound(max * static_cast(channel_half_height))); } } } diff --git a/app/cli/cliexport/cliexportmanager.cpp b/app/cli/cliexport/cliexportmanager.cpp new file mode 100644 index 000000000..de0145729 --- /dev/null +++ b/app/cli/cliexport/cliexportmanager.cpp @@ -0,0 +1,30 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "cliexportmanager.h" + +OLIVE_NAMESPACE_ENTER + +CLIExportManager::CLIExportManager() +{ + +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/cli/cliexport/cliexportmanager.h b/app/cli/cliexport/cliexportmanager.h new file mode 100644 index 000000000..6d3fc346b --- /dev/null +++ b/app/cli/cliexport/cliexportmanager.h @@ -0,0 +1,36 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef CLIEXPORTMANAGER_H +#define CLIEXPORTMANAGER_H + +#include "task/export/export.h" + +OLIVE_NAMESPACE_ENTER + +class CLIExportManager : public QObject +{ +public: + CLIExportManager(); +}; + +OLIVE_NAMESPACE_EXIT + +#endif // CLIEXPORTMANAGER_H diff --git a/app/cli/cliprogress/cliprogressdialog.cpp b/app/cli/cliprogress/cliprogressdialog.cpp index fa846c1ff..88c0cfd57 100644 --- a/app/cli/cliprogress/cliprogressdialog.cpp +++ b/app/cli/cliprogress/cliprogressdialog.cpp @@ -27,9 +27,10 @@ OLIVE_NAMESPACE_ENTER CLIProgressDialog::CLIProgressDialog(const QString& title, QObject *parent) : QObject(parent), title_(title), - progress_(0), + progress_(-1), drawn_(false) { + SetProgress(0); } void CLIProgressDialog::Update() @@ -68,7 +69,7 @@ void CLIProgressDialog::Update() std::cout << "["; // Get UI bar progress - int bar_prog = qRound(progress_ * 0.01 * progress_bar_columns); + int bar_prog = qRound(progress_ * progress_bar_columns); // Draw filled in bar for (int i=0;iGetTitle(), parent) + CLIProgressDialog(task->GetTitle(), parent), + task_(task) { - // FIXME: Still developing this, don't try to use + connect(task_, &Task::ProgressChanged, this, &CLITaskDialog::SetProgress); +} + +bool CLITaskDialog::Run() +{ + return task_->Start(); } OLIVE_NAMESPACE_EXIT diff --git a/app/cli/clitask/clitaskdialog.h b/app/cli/clitask/clitaskdialog.h index 6408b238e..c574b6011 100644 --- a/app/cli/clitask/clitaskdialog.h +++ b/app/cli/clitask/clitaskdialog.h @@ -28,9 +28,15 @@ OLIVE_NAMESPACE_ENTER class CLITaskDialog : public CLIProgressDialog { + Q_OBJECT public: CLITaskDialog(Task *task, QObject* parent = nullptr); + bool Run(); + +private: + Task* task_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 00b3fe8a4..7b5fc8912 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -29,7 +29,9 @@ #include "codec/oiio/oiiodecoder.h" #include "codec/waveinput.h" #include "codec/waveoutput.h" +#include "common/filefunctions.h" #include "task/taskmanager.h" +#include "project/project.h" OLIVE_NAMESPACE_ENTER @@ -175,6 +177,11 @@ QString Decoder::GetConformedFilename(const AudioParams ¶ms) return index_fn; } +QString Decoder::GetIndexFilename() +{ + return QDir(stream_->footage()->project()->cache_path()).filePath(FileFunctions::GetUniqueFileIdentifier(stream()->footage()->filename()).append(QString::number(stream()->index()))); +} + bool Decoder::ConformAudio(const QAtomicInt *, const AudioParams& ) { return false; diff --git a/app/codec/decoder.h b/app/codec/decoder.h index 1e2bfddfa..60c1b6e90 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -242,19 +242,13 @@ signals: protected: void SignalProcessingProgress(const int64_t& ts); - /** - * @brief Returns the filename for the index - * - * Retrieves the absolute filename of the index file for this stream. Decoder must be open for - * this to work correctly. - */ - virtual QString GetIndexFilename() const = 0; - /** * @brief Get the destination filename of an audio stream conformed to a set of parameters */ QString GetConformedFilename(const AudioParams ¶ms); + QString GetIndexFilename(); + bool open_; QMutex mutex_; diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 43e0ca1a6..ea3750720 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -88,6 +88,11 @@ void EncodingParams::set_video_threads(const int &threads) video_threads_ = threads; } +void EncodingParams::set_video_pix_fmt(const QString &s) +{ + video_pix_fmt_ = s; +} + const QString &EncodingParams::filename() const { return filename_; @@ -133,6 +138,11 @@ const int &EncodingParams::video_threads() const return video_threads_; } +const QString &EncodingParams::video_pix_fmt() const +{ + return video_pix_fmt_; +} + bool EncodingParams::audio_enabled() const { return audio_enabled_; @@ -158,6 +168,59 @@ void EncodingParams::SetExportLength(const rational &export_length) export_length_ = export_length; } +void EncodingParams::Save(QXmlStreamWriter *writer) const +{ + writer->writeTextElement(QStringLiteral("filename"), filename_); + + writer->writeStartElement(QStringLiteral("video")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(video_enabled_)); + + if (video_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(video_codec_)); + writer->writeTextElement(QStringLiteral("width"), QString::number(video_params_.width())); + writer->writeTextElement(QStringLiteral("height"), QString::number(video_params_.height())); + writer->writeTextElement(QStringLiteral("format"), QString::number(video_params_.format())); + writer->writeTextElement(QStringLiteral("timebase"), video_params_.time_base().toString()); + writer->writeTextElement(QStringLiteral("divider"), QString::number(video_params_.divider())); + writer->writeTextElement(QStringLiteral("bitrate"), QString::number(video_bit_rate_)); + writer->writeTextElement(QStringLiteral("maxbitrate"), QString::number(video_max_bit_rate_)); + writer->writeTextElement(QStringLiteral("bufsize"), QString::number(video_buffer_size_)); + writer->writeTextElement(QStringLiteral("threads"), QString::number(video_threads_)); + + if (!video_opts_.isEmpty()) { + writer->writeStartElement(QStringLiteral("opts")); + + QHash::const_iterator i; + for (i=video_opts_.constBegin(); i!=video_opts_.constEnd(); i++) { + writer->writeStartElement(QStringLiteral("entry")); + + writer->writeTextElement(QStringLiteral("key"), i.key()); + writer->writeTextElement(QStringLiteral("value"), i.value()); + + writer->writeEndElement(); // entry + } + + writer->writeEndElement(); // opts + } + } + + writer->writeEndElement(); // video + + writer->writeStartElement(QStringLiteral("audio")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(audio_enabled_)); + + if (audio_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(audio_codec_)); + writer->writeTextElement(QStringLiteral("samplerate"), QString::number(audio_params_.sample_rate())); + writer->writeTextElement(QStringLiteral("channellayout"), QString::number(audio_params_.channel_layout())); + writer->writeTextElement(QStringLiteral("format"), QString::number(audio_params_.format())); + } + + writer->writeEndElement(); // audio +} + Encoder* Encoder::CreateFromID(const QString &id, const EncodingParams& params) { Q_UNUSED(id) diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 8a60a75cc..412306388 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -23,6 +23,7 @@ #include #include +#include #include "codec/exportcodec.h" #include "codec/exportformat.h" @@ -50,6 +51,7 @@ public: void set_video_max_bit_rate(const int64_t& rate); void set_video_buffer_size(const int64_t& sz); void set_video_threads(const int& threads); + void set_video_pix_fmt(const QString& s); const QString& filename() const; @@ -61,6 +63,7 @@ public: const int64_t& video_max_bit_rate() const; const int64_t& video_buffer_size() const; const int& video_threads() const; + const QString& video_pix_fmt() const; bool audio_enabled() const; const ExportCodec::Codec &audio_codec() const; @@ -69,6 +72,8 @@ public: const rational& GetExportLength() const; void SetExportLength(const rational& GetExportLength); + virtual void Save(QXmlStreamWriter* writer) const; + private: QString filename_; @@ -80,6 +85,7 @@ private: int64_t video_max_bit_rate_; int64_t video_buffer_size_; int video_threads_; + QString video_pix_fmt_; bool audio_enabled_; ExportCodec::Codec audio_codec_; diff --git a/app/codec/exportcodec.cpp b/app/codec/exportcodec.cpp index c68ff33ae..4f8aca533 100644 --- a/app/codec/exportcodec.cpp +++ b/app/codec/exportcodec.cpp @@ -20,6 +20,11 @@ #include "exportcodec.h" +extern "C" { +#include +#include +} + OLIVE_NAMESPACE_ENTER QString ExportCodec::GetCodecName(ExportCodec::Codec c) @@ -77,4 +82,47 @@ bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c) return false; } +QStringList ExportCodec::GetPixelFormatsForCodec(ExportCodec::Codec c) +{ + QStringList pix_fmts; + + AVCodec* codec_info = nullptr; + + switch (c) { + case kCodecH264: + codec_info = avcodec_find_encoder(AV_CODEC_ID_H264); + break; + case kCodecDNxHD: + codec_info = avcodec_find_encoder(AV_CODEC_ID_DNXHD); + break; + case kCodecProRes: + codec_info = avcodec_find_encoder(AV_CODEC_ID_PRORES); + break; + case kCodecH265: + codec_info = avcodec_find_encoder(AV_CODEC_ID_HEVC); + break; + case kCodecOpenEXR: + case kCodecPNG: + case kCodecTIFF: + // FIXME: Add these in (these will most likely use an OIIOEncoder which doesn't exist yet) + break; + case kCodecMP2: + case kCodecMP3: + case kCodecAAC: + case kCodecPCM: + case kCodecCount: + // These are audio or invalid codecs and therefore have no pixel formats + break; + } + + if (codec_info) { + for (int i=0; codec_info->pix_fmts[i]!=-1; i++) { + const char* pix_fmt_name = av_get_pix_fmt_name(codec_info->pix_fmts[i]); + pix_fmts.append(pix_fmt_name); + } + } + + return pix_fmts; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/codec/exportcodec.h b/app/codec/exportcodec.h index 7fc4f5898..d7a41124b 100644 --- a/app/codec/exportcodec.h +++ b/app/codec/exportcodec.h @@ -51,6 +51,8 @@ public: static bool IsCodecAStillImage(Codec c); + static QStringList GetPixelFormatsForCodec(Codec c); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 4d2430e30..b5c64f81c 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -52,7 +52,8 @@ QHash< Stream*, QList > FFmpegDecoder::instance_map_; QMutex FFmpegDecoder::instance_map_lock_; QHash< Stream*, FFmpegFramePool* > FFmpegDecoder::frame_pool_map_; -// FIXME: Hardcoded, ideally this value is dynamically chosen based on memory restraints +// FIXME: Hardcoded value. It seems to work fine, but is there a possibility we should make +// this a dynamic value somehow or a configurable value? const int FFmpegDecoderInstance::kMaxFrameLife = 2000; FFmpegDecoder::FFmpegDecoder() : @@ -86,19 +87,20 @@ bool FFmpegDecoder::Open() return false; } - if (stream()->type() == Stream::kVideo) { + if (stream()->type() == Stream::kImage || stream()->type() == Stream::kVideo) { // Get an Olive compatible AVPixelFormat src_pix_fmt_ = static_cast(our_instance->stream()->codecpar->format); ideal_pix_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(src_pix_fmt_); - { + if (stream()->type() == Stream::kVideo) { QMutexLocker map_locker(&instance_map_lock_); - // FIXME: Test code, this should be changed later FFmpegFramePool* frame_pool = frame_pool_map_.value(stream().get()); if (!frame_pool) { - frame_pool = new FFmpegFramePool(256, + // FIXME: Hardcoded value. It seems to work fine, but is there a possibility we should make + // this a dynamic value somehow or a configurable value? + frame_pool = new FFmpegFramePool(32, our_instance->stream()->codecpar->width, our_instance->stream()->codecpar->height, static_cast(our_instance->stream()->codecpar->format)); @@ -106,7 +108,6 @@ bool FFmpegDecoder::Open() } our_instance->SetFramePool(frame_pool); - // End test code } // Determine which Olive native pixel format we retrieved @@ -114,8 +115,6 @@ bool FFmpegDecoder::Open() native_pix_fmt_ = GetNativePixelFormat(ideal_pix_fmt_); Q_ASSERT(native_pix_fmt_ != PixelFormat::PIX_FMT_INVALID); - - aspect_ratio_ = our_instance->sample_aspect_ratio(); } time_base_ = our_instance->stream()->time_base; @@ -144,165 +143,177 @@ FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode, const int &divid return nullptr; } - if (stream()->type() != Stream::kVideo) { + if (stream()->type() != Stream::kImage && stream()->type() != Stream::kVideo) { return nullptr; } - int64_t target_ts = Timecode::time_to_timestamp(timecode, time_base_) + start_time_; + ImageStreamPtr is = std::static_pointer_cast(stream()); - VideoStreamPtr vs = std::static_pointer_cast(stream()); + if (stream()->type() == Stream::kImage) { - FFmpegDecoderInstance* working_instance = nullptr; - FFmpegFramePool::ElementPtr return_frame = nullptr; + // FIXME: Hacky + FFmpegDecoderInstance i(stream()->footage()->filename().toUtf8(), stream()->index()); - // Find instance - do { - QMutexLocker list_locker(&instance_map_lock_); + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + FramePtr output_frame = nullptr; - QList non_ideal_contenders; + int ret = i.GetFrame(pkt, frame); - QList instances = instance_map_.value(stream().get()); + if (ret >= 0) { + output_frame = BuffersToNativeFrame(divider, + is->width(), + is->height(), + 0, + frame->data, + frame->linesize); + } else { + qWarning() << "Failed to retrieve still image from decoder"; + } - foreach (FFmpegDecoderInstance* i, instances) { + av_frame_free(&frame); + av_packet_free(&pkt); - i->cache_lock()->lock(); + return output_frame; - if (i->CacheContainsTime(target_ts)) { + } else { - // Found our instance, allow others to enter the list + FFmpegFramePool::ElementPtr return_frame = nullptr; - list_locker.unlock(); + int64_t target_ts = Timecode::time_to_timestamp(timecode, time_base_) + start_time_; - // Get the frame from this cache - return_frame = i->GetFrameFromCache(target_ts); + VideoStreamPtr vs = std::static_pointer_cast(stream()); - // Got our frame, allow cache to continue - i->cache_lock()->unlock(); - break; + FFmpegDecoderInstance* working_instance = nullptr; - } else if (i->CacheWillContainTime(target_ts) || i->CacheCouldContainTime(target_ts)) { + // Find instance + do { + QMutexLocker list_locker(&instance_map_lock_); - // Found our instance, allow others to enter the list - list_locker.unlock(); + QList non_ideal_contenders; - // If the instance is currently in use, enter into a loop of seeing from frames come up next in case one is ours - if (i->IsWorking()) { + QList instances = instance_map_.value(stream().get()); - do { - // Allow instance to continue to the next frame - i->cache_wait_cond()->wait(i->cache_lock()); + foreach (FFmpegDecoderInstance* i, instances) { - // See if the cache now contains this frame, if so we'll exit this loop - if (i->CacheContainsTime(target_ts)) { + i->cache_lock()->lock(); - // Grab the frame - return_frame = i->GetFrameFromCache(target_ts); + if (i->CacheContainsTime(target_ts)) { - // We can release this worker now since we don't need it anymore - i->cache_lock()->unlock(); + // Found our instance, allow others to enter the list - } else if (!i->IsWorking()) { + list_locker.unlock(); - // This instance finished and we didn't get our frame, we'll take it and continue it - working_instance = i; - break; + // Get the frame from this cache + return_frame = i->GetFrameFromCache(target_ts); - } - } while (!return_frame); + // Got our frame, allow cache to continue + i->cache_lock()->unlock(); + break; + + } else if (i->CacheWillContainTime(target_ts) || i->CacheCouldContainTime(target_ts)) { + + // Found our instance, allow others to enter the list + list_locker.unlock(); + + // If the instance is currently in use, enter into a loop of seeing from frames come up next in case one is ours + if (i->IsWorking()) { + + do { + // Allow instance to continue to the next frame + i->cache_wait_cond()->wait(i->cache_lock()); + + // See if the cache now contains this frame, if so we'll exit this loop + if (i->CacheContainsTime(target_ts)) { + + // Grab the frame + return_frame = i->GetFrameFromCache(target_ts); + + // We can release this worker now since we don't need it anymore + i->cache_lock()->unlock(); + + } else if (!i->IsWorking()) { + + // This instance finished and we didn't get our frame, we'll take it and continue it + working_instance = i; + break; + + } + } while (!return_frame); + + } else { + // Otherwise, we'll grab this instance and continue it ourselves + working_instance = i; + } + + break; + + } else if (i->IsWorking()) { + + // Ignore currently working instances + i->cache_lock()->unlock(); + + } else if (i->CacheIsEmpty()) { + + // Prioritize this cache over others (leaves this instance LOCKED in case we end up using it later) + non_ideal_contenders.prepend(i); } else { - // Otherwise, we'll grab this instance and continue it ourselves - working_instance = i; + + // De-prioritize this cache (leaves this instance LOCKED in case we end up using it later) + non_ideal_contenders.append(i); + } - - break; - - } else if (i->IsWorking()) { - - // Ignore currently working instances - i->cache_lock()->unlock(); - - } else if (i->CacheIsEmpty()) { - - // Prioritize this cache over others (leaves this instance LOCKED in case we end up using it later) - non_ideal_contenders.prepend(i); - - } else { - - // De-prioritize this cache (leaves this instance LOCKED in case we end up using it later) - non_ideal_contenders.append(i); - } + + // If we didn't find a suitable contender, grab the first non-suitable and roll with that + if (!return_frame && !working_instance && !non_ideal_contenders.isEmpty()) { + working_instance = non_ideal_contenders.takeFirst(); + } + + // For all instances we left locked but didn't end up using, lock them now + foreach (FFmpegDecoderInstance* unsuitable_instance, non_ideal_contenders) { + unsuitable_instance->cache_lock()->unlock(); + } + } while (!return_frame && !working_instance); + + if (!return_frame && working_instance) { + + // This instance SHOULD remain locked from our earlier loop, making this operation safe + working_instance->SetWorking(true); + + // Retrieve frame + return_frame = working_instance->RetrieveFrame(target_ts, true); + + // Set working to false and wake any threads waiting + working_instance->cache_lock()->lock(); + working_instance->SetWorking(false); + working_instance->cache_wait_cond()->wakeAll(); + working_instance->cache_lock()->unlock(); } - // If we didn't find a suitable contender, grab the first non-suitable and roll with that - if (!return_frame && !working_instance && !non_ideal_contenders.isEmpty()) { - working_instance = non_ideal_contenders.takeFirst(); + // We found the frame, we'll return a copy + if (return_frame) { + // Align buffer to data/linesize points that can be passed to sws_scale + uint8_t* input_data[4]; + int input_linesize[4]; + + av_image_fill_arrays(input_data, + input_linesize, + reinterpret_cast(return_frame->data()), + src_pix_fmt_, + vs->width(), + vs->height(), + 1); + + return BuffersToNativeFrame(divider, + vs->width(), + vs->height(), + target_ts, + input_data, + input_linesize); } - // For all instances we left locked but didn't end up using, lock them now - foreach (FFmpegDecoderInstance* unsuitable_instance, non_ideal_contenders) { - unsuitable_instance->cache_lock()->unlock(); - } - } while (!return_frame && !working_instance); - - if (!return_frame && working_instance) { - - // This instance SHOULD remain locked from our earlier loop, making this operation safe - working_instance->SetWorking(true); - - // Retrieve frame - return_frame = working_instance->RetrieveFrame(target_ts, true); - - // Set working to false and wake any threads waiting - working_instance->cache_lock()->lock(); - working_instance->SetWorking(false); - working_instance->cache_wait_cond()->wakeAll(); - working_instance->cache_lock()->unlock(); - } - - // We found the frame, we'll return a copy - if (return_frame) { - if (divider != scale_divider_) { - FreeScaler(); - InitScaler(divider); - } - - // Create frame to return - FramePtr copy = Frame::Create(); - copy->set_video_params(VideoParams(vs->width(), - vs->height(), - native_pix_fmt_, - divider)); - copy->set_timestamp(Timecode::timestamp_to_time(target_ts, time_base_)); - copy->set_sample_aspect_ratio(aspect_ratio_); - copy->allocate(); - - // Align buffer to data/linesize points that can be passed to sws_scale - uint8_t* input_data[4]; - int input_linesize[4]; - - av_image_fill_arrays(input_data, - input_linesize, - reinterpret_cast(return_frame->data()), - src_pix_fmt_, - vs->width(), - vs->height(), - 1); - - // Convert frame to RGB/A for the rest of the pipeline - uint8_t* output_data = reinterpret_cast(copy->data()); - int output_linesize = copy->linesize_bytes(); - - sws_scale(scale_ctx_, - input_data, - input_linesize, - 0, - vs->height(), - &output_data, - &output_linesize); - - return copy; } return nullptr; @@ -436,8 +447,6 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) AVFormatContext* fmt_ctx = nullptr; error_code = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr); - QList streams_that_need_manual_duration; - // Handle format context error if (error_code == 0) { @@ -453,15 +462,83 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) if (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - // Create a video stream object - VideoStreamPtr video_stream = std::make_shared(); + bool image_is_still = false; + rational pixel_aspect_ratio; + rational frame_rate; + VideoParams::Interlacing interlacing = VideoParams::kInterlaceNone; - video_stream->set_width(avstream->codecpar->width); - video_stream->set_height(avstream->codecpar->height); - video_stream->set_frame_rate(av_guess_frame_rate(fmt_ctx, avstream, nullptr)); - video_stream->set_start_time(avstream->start_time); + { + // Read at least two frames to get more information about this video stream + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); - str = video_stream; + { + FFmpegDecoderInstance instance(filename, i); + + // Read first frame and retrieve some metadata + if (instance.GetFrame(pkt, frame) >= 0) { + // Check if video is interlaced and what field dominance it has if so + if (frame->interlaced_frame) { + if (frame->top_field_first) { + interlacing = VideoParams::kInterlacedTopFirst; + } else { + interlacing = VideoParams::kInterlacedBottomFirst; + } + } + + pixel_aspect_ratio = av_guess_sample_aspect_ratio(instance.fmt_ctx(), + instance.stream(), + frame); + + frame_rate = av_guess_frame_rate(instance.fmt_ctx(), + instance.stream(), + frame); + } + + // Read second frame + int ret = instance.GetFrame(pkt, frame); + + if (ret >= 0) { + // Check if we need a manual duration + if (avstream->duration == AV_NOPTS_VALUE) { + int64_t new_dur; + + do { + new_dur = frame->pts; + } while (instance.GetFrame(pkt, frame) >= 0); + + avstream->duration = new_dur; + } + } else if (ret == AVERROR_EOF) { + // Video has only one frame in it, treat it like a still image + image_is_still = true; + } + } + + av_frame_free(&frame); + av_packet_free(&pkt); + } + + ImageStreamPtr image_stream; + + if (image_is_still) { + image_stream = std::make_shared(); + } else { + VideoStreamPtr video_stream = std::make_shared(); + + video_stream->set_frame_rate(frame_rate); + video_stream->set_start_time(avstream->start_time); + + image_stream = video_stream; + } + + image_stream->set_width(avstream->codecpar->width); + image_stream->set_height(avstream->codecpar->height); + image_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast(avstream->codecpar->format)))); + image_stream->set_interlacing(interlacing); + image_stream->set_pixel_aspect_ratio(pixel_aspect_ratio); + + str = image_stream; } else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { @@ -510,11 +587,6 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) str->set_timebase(avstream->time_base); str->set_duration(avstream->duration); - // The container/stream info may not contain a duration, so we'll need to manually retrieve it - if (avstream->duration == AV_NOPTS_VALUE) { - streams_that_need_manual_duration.append(str.get()); - } - f->add_stream(str); } @@ -522,51 +594,6 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) result = true; } - // If the metadata did not contain a duration, we'll need to loop through the file to retrieve it - if (!streams_that_need_manual_duration.isEmpty()) { - - AVPacket* pkt = av_packet_alloc(); - - QVector durations(streams_that_need_manual_duration.size()); - durations.fill(0); - - while (true) { - if (cancelled && *cancelled) { - break; - } - - // Ensure previous buffers are cleared - av_packet_unref(pkt); - - // Read packet from file - int ret = av_read_frame(fmt_ctx, pkt); - - if (ret < 0) { - // Handle errors that aren't EOF (which simply means the file is finished) - if (ret != AVERROR_EOF) { - qWarning() << "Error while finding duration"; - } - break; - } else { - for (int i=0;iindex() == pkt->stream_index - && pkt->pts > durations.at(i)) { - durations.replace(i, pkt->pts); - } - } - } - } - - av_packet_free(&pkt); - - if (!cancelled || !*cancelled) { - for (int i=0;iset_duration(durations.at(i)); - } - } - - } - // Free all memory avformat_close_input(&fmt_ctx); @@ -590,40 +617,6 @@ void FFmpegDecoder::Error(const QString &s) ClearResources(); } -QMutex scaler_lock; -void SaveCacheFrame(FFmpegDecoder* decoder, - SwsContext* scaler, - AVFrame* frame, - VideoParams params, - QString dst_fn) -{ - QByteArray converted_buffer(PixelFormat::GetBufferSize(params.format(), - params.width(), - params.height()), - Qt::Uninitialized); - - uint8_t* converted_data = reinterpret_cast(converted_buffer.data()); - int converted_linesize = PixelFormat::GetBufferSize(params.format(), - params.width(), - 1); - - scaler_lock.lock(); - sws_scale(scaler, - frame->data, - frame->linesize, - 0, - frame->height, - &converted_data, - &converted_linesize); - scaler_lock.unlock(); - - if (!FrameHashCache::SaveCacheFrame(dst_fn, converted_buffer.data(), params, converted_linesize)) { - qCritical() <<" Failed to save cache frame" << dst_fn; - } - - av_frame_free(&frame); -} - bool FFmpegDecoder::ConformAudio(const QAtomicInt *cancelled, const AudioParams &p) { // Iterate through each audio frame and extract the PCM data @@ -753,17 +746,6 @@ bool FFmpegDecoder::ConformAudio(const QAtomicInt *cancelled, const AudioParams return success; } -QString FFmpegDecoder::GetIndexFilename() const -{ - return FileFunctions::GetMediaIndexFilename(FileFunctions::GetUniqueFileIdentifier(stream()->footage()->filename())) - .append(QString::number(stream()->index())); -} - -QString FFmpegDecoder::GetProxyFilename(int divider) const -{ - return GetIndexFilename().append('d').append(QString::number(divider)); -} - int FFmpegDecoder::GetScaledDimension(int dim, int divider) { return dim / divider; @@ -794,6 +776,39 @@ uint64_t FFmpegDecoder::ValidateChannelLayout(AVStream* stream) return av_get_default_channel_layout(stream->codecpar->channels); } +FramePtr FFmpegDecoder::BuffersToNativeFrame(int divider, int width, int height, int64_t ts, uint8_t** input_data, int* input_linesize) +{ + if (divider != scale_divider_) { + FreeScaler(); + InitScaler(divider); + } + + // Create frame to return + FramePtr copy = Frame::Create(); + copy->set_video_params(VideoParams(width, + height, + native_pix_fmt_, + std::static_pointer_cast(stream())->pixel_aspect_ratio(), + std::static_pointer_cast(stream())->interlacing(), + divider)); + copy->set_timestamp(Timecode::timestamp_to_time(ts, time_base_)); + copy->allocate(); + + // Convert frame to RGB/A for the rest of the pipeline + uint8_t* output_data = reinterpret_cast(copy->data()); + int output_linesize = copy->linesize_bytes(); + + sws_scale(scale_ctx_, + input_data, + input_linesize, + 0, + height, + &output_data, + &output_linesize); + + return copy; +} + int FFmpegDecoderInstance::GetFrame(AVPacket *pkt, AVFrame *frame) { bool eof = false; @@ -1008,7 +1023,11 @@ FFmpegFramePool::ElementPtr FFmpegDecoderInstance::RetrieveFrame(const int64_t& // Handle an "expected" EOF by using the last frame of our cache cache_at_eof_ = true; - return_frame = cached_frames_.last(); + if (cached_frames_.isEmpty()) { + qCritical() << "Unexpected codec EOF - unable to retrieve frame"; + } else { + return_frame = cached_frames_.last(); + } cache_wait_cond_.wakeAll(); cache_lock_.unlock(); @@ -1113,14 +1132,6 @@ void FFmpegDecoder::FreeScaler() } } -QString FFmpegDecoder::GetProxyFrameFilename(const int64_t ×tamp, const int& divider) const -{ - QString dst_fn = GetProxyFilename(divider); - dst_fn.append(QString::number(timestamp)); - dst_fn.append(FrameHashCache::GetFormatExtension()); - return dst_fn; -} - int64_t FFmpegDecoderInstance::RangeStart() const { if (cached_frames_.isEmpty()) { @@ -1213,16 +1224,6 @@ void FFmpegDecoderInstance::TruncateCacheRangeTo(const qint64 &t) } } -rational FFmpegDecoderInstance::sample_aspect_ratio() const -{ - return av_guess_sample_aspect_ratio(fmt_ctx_, avstream_, nullptr); -} - -AVStream *FFmpegDecoderInstance::stream() const -{ - return avstream_; -} - FFmpegDecoderInstance::FFmpegDecoderInstance(const char *filename, int stream_index) : fmt_ctx_(nullptr), opts_(nullptr), @@ -1306,8 +1307,8 @@ FFmpegDecoderInstance::FFmpegDecoderInstance(const char *filename, int stream_in // Start clear timer clear_timer_ = new QTimer(); clear_timer_->setInterval(kMaxFrameLife); - //clear_timer_->moveToThread(qApp->thread()); - connect(clear_timer_, &QTimer::timeout, this, &FFmpegDecoderInstance::ClearTimerEvent); + clear_timer_->moveToThread(qApp->thread()); + connect(clear_timer_, &QTimer::timeout, this, &FFmpegDecoderInstance::ClearTimerEvent, Qt::DirectConnection); QMetaObject::invokeMethod(clear_timer_, "start", Qt::QueuedConnection); } @@ -1336,16 +1337,9 @@ void FFmpegDecoderInstance::ClearResources() // Stop timer if (clear_timer_) { - - if (clear_timer_->thread() == QThread::currentThread()) { - clear_timer_->stop(); - } else { - QMetaObject::invokeMethod(clear_timer_, "stop", Qt::BlockingQueuedConnection); - } - - QMetaObject::invokeMethod(clear_timer_, "deleteLater", Qt::QueuedConnection); + QMetaObject::invokeMethod(clear_timer_, "stop", Qt::QueuedConnection); + clear_timer_->deleteLater(); clear_timer_ = nullptr; - } if (opts_) { diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index 009a9a5ae..9922cac91 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -64,8 +64,15 @@ public: void RemoveFramesBefore(const qint64& t); void TruncateCacheRangeTo(const qint64& t); - rational sample_aspect_ratio() const; - AVStream* stream() const; + AVFormatContext* fmt_ctx() const + { + return fmt_ctx_; + } + + AVStream* stream() const + { + return avstream_; + } void ClearFrameCache(); @@ -165,23 +172,19 @@ private: */ void FFmpegError(int error_code); - virtual QString GetIndexFilename() const override; - - QString GetProxyFilename(int divider) const; - void ClearResources(); void InitScaler(int divider); void FreeScaler(); - QString GetProxyFrameFilename(const int64_t& timestamp, const int ÷r) const; - static int GetScaledDimension(int dim, int divider); static PixelFormat::Format GetNativePixelFormat(AVPixelFormat pix_fmt); static uint64_t ValidateChannelLayout(AVStream *stream); + FramePtr BuffersToNativeFrame(int divider, int width, int height, int64_t ts, uint8_t **input_data, int* input_linesize); + SwsContext* scale_ctx_; int scale_divider_; AVPixelFormat src_pix_fmt_; @@ -189,7 +192,6 @@ private: PixelFormat::Format native_pix_fmt_; rational time_base_; - rational aspect_ratio_; int64_t start_time_; static QHash< Stream*, QList > instance_map_; diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index 3b034f7ad..562d4f82e 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -20,6 +20,10 @@ #include "ffmpegencoder.h" +extern "C" { +#include +} + #include #include "ffmpegcommon.h" @@ -134,6 +138,17 @@ bool FFmpegEncoder::WriteFrame(FramePtr frame, rational time) encoded_frame->height = frame->height(); encoded_frame->format = video_codec_ctx_->pix_fmt; + // Set interlacing + if (frame->video_params().interlacing() != VideoParams::kInterlaceNone) { + encoded_frame->interlaced_frame = 1; + + if (frame->video_params().interlacing() == VideoParams::kInterlacedTopFirst) { + encoded_frame->top_field_first = 1; + } else { + encoded_frame->top_field_first = 0; + } + } + error_code = av_frame_get_buffer(encoded_frame, 0); if (error_code < 0) { FFmpegError("Failed to create AVFrame buffer", error_code); @@ -439,11 +454,27 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV if (type == AVMEDIA_TYPE_VIDEO) { codec_ctx->width = params().video_params().width(); codec_ctx->height = params().video_params().height(); - codec_ctx->sample_aspect_ratio = {1, 1}; + codec_ctx->sample_aspect_ratio = params().video_params().pixel_aspect_ratio().toAVRational(); codec_ctx->time_base = params().video_params().time_base().toAVRational(); + codec_ctx->pix_fmt = av_get_pix_fmt(params().video_pix_fmt().toUtf8()); - // FIXME: Make this customizable again - codec_ctx->pix_fmt = encoder->pix_fmts[0]; + if (params().video_params().interlacing() != VideoParams::kInterlaceNone) { + // FIXME: I actually don't know what these flags do, the documentation helpfully doesn't + // explain them at all. I hope using both of them is the right thing to do. + codec_ctx->flags |= AV_CODEC_FLAG_INTERLACED_DCT | AV_CODEC_FLAG_INTERLACED_ME; + + + if (params().video_params().interlacing() == VideoParams::kInterlacedTopFirst) { + codec_ctx->field_order = AV_FIELD_TT; + } else { + codec_ctx->field_order = AV_FIELD_BB; + + if (codec_id == AV_CODEC_ID_H264) { + // For some reason, FFmpeg doesn't set libx264's bff flag so we have to do it ourselves + av_opt_set(video_codec_ctx_->priv_data, "x264opts", "bff=1", AV_OPT_SEARCH_CHILDREN); + } + } + } // Set custom options { diff --git a/app/codec/frame.cpp b/app/codec/frame.cpp index 00dcb1806..4ca93f42b 100644 --- a/app/codec/frame.cpp +++ b/app/codec/frame.cpp @@ -27,8 +27,7 @@ OLIVE_NAMESPACE_ENTER Frame::Frame() : - timestamp_(0), - sample_aspect_ratio_(1) + timestamp_(0) { } @@ -106,16 +105,6 @@ void Frame::set_pixel(int x, int y, const Color &c) c.toData(data_.data() + byte_offset, video_params().format()); } -const rational &Frame::sample_aspect_ratio() const -{ - return sample_aspect_ratio_; -} - -void Frame::set_sample_aspect_ratio(const rational &aspect_ratio) -{ - sample_aspect_ratio_ = aspect_ratio; -} - const rational &Frame::timestamp() const { return timestamp_; diff --git a/app/codec/frame.h b/app/codec/frame.h index daea040ce..7f49948a8 100644 --- a/app/codec/frame.h +++ b/app/codec/frame.h @@ -57,9 +57,6 @@ public: bool contains_pixel(int x, int y) const; void set_pixel(int x, int y, const Color& c); - const rational& sample_aspect_ratio() const; - void set_sample_aspect_ratio(const rational& sample_aspect_ratio); - /** * @brief Get frame's timestamp. * @@ -116,8 +113,6 @@ private: int64_t native_timestamp_; - rational sample_aspect_ratio_; - int linesize_; }; diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index e5f7b74c8..11e73ce03 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -117,13 +117,15 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled) image_stream->set_width(in->spec().width); image_stream->set_height(in->spec().height); + image_stream->set_format(GetFormatFromOIIOBasetype(in->spec())); + image_stream->set_pixel_aspect_ratio(GetPixelAspectRatioFromOIIO(in->spec())); // Images will always have just one stream image_stream->set_index(0); // OIIO automatically premultiplies alpha - // FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this likely reduces the - // fidelity? + // FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this + // likely reduces the fidelity? image_stream->set_premultiplied_alpha(true); // Get stats for this image and dump them into the Footage file @@ -177,6 +179,8 @@ FramePtr OIIODecoder::RetrieveVideo(const rational &timecode, const int& divider frame->set_video_params(VideoParams(buffer_->spec().width, buffer_->spec().height, pix_fmt_, + GetPixelAspectRatioFromOIIO(buffer_->spec()), + VideoParams::kInterlaceNone, // FIXME: Does OIIO deinterlace for us? divider)); frame->allocate(); @@ -216,11 +220,6 @@ bool OIIODecoder::SupportsVideo() return true; } -QString OIIODecoder::GetIndexFilename() const -{ - return QString(); -} - void OIIODecoder::FrameToBuffer(FramePtr frame, OIIO::ImageBuf *buf) { #if OIIO_VERSION < 20112 @@ -278,6 +277,28 @@ void OIIODecoder::BufferToFrame(OIIO::ImageBuf *buf, FramePtr frame) #endif } +PixelFormat::Format OIIODecoder::GetFormatFromOIIOBasetype(const OIIO::ImageSpec& spec) +{ + bool has_alpha = (spec.nchannels == kRGBAChannels); + + if (spec.format == OIIO::TypeDesc::UINT8) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA8 : PixelFormat::PIX_FMT_RGB8; + } else if (spec.format == OIIO::TypeDesc::UINT16) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA16U : PixelFormat::PIX_FMT_RGB16U; + } else if (spec.format == OIIO::TypeDesc::HALF) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA16F : PixelFormat::PIX_FMT_RGB16F; + } else if (spec.format == OIIO::TypeDesc::FLOAT) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA32F : PixelFormat::PIX_FMT_RGB32F; + } else { + return PixelFormat::PIX_FMT_INVALID; + } +} + +rational OIIODecoder::GetPixelAspectRatioFromOIIO(const OIIO::ImageSpec &spec) +{ + return rational::fromDouble(spec.extra_attribs.get_float("PixelAspectRatio", 1)); +} + bool OIIODecoder::FileTypeIsSupported(const QString& fn) { // We prioritize OIIO over FFmpeg to pick up still images more effectively, but some OIIO decoders (notably OpenJPEG) @@ -361,16 +382,9 @@ bool OIIODecoder::OpenImageHandler(const QString &fn) is_rgba_ = (spec.nchannels == kRGBAChannels); - // Weirdly, switch statement doesn't work correctly here - if (spec.format == OIIO::TypeDesc::UINT8) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA8 : PixelFormat::PIX_FMT_RGB8; - } else if (spec.format == OIIO::TypeDesc::UINT16) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA16U : PixelFormat::PIX_FMT_RGB16U; - } else if (spec.format == OIIO::TypeDesc::HALF) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA16F : PixelFormat::PIX_FMT_RGB16F; - } else if (spec.format == OIIO::TypeDesc::FLOAT) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA32F : PixelFormat::PIX_FMT_RGB32F; - } else { + pix_fmt_ = GetFormatFromOIIOBasetype(spec); + + if (pix_fmt_ == PixelFormat::PIX_FMT_INVALID) { qWarning() << "Failed to convert OIIO::ImageDesc to native pixel format"; return false; } diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index f481481b1..1b0a052c0 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -51,12 +51,14 @@ public: virtual bool SupportsVideo() override; - virtual QString GetIndexFilename() const override; - static void FrameToBuffer(FramePtr frame, OIIO::ImageBuf* buf); static void BufferToFrame(OIIO::ImageBuf* buf, FramePtr frame); + static PixelFormat::Format GetFormatFromOIIOBasetype(const OIIO::ImageSpec& spec); + + static rational GetPixelAspectRatioFromOIIO(const OIIO::ImageSpec& spec); + private: #if OIIO_VERSION < 10903 OIIO::ImageInput* image_; diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index 9b6a1e6f5..0ee6714aa 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -23,6 +23,8 @@ set(OLIVE_SOURCES common/clamp.h common/crashhandler.h common/crashhandler.cpp + common/crashpadinterface.cpp + common/crashpadinterface.h common/debug.h common/debug.cpp common/define.h @@ -36,6 +38,8 @@ set(OLIVE_SOURCES common/qtutils.h common/qtutils.cpp common/range.h + common/ratiodialog.h + common/ratiodialog.cpp common/rational.h common/rational.cpp common/threadedobject.h diff --git a/app/common/crashpadinterface.cpp b/app/common/crashpadinterface.cpp new file mode 100644 index 000000000..85cf37fad --- /dev/null +++ b/app/common/crashpadinterface.cpp @@ -0,0 +1,82 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "crashpadinterface.h" + +#ifdef USE_CRASHPAD + +#include +#include +#include + +#include "filefunctions.h" + +// Copied from base::FilePath to match its macro +#if defined(OS_POSIX) + // On most platforms, native pathnames are char arrays, and the encoding + // may or may not be specified. On Mac OS X, native pathnames are encoded + // in UTF-8. + #define TO_BASE_STRING_TYPE(x) x.toStdString() +#elif defined(OS_WIN) + // On Windows, for Unicode-aware applications, native pathnames are wchar_t + // arrays encoded in UTF-16. + #define TO_BASE_STRING_TYPE(x) x.toStdWString() +#endif // OS_WIN + +bool InitializeCrashpad() +{ + QString exe_dir = QCoreApplication::applicationDirPath(); + + // FIXME: On Linux, probably should put this in a subdir so that it doesn't conflict with + // anything else in /usr/bin + + base::FilePath handler(TO_BASE_STRING_TYPE(QDir(exe_dir).filePath(QStringLiteral("crashpad_handler.exe")))); + + base::FilePath reports_dir(TO_BASE_STRING_TYPE(QDir(OLIVE_NAMESPACE::FileFunctions::GetTempFilePath()).filePath("reports"))); + + base::FilePath metrics_dir(TO_BASE_STRING_TYPE(QDir(OLIVE_NAMESPACE::FileFunctions::GetTempFilePath()).filePath("metrics"))); + + std::string url = "https://olivevideoeditor.org/crashpad/report.php"; + + // Metadata that will be posted to the server with the crash report map + std::map annotations; + + // Disable crashpad rate limiting so that all crashes have dmp files + std::vector arguments; + arguments.push_back("--no-rate-limit"); + arguments.push_back("--no-upload-gzip"); + + // Initialize Crashpad database + std::unique_ptr database = crashpad::CrashReportDatabase::Initialize(reports_dir); + if (database == NULL) return false; + + // Enable automated crash uploads + crashpad::Settings *settings = database->GetSettings(); + if (settings == NULL) return false; + settings->SetUploadsEnabled(true); + + // Start crash handler + crashpad::CrashpadClient *client = new crashpad::CrashpadClient(); + bool status = client->StartHandler(handler, reports_dir, metrics_dir, + url, annotations, arguments, true, true); + return status; +} + +#endif // USE_CRASHPAD diff --git a/app/common/crashpadinterface.h b/app/common/crashpadinterface.h new file mode 100644 index 000000000..26bd17b65 --- /dev/null +++ b/app/common/crashpadinterface.h @@ -0,0 +1,34 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef CRASHPAD_INTERFACE_H +#define CRASHPAD_INTERFACE_H + +#ifdef USE_CRASHPAD + +#include +#include +#include + +bool InitializeCrashpad(); + +#endif // USE_CRASHPAD + +#endif // CRASHPAD_INTERFACE_H diff --git a/app/common/define.h b/app/common/define.h index dd62ba73d..aa968fcf7 100644 --- a/app/common/define.h +++ b/app/common/define.h @@ -42,6 +42,8 @@ const int kProjectIconSizeMaximum = 256; /// The default size an icon in ProjectExplorer can be const int kProjectIconSizeDefault = 64; +const int kBytesInGigabyte = 1073741824; + OLIVE_NAMESPACE_EXIT #define MACRO_NAME_AS_STR(s) #s diff --git a/app/common/filefunctions.cpp b/app/common/filefunctions.cpp index 7ed065829..f087dd90b 100644 --- a/app/common/filefunctions.cpp +++ b/app/common/filefunctions.cpp @@ -50,35 +50,6 @@ QString FileFunctions::GetUniqueFileIdentifier(const QString &filename) return QString(result.toHex()); } -QString FileFunctions::GetMediaIndexLocation() -{ - QDir local_appdata_dir(Config::Current()["DiskCachePath"].toString()); - - QDir media_index_dir = local_appdata_dir.filePath("mediaindex"); - - // Attempt to ensure this folder exists - media_index_dir.mkpath("."); - - return media_index_dir.absolutePath(); -} - -QString FileFunctions::GetMediaIndexFilename(const QString &filename) -{ - return QDir(GetMediaIndexLocation()).filePath(filename); -} - -QString FileFunctions::GetMediaCacheLocation() -{ - QDir local_appdata_dir(Config::Current()["DiskCachePath"].toString()); - - QDir media_cache_dir = local_appdata_dir.filePath("mediacache"); - - // Attempt to ensure this folder exists - media_cache_dir.mkpath("."); - - return media_cache_dir.absolutePath(); -} - QString FileFunctions::GetConfigurationLocation() { if (IsPortable()) { @@ -112,6 +83,30 @@ QString FileFunctions::GetTempFilePath() return temp_path; } +bool FileFunctions::CanCopyDirectoryWithoutOverwriting(const QString& source, const QString& dest) +{ + QFileInfoList info_list = QDir(source).entryInfoList(); + + foreach (const QFileInfo& info, info_list) { + // QDir::NoDotAndDotDot continues to not work, so we have to check manually + if (info.fileName() == QStringLiteral(".") || info.fileName() == QStringLiteral("..")) { + continue; + } + + QString dest_equivalent = QDir(dest).filePath(info.fileName()); + + if (info.isDir()) { + if (!CanCopyDirectoryWithoutOverwriting(info.absoluteFilePath(), dest_equivalent)) { + return false; + } + } else if (QFileInfo::exists(dest_equivalent)) { + return false; + } + } + + return true; +} + void FileFunctions::CopyDirectory(const QString &source, const QString &dest, bool overwrite) { QDir d(source); @@ -152,4 +147,27 @@ void FileFunctions::CopyDirectory(const QString &source, const QString &dest, bo } } +bool FileFunctions::DirectoryIsValid(const QString &dir, bool try_to_create) +{ + // Empty string is invalid + if (dir.isEmpty()) { + return false; + } + + QDir d(dir); + + // If directory already exists, this is valid + if (d.exists()) { + return true; + } + + // If we can create and creation is successful, this is valid + if (try_to_create && d.mkpath(".")) { + return true; + } + + // Otherwise, invalid + return false; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/common/filefunctions.h b/app/common/filefunctions.h index d1f147c9b..fcfb875be 100644 --- a/app/common/filefunctions.h +++ b/app/common/filefunctions.h @@ -42,20 +42,18 @@ public: static QString GetUniqueFileIdentifier(const QString& filename); - static QString GetMediaIndexLocation(); - - static QString GetMediaIndexFilename(const QString& filename); - - static QString GetMediaCacheLocation(); - static QString GetConfigurationLocation(); static QString GetApplicationPath(); static QString GetTempFilePath(); + static bool CanCopyDirectoryWithoutOverwriting(const QString& source, const QString& dest); + static void CopyDirectory(const QString& source, const QString& dest, bool overwrite = false); + static bool DirectoryIsValid(const QString& dir, bool try_to_create); + }; diff --git a/app/common/ratiodialog.cpp b/app/common/ratiodialog.cpp new file mode 100644 index 000000000..cb01147e3 --- /dev/null +++ b/app/common/ratiodialog.cpp @@ -0,0 +1,91 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "ratiodialog.h" + +#include +#include + +OLIVE_NAMESPACE_ENTER + +double GetFloatRatioFromUser(QWidget* parent, + const QString& title, + bool* ok_in) +{ + QString s; + + forever { + bool ok; + + s = QInputDialog::getText(parent, + title, + QCoreApplication::translate("RatioDialog", "Enter custom ratio (e.g. \"4:3\", \"16/9\", etc.):"), + QLineEdit::Normal, + s, + &ok); + + if (!ok) { + // User cancelled dialog, do nothing + if (ok_in) { + *ok_in = false; + } + return qSNaN(); + } + + QStringList ratio_components = s.split(QRegExp(QStringLiteral(":|;|\\/"))); + + if (ratio_components.size() == 1) { + bool float_ok; + + double flt = ratio_components.at(0).toDouble(&float_ok); + + if (float_ok && flt > 0) { + if (ok_in) { + *ok_in = true; + } + return flt; + } + } else if (ratio_components.size() == 2) { + bool numer_ok, denom_ok; + + double num = ratio_components.at(0).toDouble(&numer_ok); + double den = ratio_components.at(1).toDouble(&denom_ok); + + if (numer_ok + && denom_ok + && num > 0 + && den > 0) { + // Exit loop and set this ratio + if (ok_in) { + *ok_in = true; + } + return num / den; + } + } + + QMessageBox::warning(parent, + QCoreApplication::translate("RatioDialog", "Invalid custom ratio"), + QCoreApplication::translate("RatioDialog", "Failed to parse \"%1\" into an aspect ratio. Please format a " + "rational fraction with a ':' or a '/' separator.").arg(s), + QMessageBox::Ok); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/common/ratiodialog.h b/app/common/ratiodialog.h new file mode 100644 index 000000000..306e36d22 --- /dev/null +++ b/app/common/ratiodialog.h @@ -0,0 +1,36 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef RATIODIALOG_H +#define RATIODIALOG_H + +#include + +#include "common/rational.h" + +OLIVE_NAMESPACE_ENTER + +double GetFloatRatioFromUser(QWidget* parent, + const QString& title, + bool* ok_in); + +OLIVE_NAMESPACE_EXIT + +#endif // RATIODIALOG_H diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h index d8c012559..ec406dbca 100644 --- a/app/common/xmlutils.h +++ b/app/common/xmlutils.h @@ -64,6 +64,9 @@ struct XMLNodeData { QList block_links; QHash item_ptrs; + QString real_project_url; + QString saved_project_url; + }; void XMLConnectNodes(const XMLNodeData& xml_node_data, QUndoCommand* command = nullptr); diff --git a/app/config/config.cpp b/app/config/config.cpp index d2c2b0866..4bf04fc9b 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -31,6 +31,7 @@ #include "common/filefunctions.h" #include "common/xmlutils.h" #include "core.h" +#include "ui/style/style.h" #include "window/mainwindow/mainwindow.h" OLIVE_NAMESPACE_ENTER @@ -42,6 +43,11 @@ Config::Config() SetDefaults(); } +void Config::SetEntryInternal(const QString &key, NodeParam::DataType type, const QVariant &data) +{ + config_map_[key] = {type, data}; +} + QString Config::GetConfigFilePath() { return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("config.xml")); @@ -55,66 +61,67 @@ Config &Config::Current() void Config::SetDefaults() { config_map_.clear(); - config_map_["TimecodeDisplay"] = Timecode::kTimecodeDropFrame; - config_map_["DefaultStillLength"] = QVariant::fromValue(rational(2)); - config_map_["HoverFocus"] = false; - config_map_["AudioScrubbing"] = true; - config_map_["AutorecoveryInterval"] = 1; - config_map_["Language"] = "en_US"; - config_map_["ScrollZooms"] = false; - config_map_["EnableSeekToImport"] = false; - config_map_["EditToolAlsoSeeks"] = false; - config_map_["EditToolSelectsLinks"] = false; - config_map_["EnableDragFilesToTimeline"] = true; - config_map_["InvertTimelineScrollAxes"] = true; - config_map_["SelectAlsoSeeks"] = false; - config_map_["PasteSeeks"] = true; - config_map_["SelectAlsoSeeks"] = false; - config_map_["SetNameWithMarker"] = false; - config_map_["AutoSeekToBeginning"] = true; - config_map_["DropFileOnMediaToReplace"] = false; - config_map_["AddDefaultEffectsToClips"] = true; - config_map_["AutoscaleByDefault"] = false; - config_map_["Autoscroll"] = AutoScroll::kPage; - config_map_["AutoSelectDivider"] = true; - config_map_["SetNameWithMarker"] = false; - config_map_["RectifiedWaveforms"] = false; - config_map_["DropWithoutSequenceBehavior"] = TimelineWidget::kDWSAsk; - config_map_["Loop"] = false; + SetEntryInternal(QStringLiteral("Style"), NodeParam::kString, StyleManager::kDefaultStyle); + SetEntryInternal(QStringLiteral("TimecodeDisplay"), NodeParam::kInt, Timecode::kTimecodeDropFrame); + SetEntryInternal(QStringLiteral("DefaultStillLength"), NodeParam::kRational, QVariant::fromValue(rational(2))); + SetEntryInternal(QStringLiteral("HoverFocus"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("AudioScrubbing"), NodeParam::kBoolean, true); + SetEntryInternal(QStringLiteral("AutorecoveryInterval"), NodeParam::kInt, 1); + SetEntryInternal(QStringLiteral("DiskCacheSaveInterval"), NodeParam::kInt, 10000); + SetEntryInternal(QStringLiteral("Language"), NodeParam::kString, QLocale::system().name()); + SetEntryInternal(QStringLiteral("ScrollZooms"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("EnableSeekToImport"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("EditToolAlsoSeeks"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("EditToolSelectsLinks"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("EnableDragFilesToTimeline"), NodeParam::kBoolean, true); + SetEntryInternal(QStringLiteral("InvertTimelineScrollAxes"), NodeParam::kBoolean, true); + SetEntryInternal(QStringLiteral("SelectAlsoSeeks"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("PasteSeeks"), NodeParam::kBoolean, true); + SetEntryInternal(QStringLiteral("SelectAlsoSeeks"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("SetNameWithMarker"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("AutoSeekToBeginning"), NodeParam::kBoolean, true); + SetEntryInternal(QStringLiteral("DropFileOnMediaToReplace"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("AddDefaultEffectsToClips"), NodeParam::kBoolean, true); + SetEntryInternal(QStringLiteral("AutoscaleByDefault"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("Autoscroll"), NodeParam::kInt, AutoScroll::kPage); + SetEntryInternal(QStringLiteral("AutoSelectDivider"), NodeParam::kBoolean, true); + SetEntryInternal(QStringLiteral("SetNameWithMarker"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("RectifiedWaveforms"), NodeParam::kBoolean, false); + SetEntryInternal(QStringLiteral("DropWithoutSequenceBehavior"), NodeParam::kInt, TimelineWidget::kDWSAsk); + SetEntryInternal(QStringLiteral("Loop"), NodeParam::kBoolean, false); - config_map_["AutoCacheInterval"] = 250; + SetEntryInternal(QStringLiteral("AutoCacheInterval"), NodeParam::kInt, 250); - config_map_["NodeCatColor0"] = QVariant::fromValue(Color(0.75f, 0.75f, 0.75f)); - config_map_["NodeCatColor1"] = QVariant::fromValue(Color(0.25f, 0.25f, 0.25f)); - config_map_["NodeCatColor2"] = QVariant::fromValue(Color(0.75f, 0.75f, 0.25f)); - config_map_["NodeCatColor3"] = QVariant::fromValue(Color(0.75f, 0.25f, 0.75f)); - config_map_["NodeCatColor4"] = QVariant::fromValue(Color(0.25f, 0.75f, 0.75f)); - config_map_["NodeCatColor5"] = QVariant::fromValue(Color(0.50f, 0.50f, 0.50f)); - config_map_["NodeCatColor6"] = QVariant::fromValue(Color(0.25f, 0.75f, 0.25f)); - config_map_["NodeCatColor7"] = QVariant::fromValue(Color(0.25f, 0.25f, 0.75f)); - config_map_["NodeCatColor8"] = QVariant::fromValue(Color(0.75f, 0.25f, 0.25f)); + SetEntryInternal(QStringLiteral("NodeCatColor0"), NodeParam::kColor, QVariant::fromValue(Color(0.75f, 0.75f, 0.75f))); + SetEntryInternal(QStringLiteral("NodeCatColor1"), NodeParam::kColor, QVariant::fromValue(Color(0.25f, 0.25f, 0.25f))); + SetEntryInternal(QStringLiteral("NodeCatColor2"), NodeParam::kColor, QVariant::fromValue(Color(0.75f, 0.75f, 0.25f))); + SetEntryInternal(QStringLiteral("NodeCatColor3"), NodeParam::kColor, QVariant::fromValue(Color(0.75f, 0.25f, 0.75f))); + SetEntryInternal(QStringLiteral("NodeCatColor4"), NodeParam::kColor, QVariant::fromValue(Color(0.25f, 0.75f, 0.75f))); + SetEntryInternal(QStringLiteral("NodeCatColor5"), NodeParam::kColor, QVariant::fromValue(Color(0.50f, 0.50f, 0.50f))); + SetEntryInternal(QStringLiteral("NodeCatColor6"), NodeParam::kColor, QVariant::fromValue(Color(0.25f, 0.75f, 0.25f))); + SetEntryInternal(QStringLiteral("NodeCatColor7"), NodeParam::kColor, QVariant::fromValue(Color(0.25f, 0.25f, 0.75f))); + SetEntryInternal(QStringLiteral("NodeCatColor8"), NodeParam::kColor, QVariant::fromValue(Color(0.75f, 0.25f, 0.25f))); - config_map_["AudioOutput"] = QString(); - config_map_["AudioInput"] = QString(); + SetEntryInternal(QStringLiteral("AudioOutput"), NodeParam::kString, QString()); + SetEntryInternal(QStringLiteral("AudioInput"), NodeParam::kString, QString()); - config_map_["DiskCachePath"] = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); - config_map_["DiskCacheSize"] = 20.0; - config_map_["DiskCacheBehind"] = QVariant::fromValue(rational(1)); - config_map_["DiskCacheAhead"] = QVariant::fromValue(rational(5)); - config_map_["ClearDiskCacheOnClose"] = false; + SetEntryInternal(QStringLiteral("DiskCacheBehind"), NodeParam::kRational, QVariant::fromValue(rational(1))); + SetEntryInternal(QStringLiteral("DiskCacheAhead"), NodeParam::kRational, QVariant::fromValue(rational(5))); - config_map_["DefaultSequenceWidth"] = 1920; - config_map_["DefaultSequenceHeight"] = 1080; - config_map_["DefaultSequenceFrameRate"] = QVariant::fromValue(rational(1001, 30000)); - config_map_["DefaultSequenceAudioFrequency"] = 48000; - config_map_["DefaultSequenceAudioLayout"] = QVariant::fromValue(static_cast(AV_CH_LAYOUT_STEREO)); - config_map_["DefaultSequencePreviewFormat"] = PixelFormat::PIX_FMT_RGBA16F; + SetEntryInternal(QStringLiteral("DefaultSequenceWidth"), NodeParam::kInt, 1920); + SetEntryInternal(QStringLiteral("DefaultSequenceHeight"), NodeParam::kInt, 1080); + SetEntryInternal(QStringLiteral("DefaultSequencePixelAspect"), NodeParam::kRational, QVariant::fromValue(rational(1))); + SetEntryInternal(QStringLiteral("DefaultSequenceFrameRate"), NodeParam::kRational, QVariant::fromValue(rational(1001, 30000))); + SetEntryInternal(QStringLiteral("DefaultSequenceInterlacing"), NodeParam::kInt, VideoParams::kInterlaceNone); + SetEntryInternal(QStringLiteral("DefaultSequenceAudioFrequency"), NodeParam::kInt, 48000); + SetEntryInternal(QStringLiteral("DefaultSequenceAudioLayout"), NodeParam::kInt, QVariant::fromValue(static_cast(AV_CH_LAYOUT_STEREO))); + SetEntryInternal(QStringLiteral("DefaultSequencePreviewFormat"), NodeParam::kInt, PixelFormat::PIX_FMT_RGBA16F); // Online/offline settings - config_map_["OnlinePixelFormat"] = PixelFormat::PIX_FMT_RGBA32F; - config_map_["OfflinePixelFormat"] = PixelFormat::PIX_FMT_RGBA16F; - config_map_["OnlineOCIOMethod"] = ColorManager::kOCIOAccurate; - config_map_["OfflineOCIOMethod"] = ColorManager::kOCIOFast; + SetEntryInternal(QStringLiteral("OnlinePixelFormat"), NodeParam::kInt, PixelFormat::PIX_FMT_RGBA32F); + SetEntryInternal(QStringLiteral("OfflinePixelFormat"), NodeParam::kInt, PixelFormat::PIX_FMT_RGBA16F); + SetEntryInternal(QStringLiteral("OnlineOCIOMethod"), NodeParam::kInt, ColorManager::kOCIOAccurate); + SetEntryInternal(QStringLiteral("OfflineOCIOMethod"), NodeParam::kInt, ColorManager::kOCIOFast); } void Config::Load() @@ -156,7 +163,7 @@ void Config::Load() double config_fr = value.toDouble(); - QList supported_frame_rates = Core::SupportedFrameRates(); + const QVector& supported_frame_rates = VideoParams::kSupportedFrameRates; rational match = supported_frame_rates.first(); double match_diff = qAbs(match.toDouble() - config_fr); @@ -174,7 +181,7 @@ void Config::Load() current_config_[key] = QVariant::fromValue(match.flipped()); } else { - current_config_[key] = value; + current_config_[key] = NodeInput::StringToValue(current_config_.GetConfigEntryType(key), value, false); } } @@ -219,10 +226,10 @@ void Config::Save() // Anything after the hyphen is considered "unimportant" information writer.writeTextElement("Version", QCoreApplication::applicationVersion().split('-').first()); - QMapIterator iterator(current_config_.config_map_); + QMapIterator iterator(current_config_.config_map_); while (iterator.hasNext()) { iterator.next(); - writer.writeTextElement(iterator.key(), iterator.value().toString()); + writer.writeTextElement(iterator.key(), NodeInput::ValueToString(iterator.value().type, iterator.value().data, false)); } writer.writeEndElement(); // Configuration @@ -234,12 +241,17 @@ void Config::Save() QVariant Config::operator[](const QString &key) const { - return config_map_[key]; + return config_map_[key].data; } QVariant &Config::operator[](const QString &key) { - return config_map_[key]; + return config_map_[key].data; +} + +NodeParam::DataType Config::GetConfigEntryType(const QString &key) const +{ + return config_map_[key].type; } OLIVE_NAMESPACE_EXIT diff --git a/app/config/config.h b/app/config/config.h index 342d50484..dfed5a23d 100644 --- a/app/config/config.h +++ b/app/config/config.h @@ -26,6 +26,7 @@ #include #include "common/timecodefunctions.h" +#include "node/param.h" OLIVE_NAMESPACE_ENTER @@ -43,10 +44,19 @@ public: QVariant& operator[](const QString&); + NodeParam::DataType GetConfigEntryType(const QString& key) const; + private: Config(); - QMap config_map_; + struct ConfigEntry { + NodeParam::DataType type; + QVariant data; + }; + + void SetEntryInternal(const QString& key, NodeParam::DataType type, const QVariant& data); + + QMap config_map_; static Config current_config_; diff --git a/app/core.cpp b/app/core.cpp index 70fb71f6c..4ab066d1c 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -51,7 +51,6 @@ #include "render/diskmanager.h" #include "render/pixelformat.h" #include "render/shaderinfo.h" -#include "task/cache/cache.h" #include "task/project/import/import.h" #include "task/project/load/load.h" #include "task/project/save/save.h" @@ -81,21 +80,17 @@ Core *Core::instance() return &instance_; } -bool Core::Start() +int Core::execute(QCoreApplication* a) { - // Reset config (Config sets to default on construction already, but we do it again here as a workaround that fixes - // the fact that some of the config paths set by default rely on the app name having been set (in main()) - Config::Current().SetDefaults(); + int exit_code = 1; // // Parse command line arguments // - QCoreApplication* app = QCoreApplication::instance(); - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); + QCommandLineOption help_option = parser.addHelpOption(); + QCommandLineOption version_option = parser.addVersionOption(); // Project from command line option // FIXME: What's the correct way to make a visually "optional" positional argument, or is manually adding square @@ -111,7 +106,15 @@ bool Core::Start() parser.addOption(headless_export_option); // Parse options - parser.process(*app); + parser.process(*a); + + if (parser.isSet(help_option) || parser.isSet(version_option)) { + // These options don't launch any of the application proper + return a->exec(); + } + + // Start core + OLIVE_NAMESPACE::Core::instance()->Start(); QStringList args = parser.positionalArguments(); @@ -120,6 +123,67 @@ bool Core::Start() startup_project_ = args.first(); } + gui_active_ = !parser.isSet(headless_export_option); + + if (gui_active_) { + + // Start GUI + StartGUI(parser.isSet(fullscreen_option)); + + // If we have a startup + QMetaObject::invokeMethod(this, "OpenStartupProject", Qt::QueuedConnection); + + // Run application loop and receive exit code + exit_code = a->exec(); + + } else { + + if (parser.isSet(headless_export_option)) { + // Start a headless export + if (StartHeadlessExport()) { + exit_code = 0; + } + } + + } + + // Clear core memory + OLIVE_NAMESPACE::Core::instance()->Stop(); + + return exit_code; +} + +void Core::DeclareTypesForQt() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); +} + +void Core::Start() +{ + // Load application config + Config::Load(); + // Declare custom types for Qt signal/slot system DeclareTypesForQt(); @@ -132,68 +196,20 @@ bool Core::Start() // Initialize task manager TaskManager::CreateInstance(); - // Load application config - Config::Load(); - + // Initialize OpenGL service + OpenGLProxy::CreateInstance(); // // Start application // qInfo() << "Using Qt version:" << qVersion(); - - gui_active_ = !parser.isSet(headless_export_option); - - if (gui_active_) { - - // Start GUI - StartGUI(parser.isSet(fullscreen_option)); - - // Load startup project - if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) { - QMessageBox::warning(main_window(), - tr("Failed to open startup file"), - tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_), - QMessageBox::Ok); - - startup_project_.clear(); - } - - if (startup_project_.isEmpty()) { - // If no load project is set, create a new one on open - CreateNewProject(); - } else { - OpenProjectInternal(startup_project_); - } - - return true; - - } else { - - if (parser.isSet(headless_export_option)) { - - if (startup_project_.isEmpty()) { - qCritical().noquote() << tr("You must specify a project file to export"); - } else { - OpenProjectInternal(startup_project_); - - qDebug() << "Ready for exporting!"; - - return true; - } - - } - - // Error fallback - return false; - - } } void Core::Stop() { // Save Config - //Config::Save(); + Config::Save(); // Save recently opened projects { @@ -209,6 +225,8 @@ void Core::Stop() } } + OpenGLProxy::DestroyInstance(); + MenuShared::DestroyInstance(); TaskManager::DestroyInstance(); @@ -263,16 +281,26 @@ const Tool::Item &Core::tool() const return tool_; } -const Tool::AddableObject &Core::selected_addable_object() const +const Tool::AddableObject &Core::GetSelectedAddableObject() const { return addable_object_; } +const QString &Core::GetSelectedTransition() const +{ + return selected_transition_; +} + void Core::SetSelectedAddableObject(const Tool::AddableObject &obj) { addable_object_ = obj; } +void Core::SetSelectedTransitionObject(const QString &obj) +{ + selected_transition_ = obj; +} + void Core::ClearOpenRecentList() { recent_projects_.clear(); @@ -490,9 +518,11 @@ void Core::AddOpenProject(ProjectPtr p) void Core::AddOpenProjectFromTask(Task *task) { QList projects = static_cast(task)->GetLoadedProjects(); + QList layouts = static_cast(task)->GetLoadedLayouts(); - foreach (ProjectPtr p, projects) { - AddOpenProject(p); + for (int i=0; iLoadLayout(layouts.at(i)); } } @@ -539,33 +569,110 @@ void Core::ProjectWasModified(bool e) } } -void Core::DeclareTypesForQt() +bool Core::StartHeadlessExport() { - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); + if (startup_project_.isEmpty()) { + qCritical().noquote() << tr("You must specify a project file to export"); + return false; + } + + if (!QFileInfo::exists(startup_project_)) { + qCritical().noquote() << tr("Specified project does not exist"); + return false; + } + + // Start a load task and try running it + ProjectLoadTask plm(startup_project_); + CLITaskDialog task_dialog(&plm); + + if (task_dialog.Run()) { + ProjectPtr p = plm.GetLoadedProjects().first(); + QList items = p->get_items_of_type(Item::kSequence); + + // Check if this project contains sequences + if (items.isEmpty()) { + qCritical().noquote() << tr("Project contains no sequences, nothing to export"); + return false; + } + + SequencePtr sequence = nullptr; + + // Check if this project contains multiple sequences + if (items.size() > 1) { + qInfo().noquote() << tr("This project has multiple sequences. Which do you wish to export?"); + for (int i=0;iname().toStdString(); + } + + QTextStream stream(stdin); + QString sequence_read; + int sequence_index = -1; + QString quit_code = QStringLiteral("q"); + std::string prompt = tr("Enter number (or %1 to cancel): ").arg(quit_code).toStdString(); + forever { + std::cout << prompt; + + stream.readLineInto(&sequence_read); + + if (!QString::compare(sequence_read, quit_code, Qt::CaseInsensitive)) { + return false; + } + + bool ok; + sequence_index = sequence_read.toInt(&ok); + + if (ok && sequence_index >= 0 && sequence_index < items.size()) { + break; + } else { + qCritical().noquote() << tr("Invalid sequence number"); + } + } + + sequence = std::static_pointer_cast(items.at(sequence_index)); + } else { + sequence = std::static_pointer_cast(items.first()); + } + + ExportParams params; + ExportTask export_task(sequence->viewer_output(), p->color_manager(), params); + CLITaskDialog export_dialog(&export_task); + if (export_dialog.Run()) { + qInfo().noquote() << tr("Export succeeded"); + return true; + } else { + qInfo().noquote() << tr("Export failed: %1").arg(export_task.GetError()); + return false; + } + } else { + qCritical().noquote() << tr("Project failed to load: %1").arg(plm.GetError()); + return false; + } +} + +void Core::OpenStartupProject() +{ + // Load startup project + if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) { + QMessageBox::warning(main_window_, + tr("Failed to open startup file"), + tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_), + QMessageBox::Ok); + + startup_project_.clear(); + } + + if (startup_project_.isEmpty()) { + // If no load project is set, create a new one on open + CreateNewProject(); + } else { + OpenProjectInternal(startup_project_); + } } void Core::StartGUI(bool full_screen) { // Set UI style - qApp->setStyle(QStyleFactory::create("Fusion")); - StyleManager::SetStyle(StyleManager::DefaultStyle()); + StyleManager::Init(); // Set up shared menus MenuShared::CreateInstance(); @@ -764,90 +871,6 @@ bool Core::CloseAllExceptActiveProject() return true; } -QList Core::SupportedFrameRates() -{ - QList frame_rates; - - frame_rates.append(rational(10, 1)); // 10 FPS - frame_rates.append(rational(15, 1)); // 15 FPS - frame_rates.append(rational(24000, 1001)); // 23.976 FPS - frame_rates.append(rational(24, 1)); // 24 FPS - frame_rates.append(rational(25, 1)); // 25 FPS - frame_rates.append(rational(30000, 1001)); // 29.97 FPS - frame_rates.append(rational(30, 1)); // 30 FPS - frame_rates.append(rational(48000, 1001)); // 47.952 FPS - frame_rates.append(rational(48, 1)); // 48 FPS - frame_rates.append(rational(50, 1)); // 50 FPS - frame_rates.append(rational(60000, 1001)); // 59.94 FPS - frame_rates.append(rational(60, 1)); // 60 FPS - - return frame_rates; -} - -QList Core::SupportedSampleRates() -{ - QList sample_rates; - - sample_rates.append(8000); // 8000 Hz - sample_rates.append(11025); // 11025 Hz - sample_rates.append(16000); // 16000 Hz - sample_rates.append(22050); // 22050 Hz - sample_rates.append(24000); // 24000 Hz - sample_rates.append(32000); // 32000 Hz - sample_rates.append(44100); // 44100 Hz - sample_rates.append(48000); // 48000 Hz - sample_rates.append(88200); // 88200 Hz - sample_rates.append(96000); // 96000 Hz - - return sample_rates; -} - -QList Core::SupportedChannelLayouts() -{ - QList channel_layouts; - - channel_layouts.append(AV_CH_LAYOUT_MONO); - channel_layouts.append(AV_CH_LAYOUT_STEREO); - channel_layouts.append(AV_CH_LAYOUT_2_1); - channel_layouts.append(AV_CH_LAYOUT_5POINT1); - channel_layouts.append(AV_CH_LAYOUT_7POINT1); - - return channel_layouts; -} - -QList Core::SupportedDividers() -{ - return {1, 2, 3, 4, 6, 8, 12, 16}; -} - -QString Core::FrameRateToString(const rational &frame_rate) -{ - return tr("%1 FPS").arg(frame_rate.toDouble()); -} - -QString Core::SampleRateToString(const int &sample_rate) -{ - return tr("%1 Hz").arg(sample_rate); -} - -QString Core::ChannelLayoutToString(const uint64_t &layout) -{ - switch (layout) { - case AV_CH_LAYOUT_MONO: - return tr("Mono"); - case AV_CH_LAYOUT_STEREO: - return tr("Stereo"); - case AV_CH_LAYOUT_2_1: - return tr("2.1"); - case AV_CH_LAYOUT_5POINT1: - return tr("5.1"); - case AV_CH_LAYOUT_7POINT1: - return tr("7.1"); - default: - return tr("Unknown (0x%1)").arg(layout, 1, 16); - } -} - QString Core::GetProjectFilter() { return QStringLiteral("%1 (*.ove)").arg(tr("Olive Project")); @@ -877,6 +900,11 @@ bool Core::SaveProjectAs(ProjectPtr p) GetProjectFilter()); if (!fn.isEmpty()) { + QString extension(QStringLiteral(".ove")); + if (!fn.endsWith(extension, Qt::CaseInsensitive)) { + fn.append(extension); + } + p->set_filename(fn); SaveProjectInternal(p); @@ -915,21 +943,11 @@ void Core::OpenProjectInternal(const QString &filename) ProjectLoadTask* plm = new ProjectLoadTask(filename); - if (gui_active_) { + TaskDialog* task_dialog = new TaskDialog(plm, tr("Load Project"), main_window()); - TaskDialog* task_dialog = new TaskDialog(plm, tr("Load Project"), main_window()); + connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask); - connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask); - - task_dialog->open(); - - } else { - - //connect(plm, &ProjectLoadManager::ProjectLoaded, this, &Core::AddOpenProject); - - CLITaskDialog task_dialog(plm); - - } + task_dialog->open(); } int Core::CountFilesInFileList(const QFileInfoList &filenames) @@ -1164,23 +1182,32 @@ void Core::CacheActiveSequence(bool in_out_only) TimeBasedPanel* p = PanelManager::instance()->MostRecentlyFocused(); if (p && p->GetConnectedViewer()) { - CacheTask* task = new CacheTask(p->GetConnectedViewer(), - p->GetConnectedViewer()->video_params(), - p->GetConnectedViewer()->audio_params(), - in_out_only); + // Hacky but works for now - // Stop any current auto-cache tasks - ViewerWidget::StopAllBackgroundCacheTasks(true); - ViewerWidget::SetBackgroundCacheTask(task); + // Find Viewer attached to this TimeBasedPanel + QList all_viewers = PanelManager::instance()->GetPanelsOfType(); - TaskDialog* dialog = new TaskDialog(task, tr("Caching Sequence"), main_window_); + ViewerPanel* found_panel = nullptr; - connect(dialog, - &TaskDialog::TaskSucceeded, - this, - [] { ViewerWidget::SetBackgroundCacheTask(nullptr); }); + foreach (ViewerPanel* viewer, all_viewers) { + if (viewer->GetConnectedViewer() == p->GetConnectedViewer()) { + found_panel = viewer; + break; + } + } - dialog->open(); + if (found_panel) { + if (in_out_only) { + found_panel->CacheSequenceInOut(); + } else { + found_panel->CacheEntireSequence(); + } + } else { + QMessageBox::critical(main_window_, + tr("Failed to cache sequence"), + tr("No active viewer found with this sequence."), + QMessageBox::Ok); + } } } diff --git a/app/core.h b/app/core.h index 07aeccb08..01d941f0a 100644 --- a/app/core.h +++ b/app/core.h @@ -66,12 +66,14 @@ public: */ static Core* instance(); + int execute(QCoreApplication *a); + /** * @brief Start Olive Core * * Main application launcher. Parses command line arguments and constructs main window (if entering a GUI mode). */ - bool Start(); + void Start(); /** * @brief Stop Olive Core @@ -112,7 +114,12 @@ public: /** * @brief Get the currently selected object that the add tool should make (if the add tool is active) */ - const Tool::AddableObject& selected_addable_object() const; + const Tool::AddableObject& GetSelectedAddableObject() const; + + /** + * @brief Get the currently selected node that the transition tool should make (if the transition tool is active) + */ + const QString& GetSelectedTransition() const; /** * @brief Get current snapping value @@ -162,42 +169,6 @@ public: static QString PasteStringFromClipboard(); - /** - * @brief Return a list of supported frame rates in rational form - * - * These rationals can be flipped to create a timebase in this frame rate. - */ - static QList SupportedFrameRates(); - - /** - * @brief Return a list of supported sample rates in integer form - */ - static QList SupportedSampleRates(); - /** - * @brief Return a list of supported channel layouts as or'd flags - */ - static QList SupportedChannelLayouts(); - - /** - * @brief Return a list of supported dividers - */ - static QList SupportedDividers(); - - /** - * @brief Convert rational frame rate (i.e. flipped timebase) to a user-friendly string - */ - static QString FrameRateToString(const rational& frame_rate); - - /** - * @brief Convert integer sample rate to a user-friendly string - */ - static QString SampleRateToString(const int &sample_rate); - - /** - * @brief Convert channel layout to a user-friendly string - */ - static QString ChannelLayoutToString(const uint64_t &layout); - /** * @brief Recursively count files in a file/directory list */ @@ -339,6 +310,11 @@ public slots: */ void SetSelectedAddableObject(const Tool::AddableObject& obj); + /** + * @brief Set the currently selected object that the add tool should make + */ + void SetSelectedTransitionObject(const QString& obj); + /** * @brief Clears the list of recently opened/saved projects */ @@ -405,11 +381,6 @@ private: */ void PushRecentlyOpenedProject(const QString &s); - /** - * @brief Internal project open - */ - void OpenProjectInternal(const QString& filename); - /** * @brief Declare custom types/classes for Qt's signal/slot system * @@ -459,6 +430,11 @@ private: */ Tool::AddableObject addable_object_; + /** + * @brief Currently selected transition + */ + QString selected_transition_; + /** * @brief Current snapping setting */ @@ -507,6 +483,15 @@ private slots: void ProjectWasModified(bool e); + bool StartHeadlessExport(); + + void OpenStartupProject(); + + /** + * @brief Internal project open + */ + void OpenProjectInternal(const QString& filename); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index 1eed0ae0a..442f760e0 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory(about) add_subdirectory(actionsearch) add_subdirectory(color) +add_subdirectory(diskcache) add_subdirectory(export) add_subdirectory(footageproperties) add_subdirectory(keyframeproperties) diff --git a/app/task/cache/CMakeLists.txt b/app/dialog/diskcache/CMakeLists.txt similarity index 88% rename from app/task/cache/CMakeLists.txt rename to app/dialog/diskcache/CMakeLists.txt index 5557f3f39..6ca59d549 100644 --- a/app/task/cache/CMakeLists.txt +++ b/app/dialog/diskcache/CMakeLists.txt @@ -16,9 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - task/cache/cache.h - task/cache/cache.cpp - task/cache/footagecache.h - task/cache/footagecache.cpp + dialog/diskcache/diskcachedialog.h + dialog/diskcache/diskcachedialog.cpp PARENT_SCOPE ) diff --git a/app/dialog/diskcache/diskcachedialog.cpp b/app/dialog/diskcache/diskcachedialog.cpp new file mode 100644 index 000000000..6e542b7c3 --- /dev/null +++ b/app/dialog/diskcache/diskcachedialog.cpp @@ -0,0 +1,107 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "diskcachedialog.h" + +#include +#include +#include +#include + +#include "config/config.h" + +OLIVE_NAMESPACE_ENTER + +DiskCacheDialog::DiskCacheDialog(DiskCacheFolder *folder, QWidget* parent) : + QDialog(parent), + folder_(folder) +{ + QGridLayout* layout = new QGridLayout(this); + + int row = 0; + + layout->addWidget(new QLabel(tr("Disk Cache: %1").arg(folder->GetPath())), row, 0, 1, 2); + setWindowTitle(tr("Disk Cache Settings")); + + row++; + + layout->addWidget(new QLabel(tr("Maximum Disk Cache:")), row, 0); + + maximum_cache_slider_ = new FloatSlider(); + maximum_cache_slider_->SetFormat(tr("%1 GB")); + maximum_cache_slider_->SetMinimum(1.0); + maximum_cache_slider_->SetValue(static_cast(folder->GetLimit()) / static_cast(kBytesInGigabyte)); + layout->addWidget(maximum_cache_slider_, row, 1); + + row++; + + clear_cache_btn_ = new QPushButton(tr("Clear Disk Cache")); + connect(clear_cache_btn_, &QPushButton::clicked, this, &DiskCacheDialog::ClearDiskCache); + layout->addWidget(clear_cache_btn_, row, 1); + + row++; + + clear_disk_cache_ = new QCheckBox(tr("Automatically clear disk cache on close")); + clear_disk_cache_->setChecked(folder->GetClearOnClose()); + layout->addWidget(clear_disk_cache_, row, 1); + + row++; + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &DiskCacheDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &DiskCacheDialog::reject); + layout->addWidget(buttons, row, 0, 1, 2); +} + +void DiskCacheDialog::accept() +{ + qint64 new_disk_cache_limit = qRound64(maximum_cache_slider_->GetValue() * kBytesInGigabyte); + if (new_disk_cache_limit != folder_->GetLimit()) { + folder_->SetLimit(new_disk_cache_limit); + } + + if (folder_->GetClearOnClose() != clear_disk_cache_->isChecked()) { + folder_->SetClearOnClose(clear_disk_cache_->isChecked()); + } + + QDialog::accept(); +} + +void DiskCacheDialog::ClearDiskCache() +{ + if (QMessageBox::question(this, + tr("Clear Disk Cache"), + tr("Are you sure you want to clear the disk cache in '%1'?").arg(Config::Current()["DiskCachePath"].toString()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + clear_cache_btn_->setEnabled(false); + + if (DiskManager::instance()->ClearDiskCache(folder_->GetPath())) { + clear_cache_btn_->setText(tr("Disk Cache Cleared")); + } else { + QMessageBox::information(this, + tr("Clear Disk Cache"), + tr("Disk cache failed to fully clear. You may have to delete the cache files manually."), + QMessageBox::Ok); + clear_cache_btn_->setText(tr("Disk Cache Partially Cleared")); + } + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/diskcache/diskcachedialog.h b/app/dialog/diskcache/diskcachedialog.h new file mode 100644 index 000000000..69c3a20f3 --- /dev/null +++ b/app/dialog/diskcache/diskcachedialog.h @@ -0,0 +1,58 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef DISKCACHEDIALOG_H +#define DISKCACHEDIALOG_H + +#include +#include +#include + +#include "render/diskmanager.h" +#include "widget/slider/floatslider.h" + +OLIVE_NAMESPACE_ENTER + +class DiskCacheDialog : public QDialog +{ + Q_OBJECT +public: + DiskCacheDialog(DiskCacheFolder* folder, QWidget* parent = nullptr); + +public slots: + virtual void accept() override; + +private: + DiskCacheFolder* folder_; + + FloatSlider* maximum_cache_slider_; + + QCheckBox* clear_disk_cache_; + + QPushButton* clear_cache_btn_; + +private slots: + void ClearDiskCache(); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // DISKCACHEDIALOG_H diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index d6e0deb72..9e59c94d6 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -177,9 +177,11 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : video_tab_->width_slider()->SetDefaultValue(viewer_node_->video_params().width()); video_tab_->height_slider()->SetValue(viewer_node_->video_params().height()); video_tab_->height_slider()->SetDefaultValue(viewer_node_->video_params().height()); - video_tab_->set_frame_rate(viewer_node_->video_params().time_base().flipped()); - audio_tab_->set_sample_rate(viewer_node_->audio_params().sample_rate()); - audio_tab_->set_channel_layout(viewer_node_->audio_params().channel_layout()); + video_tab_->frame_rate_combobox()->SetFrameRate(viewer_node_->video_params().time_base().flipped()); + video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(viewer_node_->video_params().pixel_aspect_ratio()); + video_tab_->interlaced_combobox()->SetInterlaceMode(viewer_node_->video_params().interlacing()); + audio_tab_->sample_rate_combobox()->SetSampleRate(viewer_node_->audio_params().sample_rate()); + audio_tab_->channel_layout_combobox()->SetChannelLayout(viewer_node_->audio_params().channel_layout()); video_aspect_ratio_ = static_cast(viewer_node_->video_params().width()) / static_cast(viewer_node_->video_params().height()); @@ -203,11 +205,6 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : this, &ExportDialog::ResolutionChanged); - connect(video_tab_->codec_combobox(), - static_cast(&QComboBox::currentIndexChanged), - this, - &ExportDialog::VideoCodecChanged); - connect(video_tab_, &ExportVideoTab::ColorSpaceChanged, preview_viewer_, @@ -232,8 +229,8 @@ void ExportDialog::StartExport() return; } - // Validate if the entered filename contains the correct extension (the extension is necessary for both FFmpeg and - // OIIO to determine the output format) + // Validate if the entered filename contains the correct extension (the extension is necessary + // for both FFmpeg and OIIO to determine the output format) QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(static_cast(format_combobox_->currentIndex()))); // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. @@ -356,7 +353,6 @@ void ExportDialog::FormatChanged(int index) foreach (ExportCodec::Codec vcodec, ExportFormat::GetVideoCodecs(current_format)) { video_tab_->codec_combobox()->addItem(ExportCodec::GetCodecName(vcodec), vcodec); } - VideoCodecChanged(); audio_tab_->codec_combobox()->clear(); foreach (ExportCodec::Codec acodec, ExportFormat::GetAudioCodecs(current_format)) { @@ -396,17 +392,6 @@ void ExportDialog::ResolutionChanged() UpdateViewerDimensions(); } -void ExportDialog::VideoCodecChanged() -{ - ExportCodec::Codec codec = static_cast(video_tab_->codec_combobox()->currentData().toInt()); - - if (codec == ExportCodec::kCodecH264) { - video_tab_->SetCodecSection(video_tab_->h264_section()); - } else if (ExportCodec::IsCodecAStillImage(codec)) { - video_tab_->SetCodecSection(video_tab_->image_section()); - } -} - void ExportDialog::LoadPresets() { @@ -440,12 +425,14 @@ ExportParams ExportDialog::GenerateParams() const VideoParams video_render_params(static_cast(video_tab_->width_slider()->GetValue()), static_cast(video_tab_->height_slider()->GetValue()), - video_tab_->frame_rate().flipped(), + video_tab_->frame_rate_combobox()->GetFrameRate().flipped(), PixelFormat::instance()->GetConfiguredFormatForMode(render_mode), + video_tab_->pixel_aspect_combobox()->GetPixelAspectRatio(), + video_tab_->interlaced_combobox()->GetInterlaceMode(), render_mode); AudioParams audio_render_params(audio_tab_->sample_rate_combobox()->currentData().toInt(), - audio_tab_->channel_layout_combobox()->currentData().toULongLong(), + audio_tab_->channel_layout_combobox()->GetChannelLayout(), SampleFormat::kInternalFormat); ExportParams params; @@ -457,7 +444,7 @@ ExportParams ExportDialog::GenerateParams() const } if (video_enabled_->isChecked()) { - ExportCodec::Codec video_codec = static_cast(video_tab_->codec_combobox()->currentData().toInt()); + ExportCodec::Codec video_codec = video_tab_->GetSelectedCodec(); params.EnableVideo(video_render_params, video_codec); params.set_video_threads(video_tab_->threads()); @@ -465,6 +452,8 @@ ExportParams ExportDialog::GenerateParams() const video_tab_->GetCodecSection()->AddOpts(¶ms); params.set_color_transform(video_tab_->CurrentOCIOColorSpace()); + + params.set_video_pix_fmt(video_tab_->pix_fmt()); } if (audio_enabled_->isChecked()) { @@ -477,8 +466,8 @@ ExportParams ExportDialog::GenerateParams() const void ExportDialog::UpdateViewerDimensions() { - preview_viewer_->SetOverrideSize(static_cast(video_tab_->width_slider()->GetValue()), - static_cast(video_tab_->height_slider()->GetValue())); + preview_viewer_->SetViewerResolution(static_cast(video_tab_->width_slider()->GetValue()), + static_cast(video_tab_->height_slider()->GetValue())); QMatrix4x4 transform = ExportParams::GenerateMatrix(static_cast(video_tab_->scaling_method_combobox()->currentData().toInt()), diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 60a301af4..8313e59cf 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -81,8 +81,6 @@ private slots: void ResolutionChanged(); - void VideoCodecChanged(); - void UpdateViewerDimensions(); void StartExport(); diff --git a/app/dialog/export/exportadvancedvideodialog.cpp b/app/dialog/export/exportadvancedvideodialog.cpp index 1b11d85b9..f4fc6b6e6 100644 --- a/app/dialog/export/exportadvancedvideodialog.cpp +++ b/app/dialog/export/exportadvancedvideodialog.cpp @@ -2,42 +2,61 @@ #include #include +#include #include OLIVE_NAMESPACE_ENTER -ExportAdvancedVideoDialog::ExportAdvancedVideoDialog(QWidget *parent) : +ExportAdvancedVideoDialog::ExportAdvancedVideoDialog(const QList &pix_fmts, QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Advanced")); - QGridLayout* layout = new QGridLayout(this); + QVBoxLayout* layout = new QVBoxLayout(this); - int row = 0; + { + // Pixel Settings + QGroupBox* pixel_group = new QGroupBox(); + layout->addWidget(pixel_group); + pixel_group->setTitle(tr("Pixel")); - layout->addWidget(new QLabel(tr("Threads:")), row, 0); + QGridLayout* pixel_layout = new QGridLayout(pixel_group); - thread_slider_ = new IntegerSlider(); - thread_slider_->SetMinimum(0); - thread_slider_->SetDefaultValue(0); - layout->addWidget(thread_slider_, row, 1); + int row = 0; - row++; + pixel_layout->addWidget(new QLabel(tr("Pixel Format:")), row, 0); + + pixel_format_combobox_ = new QComboBox(); + pixel_format_combobox_->addItems(pix_fmts); + pixel_layout->addWidget(pixel_format_combobox_, row, 1); + + row++; + } + + { + // Performance Settings + QGroupBox* performance_group = new QGroupBox(); + layout->addWidget(performance_group); + performance_group->setTitle(tr("Performance")); + + QGridLayout* performance_layout = new QGridLayout(performance_group); + + int row = 0; + + performance_layout->addWidget(new QLabel(tr("Threads:")), row, 0); + + thread_slider_ = new IntegerSlider(); + thread_slider_->SetMinimum(0); + thread_slider_->SetDefaultValue(0); + performance_layout->addWidget(thread_slider_, row, 1); + + row++; + } QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttons, &QDialogButtonBox::accepted, this, &ExportAdvancedVideoDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &ExportAdvancedVideoDialog::reject); - layout->addWidget(buttons, row, 0, 1, 2); -} - -int ExportAdvancedVideoDialog::threads() const -{ - return static_cast(thread_slider_->GetValue()); -} - -void ExportAdvancedVideoDialog::set_threads(int t) -{ - thread_slider_->SetValue(t); + layout->addWidget(buttons); } OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportadvancedvideodialog.h b/app/dialog/export/exportadvancedvideodialog.h index 71d3dc6e0..bdadbbebe 100644 --- a/app/dialog/export/exportadvancedvideodialog.h +++ b/app/dialog/export/exportadvancedvideodialog.h @@ -1,6 +1,7 @@ #ifndef EXPORTADVANCEDVIDEODIALOG_H #define EXPORTADVANCEDVIDEODIALOG_H +#include #include #include "widget/slider/integerslider.h" @@ -11,14 +12,34 @@ class ExportAdvancedVideoDialog : public QDialog { Q_OBJECT public: - ExportAdvancedVideoDialog(QWidget* parent = nullptr); + ExportAdvancedVideoDialog(const QList& pix_fmts, + QWidget* parent = nullptr); - int threads() const; - void set_threads(int t); + int threads() const + { + return static_cast(thread_slider_->GetValue()); + } + + void set_threads(int t) + { + thread_slider_->SetValue(t); + } + + QString pix_fmt() const + { + return pixel_format_combobox_->currentText(); + } + + void set_pix_fmt(const QString& s) + { + pixel_format_combobox_->setCurrentText(s); + } private: IntegerSlider* thread_slider_; + QComboBox* pixel_format_combobox_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportaudiotab.cpp b/app/dialog/export/exportaudiotab.cpp index d83cdf39f..c6f4ae003 100644 --- a/app/dialog/export/exportaudiotab.cpp +++ b/app/dialog/export/exportaudiotab.cpp @@ -46,22 +46,14 @@ ExportAudioTab::ExportAudioTab(QWidget* parent) : layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0); - sample_rate_combobox_ = new QComboBox(); - sample_rates_ = Core::SupportedSampleRates(); - foreach (const int& sr, sample_rates_) { - sample_rate_combobox_->addItem(Core::SampleRateToString(sr), sr); - } + sample_rate_combobox_ = new SampleRateComboBox(); layout->addWidget(sample_rate_combobox_, row, 1); row++; layout->addWidget(new QLabel(tr("Channel Layout:")), row, 0); - channel_layout_combobox_ = new QComboBox(); - channel_layouts_ = Core::SupportedChannelLayouts(); - foreach (const uint64_t& ch_layout, channel_layouts_) { - channel_layout_combobox_->addItem(Core::ChannelLayoutToString(ch_layout), QVariant::fromValue(ch_layout)); - } + channel_layout_combobox_ = new ChannelLayoutComboBox(); layout->addWidget(channel_layout_combobox_, row, 1); row++; @@ -72,29 +64,4 @@ ExportAudioTab::ExportAudioTab(QWidget* parent) : outer_layout->addStretch(); } -QComboBox *ExportAudioTab::codec_combobox() const -{ - return codec_combobox_; -} - -QComboBox *ExportAudioTab::sample_rate_combobox() const -{ - return sample_rate_combobox_; -} - -QComboBox *ExportAudioTab::channel_layout_combobox() const -{ - return channel_layout_combobox_; -} - -void ExportAudioTab::set_sample_rate(int rate) -{ - sample_rate_combobox_->setCurrentIndex(sample_rates_.indexOf(rate)); -} - -void ExportAudioTab::set_channel_layout(uint64_t layout) -{ - channel_layout_combobox_->setCurrentIndex(channel_layouts_.indexOf(layout)); -} - OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportaudiotab.h b/app/dialog/export/exportaudiotab.h index b14a1e3b9..924d0b655 100644 --- a/app/dialog/export/exportaudiotab.h +++ b/app/dialog/export/exportaudiotab.h @@ -25,6 +25,7 @@ #include #include "common/define.h" +#include "widget/standardcombos/standardcombos.h" OLIVE_NAMESPACE_ENTER @@ -33,20 +34,26 @@ class ExportAudioTab : public QWidget public: ExportAudioTab(QWidget* parent = nullptr); - QComboBox* codec_combobox() const; - QComboBox* sample_rate_combobox() const; - QComboBox* channel_layout_combobox() const; + QComboBox* codec_combobox() const + { + return codec_combobox_; + } - void set_sample_rate(int rate); - void set_channel_layout(uint64_t layout); + SampleRateComboBox* sample_rate_combobox() const + { + return sample_rate_combobox_; + } + + ChannelLayoutComboBox* channel_layout_combobox() const + { + return channel_layout_combobox_; + } private: QComboBox* codec_combobox_; - QComboBox* sample_rate_combobox_; - QComboBox* channel_layout_combobox_; + SampleRateComboBox* sample_rate_combobox_; + ChannelLayoutComboBox* channel_layout_combobox_; - QList sample_rates_; - QList channel_layouts_; }; OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index 476dc495a..9d62c360f 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -49,71 +49,6 @@ ExportVideoTab::ExportVideoTab(ColorManager* color_manager, QWidget *parent) : outer_layout->addStretch(); } -QComboBox *ExportVideoTab::codec_combobox() const -{ - return codec_combobox_; -} - -IntegerSlider *ExportVideoTab::width_slider() const -{ - return width_slider_; -} - -IntegerSlider *ExportVideoTab::height_slider() const -{ - return height_slider_; -} - -QCheckBox *ExportVideoTab::maintain_aspect_checkbox() const -{ - return maintain_aspect_checkbox_; -} - -QComboBox *ExportVideoTab::scaling_method_combobox() const -{ - return scaling_method_combobox_; -} - -const rational &ExportVideoTab::frame_rate() const -{ - return frame_rates_.at(frame_rate_combobox_->currentIndex()); -} - -void ExportVideoTab::set_frame_rate(const rational &frame_rate) -{ - frame_rate_combobox_->setCurrentIndex(frame_rates_.indexOf(frame_rate)); -} - -QString ExportVideoTab::CurrentOCIOColorSpace() -{ - return color_space_chooser_->input(); -} - -CodecSection *ExportVideoTab::GetCodecSection() const -{ - return static_cast(codec_stack_->currentWidget()); -} - -void ExportVideoTab::SetCodecSection(CodecSection *section) -{ - codec_stack_->setCurrentWidget(section); -} - -ImageSection *ExportVideoTab::image_section() const -{ - return image_section_; -} - -H264Section *ExportVideoTab::h264_section() const -{ - return h264_section_; -} - -const int &ExportVideoTab::threads() const -{ - return threads_; -} - QWidget* ExportVideoTab::SetupResolutionSection() { int row = 0; @@ -163,14 +98,23 @@ QWidget* ExportVideoTab::SetupResolutionSection() layout->addWidget(new QLabel(tr("Frame Rate:")), row, 0); - frame_rate_combobox_ = new QComboBox(); - frame_rates_ = Core::SupportedFrameRates(); - foreach (const rational& fr, frame_rates_) { - frame_rate_combobox_->addItem(Core::FrameRateToString(fr)); - } - + frame_rate_combobox_ = new FrameRateComboBox(); layout->addWidget(frame_rate_combobox_, row, 1); + row++; + + layout->addWidget(new QLabel(tr("Pixel Aspect Ratio:")), row, 0); + + pixel_aspect_combobox_ = new PixelAspectRatioComboBox(); + layout->addWidget(pixel_aspect_combobox_, row, 1); + + row++; + + layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + + interlaced_combobox_ = new InterlacedComboBox(); + layout->addWidget(interlaced_combobox_, row, 1); + return resolution_group; } @@ -194,6 +138,10 @@ QWidget *ExportVideoTab::SetupCodecSection() codec_combobox_ = new QComboBox(); codec_layout->addWidget(codec_combobox_, row, 1); + connect(codec_combobox_, + static_cast(&QComboBox::currentIndexChanged), + this, + &ExportVideoTab::VideoCodecChanged); row++; @@ -222,13 +170,33 @@ void ExportVideoTab::MaintainAspectRatioChanged(bool val) void ExportVideoTab::OpenAdvancedDialog() { - ExportAdvancedVideoDialog d(this); + // Find export formats compatible with this encoder + QStringList pixel_formats = ExportCodec::GetPixelFormatsForCodec(GetSelectedCodec()); + + ExportAdvancedVideoDialog d(pixel_formats, this); d.set_threads(threads_); + d.set_pix_fmt(pix_fmt_); if (d.exec() == QDialog::Accepted) { threads_ = d.threads(); + pix_fmt_ = d.pix_fmt(); } } +void ExportVideoTab::VideoCodecChanged() +{ + ExportCodec::Codec codec = GetSelectedCodec(); + + if (codec == ExportCodec::kCodecH264) { + SetCodecSection(h264_section()); + } else if (ExportCodec::IsCodecAStillImage(codec)) { + SetCodecSection(image_section()); + } + + // Set default pixel format + pix_fmt_ = ExportCodec::GetPixelFormatsForCodec(codec).first(); + qDebug() << "Set default pix fmt" << pix_fmt_; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index 7ad0c72d6..fe35d884b 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -31,6 +31,7 @@ #include "render/colormanager.h" #include "widget/colorwheel/colorspacechooser.h" #include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" OLIVE_NAMESPACE_ENTER @@ -40,24 +41,87 @@ class ExportVideoTab : public QWidget public: ExportVideoTab(ColorManager* color_manager, QWidget* parent = nullptr); - QComboBox* codec_combobox() const; + ExportCodec::Codec GetSelectedCodec() const + { + return static_cast(codec_combobox()->currentData().toInt()); + } - IntegerSlider* width_slider() const; - IntegerSlider* height_slider() const; - QCheckBox* maintain_aspect_checkbox() const; - QComboBox* scaling_method_combobox() const; + QComboBox* codec_combobox() const + { + return codec_combobox_; + } - const rational& frame_rate() const; - void set_frame_rate(const rational& frame_rate); + IntegerSlider* width_slider() const + { + return width_slider_; + } - QString CurrentOCIOColorSpace(); + IntegerSlider* height_slider() const + { + return height_slider_; + } - CodecSection* GetCodecSection() const; - void SetCodecSection(CodecSection* section); - ImageSection* image_section() const; - H264Section* h264_section() const; + QCheckBox* maintain_aspect_checkbox() const + { + return maintain_aspect_checkbox_; + } - const int& threads() const; + QComboBox* scaling_method_combobox() const + { + return scaling_method_combobox_; + } + + FrameRateComboBox* frame_rate_combobox() const + { + return frame_rate_combobox_; + } + + QString CurrentOCIOColorSpace() + { + return color_space_chooser_->input(); + } + + CodecSection* GetCodecSection() const + { + return static_cast(codec_stack_->currentWidget()); + } + + void SetCodecSection(CodecSection* section) + { + codec_stack_->setCurrentWidget(section); + } + + ImageSection* image_section() const + { + return image_section_; + } + + H264Section* h264_section() const + { + return h264_section_; + } + + InterlacedComboBox* interlaced_combobox() const + { + return interlaced_combobox_; + } + + PixelAspectRatioComboBox* pixel_aspect_combobox() const + { + return pixel_aspect_combobox_; + } + + const int& threads() const + { + return threads_; + } + + const QString& pix_fmt() const { + return pix_fmt_; + } + +public slots: + void VideoCodecChanged(); signals: void ColorSpaceChanged(const QString& colorspace); @@ -68,7 +132,7 @@ private: QWidget* SetupCodecSection(); QComboBox* codec_combobox_; - QComboBox* frame_rate_combobox_; + FrameRateComboBox* frame_rate_combobox_; QCheckBox* maintain_aspect_checkbox_; QComboBox* scaling_method_combobox_; @@ -81,12 +145,15 @@ private: IntegerSlider* width_slider_; IntegerSlider* height_slider_; - QList frame_rates_; - ColorManager* color_manager_; + InterlacedComboBox* interlaced_combobox_; + PixelAspectRatioComboBox* pixel_aspect_combobox_; + int threads_; + QString pix_fmt_; + private slots: void MaintainAspectRatioChanged(bool val); diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 6f565aa33..f42da0d1a 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -22,11 +22,13 @@ #include #include +#include #include #include #include namespace OCIO = OCIO_NAMESPACE::v1; +#include "core.h" #include "project/item/footage/footage.h" #include "project/project.h" #include "undo/undostack.h" @@ -41,6 +43,23 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : int row = 0; + video_layout->addWidget(new QLabel(tr("Pixel Aspect:")), row, 0); + + pixel_aspect_combo_ = new PixelAspectRatioComboBox(); + pixel_aspect_combo_->SetPixelAspectRatio(stream->pixel_aspect_ratio()); + video_layout->addWidget(pixel_aspect_combo_, row, 1); + + row++; + + video_layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + + video_interlace_combo_ = new InterlacedComboBox(); + video_interlace_combo_->SetInterlaceMode(stream->interlacing()); + + video_layout->addWidget(video_interlace_combo_, row, 1); + + row++; + video_layout->addWidget(new QLabel(tr("Color Space:")), row, 0); video_color_space_ = new QComboBox(); @@ -91,6 +110,14 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : imgseq_end_time_->SetValue(video_stream->start_time() + video_stream->duration() - 1); imgseq_layout->addWidget(imgseq_end_time_, imgseq_row, 1); + imgseq_row++; + + imgseq_layout->addWidget(new QLabel(tr("Frame Rate:")), imgseq_row, 0); + + imgseq_frame_rate_ = new FrameRateComboBox(); + imgseq_frame_rate_->SetFrameRate(video_stream->frame_rate()); + imgseq_layout->addWidget(imgseq_frame_rate_, imgseq_row, 1); + video_layout->addWidget(imgseq_group, row, 0, 1, 2); } } @@ -104,11 +131,15 @@ void VideoStreamProperties::Accept(QUndoCommand *parent) } if (video_premultiply_alpha_->isChecked() != stream_->premultiplied_alpha() - || set_colorspace != stream_->colorspace(false)) { + || set_colorspace != stream_->colorspace(false) + || static_cast(video_interlace_combo_->currentIndex()) != stream_->interlacing() + || pixel_aspect_combo_->GetPixelAspectRatio() != stream_->pixel_aspect_ratio()) { new VideoStreamChangeCommand(stream_, video_premultiply_alpha_->isChecked(), set_colorspace, + static_cast(video_interlace_combo_->currentIndex()), + pixel_aspect_combo_->GetPixelAspectRatio(), parent); } @@ -118,10 +149,12 @@ void VideoStreamProperties::Accept(QUndoCommand *parent) int64_t new_dur = imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue() + 1; if (video_stream->start_time() != imgseq_start_time_->GetValue() - || video_stream->duration() != new_dur) { + || video_stream->duration() != new_dur + || video_stream->frame_rate() != imgseq_frame_rate_->GetFrameRate()) { new ImageSequenceChangeCommand(video_stream, imgseq_start_time_->GetValue(), new_dur, + imgseq_frame_rate_->GetFrameRate(), parent); } } @@ -150,11 +183,15 @@ bool VideoStreamProperties::IsImageSequence(ImageStream *stream) VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageStreamPtr stream, bool premultiplied, QString colorspace, + VideoParams::Interlacing interlacing, + const rational &pixel_ar, QUndoCommand *parent) : UndoCommand(parent), stream_(stream), new_premultiplied_(premultiplied), - new_colorspace_(colorspace) + new_colorspace_(colorspace), + new_interlacing_(interlacing), + new_pixel_ar_(pixel_ar) { } @@ -167,22 +204,29 @@ void VideoStreamProperties::VideoStreamChangeCommand::redo_internal() { old_premultiplied_ = stream_->premultiplied_alpha(); old_colorspace_ = stream_->colorspace(false); + old_interlacing_ = stream_->interlacing(); + old_pixel_ar_ = stream_->pixel_aspect_ratio(); stream_->set_premultiplied_alpha(new_premultiplied_); stream_->set_colorspace(new_colorspace_); + stream_->set_interlacing(new_interlacing_); + stream_->set_pixel_aspect_ratio(new_pixel_ar_); } void VideoStreamProperties::VideoStreamChangeCommand::undo_internal() { stream_->set_premultiplied_alpha(old_premultiplied_); stream_->set_colorspace(old_colorspace_); + stream_->set_interlacing(old_interlacing_); + stream_->set_pixel_aspect_ratio(old_pixel_ar_); } -VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand(VideoStreamPtr video_stream, int64_t start_index, int64_t duration, QUndoCommand *parent) : +VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand(VideoStreamPtr video_stream, int64_t start_index, int64_t duration, const rational &frame_rate, QUndoCommand *parent) : UndoCommand(parent), video_stream_(video_stream), new_start_index_(start_index), - new_duration_(duration) + new_duration_(duration), + new_frame_rate_(frame_rate) { } @@ -198,12 +242,18 @@ void VideoStreamProperties::ImageSequenceChangeCommand::redo_internal() old_duration_ = video_stream_->duration(); video_stream_->set_duration(new_duration_); + + old_frame_rate_ = video_stream_->frame_rate(); + video_stream_->set_frame_rate(new_frame_rate_); + video_stream_->set_timebase(new_frame_rate_.flipped()); } void VideoStreamProperties::ImageSequenceChangeCommand::undo_internal() { video_stream_->set_start_time(old_start_index_); video_stream_->set_duration(old_duration_); + video_stream_->set_frame_rate(old_frame_rate_); + video_stream_->set_timebase(old_frame_rate_.flipped()); } OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index 6bd164ab8..e26a2e5bb 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -28,6 +28,7 @@ #include "streamproperties.h" #include "undo/undocommand.h" #include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" OLIVE_NAMESPACE_ENTER @@ -58,6 +59,11 @@ private: */ QComboBox* video_color_space_; + /** + * @brief Setting for video interlacing + */ + InterlacedComboBox* video_interlace_combo_; + /** * @brief Sets the start index for image sequences */ @@ -68,11 +74,23 @@ private: */ IntegerSlider* imgseq_end_time_; + /** + * @brief Sets the frame rate for image sequences + */ + FrameRateComboBox* imgseq_frame_rate_; + + /** + * @brief Sets the pixel aspect ratio of the stream + */ + PixelAspectRatioComboBox* pixel_aspect_combo_; + class VideoStreamChangeCommand : public UndoCommand { public: VideoStreamChangeCommand(ImageStreamPtr stream, bool premultiplied, QString colorspace, + VideoParams::Interlacing interlacing, + const rational& pixel_ar, QUndoCommand* parent = nullptr); virtual Project* GetRelevantProject() const override; @@ -86,9 +104,13 @@ private: bool new_premultiplied_; QString new_colorspace_; + VideoParams::Interlacing new_interlacing_; + rational new_pixel_ar_; bool old_premultiplied_; QString old_colorspace_; + VideoParams::Interlacing old_interlacing_; + rational old_pixel_ar_; }; @@ -97,6 +119,7 @@ private: ImageSequenceChangeCommand(VideoStreamPtr video_stream, int64_t start_index, int64_t duration, + const rational& frame_rate, QUndoCommand* parent = nullptr); virtual Project* GetRelevantProject() const override; @@ -114,7 +137,11 @@ private: int64_t new_duration_; int64_t old_duration_; + rational new_frame_rate_; + rational old_frame_rate_; + }; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.cpp b/app/dialog/preferences/tabs/preferencesappearancetab.cpp index 4078c1f0f..737821e48 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.cpp +++ b/app/dialog/preferences/tabs/preferencesappearancetab.cpp @@ -44,19 +44,21 @@ PreferencesAppearanceTab::PreferencesAppearanceTab() // Appearance -> Theme appearance_layout->addWidget(new QLabel(tr("Theme")), row, 0); - style_ = new QComboBox(); + style_combobox_ = new QComboBox(); - style_list_ = StyleManager::ListInternal(); + { + const QMap& themes = StyleManager::available_themes(); + QMap::const_iterator i; + for (i=themes.cbegin(); i!=themes.cend(); i++) { + style_combobox_->addItem(i.value(), i.key()); - foreach (const StyleDescriptor& s, style_list_) { - style_->addItem(s.name(), s.path()); - - if (s.path() == StyleManager::GetStyle()) { - style_->setCurrentIndex(style_->count()-1); + if (StyleManager::GetStyle() == i.key()) { + style_combobox_->setCurrentIndex(style_combobox_->count()-1); + } } } - appearance_layout->addWidget(style_, row, 1); + appearance_layout->addWidget(style_combobox_, row, 1); row++; @@ -89,7 +91,7 @@ PreferencesAppearanceTab::PreferencesAppearanceTab() void PreferencesAppearanceTab::Accept() { - QString style_path = style_->currentData().toString(); + QString style_path = style_combobox_->currentData().toString(); if (style_path != StyleManager::GetStyle()) { StyleManager::SetStyle(style_path); diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.h b/app/dialog/preferences/tabs/preferencesappearancetab.h index 1ab267eb5..35dc1ffac 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.h +++ b/app/dialog/preferences/tabs/preferencesappearancetab.h @@ -49,14 +49,7 @@ private: /** * @brief UI widget for selecting the current UI style */ - QComboBox* style_; - - /** - * @brief List of internal styles - */ - QList style_list_; - - QString custom_style_path_; + QComboBox* style_combobox_; QList colors_; diff --git a/app/dialog/preferences/tabs/preferencesdisktab.cpp b/app/dialog/preferences/tabs/preferencesdisktab.cpp index a961c5adf..fef948409 100644 --- a/app/dialog/preferences/tabs/preferencesdisktab.cpp +++ b/app/dialog/preferences/tabs/preferencesdisktab.cpp @@ -27,12 +27,15 @@ #include #include -#include "render/diskmanager.h" +#include "common/filefunctions.h" OLIVE_NAMESPACE_ENTER PreferencesDiskTab::PreferencesDiskTab() { + // Get default disk cache folder + default_disk_cache_folder_ = DiskManager::instance()->GetDefaultCacheFolder(); + QVBoxLayout* outer_layout = new QVBoxLayout(this); QGroupBox* disk_management_group = new QGroupBox(tr("Disk Management")); @@ -44,37 +47,19 @@ PreferencesDiskTab::PreferencesDiskTab() disk_management_layout->addWidget(new QLabel(tr("Disk Cache Location:")), row, 0); - disk_cache_location_ = new QLineEdit(); - disk_cache_location_->setText(Config::Current()["DiskCachePath"].toString()); - connect(disk_cache_location_, &QLineEdit::textChanged, this, &PreferencesDiskTab::DiskCacheLineEditChanged); + disk_cache_location_ = new PathWidget(default_disk_cache_folder_->GetPath()); disk_management_layout->addWidget(disk_cache_location_, row, 1); - QPushButton* browse_btn = new QPushButton(tr("Browse")); - connect(browse_btn, &QPushButton::clicked, this, &PreferencesDiskTab::BrowseDiskCachePath); - disk_management_layout->addWidget(browse_btn, row, 2); - row++; - disk_management_layout->addWidget(new QLabel(tr("Maximum Disk Cache:")), row, 0); - - maximum_cache_slider_ = new FloatSlider(); - maximum_cache_slider_->SetFormat(tr("%1 GB")); - maximum_cache_slider_->SetMinimum(1.0); - maximum_cache_slider_->SetValue(Config::Current()["DiskCacheSize"].toDouble()); - disk_management_layout->addWidget(maximum_cache_slider_, row, 1, 1, 2); + QPushButton* disk_cache_settings_btn = new QPushButton(tr("Disk Cache Settings")); + connect(disk_cache_settings_btn, &QPushButton::clicked, this, [this](){ + DiskManager::instance()->ShowDiskCacheSettingsDialog(disk_cache_location_->text(), this); + }); + disk_management_layout->addWidget(disk_cache_settings_btn, row, 1); row++; - clear_cache_btn_ = new QPushButton(tr("Clear Disk Cache")); - connect(clear_cache_btn_, &QPushButton::clicked, this, &PreferencesDiskTab::ClearDiskCache); - disk_management_layout->addWidget(clear_cache_btn_, row, 1, 1, 2); - - row++; - - clear_disk_cache_ = new QCheckBox(tr("Automatically clear disk cache on close")); - clear_disk_cache_->setChecked(Config::Current()["ClearDiskCacheOnClose"].toBool()); - disk_management_layout->addWidget(clear_disk_cache_, row, 1, 1, 2); - QGroupBox* cache_behavior = new QGroupBox(tr("Cache Behavior")); outer_layout->addWidget(cache_behavior); QGridLayout* cache_behavior_layout = new QGridLayout(cache_behavior); @@ -100,53 +85,36 @@ PreferencesDiskTab::PreferencesDiskTab() outer_layout->addStretch(); } +bool PreferencesDiskTab::Validate() +{ + if (disk_cache_location_->text() != default_disk_cache_folder_->GetPath()) { + // Disk cache location is changing + + // Check if the user is okay with invalidating the current cache + if (!DiskManager::ShowDiskCacheChangeConfirmationDialog(this)) { + return false; + } + + // Check validity of the new path + if (!FileFunctions::DirectoryIsValid(disk_cache_location_->text(), true)) { + QMessageBox::critical(this, + tr("Disk Cache"), + tr("Failed to set disk cache location. Access was denied.")); + return false; + } + } + + return true; +} + void PreferencesDiskTab::Accept() { - Config::Current()["DiskCachePath"] = disk_cache_location_->text(); - Config::Current()["DiskCacheSize"] = maximum_cache_slider_->GetValue(); - Config::Current()["ClearDiskCacheOnClose"] = clear_disk_cache_->isChecked(); + if (disk_cache_location_->text() != default_disk_cache_folder_->GetPath()) { + default_disk_cache_folder_->SetPath(disk_cache_location_->text()); + } + Config::Current()["DiskCacheBehind"] = QVariant::fromValue(rational::fromDouble(cache_behind_slider_->GetValue())); Config::Current()["DiskCacheAhead"] = QVariant::fromValue(rational::fromDouble(cache_ahead_slider_->GetValue())); } -void PreferencesDiskTab::DiskCacheLineEditChanged() -{ - QString entered_dir = disk_cache_location_->text(); - - if (!entered_dir.isEmpty() && !QDir(entered_dir).exists()) { - disk_cache_location_->setStyleSheet(QStringLiteral("color: red;")); - } else { - disk_cache_location_->setStyleSheet(QString()); - } -} - -void PreferencesDiskTab::BrowseDiskCachePath() -{ - QString dir = QFileDialog::getExistingDirectory(this, tr("Browse for disk cache path"), disk_cache_location_->text()); - - if (!dir.isEmpty()) { - disk_cache_location_->setText(dir); - } -} - -void PreferencesDiskTab::ClearDiskCache() -{ - if (QMessageBox::question(this, - tr("Clear Disk Cache"), - tr("Are you sure you want to clear the disk cache in '%1'?").arg(Config::Current()["DiskCachePath"].toString()), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - clear_cache_btn_->setEnabled(false); - - if (DiskManager::instance()->ClearDiskCache(false)) { - clear_cache_btn_->setText(tr("Disk Cache Cleared")); - } else { - QMessageBox::information(this, - tr("Clear Disk Cache"), - tr("Disk cache failed to fully clear. You may have to delete the cache files manually."), - QMessageBox::Ok); - clear_cache_btn_->setText(tr("Disk Cache Partially Cleared")); - } - } -} - OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/preferences/tabs/preferencesdisktab.h b/app/dialog/preferences/tabs/preferencesdisktab.h index 052c834ad..5eb7438b7 100644 --- a/app/dialog/preferences/tabs/preferencesdisktab.h +++ b/app/dialog/preferences/tabs/preferencesdisktab.h @@ -26,7 +26,9 @@ #include #include "preferencestab.h" +#include "render/diskmanager.h" #include "widget/slider/floatslider.h" +#include "widget/path/pathwidget.h" OLIVE_NAMESPACE_ENTER @@ -36,27 +38,18 @@ class PreferencesDiskTab : public PreferencesTab public: PreferencesDiskTab(); + virtual bool Validate() override; + virtual void Accept() override; private: - QLineEdit* disk_cache_location_; - - FloatSlider* maximum_cache_slider_; + PathWidget* disk_cache_location_; FloatSlider* cache_ahead_slider_; FloatSlider* cache_behind_slider_; - QCheckBox* clear_disk_cache_; - - QPushButton* clear_cache_btn_; - -private slots: - void DiskCacheLineEditChanged(); - - void BrowseDiskCachePath(); - - void ClearDiskCache(); + DiskCacheFolder* default_disk_cache_folder_; }; diff --git a/app/dialog/projectproperties/projectproperties.cpp b/app/dialog/projectproperties/projectproperties.cpp index 423ae414f..5a8713f5c 100644 --- a/app/dialog/projectproperties/projectproperties.cpp +++ b/app/dialog/projectproperties/projectproperties.cpp @@ -20,6 +20,7 @@ #include "projectproperties.h" +#include #include #include #include @@ -29,9 +30,11 @@ #include namespace OCIO = OCIO_NAMESPACE::v1; +#include "common/filefunctions.h" #include "config/config.h" #include "core.h" #include "render/colormanager.h" +#include "render/diskmanager.h" OLIVE_NAMESPACE_ENTER @@ -51,7 +54,10 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project* p, QWidget *parent) : // Color management group QWidget* color_group = new QWidget(); - QGridLayout* color_layout = new QGridLayout(color_group); + QVBoxLayout* color_outer_layout = new QVBoxLayout(color_group); + + QGridLayout* color_layout = new QGridLayout(); + color_outer_layout->addLayout(color_layout); int row = 0; @@ -80,26 +86,58 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project* p, QWidget *parent) : OCIOFilenameUpdated(); tabs->addTab(color_group, tr("Color Management")); + + color_outer_layout->addStretch(); } - - { - // Paths group - QWidget* paths_group = new QWidget(); + // Cache group + QWidget* cache_group = new QWidget(); - QGridLayout* paths_layout = new QGridLayout(paths_group); + QVBoxLayout* cache_layout = new QVBoxLayout(cache_group); - cache_path_ = new PathWidget(working_project_->cache_path(), this); + QButtonGroup* disk_cache_btn_group = new QButtonGroup(); - int row = 0; + disk_cache_use_default_btn_ = new QRadioButton(tr("Use Default Location")); + disk_cache_store_alongside_project_btn_ = new QRadioButton(tr("Store Alongside Project")); + disk_cache_use_custom_btn_ = new QRadioButton(tr("Use Custom Location:")); - paths_layout->addWidget(new QLabel(tr("Cache Path:")), row, 0); - paths_layout->addWidget(cache_path_->path_edit(), row, 1); - paths_layout->addWidget(cache_path_->browse_btn(), row, 2); - paths_layout->addWidget(cache_path_->default_box(), row, 3); + disk_cache_btn_group->addButton(disk_cache_use_default_btn_); + disk_cache_btn_group->addButton(disk_cache_store_alongside_project_btn_); + disk_cache_btn_group->addButton(disk_cache_use_custom_btn_); - tabs->addTab(paths_group, tr("Paths")); + cache_layout->addWidget(disk_cache_use_default_btn_); + cache_layout->addWidget(disk_cache_store_alongside_project_btn_); + cache_layout->addWidget(disk_cache_use_custom_btn_); + + cache_path_ = new PathWidget(working_project_->cache_path(false), this); + cache_path_->setEnabled(false); + cache_layout->addWidget(cache_path_); + + connect(disk_cache_use_custom_btn_, &QRadioButton::toggled, cache_path_, &PathWidget::setEnabled); + + if (working_project_->cache_path(false).isEmpty()) { + disk_cache_use_default_btn_->setChecked(true); + } else { + disk_cache_use_custom_btn_->setChecked(true); + } + + cache_layout->addWidget(cache_path_); + + QPushButton* disk_cache_settings_btn = new QPushButton(tr("Disk Cache Settings")); + connect(disk_cache_settings_btn, &QPushButton::clicked, this, [this](){ + if (disk_cache_use_default_btn_->isChecked()) { + DiskManager::instance()->ShowDiskCacheSettingsDialog(DiskManager::instance()->GetDefaultCacheFolder(), this); + } else if (disk_cache_store_alongside_project_btn_->isChecked()) { + // FIXME: + QMessageBox::information(this, QString(), tr("\"Store alignside project\" functionality not implemented yet")); + } else { + DiskManager::instance()->ShowDiskCacheSettingsDialog(cache_path_->text(), this); + } + }); + cache_layout->addWidget(disk_cache_settings_btn); + + tabs->addTab(cache_group, tr("Disk Cache")); } QDialogButtonBox* dialog_btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, @@ -122,18 +160,40 @@ void ProjectPropertiesDialog::accept() return; } - if (!cache_path_->PathIsValid(true)) { - QMessageBox mb(this); - mb.setWindowModality(Qt::WindowModal); - mb.setIcon(QMessageBox::Critical); - mb.setWindowTitle(tr("Invalid path")); - mb.setText(tr("The cache path is invalid. Please check it and try again.")); - mb.addButton(QMessageBox::Ok); - mb.exec(); + QString new_cache_path; + + if (disk_cache_use_default_btn_->isChecked()) { + // Keep new cache path empty, which means default + } else if (disk_cache_store_alongside_project_btn_->isChecked()) { + // FIXME: + QMessageBox::information(this, QString(), tr("\"Store alignside project\" functionality not implemented yet")); return; + } else { + if (!FileFunctions::DirectoryIsValid(cache_path_->text(), true)) { + QMessageBox mb(this); + mb.setWindowModality(Qt::WindowModal); + mb.setIcon(QMessageBox::Critical); + mb.setWindowTitle(tr("Invalid path")); + mb.setText(tr("The cache path is invalid. Please check it and try again.")); + mb.addButton(QMessageBox::Ok); + mb.exec(); + return; + } + + // Set new path to the text as entered + new_cache_path = cache_path_->text(); } - working_project_->set_cache_path(cache_path_->path_edit()->text()); + if (new_cache_path != working_project_->cache_path(false)) { + // Check if the user is okay with invalidating the current cache + if (!DiskManager::ShowDiskCacheChangeConfirmationDialog(this)) { + return; + } + + working_project_->set_cache_path(new_cache_path); + + emit DiskManager::instance()->InvalidateProject(working_project_); + } // This should ripple changes throughout the program that the color config has changed, therefore must be done last working_project_->color_manager()->SetConfigAndDefaultInput(ocio_filename_->text(), @@ -176,7 +236,6 @@ void ProjectPropertiesDialog::OCIOFilenameUpdated() default_input_colorspace_->setCurrentIndex(default_input_colorspace_->count()-1); } } - } catch (OCIO::Exception& e) { ocio_config_is_valid_ = false; ocio_filename_->setStyleSheet(QStringLiteral("QLineEdit {color: red;}")); @@ -184,54 +243,4 @@ void ProjectPropertiesDialog::OCIOFilenameUpdated() } } -PathWidget::PathWidget(const QString &path, QWidget *parent) : - QObject(parent) -{ - path_edit_ = new QLineEdit(); - path_edit_->setText(path); - connect(path_edit_, &QLineEdit::textChanged, this, &PathWidget::LineEditChanged); - - default_box_ = new QCheckBox(tr("Default")); - - browse_btn_ = new QPushButton(tr("Browse")); - - connect(default_box_, &QCheckBox::toggled, this, &PathWidget::DefaultToggled); - - default_box_->setChecked(path.isEmpty()); - - connect(browse_btn_, &QPushButton::clicked, this, &PathWidget::BrowseClicked); -} - -bool PathWidget::PathIsValid(bool try_to_create) const -{ - return default_box_->isChecked() - || QDir(path_edit_->text()).exists() - || (try_to_create && QDir(path_edit_->text()).mkpath(QStringLiteral("."))); -} - -void PathWidget::DefaultToggled(bool e) -{ - path_edit_->setEnabled(!e); -} - -void PathWidget::BrowseClicked() -{ - QString dir = QFileDialog::getExistingDirectory(static_cast(parent()), - tr("Browse for path"), - path_edit_->text()); - - if (!dir.isEmpty()) { - path_edit_->setText(dir); - } -} - -void PathWidget::LineEditChanged() -{ - if (PathIsValid(false)) { - path_edit_->setStyleSheet(QString()); - } else { - path_edit_->setStyleSheet(QStringLiteral("QLineEdit {color: red;}")); - } -} - OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/projectproperties/projectproperties.h b/app/dialog/projectproperties/projectproperties.h index 0f7abdfab..5c0f31347 100644 --- a/app/dialog/projectproperties/projectproperties.h +++ b/app/dialog/projectproperties/projectproperties.h @@ -26,48 +26,13 @@ #include #include #include +#include #include "project/project.h" +#include "widget/path/pathwidget.h" OLIVE_NAMESPACE_ENTER -class PathWidget : public QObject -{ - Q_OBJECT -public: - PathWidget(const QString& path, - QWidget* parent = nullptr); - - bool PathIsValid(bool try_to_create) const; - - QLineEdit* path_edit() const { - return path_edit_; - } - - QCheckBox* default_box() const { - return default_box_; - } - - QPushButton* browse_btn() const { - return browse_btn_; - } - -private slots: - void DefaultToggled(bool e); - - void BrowseClicked(); - - void LineEditChanged(); - -private: - QLineEdit* path_edit_; - - QCheckBox* default_box_; - - QPushButton* browse_btn_; - -}; - class ProjectPropertiesDialog : public QDialog { Q_OBJECT @@ -89,6 +54,12 @@ private: QString ocio_config_error_; PathWidget* cache_path_; + + QRadioButton* disk_cache_use_default_btn_; + + QRadioButton* disk_cache_store_alongside_project_btn_; + + QRadioButton* disk_cache_use_custom_btn_; private slots: void BrowseForOCIOConfig(); diff --git a/app/dialog/richtext/richtext.cpp b/app/dialog/richtext/richtext.cpp index 5b4303530..40f2baabd 100644 --- a/app/dialog/richtext/richtext.cpp +++ b/app/dialog/richtext/richtext.cpp @@ -20,6 +20,7 @@ #include "richtext.h" +#include #include #include #include @@ -29,7 +30,7 @@ OLIVE_NAMESPACE_ENTER -RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : +RichTextDialog::RichTextDialog(QString start, QWidget* parent) : QDialog(parent) { QVBoxLayout* layout = new QVBoxLayout(this); @@ -37,33 +38,32 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : // Create toolbar QHBoxLayout* toolbar_layout = new QHBoxLayout(); - bold_btn_ = CreateToolbarButton(tr("B"), tr("Bold")); + bold_btn_ = CreateToolbarButton(tr("B"), tr("Bold"), {QStringLiteral("b"), QStringLiteral("strong")}); toolbar_layout->addWidget(bold_btn_); - italic_btn_ = CreateToolbarButton(tr("I"), tr("Italic")); + italic_btn_ = CreateToolbarButton(tr("I"), tr("Italic"), {QStringLiteral("i"), QStringLiteral("em")}); toolbar_layout->addWidget(italic_btn_); - underline_btn_ = CreateToolbarButton(tr("U"), tr("Underline")); + underline_btn_ = CreateToolbarButton(tr("U"), tr("Underline"), {QStringLiteral("u")}); toolbar_layout->addWidget(underline_btn_); - strikeout_btn_ = CreateToolbarButton(tr("S"), tr("Strikethrough")); + strikeout_btn_ = CreateToolbarButton(tr("S"), tr("Strikethrough"), {QStringLiteral("strike")}); toolbar_layout->addWidget(strikeout_btn_); font_combo_ = new QFontComboBox(); font_combo_->setToolTip(tr("Font Family")); toolbar_layout->addWidget(font_combo_); size_slider_ = new FloatSlider(); size_slider_->SetMinimum(0.1); - size_slider_->SetLadderEnabled(true); size_slider_->SetLadderElementCount(1); size_slider_->setToolTip(tr("Font Size")); toolbar_layout->addWidget(size_slider_); toolbar_layout->addStretch(); - left_align_btn_ = CreateToolbarButton(tr("L"), tr("Left Align")); + left_align_btn_ = CreateToolbarButton(tr("L"), tr("Left Align"), {}); toolbar_layout->addWidget(left_align_btn_); - center_align_btn_ = CreateToolbarButton(tr("C"), tr("Center Align")); + center_align_btn_ = CreateToolbarButton(tr("C"), tr("Center Align"), {}); toolbar_layout->addWidget(center_align_btn_); - right_align_btn_ = CreateToolbarButton(tr("R"), tr("Right Align")); + right_align_btn_ = CreateToolbarButton(tr("R"), tr("Right Align"), {}); toolbar_layout->addWidget(right_align_btn_); - justify_align_btn_ = CreateToolbarButton(tr("J"), tr("Justify Align")); + justify_align_btn_ = CreateToolbarButton(tr("J"), tr("Justify Align"), {}); toolbar_layout->addWidget(justify_align_btn_); layout->addLayout(toolbar_layout); @@ -72,7 +72,8 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : text_edit_ = new QTextEdit(); text_edit_->setWordWrapMode(QTextOption::NoWrap); connect(text_edit_, &QTextEdit::cursorPositionChanged, this, &RichTextDialog::UpdateButtons); - text_edit_->document()->setHtml(start); + start.replace(QStringLiteral("
"), QStringLiteral("\n")); + text_edit_->document()->setPlainText(start); layout->addWidget(text_edit_); // Create buttons @@ -82,16 +83,7 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : connect(buttons, &QDialogButtonBox::rejected, this, &RichTextDialog::reject); // Connect font buttons - connect(bold_btn_, &QPushButton::clicked, this, [this](bool e){ - text_edit_->setFontWeight(e ? QFont::Bold : QFont::Normal); - }); - connect(italic_btn_, &QPushButton::clicked, text_edit_, &QTextEdit::setFontItalic); - connect(underline_btn_, &QPushButton::clicked, text_edit_, &QTextEdit::setFontUnderline); - connect(strikeout_btn_, &QPushButton::clicked, this, [this](bool e){ - QFont current_font = text_edit_->currentFont(); - current_font.setStrikeOut(e); - text_edit_->setCurrentFont(current_font); - }); + /* connect(size_slider_, &FloatSlider::ValueChanged, text_edit_, &QTextEdit::setFontPointSize); connect(left_align_btn_, &QPushButton::clicked, this, [this](){ text_edit_->setAlignment(Qt::AlignLeft); @@ -114,23 +106,221 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : connect(font_combo_, &QFontComboBox::currentTextChanged, this, [this](const QString& s){ text_edit_->setFontFamily(s); }); + */ } -QPushButton *RichTextDialog::CreateToolbarButton(const QString& label, const QString& tooltip) +QPushButton *RichTextDialog::CreateToolbarButton(const QString& label, const QString& tooltip, const QStringList &tags) { QPushButton* btn = new QPushButton(label); btn->setCheckable(true); btn->setToolTip(tooltip); btn->setFixedWidth(btn->sizeHint().height()); + + if (!tags.isEmpty()) { + btn->setProperty("tag", tags); + connect(btn, &QPushButton::clicked, this, &RichTextDialog::TagButtonToggled); + } + return btn; } +int SnapPositionOutsideTags(const QString& text, int pos) +{ + // Look for closest opening bracket before position + int opening_bracket_pos = text.lastIndexOf('<', pos - text.size() -1); + + // Look for closest closing bracket before position + int closing_bracket_pos = text.indexOf('>', opening_bracket_pos); + + if (opening_bracket_pos > -1 && closing_bracket_pos >= pos) { + // Must be inside an angle bracket, snap to closest position outside of bracket + closing_bracket_pos++; + + if (pos - opening_bracket_pos < closing_bracket_pos - pos) { + // Closer to opening bracket pos + return opening_bracket_pos; + } else { + return closing_bracket_pos; + } + } + + return pos; +} + +void RichTextDialog::SetTags(const QStringList &t, bool enabled) +{ + QString s = text_edit_->toPlainText(); + + int selection_start, selection_end; + + { + QTextCursor c = text_edit_->textCursor(); + + if (c.hasSelection()) { + selection_start = SnapPositionOutsideTags(s, c.selectionStart()); + selection_end = SnapPositionOutsideTags(s, c.selectionEnd()); + + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + } else { + selection_start = SnapPositionOutsideTags(s, c.position()); + selection_end = selection_start; + c.setPosition(selection_start, QTextCursor::MoveAnchor); + } + + text_edit_->setTextCursor(c); + } + + QString open_tag = CreateOpeningTag(t.first()); + QString close_tag = CreateClosingTag(t.first()); + + // Insert tags + QString new_text; + + if (!enabled) { + std::swap(open_tag, close_tag); + } + + bool open_tag_cancels_out = !QString::compare(s.mid(selection_start - close_tag.size(), close_tag.size()), close_tag, Qt::CaseInsensitive); + bool close_tag_cancels_out = !QString::compare(s.mid(selection_end, open_tag.size()), open_tag, Qt::CaseInsensitive); + + QString selected_text = text_edit_->textCursor().selectedText(); + + if (open_tag_cancels_out && close_tag_cancels_out) { + + // Both tags cancel each other out, simply remove + selection_start -= close_tag.size(); + + QTextCursor c = text_edit_->textCursor(); + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end + open_tag.size(), QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + + selection_end -= close_tag.size(); + + new_text = selected_text; + + } else if (open_tag_cancels_out) { + + // Open tag cancels out, shift close tag rather than inserting new tags + selection_start -= close_tag.size(); + + QTextCursor c = text_edit_->textCursor(); + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + + selection_end -= close_tag.size(); + + new_text = selected_text; + new_text.append(close_tag); + + } else if (close_tag_cancels_out) { + + // Close tag cancels out, shift open tag rather than inserting new tags + selection_end += open_tag.size(); + + QTextCursor c = text_edit_->textCursor(); + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + + selection_start += open_tag.size(); + + new_text = open_tag; + new_text.append(selected_text); + + } else { + // Nothing is cancelled out, simply insert tags + new_text = QStringLiteral("%1%2%3").arg(open_tag, + selected_text, + close_tag); + + selection_start += open_tag.size(); + selection_end += open_tag.size(); + } + + text_edit_->insertPlainText(new_text); + + text_edit_->setFocus(); + + { + // Re-select text + QTextCursor c = text_edit_->textCursor(); + + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + } +} + +QString RichTextDialog::CreateOpeningTag(const QString &s) +{ + return QStringLiteral("<%1>").arg(s); +} + +QString RichTextDialog::CreateClosingTag(const QString &s) +{ + return QStringLiteral("").arg(s); +} + +void RichTextDialog::UpdateTagButton(QPushButton *btn, + const QString &text, + int cursor_pos) +{ + QStringList tags = btn->property("tag").toStringList(); + foreach (const QString& t, tags) { + QString opening = CreateOpeningTag(t); + QString closing = CreateClosingTag(t); + + int opening_index = text.lastIndexOf(opening, + cursor_pos - text.size() - 1, + Qt::CaseInsensitive); + int closing_index = text.indexOf(closing, + opening_index, + Qt::CaseInsensitive); + + if (opening_index > -1 && closing_index + closing.size() > cursor_pos) { + btn->setChecked(true); + btn->setProperty("foundtag", t); + return; + } + } + + btn->setChecked(false); + btn->setProperty("foundtag", QVariant()); +} + +void RichTextDialog::TagButtonToggled(bool checked) +{ + QPushButton* src = static_cast(sender()); + QStringList tags; + + if (src->property("foundtag").isNull()) { + tags = src->property("tag").toStringList(); + } else { + tags = QStringList({src->property("foundtag").toString()}); + } + + SetTags(tags, checked); +} + void RichTextDialog::UpdateButtons() { - bold_btn_->setChecked(text_edit_->fontWeight() > QFont::Normal); - italic_btn_->setChecked(text_edit_->fontItalic()); - underline_btn_->setChecked(text_edit_->fontUnderline()); - strikeout_btn_->setChecked(text_edit_->currentFont().strikeOut()); + QString text = text_edit_->toPlainText(); + int cursor_pos = text_edit_->textCursor().position(); + + UpdateTagButton(bold_btn_, text, cursor_pos); + UpdateTagButton(italic_btn_, text, cursor_pos); + UpdateTagButton(underline_btn_, text, cursor_pos); + UpdateTagButton(strikeout_btn_, text, cursor_pos); + + /* // Update font family font_combo_->blockSignals(true); @@ -143,6 +333,7 @@ void RichTextDialog::UpdateButtons() center_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignCenter); right_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignRight); justify_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignJustify); + */ } OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/richtext/richtext.h b/app/dialog/richtext/richtext.h index 7e2bbc252..b3514df4b 100644 --- a/app/dialog/richtext/richtext.h +++ b/app/dialog/richtext/richtext.h @@ -34,15 +34,31 @@ class RichTextDialog : public QDialog { Q_OBJECT public: - RichTextDialog(const QString& start, QWidget* parent = nullptr); + RichTextDialog(QString start, QWidget* parent = nullptr); QString text() const { - return text_edit_->document()->toHtml("utf-8"); + QString s = text_edit_->document()->toPlainText(); + + // Convert linebreaks + s.replace('\n', QStringLiteral("
")); + + return s; } private: - QPushButton* CreateToolbarButton(const QString &label, const QString &tooltip); + QPushButton* CreateToolbarButton(const QString &label, + const QString &tooltip, + const QStringList& tags); + + void SetTags(const QStringList& t, bool enabled); + + static QString CreateOpeningTag(const QString& s); + static QString CreateClosingTag(const QString& s); + + static void UpdateTagButton(QPushButton* btn, + const QString &text, + int cursor_pos); QFontDatabase font_db_; @@ -60,6 +76,8 @@ private: QPushButton* justify_align_btn_; private slots: + void TagButtonToggled(bool checked); + void UpdateButtons(); }; diff --git a/app/dialog/sequence/CMakeLists.txt b/app/dialog/sequence/CMakeLists.txt index 39e521400..d4a05877d 100644 --- a/app/dialog/sequence/CMakeLists.txt +++ b/app/dialog/sequence/CMakeLists.txt @@ -16,6 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + dialog/sequence/presetmanager.h dialog/sequence/sequence.h dialog/sequence/sequence.cpp dialog/sequence/sequencedialogparametertab.h diff --git a/app/dialog/sequence/presetmanager.h b/app/dialog/sequence/presetmanager.h new file mode 100644 index 000000000..08f88f89f --- /dev/null +++ b/app/dialog/sequence/presetmanager.h @@ -0,0 +1,239 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef PRESETMANAGER_H +#define PRESETMANAGER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/define.h" +#include "common/filefunctions.h" +#include "common/xmlutils.h" + +OLIVE_NAMESPACE_ENTER + +class Preset +{ +public: + Preset() = default; + + virtual ~Preset(){} + + const QString& GetName() const + { + return name_; + } + + void SetName(const QString& s) + { + name_ = s; + } + + virtual void Load(QXmlStreamReader* reader) = 0; + + virtual void Save(QXmlStreamWriter* writer) const = 0; + +private: + QString name_; + +}; + +using PresetPtr = std::shared_ptr; + +template +class PresetManager +{ +public: + PresetManager(QWidget* parent, const QString& preset_name) : + preset_name_(preset_name), + parent_(parent) + { + // Load custom preset data from file + QFile preset_file(GetCustomPresetFilename()); + if (preset_file.open(QFile::ReadOnly)) { + QXmlStreamReader reader(&preset_file); + + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("presets")) { + while (XMLReadNextStartElement(&reader)) { + if (reader.name() == QStringLiteral("preset")) { + PresetPtr p = std::make_shared(); + + p->Load(&reader); + + custom_preset_data_.append(p); + } else { + reader.skipCurrentElement(); + } + } + } else { + reader.skipCurrentElement(); + } + } + + preset_file.close(); + } + } + + ~PresetManager() + { + // Save custom presets to disk + QFile preset_file(GetCustomPresetFilename()); + if (preset_file.open(QFile::WriteOnly)) { + QXmlStreamWriter writer(&preset_file); + writer.setAutoFormatting(true); + + writer.writeStartDocument(); + + writer.writeStartElement(QStringLiteral("presets")); + + foreach (PresetPtr p, custom_preset_data_) { + writer.writeStartElement(QStringLiteral("preset")); + + p->Save(&writer); + + writer.writeEndElement(); // preset + } + + writer.writeEndElement(); // presets + + writer.writeEndDocument(); + + preset_file.close(); + } + } + + QString GetPresetName(QString start) const + { + bool ok; + + forever { + start = QInputDialog::getText(parent_, + QCoreApplication::translate("PresetManager", "Save Preset"), + QCoreApplication::translate("PresetManager", "Set preset name:"), + QLineEdit::Normal, + start, + &ok); + + if (!ok) { + // Dialog cancelled - leave function entirely + return QString(); + } + + if (start.isEmpty()) { + // No preset name entered, start loop over + QMessageBox::critical(parent_, + QCoreApplication::translate("PresetManager", "Invalid preset name"), + QCoreApplication::translate("PresetManager", "You must enter a preset name"), + QMessageBox::Ok); + } else { + break; + } + } + + return start; + } + + bool SavePreset(PresetPtr preset) + { + QString preset_name; + int existing_preset; + + forever { + preset_name = GetPresetName(preset_name); + + if (preset_name.isEmpty()) { + // Dialog cancelled - leave function entirely + return false; + } + + existing_preset = -1; + for (int i=0; iGetName() == preset_name) { + existing_preset = i; + break; + } + } + + if (existing_preset == -1 + || QMessageBox::question(parent_, + QCoreApplication::translate("PresetManager", "Preset exists"), + QCoreApplication::translate("PresetManager", + "A preset with this name already exists. " + "Would you like to replace it?")) == QMessageBox::Yes) { + break; + } + } + + preset->SetName(preset_name); + + if (existing_preset >= 0) { + custom_preset_data_.replace(existing_preset, preset); + return false; + } else { + custom_preset_data_.append(preset); + return true; + } + } + + QString GetCustomPresetFilename() const + { + return QDir(FileFunctions::GetConfigurationLocation()).filePath(preset_name_); + } + + PresetPtr GetPreset(int index) + { + return custom_preset_data_.at(index); + } + + void DeletePreset(int index) + { + custom_preset_data_.removeAt(index); + } + + int GetNumberOfPresets() const + { + return custom_preset_data_.size(); + } + + const QVector& GetPresetData() const + { + return custom_preset_data_; + } + +private: + QVector custom_preset_data_; + + QString preset_name_; + + QWidget* parent_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // PRESETMANAGER_H diff --git a/app/dialog/sequence/sequence.cpp b/app/dialog/sequence/sequence.cpp index 69d79a66e..3006c2b73 100644 --- a/app/dialog/sequence/sequence.cpp +++ b/app/dialog/sequence/sequence.cpp @@ -68,8 +68,8 @@ SequenceDialog::SequenceDialog(Sequence* s, Type t, QWidget* parent) : // Set up dialog buttons QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttons->setCenterButtons(true); - connect(buttons, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttons, SIGNAL(rejected()), this, SLOT(reject())); + connect(buttons, &QDialogButtonBox::accepted, this, &SequenceDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &SequenceDialog::reject); layout->addWidget(buttons); // Set window title based on type @@ -102,24 +102,17 @@ void SequenceDialog::accept() return; } - // Get the rational at the combobox's index (which will be correct provided AddFrameRate() was used at all time) - rational video_time_base = parameter_tab_->GetSelectedVideoFrameRate().flipped(); - - // Get the rational at the combobox's index (which will be correct provided AddFrameRate() was used at all time) - int audio_sample_rate = parameter_tab_->GetSelectedAudioSampleRate(); - - // Get the audio channel layout value - uint64_t channels = parameter_tab_->GetSelectedAudioChannelLayout(); - // Generate video and audio parameter structs from data VideoParams video_params = VideoParams(parameter_tab_->GetSelectedVideoWidth(), parameter_tab_->GetSelectedVideoHeight(), - video_time_base, + parameter_tab_->GetSelectedVideoFrameRate().flipped(), parameter_tab_->GetSelectedPreviewFormat(), + parameter_tab_->GetSelectedVideoPixelAspect(), + parameter_tab_->GetSelectedVideoInterlacingMode(), parameter_tab_->GetSelectedPreviewResolution()); - AudioParams audio_params = AudioParams(audio_sample_rate, - channels, + AudioParams audio_params = AudioParams(parameter_tab_->GetSelectedAudioSampleRate(), + parameter_tab_->GetSelectedAudioChannelLayout(), SampleFormat::kInternalFormat); if (make_undoable_) { diff --git a/app/dialog/sequence/sequencedialogparametertab.cpp b/app/dialog/sequence/sequencedialogparametertab.cpp index 83c5fda9b..98689b16c 100644 --- a/app/dialog/sequence/sequencedialogparametertab.cpp +++ b/app/dialog/sequence/sequencedialogparametertab.cpp @@ -35,8 +35,16 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg video_layout->addWidget(video_height_field_, row, 1); row++; video_layout->addWidget(new QLabel(tr("Frame Rate:")), row, 0); - video_frame_rate_field_ = new QComboBox(); + video_frame_rate_field_ = new FrameRateComboBox(); video_layout->addWidget(video_frame_rate_field_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Pixel Aspect Ratio:")), row, 0); + video_pixel_aspect_field_ = new PixelAspectRatioComboBox(); + video_layout->addWidget(video_pixel_aspect_field_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Interlacing:"))); + video_interlaced_field_ = new InterlacedComboBox(); + video_layout->addWidget(video_interlaced_field_, row, 1); layout->addWidget(video_group); row = 0; @@ -46,11 +54,11 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg audio_group->setTitle(tr("Audio")); QGridLayout* audio_layout = new QGridLayout(audio_group); audio_layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0); - audio_sample_rate_field_ = new QComboBox(); + audio_sample_rate_field_ = new SampleRateComboBox(); audio_layout->addWidget(audio_sample_rate_field_, row, 1); row++; audio_layout->addWidget(new QLabel(tr("Channels:")), row, 0); - audio_channels_field_ = new QComboBox(); + audio_channels_field_ = new ChannelLayoutComboBox(); audio_layout->addWidget(audio_channels_field_, row, 1); layout->addWidget(audio_group); @@ -61,83 +69,29 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg preview_group->setTitle(tr("Preview")); QGridLayout* preview_layout = new QGridLayout(preview_group); preview_layout->addWidget(new QLabel(tr("Resolution:")), row, 0); - preview_resolution_field_ = new QComboBox(); + preview_resolution_field_ = new VideoDividerComboBox(); preview_layout->addWidget(preview_resolution_field_, row, 1); preview_resolution_label_ = new QLabel(); preview_layout->addWidget(preview_resolution_label_, row, 2); row++; preview_layout->addWidget(new QLabel(tr("Format:")), row, 0); - preview_format_field_ = new QComboBox(); + preview_format_field_ = new PixelFormatComboBox(true, true); preview_layout->addWidget(preview_format_field_, row, 1, 1, 2); layout->addWidget(preview_group); - // Set up available frame rates - frame_rate_list_ = Core::SupportedFrameRates(); - foreach (const rational& fr, frame_rate_list_) { - video_frame_rate_field_->addItem(Core::FrameRateToString(fr)); - } - - // Set up available sample rates - sample_rate_list_ = Core::SupportedSampleRates(); - foreach (const int& sr, sample_rate_list_) { - audio_sample_rate_field_->addItem(Core::SampleRateToString(sr)); - } - - // Set up available channel layouts - channel_layout_list_ = Core::SupportedChannelLayouts(); - foreach (const uint64_t& ch_layout, channel_layout_list_) { - audio_channels_field_->addItem(Core::ChannelLayoutToString(ch_layout), QVariant::fromValue(ch_layout)); - } - - // Set up preview dividers - divider_list_ = Core::SupportedDividers(); - foreach (int d, divider_list_) { - QString name; - - if (d == 1) { - name = tr("Full"); - } else { - name = tr("1/%1").arg(d); - } - - preview_resolution_field_->addItem(name); - } - connect(preview_resolution_field_, static_cast(&QComboBox::currentIndexChanged), - this, &SequenceDialogParameterTab::UpdatePreviewResolutionLabel); - - // Set up preview formats - for (int i=0;i(i); - - // We always render with an alpha channel internally - if (PixelFormat::FormatHasAlphaChannel(pix_fmt) - && PixelFormat::FormatIsFloat(pix_fmt)) { - preview_format_field_->addItem(PixelFormat::GetName(pix_fmt)); - - preview_format_list_.append(pix_fmt); - } - } - // Set values based on input sequence video_width_field_->SetValue(sequence->video_params().width()); video_height_field_->SetValue(sequence->video_params().height()); + video_frame_rate_field_->SetFrameRate(sequence->video_params().time_base().flipped()); + video_pixel_aspect_field_->SetPixelAspectRatio(sequence->video_params().pixel_aspect_ratio()); + video_interlaced_field_->SetInterlaceMode(sequence->video_params().interlacing()); + preview_resolution_field_->SetDivider(sequence->video_params().divider()); + preview_format_field_->SetPixelFormat(sequence->video_params().format()); + audio_sample_rate_field_->SetSampleRate(sequence->audio_params().sample_rate()); + audio_channels_field_->SetChannelLayout(sequence->audio_params().channel_layout()); - int frame_rate_index = frame_rate_list_.indexOf(sequence->video_params().time_base().flipped()); - video_frame_rate_field_->setCurrentIndex(frame_rate_index); - - int sample_rate_index = sample_rate_list_.indexOf(sequence->audio_params().sample_rate()); - audio_sample_rate_field_->setCurrentIndex(sample_rate_index); - - for (int i=0;icount();i++) { - if (audio_channels_field_->itemData(i).toULongLong() == sequence->audio_params().channel_layout()) { - audio_channels_field_->setCurrentIndex(i); - break; - } - } - - preview_resolution_field_->setCurrentIndex(divider_list_.indexOf(sequence->video_params().divider())); - - preview_format_field_->setCurrentIndex(preview_format_list_.indexOf(sequence->video_params().format())); + connect(preview_resolution_field_, static_cast(&QComboBox::currentIndexChanged), + this, &SequenceDialogParameterTab::UpdatePreviewResolutionLabel); layout->addStretch(); @@ -148,62 +102,31 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg UpdatePreviewResolutionLabel(); } -int SequenceDialogParameterTab::GetSelectedVideoWidth() const -{ - return video_width_field_->GetValue(); -} - -int SequenceDialogParameterTab::GetSelectedVideoHeight() const -{ - return video_height_field_->GetValue(); -} - -const rational &SequenceDialogParameterTab::GetSelectedVideoFrameRate() const -{ - return frame_rate_list_.at(video_frame_rate_field_->currentIndex()); -} - -int SequenceDialogParameterTab::GetSelectedAudioSampleRate() const -{ - return sample_rate_list_.at(audio_sample_rate_field_->currentIndex()); -} - -uint64_t SequenceDialogParameterTab::GetSelectedAudioChannelLayout() const -{ - return audio_channels_field_->currentData().toULongLong(); -} - -int SequenceDialogParameterTab::GetSelectedPreviewResolution() const -{ - return divider_list_.at(preview_resolution_field_->currentIndex()); -} - -PixelFormat::Format SequenceDialogParameterTab::GetSelectedPreviewFormat() const -{ - return preview_format_list_.at(preview_format_field_->currentIndex()); -} - void SequenceDialogParameterTab::PresetChanged(const SequencePreset &preset) { - video_width_field_->SetValue(preset.width); - video_height_field_->SetValue(preset.height); - video_frame_rate_field_->setCurrentIndex(frame_rate_list_.indexOf(preset.frame_rate)); - audio_sample_rate_field_->setCurrentIndex(sample_rate_list_.indexOf(preset.sample_rate)); - audio_channels_field_->setCurrentIndex(channel_layout_list_.indexOf(preset.channel_layout)); - preview_resolution_field_->setCurrentIndex(divider_list_.indexOf(preset.preview_divider)); - preview_format_field_->setCurrentIndex(preview_format_list_.indexOf(preset.preview_format)); + video_width_field_->SetValue(preset.width()); + video_height_field_->SetValue(preset.height()); + video_frame_rate_field_->SetFrameRate(preset.frame_rate()); + video_pixel_aspect_field_->SetPixelAspectRatio(preset.pixel_aspect()); + video_interlaced_field_->SetInterlaceMode(preset.interlacing()); + audio_sample_rate_field_->SetSampleRate(preset.sample_rate()); + audio_channels_field_->SetChannelLayout(preset.channel_layout()); + preview_resolution_field_->SetDivider(preset.preview_divider()); + preview_format_field_->SetPixelFormat(preset.preview_format()); } void SequenceDialogParameterTab::SavePresetClicked() { emit SaveParametersAsPreset({QString(), - static_cast(video_width_field_->GetValue()), - static_cast(video_height_field_->GetValue()), - frame_rate_list_.at(video_frame_rate_field_->currentIndex()), - sample_rate_list_.at(audio_sample_rate_field_->currentIndex()), - channel_layout_list_.at(audio_channels_field_->currentIndex()), - divider_list_.at(preview_resolution_field_->currentIndex()), - preview_format_list_.at(preview_format_field_->currentIndex())}); + GetSelectedVideoWidth(), + GetSelectedVideoHeight(), + GetSelectedVideoFrameRate(), + GetSelectedVideoPixelAspect(), + GetSelectedVideoInterlacingMode(), + GetSelectedAudioSampleRate(), + GetSelectedAudioChannelLayout(), + GetSelectedPreviewResolution(), + GetSelectedPreviewFormat()}); } void SequenceDialogParameterTab::UpdatePreviewResolutionLabel() @@ -211,7 +134,9 @@ void SequenceDialogParameterTab::UpdatePreviewResolutionLabel() VideoParams test_param(video_width_field_->GetValue(), video_height_field_->GetValue(), PixelFormat::PIX_FMT_INVALID, - divider_list_.at(preview_resolution_field_->currentIndex())); + rational(1), + VideoParams::kInterlaceNone, + preview_resolution_field_->currentData().toInt()); preview_resolution_label_->setText(tr("(%1x%2)").arg(QString::number(test_param.effective_width()), QString::number(test_param.effective_height()))); diff --git a/app/dialog/sequence/sequencedialogparametertab.h b/app/dialog/sequence/sequencedialogparametertab.h index 2baab8fb2..794abe505 100644 --- a/app/dialog/sequence/sequencedialogparametertab.h +++ b/app/dialog/sequence/sequencedialogparametertab.h @@ -8,6 +8,7 @@ #include "project/item/sequence/sequence.h" #include "sequencepreset.h" #include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" OLIVE_NAMESPACE_ENTER @@ -17,19 +18,50 @@ class SequenceDialogParameterTab : public QWidget public: SequenceDialogParameterTab(Sequence* sequence, QWidget* parent = nullptr); - int GetSelectedVideoWidth() const; + int GetSelectedVideoWidth() const + { + return video_width_field_->GetValue(); + } - int GetSelectedVideoHeight() const; + int GetSelectedVideoHeight() const + { + return video_height_field_->GetValue(); + } - const rational& GetSelectedVideoFrameRate() const; + rational GetSelectedVideoFrameRate() const + { + return video_frame_rate_field_->GetFrameRate(); + } - int GetSelectedAudioSampleRate() const; + rational GetSelectedVideoPixelAspect() const + { + return video_pixel_aspect_field_->GetPixelAspectRatio(); + } - uint64_t GetSelectedAudioChannelLayout() const; + VideoParams::Interlacing GetSelectedVideoInterlacingMode() const + { + return video_interlaced_field_->GetInterlaceMode(); + } - int GetSelectedPreviewResolution() const; + int GetSelectedAudioSampleRate() const + { + return audio_sample_rate_field_->GetSampleRate(); + } - PixelFormat::Format GetSelectedPreviewFormat() const; + uint64_t GetSelectedAudioChannelLayout() const + { + return audio_channels_field_->GetChannelLayout(); + } + + int GetSelectedPreviewResolution() const + { + return preview_resolution_field_->GetDivider(); + } + + PixelFormat::Format GetSelectedPreviewFormat() const + { + return preview_format_field_->GetPixelFormat(); + } public slots: void PresetChanged(const SequencePreset& preset); @@ -42,27 +74,21 @@ private: IntegerSlider* video_height_field_; - QComboBox* video_frame_rate_field_; + FrameRateComboBox* video_frame_rate_field_; - QComboBox* audio_sample_rate_field_; + PixelAspectRatioComboBox* video_pixel_aspect_field_; - QComboBox* audio_channels_field_; + InterlacedComboBox* video_interlaced_field_; - QComboBox* preview_resolution_field_; + SampleRateComboBox* audio_sample_rate_field_; + + ChannelLayoutComboBox* audio_channels_field_; + + VideoDividerComboBox* preview_resolution_field_; QLabel* preview_resolution_label_; - QComboBox* preview_format_field_; - - QList frame_rate_list_; - - QList sample_rate_list_; - - QList channel_layout_list_; - - QList divider_list_; - - QList preview_format_list_; + PixelFormatComboBox* preview_format_field_; private slots: void SavePresetClicked(); diff --git a/app/dialog/sequence/sequencedialogpresettab.cpp b/app/dialog/sequence/sequencedialogpresettab.cpp index cfb0406fa..18c9419db 100644 --- a/app/dialog/sequence/sequencedialogpresettab.cpp +++ b/app/dialog/sequence/sequencedialogpresettab.cpp @@ -1,3 +1,23 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + #include "sequencedialogpresettab.h" #include @@ -11,6 +31,7 @@ #include "common/filefunctions.h" #include "node/input.h" +#include "render/videoparams.h" #include "ui/icons/icons.h" #include "widget/menu/menu.h" @@ -20,8 +41,11 @@ const int kDataIsPreset = Qt::UserRole; const int kDataPresetIsCustomRole = Qt::UserRole + 1; const int kDataPresetDataRole = Qt::UserRole + 2; +const PixelFormat::Format kDefaultPreviewFormat = PixelFormat::PIX_FMT_RGBA16F; + SequenceDialogPresetTab::SequenceDialogPresetTab(QWidget* parent) : - QWidget(parent) + QWidget(parent), + PresetManager(this, QStringLiteral("sequencepresets")) { QVBoxLayout* outer_layout = new QVBoxLayout(this); outer_layout->setMargin(0); @@ -44,127 +68,25 @@ SequenceDialogPresetTab::SequenceDialogPresetTab(QWidget* parent) : preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("1080p"), 1920, 1080, 3)); preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("720p"), 1280, 720, 2)); - preset_tree_->addTopLevelItem(CreateSDPresetFolder(tr("NTSC"), 720, 480, rational(30000, 1001), 1)); - preset_tree_->addTopLevelItem(CreateSDPresetFolder(tr("PAL"), 720, 576, rational(25, 1), 1)); + preset_tree_->addTopLevelItem(CreateSDPresetFolder(tr("NTSC"), 720, 480, rational(30000, 1001), + VideoParams::kPixelAspectNTSCStandard, + VideoParams::kPixelAspectNTSCWidescreen, 1)); + preset_tree_->addTopLevelItem(CreateSDPresetFolder(tr("PAL"), 720, 576, rational(25, 1), + VideoParams::kPixelAspectPALStandard, + VideoParams::kPixelAspectPALWidescreen, 1)); // Load custom presets - QFile preset_file(GetCustomPresetFilename()); - if (preset_file.open(QFile::ReadOnly)) { - QXmlStreamReader reader(&preset_file); - - while (XMLReadNextStartElement(&reader)) { - if (reader.name() == QStringLiteral("presets")) { - while (XMLReadNextStartElement(&reader)) { - if (reader.name() == QStringLiteral("preset")) { - SequencePreset p; - - while (XMLReadNextStartElement(&reader)) { - if (reader.name() == QStringLiteral("name")) { - p.name = reader.readElementText(); - } else if (reader.name() == QStringLiteral("width")) { - p.width = reader.readElementText().toInt(); - } else if (reader.name() == QStringLiteral("height")) { - p.height = reader.readElementText().toInt(); - } else if (reader.name() == QStringLiteral("framerate")) { - p.frame_rate = rational::fromString(reader.readElementText()); - } else if (reader.name() == QStringLiteral("samplerate")) { - p.sample_rate = reader.readElementText().toInt(); - } else if (reader.name() == QStringLiteral("chlayout")) { - p.channel_layout = reader.readElementText().toULongLong(); - } else if (reader.name() == QStringLiteral("divider")) { - p.preview_divider = reader.readElementText().toInt(); - } else if (reader.name() == QStringLiteral("format")) { - p.preview_format = static_cast(reader.readElementText().toInt()); - } else { - reader.skipCurrentElement(); - } - } - - AddItem(my_presets_folder_, p, true); - } else { - reader.skipCurrentElement(); - } - } - } else { - reader.skipCurrentElement(); - } - } - - preset_file.close(); - } -} - -SequenceDialogPresetTab::~SequenceDialogPresetTab() -{ - // Save custom presets to disk - QFile preset_file(GetCustomPresetFilename()); - if (preset_file.open(QFile::WriteOnly)) { - QXmlStreamWriter writer(&preset_file); - writer.setAutoFormatting(true); - - writer.writeStartDocument(); - - writer.writeStartElement(QStringLiteral("presets")); - - foreach (const SequencePreset& p, custom_preset_data_) { - writer.writeStartElement(QStringLiteral("preset")); - - writer.writeTextElement(QStringLiteral("name"), p.name); - writer.writeTextElement(QStringLiteral("width"), QString::number(p.width)); - writer.writeTextElement(QStringLiteral("height"), QString::number(p.height)); - writer.writeTextElement(QStringLiteral("framerate"), p.frame_rate.toString()); - writer.writeTextElement(QStringLiteral("samplerate"), QString::number(p.sample_rate)); - writer.writeTextElement(QStringLiteral("chlayout"), QString::number(p.channel_layout)); - writer.writeTextElement(QStringLiteral("divider"), QString::number(p.preview_divider)); - writer.writeTextElement(QStringLiteral("format"), QString::number(p.preview_format)); - - writer.writeEndElement(); // preset - } - - writer.writeEndElement(); // presets - - writer.writeEndDocument(); - - preset_file.close(); + for (int i=0;i(preset); - forever { - preset_name = GetPresetName(preset_name); - - if (preset_name.isEmpty()) { - // Dialog cancelled - leave function entirely - return; - } - - existing_preset = -1; - for (int i=0; i= 0) { - custom_preset_data_.replace(existing_preset, preset); - } else { - AddItem(my_presets_folder_, preset, true); + if (SavePreset(preset_ptr)) { + AddCustomItem(my_presets_folder_, preset_ptr, GetNumberOfPresets() - 1); } } @@ -179,101 +101,86 @@ QTreeWidgetItem* SequenceDialogPresetTab::CreateFolder(const QString &name) QTreeWidgetItem *SequenceDialogPresetTab::CreateHDPresetFolder(const QString &name, int width, int height, int divider) { QTreeWidgetItem* parent = CreateFolder(name); - AddItem(parent, {tr("%1 23.976 FPS").arg(name), - width, - height, - rational(24000, 1001), - 48000, - AV_CH_LAYOUT_STEREO, - divider, - PixelFormat::PIX_FMT_RGBA16F}); - AddItem(parent, {tr("%1 25 FPS").arg(name), - width, - height, - rational(25, 1), - 48000, - AV_CH_LAYOUT_STEREO, - divider, - PixelFormat::PIX_FMT_RGBA16F}); - AddItem(parent, {tr("%1 29.97 FPS").arg(name), - width, - height, - rational(30000, 1001), - 48000, - AV_CH_LAYOUT_STEREO, - divider, - PixelFormat::PIX_FMT_RGBA16F}); - AddItem(parent, {tr("%1 50 FPS").arg(name), - width, - height, - rational(50, 1), - 48000, - AV_CH_LAYOUT_STEREO, - divider, - PixelFormat::PIX_FMT_RGBA16F}); - AddItem(parent, {tr("%1 59.94 FPS").arg(name), - width, - height, - rational(60000, 1001), - 48000, - AV_CH_LAYOUT_STEREO, - divider, - PixelFormat::PIX_FMT_RGBA16F}); + AddStandardItem(parent, SequencePreset::Create(tr("%1 23.976 FPS").arg(name), + width, + height, + rational(24000, 1001), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + kDefaultPreviewFormat)); + AddStandardItem(parent, SequencePreset::Create(tr("%1 25 FPS").arg(name), + width, + height, + rational(25, 1), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + kDefaultPreviewFormat)); + AddStandardItem(parent, SequencePreset::Create(tr("%1 29.97 FPS").arg(name), + width, + height, + rational(30000, 1001), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + kDefaultPreviewFormat)); + AddStandardItem(parent, SequencePreset::Create(tr("%1 50 FPS").arg(name), + width, + height, + rational(50, 1), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + kDefaultPreviewFormat)); + AddStandardItem(parent, SequencePreset::Create(tr("%1 59.94 FPS").arg(name), + width, + height, + rational(60000, 1001), + VideoParams::kPixelAspectSquare, + VideoParams::kInterlaceNone, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + kDefaultPreviewFormat)); return parent; } -QTreeWidgetItem *SequenceDialogPresetTab::CreateSDPresetFolder(const QString &name, int width, int height, const rational& frame_rate, int divider) +QTreeWidgetItem *SequenceDialogPresetTab::CreateSDPresetFolder(const QString &name, int width, int height, const rational& frame_rate, const rational &standard_par, const rational &wide_par, int divider) { QTreeWidgetItem* parent = CreateFolder(name); preset_tree_->addTopLevelItem(parent); - AddItem(parent, {tr("%1 Standard").arg(name), - width, - height, - frame_rate, - 48000, - AV_CH_LAYOUT_STEREO, - divider, - PixelFormat::PIX_FMT_RGBA16F}); - AddItem(parent, {tr("%1 Widescreen").arg(name), - width, - height, - frame_rate, - 48000, - AV_CH_LAYOUT_STEREO, - divider, - PixelFormat::PIX_FMT_RGBA16F}); + AddStandardItem(parent, SequencePreset::Create(tr("%1 Standard").arg(name), + width, + height, + frame_rate, + standard_par, + VideoParams::kInterlacedBottomFirst, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + kDefaultPreviewFormat)); + AddStandardItem(parent, SequencePreset::Create(tr("%1 Widescreen").arg(name), + width, + height, + frame_rate, + wide_par, + VideoParams::kInterlacedBottomFirst, + 48000, + AV_CH_LAYOUT_STEREO, + divider, + kDefaultPreviewFormat)); return parent; } -QString SequenceDialogPresetTab::GetPresetName(QString start) -{ - bool ok; - - forever { - start = QInputDialog::getText(this, - tr("Save Preset"), - tr("Set preset name:"), - QLineEdit::Normal, - start, - &ok); - - if (!ok) { - // Dialog cancelled - leave function entirely - return QString(); - } - - if (start.isEmpty()) { - // No preset name entered, start loop over - QMessageBox::critical(this, tr("Invalid preset name"), - tr("You must enter a preset name"), QMessageBox::Ok); - } else { - break; - } - } - - return start; -} - QTreeWidgetItem *SequenceDialogPresetTab::GetSelectedItem() { QList selected_items = preset_tree_->selectedItems(); @@ -298,23 +205,28 @@ QTreeWidgetItem *SequenceDialogPresetTab::GetSelectedCustomPreset() return nullptr; } -QString SequenceDialogPresetTab::GetCustomPresetFilename() +void SequenceDialogPresetTab::AddStandardItem(QTreeWidgetItem *folder, PresetPtr preset, const QString& description) { - return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("presets")); + int index = default_preset_data_.size(); + default_preset_data_.append(preset); + AddItemInternal(folder, preset, false, index, description); } -void SequenceDialogPresetTab::AddItem(QTreeWidgetItem *folder, const SequencePreset& preset, bool is_custom, const QString &description) +void SequenceDialogPresetTab::AddCustomItem(QTreeWidgetItem *folder, PresetPtr preset, int index, const QString &description) +{ + AddItemInternal(folder, preset, true, index, description); +} + +void SequenceDialogPresetTab::AddItemInternal(QTreeWidgetItem *folder, PresetPtr preset, bool is_custom, int index, const QString &description) { QTreeWidgetItem* item = new QTreeWidgetItem(); - item->setText(0, preset.name); + + item->setText(0, preset->GetName()); item->setIcon(0, icon::Video); item->setToolTip(0, description); item->setData(0, kDataIsPreset, true); item->setData(0, kDataPresetIsCustomRole, is_custom); - - QList& list = is_custom ? custom_preset_data_ : default_preset_data_; - item->setData(0, kDataPresetDataRole, list.size()); - list.append(preset); + item->setData(0, kDataPresetDataRole, index); folder->addChild(item); } @@ -326,11 +238,11 @@ void SequenceDialogPresetTab::SelectedItemChanged(QTreeWidgetItem* current, QTre if (current->data(0, kDataIsPreset).toBool()) { int preset_index = current->data(0, kDataPresetDataRole).toInt(); - const SequencePreset& preset_data = (current->data(0, kDataPresetIsCustomRole).toBool()) - ? custom_preset_data_.at(preset_index) + PresetPtr preset_data = (current->data(0, kDataPresetIsCustomRole).toBool()) + ? GetPreset(preset_index) : default_preset_data_.at(preset_index); - emit PresetChanged(preset_data); + emit PresetChanged(*static_cast(preset_data.get())); } } @@ -375,7 +287,7 @@ void SequenceDialogPresetTab::DeleteSelectedPreset() } // Remove the preset - custom_preset_data_.removeAt(preset_index); + DeletePreset(preset_index); // Delete the item delete sel; diff --git a/app/dialog/sequence/sequencedialogpresettab.h b/app/dialog/sequence/sequencedialogpresettab.h index 3f426fa79..a2ef8a95d 100644 --- a/app/dialog/sequence/sequencedialogpresettab.h +++ b/app/dialog/sequence/sequencedialogpresettab.h @@ -1,3 +1,23 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + #ifndef SEQUENCEDIALOGPRESETTAB_H #define SEQUENCEDIALOGPRESETTAB_H @@ -5,18 +25,17 @@ #include #include +#include "presetmanager.h" #include "sequencepreset.h" OLIVE_NAMESPACE_ENTER -class SequenceDialogPresetTab : public QWidget +class SequenceDialogPresetTab : public QWidget, public PresetManager { Q_OBJECT public: SequenceDialogPresetTab(QWidget* parent = nullptr); - virtual ~SequenceDialogPresetTab() override; - public slots: void SaveParametersAsPreset(SequencePreset preset); @@ -30,26 +49,22 @@ private: QTreeWidgetItem *CreateHDPresetFolder(const QString& name, int width, int height, int divider); - QTreeWidgetItem *CreateSDPresetFolder(const QString& name, int width, int height, const rational &frame_rate, int divider); - - QString GetPresetName(QString start); + QTreeWidgetItem *CreateSDPresetFolder(const QString& name, int width, int height, const rational &frame_rate, const rational& standard_par, const rational& wide_par, int divider); QTreeWidgetItem* GetSelectedItem(); QTreeWidgetItem* GetSelectedCustomPreset(); - static QString GetCustomPresetFilename(); + void AddStandardItem(QTreeWidgetItem* folder, PresetPtr preset, const QString &description = QString()); - void AddItem(QTreeWidgetItem* folder, - const SequencePreset& preset, - bool is_custom = false, - const QString& description = QString()); + void AddCustomItem(QTreeWidgetItem* folder, PresetPtr preset, int index, const QString& description = QString()); + + void AddItemInternal(QTreeWidgetItem* folder, PresetPtr preset, bool is_custom, int index, const QString& description = QString()); QTreeWidget* preset_tree_; QTreeWidgetItem* my_presets_folder_; - QList default_preset_data_; - QList custom_preset_data_; + QList default_preset_data_; private slots: void SelectedItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous); diff --git a/app/dialog/sequence/sequencepreset.h b/app/dialog/sequence/sequencepreset.h index fb4ba9ab2..6b134b8f1 100644 --- a/app/dialog/sequence/sequencepreset.h +++ b/app/dialog/sequence/sequencepreset.h @@ -21,20 +21,158 @@ #ifndef SEQUENCEPARAM_H #define SEQUENCEPARAM_H +#include + #include "common/rational.h" +#include "common/xmlutils.h" +#include "dialog/sequence/presetmanager.h" #include "render/pixelformat.h" +#include "render/videoparams.h" OLIVE_NAMESPACE_ENTER -struct SequencePreset { - QString name; - int width; - int height; - rational frame_rate; - int sample_rate; - uint64_t channel_layout; - int preview_divider; - PixelFormat::Format preview_format; +class SequencePreset : public Preset { +public: + SequencePreset() = default; + + SequencePreset(const QString& name, + int width, + int height, + const rational& frame_rate, + const rational& pixel_aspect, + VideoParams::Interlacing interlacing, + int sample_rate, + uint64_t channel_layout, + int preview_divider, + PixelFormat::Format preview_format) : + width_(width), + height_(height), + frame_rate_(frame_rate), + pixel_aspect_(pixel_aspect), + interlacing_(interlacing), + sample_rate_(sample_rate), + channel_layout_(channel_layout), + preview_divider_(preview_divider), + preview_format_(preview_format) + { + SetName(name); + } + + static PresetPtr Create(const QString& name, + int width, + int height, + const rational& frame_rate, + const rational& pixel_aspect, + VideoParams::Interlacing interlacing, + int sample_rate, + uint64_t channel_layout, + int preview_divider, + PixelFormat::Format preview_format) + { + return std::make_shared(name, width, height, frame_rate, pixel_aspect, + interlacing, sample_rate, channel_layout, + preview_divider, preview_format); + } + + virtual void Load(QXmlStreamReader* reader) override + { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("name")) { + SetName(reader->readElementText()); + } else if (reader->name() == QStringLiteral("width")) { + width_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("height")) { + height_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("framerate")) { + frame_rate_ = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("pixelaspect")) { + pixel_aspect_ = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("interlacing")) { + interlacing_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("samplerate")) { + sample_rate_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("chlayout")) { + channel_layout_ = reader->readElementText().toULongLong(); + } else if (reader->name() == QStringLiteral("divider")) { + preview_divider_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("format")) { + preview_format_ = static_cast(reader->readElementText().toInt()); + } else { + reader->skipCurrentElement(); + } + } + } + + virtual void Save(QXmlStreamWriter* writer) const override + { + writer->writeTextElement(QStringLiteral("name"), GetName()); + writer->writeTextElement(QStringLiteral("width"), QString::number(width_)); + writer->writeTextElement(QStringLiteral("height"), QString::number(height_)); + writer->writeTextElement(QStringLiteral("framerate"), frame_rate_.toString()); + writer->writeTextElement(QStringLiteral("pixelaspect"), pixel_aspect_.toString()); + writer->writeTextElement(QStringLiteral("interlacing_"), QString::number(interlacing_)); + writer->writeTextElement(QStringLiteral("samplerate"), QString::number(sample_rate_)); + writer->writeTextElement(QStringLiteral("chlayout"), QString::number(channel_layout_)); + writer->writeTextElement(QStringLiteral("divider"), QString::number(preview_divider_)); + writer->writeTextElement(QStringLiteral("format"), QString::number(preview_format_)); + } + + int width() const + { + return width_; + } + + int height() const + { + return height_; + } + + const rational& frame_rate() const + { + return frame_rate_; + } + + const rational& pixel_aspect() const + { + return pixel_aspect_; + } + + VideoParams::Interlacing interlacing() const + { + return interlacing_; + } + + int sample_rate() const + { + return sample_rate_; + } + + uint64_t channel_layout() const + { + return channel_layout_; + } + + int preview_divider() const + { + return preview_divider_; + } + + PixelFormat::Format preview_format() const + { + return preview_format_; + } + +private: + int width_; + int height_; + rational frame_rate_; + rational pixel_aspect_; + VideoParams::Interlacing interlacing_; + int sample_rate_; + uint64_t channel_layout_; + int preview_divider_; + PixelFormat::Format preview_format_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/main.cpp b/app/main.cpp index e87f1fd34..d58b65ebf 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -32,17 +32,18 @@ extern "C" { } #include + #include #include #include "core.h" -#include "common/crashhandler.h" #include "common/debug.h" -int main(int argc, char *argv[]) { - signal(SIGSEGV, OLIVE_NAMESPACE::crash_handler); - signal(SIGABRT, OLIVE_NAMESPACE::crash_handler); +#ifdef USE_CRASHPAD +#include "common/crashpadinterface.h" +#endif // USE_CRASHPAD +int main(int argc, char *argv[]) { // Set OpenGL display profile (3.2 Core) QSurfaceFormat format; format.setVersion(3, 2); @@ -86,23 +87,12 @@ int main(int argc, char *argv[]) { avfilter_register_all(); #endif - int exit_code; - - // Start core - if (OLIVE_NAMESPACE::Core::instance()->Start()) { - - // Run application loop and receive exit code - exit_code = a.exec(); - - } else { - - // Core failed to start, exit now - exit_code = 1; - + // Enable Google Crashpad if compiled with it +#ifdef USE_CRASHPAD + if (!InitializeCrashpad()) { + qWarning() << "Failed to initialize Crashpad handler"; } +#endif // USE_CRASHPAD - // Clear core memory - OLIVE_NAMESPACE::Core::instance()->Stop(); - - return exit_code; + return OLIVE_NAMESPACE::Core::instance()->execute(&a); } diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index f46179786..222bc042d 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -355,16 +355,6 @@ NodeInput *Block::speed_input() const return speed_input_; } -void Block::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source) -{ - if (range.out() <= in() || range.in() >= out()) { - // Ignore this range - return; - } - - Node::InvalidateCache(TimeRange(qMax(range.in(), in()), qMin(range.out(), out())), from, source); -} - void Block::Hash(QCryptographicHash &, const rational &) const { // A block does nothing by default, so we hash nothing diff --git a/app/node/block/block.h b/app/node/block/block.h index 5694d4391..09182e7ea 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -88,8 +88,6 @@ public: NodeInput* media_in_input() const; NodeInput* speed_input() const; - virtual void InvalidateCache(const TimeRange& range, NodeInput* from, NodeInput* source) override; - virtual void Hash(QCryptographicHash &hash, const rational &time) const override; public slots: diff --git a/app/node/block/transition/CMakeLists.txt b/app/node/block/transition/CMakeLists.txt index 65f672588..4a380bc81 100644 --- a/app/node/block/transition/CMakeLists.txt +++ b/app/node/block/transition/CMakeLists.txt @@ -14,6 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(crossdissolve) +add_subdirectory(diptocolor) + set(OLIVE_SOURCES ${OLIVE_SOURCES} node/block/transition/transition.h diff --git a/app/node/block/transition/crossdissolve/CMakeLists.txt b/app/node/block/transition/crossdissolve/CMakeLists.txt new file mode 100644 index 000000000..25c90ed8e --- /dev/null +++ b/app/node/block/transition/crossdissolve/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/transition/crossdissolve/crossdissolvetransition.h + node/block/transition/crossdissolve/crossdissolvetransition.cpp + PARENT_SCOPE +) diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp new file mode 100644 index 000000000..2dfe7ace4 --- /dev/null +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp @@ -0,0 +1,89 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "crossdissolvetransition.h" + +OLIVE_NAMESPACE_ENTER + +CrossDissolveTransition::CrossDissolveTransition() +{ + +} + +Node *CrossDissolveTransition::copy() const +{ + return new CrossDissolveTransition(); +} + +QString CrossDissolveTransition::Name() const +{ + return tr("Cross Dissolve"); +} + +QString CrossDissolveTransition::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.crossdissolve"); +} + +QList CrossDissolveTransition::Category() const +{ + return {kCategoryTransition}; +} + +QString CrossDissolveTransition::Description() const +{ + return tr("Smoothly transition between two clips."); +} + +ShaderCode CrossDissolveTransition::GetShaderCode(const QString &shader_id) const +{ + Q_UNUSED(shader_id) + + return ShaderCode(Node::ReadFileAsString(":/shaders/crossdissolve.frag"), QString()); +} + +void CrossDissolveTransition::SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const +{ + for (int i=0; isample_count(); i++) { + double this_sample_time = out_samples->audio_params().samples_to_time(i).toDouble() + time_in; + double progress = GetTotalProgress(this_sample_time); + + for (int j=0; jaudio_params().channel_count(); j++) { + out_samples->data()[j][i] = 0; + + if (from_samples) { + if (i < from_samples->sample_count()) { + out_samples->data()[j][i] += from_samples->data()[j][i] * TransformCurve(1.0 - progress); + } + } + + if (to_samples) { + // Offset input samples from the end + int in_index = i - (out_samples->sample_count() - to_samples->sample_count()); + + if (in_index >= 0) { + out_samples->data()[j][i] += to_samples->data()[j][in_index] * TransformCurve(progress); + } + } + } + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.h b/app/node/block/transition/crossdissolve/crossdissolvetransition.h new file mode 100644 index 000000000..e45d1de67 --- /dev/null +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.h @@ -0,0 +1,51 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef CROSSDISSOLVETRANSITION_H +#define CROSSDISSOLVETRANSITION_H + +#include "node/block/transition/transition.h" + +OLIVE_NAMESPACE_ENTER + +class CrossDissolveTransition : public TransitionBlock +{ +public: + CrossDissolveTransition(); + + virtual Node* copy() const override; + + virtual QString Name() const override; + virtual QString id() const override; + virtual QList Category() const override; + virtual QString Description() const override; + + //virtual void Retranslate() override; + + virtual ShaderCode GetShaderCode(const QString& shader_id) const override; + +protected: + virtual void SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const override; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // CROSSDISSOLVETRANSITION_H diff --git a/app/node/block/transition/diptocolor/CMakeLists.txt b/app/node/block/transition/diptocolor/CMakeLists.txt new file mode 100644 index 000000000..7eb37f14d --- /dev/null +++ b/app/node/block/transition/diptocolor/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/transition/diptocolor/diptocolortransition.h + node/block/transition/diptocolor/diptocolortransition.cpp + PARENT_SCOPE +) diff --git a/app/node/block/transition/diptocolor/diptocolortransition.cpp b/app/node/block/transition/diptocolor/diptocolortransition.cpp new file mode 100644 index 000000000..816c75011 --- /dev/null +++ b/app/node/block/transition/diptocolor/diptocolortransition.cpp @@ -0,0 +1,68 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "diptocolortransition.h" + +OLIVE_NAMESPACE_ENTER + +DipToColorTransition::DipToColorTransition() +{ + color_input_ = new NodeInput(QStringLiteral("color_in"), NodeParam::kColor, QVariant::fromValue(Color(0, 0, 0))); + AddInput(color_input_); +} + +Node *DipToColorTransition::copy() const +{ + return new DipToColorTransition(); +} + +QString DipToColorTransition::Name() const +{ + return tr("Dip To Color"); +} + +QString DipToColorTransition::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.diptocolor"); +} + +QList DipToColorTransition::Category() const +{ + return {kCategoryTransition}; +} + +QString DipToColorTransition::Description() const +{ + return tr("Transition between clips by dipping to a color."); +} + +ShaderCode DipToColorTransition::GetShaderCode(const QString &shader_id) const +{ + Q_UNUSED(shader_id) + + return ShaderCode(Node::ReadFileAsString(":/shaders/diptoblack.frag"), QString()); +} + +void DipToColorTransition::ShaderJobEvent(NodeValueDatabase &value, ShaderJob &job) const +{ + job.InsertValue(color_input_, value); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/diptocolor/diptocolortransition.h b/app/node/block/transition/diptocolor/diptocolortransition.h new file mode 100644 index 000000000..2c19443e1 --- /dev/null +++ b/app/node/block/transition/diptocolor/diptocolortransition.h @@ -0,0 +1,52 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef DIPTOCOLORTRANSITION_H +#define DIPTOCOLORTRANSITION_H + +#include "node/block/transition/transition.h" + +OLIVE_NAMESPACE_ENTER + +class DipToColorTransition : public TransitionBlock +{ +public: + DipToColorTransition(); + + virtual Node* copy() const override; + + virtual QString Name() const override; + virtual QString id() const override; + virtual QList Category() const override; + virtual QString Description() const override; + + virtual ShaderCode GetShaderCode(const QString& shader_id) const override; + +protected: + virtual void ShaderJobEvent(NodeValueDatabase &value, ShaderJob& job) const override; + +private: + NodeInput* color_input_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // DIPTOCOLORTRANSITION_H diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index 3f820fd47..3b339f536 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -28,17 +28,22 @@ TransitionBlock::TransitionBlock() : connected_out_block_(nullptr), connected_in_block_(nullptr) { - out_block_input_ = new NodeInput("out_block_in", NodeParam::kBuffer); + out_block_input_ = new NodeInput(QStringLiteral("out_block_in"), NodeParam::kBuffer); out_block_input_->set_is_keyframable(false); connect(out_block_input_, &NodeParam::EdgeAdded, this, &TransitionBlock::BlockConnected); connect(out_block_input_, &NodeParam::EdgeRemoved, this, &TransitionBlock::BlockDisconnected); AddInput(out_block_input_); - in_block_input_ = new NodeInput("in_block_in", NodeParam::kBuffer); + in_block_input_ = new NodeInput(QStringLiteral("in_block_in"), NodeParam::kBuffer); in_block_input_->set_is_keyframable(false); connect(in_block_input_, &NodeParam::EdgeAdded, this, &TransitionBlock::BlockConnected); connect(in_block_input_, &NodeParam::EdgeRemoved, this, &TransitionBlock::BlockDisconnected); AddInput(in_block_input_); + + curve_input_ = new NodeInput(QStringLiteral("curve_in"), NodeParam::kCombo); + curve_input_->set_is_keyframable(false); + curve_input_->set_connectable(false); + AddInput(curve_input_); } Block::Type TransitionBlock::type() const @@ -62,6 +67,10 @@ void TransitionBlock::Retranslate() out_block_input_->set_name(tr("From")); in_block_input_->set_name(tr("To")); + curve_input_->set_name(tr("Curve")); + + // These must correspond to the CurveType enum + curve_input_->set_combobox_strings({ tr("Linear"), tr("Exponential"), tr("Logarithmic") }); } rational TransitionBlock::in_offset() const @@ -106,12 +115,12 @@ Block *TransitionBlock::connected_in_block() const return connected_in_block_; } -double TransitionBlock::GetTotalProgress(const rational &time) const +double TransitionBlock::GetTotalProgress(const double &time) const { return GetInternalTransitionTime(time) / length().toDouble(); } -double TransitionBlock::GetOutProgress(const rational &time) const +double TransitionBlock::GetOutProgress(const double &time) const { if (out_offset() == 0) { return 0; @@ -120,7 +129,7 @@ double TransitionBlock::GetOutProgress(const rational &time) const return clamp(1.0 - (GetInternalTransitionTime(time) / out_offset().toDouble()), 0.0, 1.0); } -double TransitionBlock::GetInProgress(const rational &time) const +double TransitionBlock::GetInProgress(const double &time) const { if (in_offset() == 0) { return 0; @@ -131,26 +140,36 @@ double TransitionBlock::GetInProgress(const rational &time) const void TransitionBlock::Hash(QCryptographicHash &hash, const rational &time) const { - double all_prog = GetTotalProgress(time); - double in_prog = GetInProgress(time); - double out_prog = GetOutProgress(time); + Node::Hash(hash, time); + + double time_dbl = time.toDouble(); + double all_prog = GetTotalProgress(time_dbl); + double in_prog = GetInProgress(time_dbl); + double out_prog = GetOutProgress(time_dbl); hash.addData(reinterpret_cast(&all_prog), sizeof(double)); hash.addData(reinterpret_cast(&in_prog), sizeof(double)); hash.addData(reinterpret_cast(&out_prog), sizeof(double)); - - if (out_block_input_->is_connected()) { - out_block_input_->get_connected_node()->Hash(hash, time); - } - - if (in_block_input_->is_connected()) { - in_block_input_->get_connected_node()->Hash(hash, time); - } } -double TransitionBlock::GetInternalTransitionTime(const rational &time) const +double TransitionBlock::GetInternalTransitionTime(const double &time) const { - return time.toDouble() - in().toDouble(); + return time - in().toDouble(); +} + +void TransitionBlock::InsertTransitionTimes(AcceleratedJob *job, const double &time) const +{ + // Provides total transition progress from 0.0 (start) - 1.0 (end) + job->InsertValue(QStringLiteral("ove_tprog_all"), + NodeValue(NodeParam::kFloat, GetTotalProgress(time), this)); + + // Provides progress of out section from 1.0 (start) - 0.0 (end) + job->InsertValue(QStringLiteral("ove_tprog_out"), + NodeValue(NodeParam::kFloat, GetOutProgress(time), this)); + + // Provides progress of in section from 0.0 (start) - 1.0 (end) + job->InsertValue(QStringLiteral("ove_tprog_in"), + NodeValue(NodeParam::kFloat, GetInProgress(time), this)); } void TransitionBlock::BlockConnected(NodeEdgePtr edge) @@ -177,4 +196,127 @@ void TransitionBlock::BlockDisconnected(NodeEdgePtr edge) } } +NodeValueTable TransitionBlock::Value(NodeValueDatabase &value) const +{ + NodeParam::DataType data_type; + + if (out_block_input()->is_connected()) { + data_type = value[out_block_input()].GetWithMeta(NodeParam::kBuffer).type(); + } else if (in_block_input()->is_connected()) { + data_type = value[in_block_input()].GetWithMeta(NodeParam::kBuffer).type(); + } else { + data_type = NodeParam::kNone; + } + + NodeParam::DataType job_type; + QVariant push_job; + + if (data_type == NodeParam::kTexture) { + // This must be a visual transition + ShaderJob job; + + job.InsertValue(out_block_input(), value); + job.InsertValue(in_block_input(), value); + job.InsertValue(curve_input_, value); + + double time = value[QStringLiteral("global")].Get(NodeParam::kFloat, QStringLiteral("time_in")).toDouble(); + InsertTransitionTimes(&job, time); + + ShaderJobEvent(value, job); + + job_type = NodeParam::kShaderJob; + push_job = QVariant::fromValue(job); + } else if (data_type == NodeParam::kSamples) { + // This must be an audio transition + SampleBufferPtr from_samples = value[out_block_input()].Take(NodeParam::kBuffer).value(); + SampleBufferPtr to_samples = value[in_block_input()].Take(NodeParam::kBuffer).value(); + + if (from_samples || to_samples) { + double time_in = value[QStringLiteral("global")].Get(NodeParam::kFloat, QStringLiteral("time_in")).toDouble(); + double time_out = value[QStringLiteral("global")].Get(NodeParam::kFloat, QStringLiteral("time_out")).toDouble(); + + const AudioParams& params = (from_samples) ? from_samples->audio_params() : to_samples->audio_params(); + + int nb_samples = params.time_to_samples(time_out - time_in); + + SampleBufferPtr out_samples = SampleBuffer::CreateAllocated(params, nb_samples); + SampleJobEvent(from_samples, to_samples, out_samples, time_in); + + job_type = NodeParam::kSamples; + push_job = QVariant::fromValue(out_samples); + } + } + + NodeValueTable table = value.Merge(); + + if (!push_job.isNull()) { + table.Push(job_type, push_job, this); + } + + return table; +} + +TransitionBlock *GetBlockTransitionInternal(Block *block, Timeline::MovementMode mode) +{ + // See if this block outputs to a transition + foreach (NodeEdgePtr edge, block->output()->edges()) { + Node* connected_node = edge->input()->parentNode(); + + if (connected_node->IsBlock()) { + Block* connected_block = static_cast(connected_node); + + if (connected_block->type() == Block::kTransition) { + TransitionBlock* connected_transition = static_cast(connected_block); + + if ((mode == Timeline::kTrimIn && edge->input() == connected_transition->in_block_input()) + || (mode == Timeline::kTrimOut && edge->input() == connected_transition->out_block_input())) { + return connected_transition; + } + } + } + } + + return nullptr; +} + +TransitionBlock *TransitionBlock::GetBlockInTransition(Block *block) +{ + return GetBlockTransitionInternal(block, Timeline::kTrimIn); +} + +TransitionBlock *TransitionBlock::GetBlockOutTransition(Block *block) +{ + return GetBlockTransitionInternal(block, Timeline::kTrimOut); +} + +void TransitionBlock::ShaderJobEvent(NodeValueDatabase &value, ShaderJob &job) const +{ + Q_UNUSED(value) + Q_UNUSED(job) +} + +void TransitionBlock::SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const +{ + Q_UNUSED(from_samples) + Q_UNUSED(to_samples) + Q_UNUSED(out_samples) + Q_UNUSED(time_in) +} + +double TransitionBlock::TransformCurve(double linear) const +{ + switch (static_cast(curve_input_->get_standard_value().toInt())) { + case kLinear: + break; + case kExponential: + linear *= linear; + break; + case kLogarithmic: + linear = qSqrt(linear); + break; + } + + return linear; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/transition.h b/app/node/block/transition/transition.h index 62f4280b5..d8c5c6a39 100644 --- a/app/node/block/transition/transition.h +++ b/app/node/block/transition/transition.h @@ -43,19 +43,42 @@ public: Block* connected_out_block() const; Block* connected_in_block() const; - double GetTotalProgress(const rational& time) const; - double GetOutProgress(const rational& time) const; - double GetInProgress(const rational& time) const; + double GetTotalProgress(const double &time) const; + double GetOutProgress(const double &time) const; + double GetInProgress(const double &time) const; virtual void Hash(QCryptographicHash& hash, const rational &time) const override; + virtual NodeValueTable Value(NodeValueDatabase &value) const override; + + static TransitionBlock* GetBlockInTransition(Block* block); + + static TransitionBlock* GetBlockOutTransition(Block* block); + +protected: + virtual void ShaderJobEvent(NodeValueDatabase &value, ShaderJob& job) const; + + virtual void SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const; + + double TransformCurve(double linear) const; + private: - double GetInternalTransitionTime(const rational& time) const; + enum CurveType { + kLinear, + kExponential, + kLogarithmic + }; + + double GetInternalTransitionTime(const double &time) const; + + void InsertTransitionTimes(AcceleratedJob* job, const double& time) const; NodeInput* out_block_input_; NodeInput* in_block_input_; + NodeInput* curve_input_; + Block* connected_out_block_; Block* connected_in_block_; diff --git a/app/node/factory.cpp b/app/node/factory.cpp index ee2fe3892..7c389f443 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -24,6 +24,8 @@ #include "audio/volume/volume.h" #include "block/clip/clip.h" #include "block/gap/gap.h" +#include "block/transition/crossdissolve/crossdissolvetransition.h" +#include "block/transition/diptocolor/diptocolortransition.h" #include "generator/matrix/matrix.h" #include "generator/polygon/polygon.h" #include "generator/solid/solid.h" @@ -48,7 +50,7 @@ void NodeFactory::Initialize() // Add internal types for (int i=0;i(i))); + library_.append(CreateFromFactoryIndex(static_cast(i))); } /* @@ -63,7 +65,7 @@ void NodeFactory::Destroy() library_.clear(); } -Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item) +Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item, Node::CategoryID restrict_to) { Menu* menu = new Menu(parent); menu->setToolTipsVisible(true); @@ -71,6 +73,11 @@ Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item) for (int i=0;iCategory().contains(restrict_to)) { + // Skip this node + continue; + } + // Make sure nodes are up-to-date with the current translation n->Retranslate(); @@ -165,7 +172,7 @@ Node *NodeFactory::CreateFromID(const QString &id) return nullptr; } -Node *NodeFactory::CreateInternal(const NodeFactory::InternalID &id) +Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) { switch (id) { case kClipBlock: @@ -204,6 +211,10 @@ Node *NodeFactory::CreateInternal(const NodeFactory::InternalID &id) return new StrokeFilterNode(); case kTextGenerator: return new TextGenerator(); + case kCrossDissolveTransition: + return new CrossDissolveTransition(); + case kDipToColorTransition: + return new DipToColorTransition(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index b5c1b6f0f..6025070ef 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -50,6 +50,8 @@ public: kMerge, kStrokeFilter, kTextGenerator, + kCrossDissolveTransition, + kDipToColorTransition, // Count value kInternalNodeCount @@ -61,7 +63,7 @@ public: static void Destroy(); - static Menu* CreateMenu(QWidget *parent, bool create_none_item = false); + static Menu* CreateMenu(QWidget *parent, bool create_none_item = false, Node::CategoryID restrict_to = Node::kCategoryUnknown); static Node* CreateFromMenuAction(QAction* action); @@ -71,10 +73,11 @@ public: static Node* CreateFromID(const QString& id); -private: - static Node* CreateInternal(const InternalID& id); + static Node* CreateFromFactoryIndex(const InternalID& id); +private: static QList library_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/generator/text/text.cpp b/app/node/generator/text/text.cpp index 656aaa8ca..de38f08bd 100644 --- a/app/node/generator/text/text.cpp +++ b/app/node/generator/text/text.cpp @@ -32,11 +32,9 @@ enum TextVerticalAlign { TextGenerator::TextGenerator() { - QString default_str = QStringLiteral("
%1
").arg(tr("Sample Text")); - text_input_ = new NodeInput(QStringLiteral("text_in"), NodeParam::kText, - default_str); + tr("Sample Text")); AddInput(text_input_); color_input_ = new NodeInput(QStringLiteral("color_in"), @@ -48,6 +46,15 @@ TextGenerator::TextGenerator() NodeParam::kCombo, 1); AddInput(valign_input_); + + font_input_ = new NodeInput(QStringLiteral("font_in"), + NodeParam::kFont); + AddInput(font_input_); + + font_size_input_ = new NodeInput(QStringLiteral("font_size_in"), + NodeParam::kFloat, + 72.0f); + AddInput(font_size_input_); } Node *TextGenerator::copy() const @@ -78,7 +85,10 @@ QString TextGenerator::Description() const void TextGenerator::Retranslate() { text_input_->set_name(tr("Text")); + font_input_->set_name(tr("Font")); + font_size_input_->set_name(tr("Font Size")); color_input_->set_name(tr("Color")); + valign_input_->set_name(tr("Vertical Align")); valign_input_->set_combobox_strings({tr("Top"), tr("Center"), tr("Bottom")}); } @@ -88,6 +98,8 @@ NodeValueTable TextGenerator::Value(NodeValueDatabase &value) const job.InsertValue(text_input_, value); job.InsertValue(color_input_, value); job.InsertValue(valign_input_, value); + job.InsertValue(font_input_, value); + job.InsertValue(font_size_input_, value); job.SetAlphaChannelRequired(true); NodeValueTable table = value.Merge(); @@ -109,24 +121,45 @@ void TextGenerator::GenerateFrame(FramePtr frame, const GenerateJob& job) const img.fill(0); QTextDocument text_doc; + + // Set default font + QFont default_font; + default_font.setFamily(job.GetValue(font_input_).data().toString()); + default_font.setPointSizeF(job.GetValue(font_size_input_).data().toFloat()); + text_doc.setDefaultFont(default_font); + + // Center by default + text_doc.setDefaultTextOption(QTextOption(Qt::AlignCenter)); + text_doc.setHtml(job.GetValue(text_input_).data().toString()); - text_doc.setTextWidth(frame->video_params().width()); + + // Align to 80% width because that's considered the "title safe" area + int tenth_of_width = frame->video_params().width() / 10; + text_doc.setTextWidth(tenth_of_width * 8); // Draw rich text onto image QPainter p(&img); p.scale(1.0 / frame->video_params().divider(), 1.0 / frame->video_params().divider()); - TextVerticalAlign valign = static_cast(job.GetValue(valign_input_).data().toInt()); - if (valign != kVerticalAlignTop) { - int doc_height = text_doc.size().height(); + // Push 10% inwards to compensate for title safe area + p.translate(tenth_of_width, 0); - if (valign == kVerticalAlignCenter) { - // Center align - p.translate(0, frame->video_params().height() / 2 - doc_height / 2); - } else { - // Must be bottom align - p.translate(0, frame->video_params().height() - doc_height); - } + TextVerticalAlign valign = static_cast(job.GetValue(valign_input_).data().toInt()); + int doc_height = text_doc.size().height(); + + switch (valign) { + case kVerticalAlignTop: + // Push 10% inwards for title safe area + p.translate(0, frame->video_params().height() / 10); + break; + case kVerticalAlignCenter: + // Center align + p.translate(0, frame->video_params().height() / 2 - doc_height / 2); + break; + case kVerticalAlignBottom: + // Push 10% inwards for title safe area + p.translate(0, frame->video_params().height() - doc_height - frame->video_params().height() / 10); + break; } text_doc.drawContents(&p); diff --git a/app/node/generator/text/text.h b/app/node/generator/text/text.h index b94399041..e4a14b868 100644 --- a/app/node/generator/text/text.h +++ b/app/node/generator/text/text.h @@ -50,6 +50,10 @@ private: NodeInput* valign_input_; + NodeInput* font_input_; + + NodeInput* font_size_input_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/input.cpp b/app/node/input.cpp index 3ec99b36f..ecc4906d8 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -81,13 +81,15 @@ QString NodeInput::name() void NodeInput::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled) { - XMLAttributeLoop(reader, attr) { - if (cancelled && *cancelled) { - return; - } + { + XMLAttributeLoop(reader, attr) { + if (cancelled && *cancelled) { + return; + } - if (attr.name() == QStringLiteral("keyframing")) { - set_is_keyframing(attr.value() == QStringLiteral("1")); + if (attr.name() == QStringLiteral("keyframing")) { + set_is_keyframing(attr.value() == QStringLiteral("1")); + } } } @@ -325,17 +327,48 @@ void NodeInput::SetDefaultValue(const QVector &default_value) QString NodeInput::ValueToString(const QVariant &value) const { - return ValueToString(data_type_, value); + return ValueToString(data_type_, value, true); } -QString NodeInput::ValueToString(const DataType& data_type, const QVariant &value) +QString NodeInput::ValueToString(const DataType& data_type, const QVariant &value, bool value_is_a_key_track) { - switch (data_type) { - case kRational: + if (!value_is_a_key_track && data_type == kVec2) { + QVector2D vec = value.value(); + + return QStringLiteral("%1:%2").arg(QString::number(vec.x()), + QString::number(vec.y())); + } else if (!value_is_a_key_track && data_type == kVec3) { + QVector3D vec = value.value(); + + return QStringLiteral("%1:%2:%3").arg(QString::number(vec.x()), + QString::number(vec.y()), + QString::number(vec.z())); + } else if (!value_is_a_key_track && data_type == kVec4) { + QVector4D vec = value.value(); + + return QStringLiteral("%1:%2:%3:%4").arg(QString::number(vec.x()), + QString::number(vec.y()), + QString::number(vec.z()), + QString::number(vec.w())); + } else if (!value_is_a_key_track && data_type == kColor) { + Color c = value.value(); + + return QStringLiteral("%1:%2:%3:%4").arg(QString::number(c.red()), + QString::number(c.green()), + QString::number(c.blue()), + QString::number(c.alpha())); + } else if (data_type == kRational) { return value.value().toString(); - case kFootage: + } else if (data_type == kFootage) { return QString::number(reinterpret_cast(value.value().get())); - default: + } else if (data_type == kTexture + || data_type == kSamples + || data_type == kBuffer) { + // These data types need no XML representation + return QString(); + } else if (data_type == kInt) { + return QString::number(value.value()); + } else { if (value.canConvert()) { return value.toString(); } @@ -344,22 +377,33 @@ QString NodeInput::ValueToString(const DataType& data_type, const QVariant &valu qWarning() << "Failed to convert type" << ToHex(data_type) << "to string"; } - /* fall through */ - - // These data types need no XML representation - case kTexture: - case kSamples: - case kBuffer: return QString(); } } -QVariant NodeInput::StringToValue(const DataType& data_type, const QString &string) +QVariant NodeInput::StringToValue(const DataType& data_type, const QString &string, bool value_is_a_key_track) { - switch (data_type) { - case kRational: + if (!value_is_a_key_track && data_type == kVec2) { + QStringList vals = string.split(':'); + + return QVector2D(vals.at(0).toFloat(), vals.at(1).toFloat()); + } else if (!value_is_a_key_track && data_type == kVec3) { + QStringList vals = string.split(':'); + + return QVector3D(vals.at(0).toFloat(), vals.at(1).toFloat(), vals.at(2).toFloat()); + } else if (!value_is_a_key_track && data_type == kVec4) { + QStringList vals = string.split(':'); + + return QVector4D(vals.at(0).toFloat(), vals.at(1).toFloat(), vals.at(2).toFloat(), vals.at(3).toFloat()); + } else if (!value_is_a_key_track && data_type == kColor) { + QStringList vals = string.split(':'); + + return QVariant::fromValue(Color(vals.at(0).toFloat(), vals.at(1).toFloat(), vals.at(2).toFloat(), vals.at(3).toFloat())); + } else if (data_type == kInt) { + return QVariant::fromValue(string.toLongLong()); + } else if (data_type == kRational) { return QVariant::fromValue(rational::fromString(string)); - default: + } else { return string; } } @@ -427,7 +471,7 @@ QVariant NodeInput::StringToValue(const QString &string, QListid() == dest->id()); @@ -1035,8 +1079,10 @@ void NodeInput::CopyValues(NodeInput *source, NodeInput *dest, bool include_conn dst_array->SetSize(src_array->GetSize()); - for (int i=0;iGetSize();i++) { - CopyValues(src_array->At(i), dst_array->At(i), include_connections); + if (traverse_arrays) { + for (int i=0;iGetSize();i++) { + CopyValues(src_array->At(i), dst_array->At(i), include_connections); + } } } diff --git a/app/node/input.h b/app/node/input.h index 4a6da4822..10d09e2a5 100644 --- a/app/node/input.h +++ b/app/node/input.h @@ -235,7 +235,7 @@ public: /** * @brief Copy all values including keyframe information and connections from another NodeInput */ - static void CopyValues(NodeInput* source, NodeInput* dest, bool include_connections = true); + static void CopyValues(NodeInput* source, NodeInput* dest, bool include_connections = true, bool traverse_arrays = true); /** * @brief Set an arbitrary property on this input to influence a UI representation's behavior @@ -278,9 +278,9 @@ public: void set_combobox_strings(const QStringList& strings); - static QString ValueToString(const DataType& data_type, const QVariant& value); + static QString ValueToString(const DataType& data_type, const QVariant& value, bool value_is_a_key_track); - static QVariant StringToValue(const DataType &data_type, const QString &string); + static QVariant StringToValue(const DataType &data_type, const QString &string, bool value_is_a_key_track); void GetDependencies(QList& list, bool traverse, bool exclusive_only) const; diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index 8ca54f435..b1ec0caa8 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -59,7 +59,7 @@ void NodeInputArray::SetSize(int size) if (size < old_size) { // If the new size is less, delete all extraneous parameters for (int i=size;ideleteLater(); + delete sub_params_.at(i); } } diff --git a/app/node/math/math/mathbase.cpp b/app/node/math/math/mathbase.cpp index 50ae6c2d8..4a793af93 100644 --- a/app/node/math/math/mathbase.cpp +++ b/app/node/math/math/mathbase.cpp @@ -65,7 +65,16 @@ ShaderCode MathNodeBase::GetShaderCodeInternal(const QString &shader_id, NodeInp operation = QStringLiteral("%1 / %2"); break; case kOpPower: - operation = QStringLiteral("pow(%1, %2)"); + if (pairing == kPairTextureNumber) { + // The "number" in this operation has to be declared a vec4 + if (type_a & NodeParam::kNumber) { + operation = QStringLiteral("pow(%2, vec4(%1))"); + } else { + operation = QStringLiteral("pow(%1, vec4(%2))"); + } + } else { + operation = QStringLiteral("pow(%1, %2)"); + } break; } @@ -285,8 +294,13 @@ NodeValueTable MathNodeBase::ValueInternal(NodeValueDatabase &value, Operation o } } else if (pairing == kPairTextureMatrix) { // Only allow matrix multiplication - if (operation != kOpMultiply - || number_val.data().value().isIdentity()) { + bool matrix_is_identity = false; + + // FIXME: The matrix in the shader is transformed around footage+sequence resolution so we + // need to do that here to determine if the matrix is truly identity. But to do that, + // we need access to the texture parameters which is currently not possible. + + if (operation != kOpMultiply || matrix_is_identity) { operation_is_noop = true; } else { // It's likely an alpha channel will result from this operation diff --git a/app/node/node.cpp b/app/node/node.cpp index 78613fa9b..f54822bfb 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -24,6 +24,7 @@ #include #include +#include "common/timecodefunctions.h" #include "common/xmlutils.h" #include "project/project.h" #include "project/item/footage/footage.h" @@ -177,6 +178,28 @@ void Node::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *s SendInvalidateCache(range, source); } +void Node::BeginOperation() +{ + foreach (NodeParam* param, params_) { + if (param->type() == NodeParam::kOutput) { + foreach (NodeEdgePtr edge, param->edges()) { + edge->input()->parentNode()->BeginOperation(); + } + } + } +} + +void Node::EndOperation() +{ + foreach (NodeParam* param, params_) { + if (param->type() == NodeParam::kOutput) { + foreach (NodeEdgePtr edge, param->edges()) { + edge->input()->parentNode()->EndOperation(); + } + } + } +} + TimeRange Node::InputTimeAdjustment(NodeInput *, const TimeRange &input_time) const { // Default behavior is no time adjustment at all @@ -195,10 +218,7 @@ void Node::SendInvalidateCache(const TimeRange &range, NodeInput *source) foreach (NodeParam* param, params_) { // If the Node is an output, relay the signal to any Nodes that are connected to it if (param->type() == NodeParam::kOutput) { - - QVector edges = param->edges(); - - foreach (NodeEdgePtr edge, edges) { + foreach (NodeEdgePtr edge, param->edges()) { NodeInput* connected_input = edge->input(); Node* connected_node = connected_input->parentNode(); @@ -359,11 +379,15 @@ void Node::Hash(QCryptographicHash &hash, const rational& time) const // Footage timestamp if (stream->type() == Stream::kVideo) { - hash.addData(QStringLiteral("%1/%2").arg(QString::number(input_time.numerator()), - QString::number(input_time.denominator())).toUtf8()); + VideoStreamPtr video_stream = std::static_pointer_cast(stream); - hash.addData(QString::number(static_cast(stream.get())->start_time()).toUtf8()); + int64_t video_ts = Timecode::time_to_timestamp(input_time, video_stream->timebase()); + // Add timestamp in units of the video stream's timebase + hash.addData(reinterpret_cast(&video_ts), sizeof(int64_t)); + + // Add start time - used for both image sequences and video streams + hash.addData(QString::number(video_stream->start_time()).toUtf8()); } } } @@ -537,6 +561,28 @@ bool Node::OutputsTo(const QString &id, bool recursively) const return false; } +bool Node::OutputsTo(NodeInput *input, bool recursively, bool include_arrays) const +{ + QList outputs = GetOutputs(); + + foreach (NodeOutput* output, outputs) { + foreach (NodeEdgePtr edge, output->edges()) { + NodeInput* connected = edge->input(); + + if (connected == input) { + return true; + } else if (include_arrays && input->IsArray() + && static_cast(input)->sub_params().contains(connected)) { + return true; + } else if (recursively && connected->parentNode()->OutputsTo(input, recursively, include_arrays)) { + return true; + } + } + } + + return false; +} + bool Node::InputsFrom(Node *n, bool recursively) const { QList inputs = GetInputsIncludingArrays(); @@ -649,6 +695,8 @@ QString Node::GetCategoryName(const CategoryID &c) return tr("Generator"); case kCategoryChannels: return tr("Channel"); + case kCategoryTransition: + return tr("Transition"); case kCategoryUnknown: case kCategoryCount: break; diff --git a/app/node/node.h b/app/node/node.h index 91209581a..ba05359ce 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -70,6 +70,7 @@ public: kCategoryGeneral, kCategoryTimeline, kCategoryChannels, + kCategoryTransition, kCategoryCount }; @@ -220,6 +221,11 @@ public: */ bool OutputsTo(const QString& id, bool recursively) const; + /** + * @brief Same as OutputsTo(Node*), but for a specific node input rather than just a node. + */ + bool OutputsTo(NodeInput* input, bool recursively, bool include_arrays) const; + /** * @brief Returns whether this node ever receives an input from a particular node instance */ @@ -298,6 +304,19 @@ public: */ virtual void InvalidateCache(const TimeRange& range, NodeInput* from, NodeInput* source); + /** + * @brief Limits cache invalidation temporarily + * + * If you intend to do a number of operations in quick succession, you can optimize it by running + * this function with EndOperation(). + */ + virtual void BeginOperation(); + + /** + * @brief Stops limiting cache invalidation and flushes changes + */ + virtual void EndOperation(); + /** * @brief Adjusts time that should be sent to nodes connected to certain inputs. * diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 96c2f6145..daa15748a 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -31,10 +31,8 @@ OLIVE_NAMESPACE_ENTER TrackOutput::TrackOutput() : track_type_(Timeline::kTrackTypeNone), - block_invalidate_cache_stack_(0), index_(-1), - locked_(false), - queued_length_change_(false) + locked_(false) { block_input_ = new NodeInputArray("block_in", NodeParam::kAny); block_input_->set_is_keyframable(false); @@ -252,11 +250,24 @@ const QList &TrackOutput::Blocks() const void TrackOutput::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source) { - if (block_invalidate_cache_stack_ == 0) { - PushLengthChangeSignal(true); + TimeRange limited; - Node::InvalidateCache(TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), track_length())), from, source); + if (block_input_->sub_params().contains(from) + && from->get_connected_node() + && from->get_connected_node()->IsBlock()) { + // Limit the range signal to the corresponding block + Block* b = static_cast(from->get_connected_node()); + + if (range.out() <= b->in() || range.in() >= b->out()) { + return; + } + + limited = TimeRange(qMax(range.in(), b->in()), qMin(range.out(), b->out())); + } else { + limited = TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), track_length())); } + + Node::InvalidateCache(limited, from, source); } void TrackOutput::InsertBlockBefore(Block* block, Block* after) @@ -279,12 +290,12 @@ void TrackOutput::InsertBlockAfter(Block *block, Block *before) void TrackOutput::PrependBlock(Block *block) { - BlockInvalidateCache(); + BeginOperation(); block_input_->Prepend(); NodeParam::ConnectEdge(block->output(), block_input_->First()); - UnblockInvalidateCache(); + EndOperation(); // Everything has shifted at this point InvalidateCache(TimeRange(0, track_length()), block_input_, block_input_); @@ -292,57 +303,47 @@ void TrackOutput::PrependBlock(Block *block) void TrackOutput::InsertBlockAtIndex(Block *block, int index) { - BlockInvalidateCache(); + BeginOperation(); int insert_index = GetInputIndexFromCacheIndex(index); block_input_->InsertAt(insert_index); NodeParam::ConnectEdge(block->output(), block_input_->At(insert_index)); - UnblockInvalidateCache(); + EndOperation(); InvalidateCache(TimeRange(block->in(), track_length()), block_input_, block_input_); } void TrackOutput::AppendBlock(Block *block) { - BlockInvalidateCache(); + BeginOperation(); block_input_->Append(); NodeParam::ConnectEdge(block->output(), block_input_->Last()); - UnblockInvalidateCache(); + EndOperation(); // Invalidate area that block was added to InvalidateCache(TimeRange(block->in(), track_length()), block_input_, block_input_); } -void TrackOutput::BlockInvalidateCache() -{ - block_invalidate_cache_stack_++; -} - -void TrackOutput::UnblockInvalidateCache() -{ - block_invalidate_cache_stack_--; -} - void TrackOutput::RippleRemoveBlock(Block *block) { - BlockInvalidateCache(); + BeginOperation(); rational remove_in = block->in(); block_input_->RemoveAt(GetInputIndexFromCacheIndex(block)); - UnblockInvalidateCache(); + EndOperation(); InvalidateCache(TimeRange(remove_in, track_length()), block_input_, block_input_); } void TrackOutput::ReplaceBlock(Block *old, Block *replace) { - BlockInvalidateCache(); + BeginOperation(); int index_of_old_block = GetInputIndexFromCacheIndex(old); @@ -352,7 +353,7 @@ void TrackOutput::ReplaceBlock(Block *old, Block *replace) NodeParam::ConnectEdge(replace->output(), block_input_->At(index_of_old_block)); - UnblockInvalidateCache(); + EndOperation(); if (old->length() == replace->length()) { InvalidateCache(TimeRange(replace->in(), replace->out()), block_input_, block_input_); @@ -443,14 +444,6 @@ void TrackOutput::Hash(QCryptographicHash &hash, const rational &time) const } } -void TrackOutput::PushLengthChangeSignal(bool invalidate) -{ - if (queued_length_change_) { - queued_length_change_ = false; - SetLengthInternal(queued_length_, invalidate); - } -} - void TrackOutput::SetTrackName(const QString &name) { track_name_ = name; @@ -507,15 +500,6 @@ int TrackOutput::GetInputIndexFromCacheIndex(Block *block) void TrackOutput::SetLengthInternal(const rational &r, bool invalidate) { - if (block_invalidate_cache_stack_ > 0) { - queued_length_change_ = true; - } - - if (queued_length_change_) { - queued_length_ = r; - return; - } - if (r != track_length_) { TimeRange invalidate_range(track_length_, r); diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 35c444a0e..ad658bcd2 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -166,10 +166,6 @@ public: */ void ReplaceBlock(Block* old, Block* replace); - void BlockInvalidateCache(); - - void UnblockInvalidateCache(); - static TrackOutput* TrackFromBlock(const Block *block); const rational& track_length() const; @@ -192,8 +188,6 @@ public: virtual void Hash(QCryptographicHash& hash, const rational &time) const override; - void PushLengthChangeSignal(bool invalidate = false); - AudioVisualWaveform& waveform() { return waveform_; @@ -274,15 +268,10 @@ private: QString track_name_; - int block_invalidate_cache_stack_; - int index_; bool locked_; - bool queued_length_change_; - rational queued_length_; - AudioVisualWaveform waveform_; QMutex waveform_lock_; diff --git a/app/node/output/track/tracklist.cpp b/app/node/output/track/tracklist.cpp index 67a19c91b..abe36e1e7 100644 --- a/app/node/output/track/tracklist.cpp +++ b/app/node/output/track/tracklist.cpp @@ -151,9 +151,9 @@ void TrackList::RemoveTrack() void TrackList::TrackConnected(NodeEdgePtr edge) { - int track_index = track_input_->IndexOfSubParameter(edge->input()); + int input_index = track_input_->IndexOfSubParameter(edge->input()); - Q_ASSERT(track_index >= 0); + Q_ASSERT(input_index >= 0); Node* connected_node = edge->output()->parentNode(); @@ -163,7 +163,7 @@ void TrackList::TrackConnected(NodeEdgePtr edge) { // Find "real" index TrackOutput* next = nullptr; - for (int i=track_index+1; iGetSize(); i++) { + for (int i=input_index+1; iGetSize(); i++) { Node* that_track = track_input_->At(i)->get_connected_node(); if (that_track && that_track->IsTrack()) { diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 8078ade32..577c7d15d 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -24,7 +24,10 @@ OLIVE_NAMESPACE_ENTER -ViewerOutput::ViewerOutput() +ViewerOutput::ViewerOutput() : + video_frame_cache_(this), + audio_playback_cache_(this), + operation_stack_(0) { texture_input_ = new NodeInput("tex_in", NodeInput::kTexture); AddInput(texture_input_); @@ -109,40 +112,60 @@ void ViewerOutput::InvalidateCache(const TimeRange &range, NodeInput *from, Node { emit GraphChangedFrom(source); - if (from == texture_input_ || from == samples_input_) { - TimeRange invalidated_range(qMax(rational(), range.in()), - qMin(GetLength(), range.out())); + if (operation_stack_ == 0) { + if (from == texture_input_ || from == samples_input_) { + TimeRange invalidated_range(qMax(rational(), range.in()), + qMin(GetLength(), range.out())); - if (invalidated_range.in() != invalidated_range.out()) { - if (from == texture_input_) { - video_frame_cache_.Invalidate(invalidated_range); - } else { - audio_playback_cache_.Invalidate(invalidated_range); + if (invalidated_range.in() != invalidated_range.out()) { + if (from == texture_input_) { + video_frame_cache_.Invalidate(invalidated_range); + } else { + audio_playback_cache_.Invalidate(invalidated_range); + } } } - } - VerifyLength(); + VerifyLength(); + } Node::InvalidateCache(range, from, source); } void ViewerOutput::set_video_params(const VideoParams &video) { + bool size_changed = video_params_.width() != video.width() || video_params_.height() != video.height(); + bool timebase_changed = video_params_.time_base() != video.time_base(); + bool pixel_aspect_changed = video_params_.pixel_aspect_ratio() != video.pixel_aspect_ratio(); + bool interlacing_changed = video_params_.interlacing() != video.interlacing(); + video_params_ = video; - video_frame_cache_.SetTimebase(video_params_.time_base()); + if (size_changed) { + emit SizeChanged(video_params_.width(), video_params_.height()); + } - emit SizeChanged(video_params_.width(), video_params_.height()); - emit TimebaseChanged(video_params_.time_base()); - emit ParamsChanged(); + if (pixel_aspect_changed) { + emit PixelAspectChanged(video_params_.pixel_aspect_ratio()); + } + + if (interlacing_changed) { + emit InterlacingChanged(video_params_.interlacing()); + } + + if (timebase_changed) { + video_frame_cache_.SetTimebase(video_params_.time_base()); + emit TimebaseChanged(video_params_.time_base()); + } + + emit VideoParamsChanged(); } void ViewerOutput::set_audio_params(const AudioParams &audio) { audio_params_ = audio; - emit ParamsChanged(); + emit AudioParamsChanged(); } rational ViewerOutput::GetLength() @@ -247,6 +270,20 @@ void ViewerOutput::set_media_name(const QString &name) emit MediaNameChanged(media_name_); } +void ViewerOutput::BeginOperation() +{ + operation_stack_++; + + Node::BeginOperation(); +} + +void ViewerOutput::EndOperation() +{ + operation_stack_--; + + Node::EndOperation(); +} + void ViewerOutput::TrackListAddedBlock(Block *block, int index) { Timeline::TrackType type = static_cast(sender())->type(); diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index b2ebb3b34..96c30cdf9 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -118,6 +118,10 @@ public: return &audio_playback_cache_; } + virtual void BeginOperation() override; + + virtual void EndOperation() override; + signals: void TimebaseChanged(const rational&); @@ -127,7 +131,12 @@ signals: void SizeChanged(int width, int height); - void ParamsChanged(); + void PixelAspectChanged(const rational& pixel_aspect); + + void InterlacingChanged(VideoParams::Interlacing mode); + + void VideoParamsChanged(); + void AudioParamsChanged(); void BlockAdded(Block* block, TrackReference track); void BlockRemoved(Block* block); @@ -164,6 +173,8 @@ private: AudioPlaybackCache audio_playback_cache_; + int operation_stack_; + private slots: void UpdateTrackCache(); diff --git a/app/node/param.cpp b/app/node/param.cpp index ff8e72b21..601b393da 100644 --- a/app/node/param.cpp +++ b/app/node/param.cpp @@ -185,16 +185,68 @@ NodeEdgePtr NodeParam::DisconnectForNewOutput(NodeInput *input) return nullptr; } +QString NodeParam::GetPrettyDataTypeName(const NodeParam::DataType &type) +{ + switch (type) { + case kNone: + return tr("None"); + case kInt: + case kCombo: + return tr("Integer"); + case kFloat: + return tr("Float"); + case kRational: + return tr("Rational"); + case kBoolean: + return tr("Boolean"); + case kColor: + return tr("Color"); + case kMatrix: + return tr("Matrix"); + case kText: + return tr("Text"); + case kFont: + return tr("Font"); + case kFile: + return tr("File"); + case kTexture: + return tr("Texture"); + case kSamples: + return tr("Samples"); + case kFootage: + return tr("Footage"); + case kVec2: + return tr("Vector 2D"); + case kVec3: + return tr("Vector 3D"); + case kVec4: + return tr("Vector 4D"); + + case kDecimal: + case kNumber: + case kString: + case kBuffer: + case kVector: + case kShaderJob: + case kSampleJob: + case kGenerateJob: + case kAny: + break; + } + + return tr("Unknown"); +} + QByteArray NodeParam::ValueToBytes(const NodeParam::DataType &type, const QVariant &value) { switch (type) { - case kInt: return ValueToBytesInternal(value); - case kFloat: return ValueToBytesInternal(value); + case kInt: return ValueToBytesInternal(value); + case kFloat: return ValueToBytesInternal(value); case kColor: return ValueToBytesInternal(value); - case kText: return ValueToBytesInternal(value); + case kText: return value.toString().toUtf8(); case kBoolean: return ValueToBytesInternal(value); - case kFont: return ValueToBytesInternal(value); // FIXME: This should probably be a QFont? - case kFile: return ValueToBytesInternal(value); + case kFont: return value.toString().toUtf8(); + case kFile: return value.toString().toUtf8(); case kMatrix: return ValueToBytesInternal(value); case kRational: return ValueToBytesInternal(value); case kVec2: return ValueToBytesInternal(value); @@ -222,39 +274,6 @@ QByteArray NodeParam::ValueToBytes(const NodeParam::DataType &type, const QVaria return QByteArray(); } -NodeParam::DataType NodeParam::StringToDataType(const QString &s) -{ - QString type_id = s.toLower(); - - if (type_id == QStringLiteral("float")) { - return kFloat; - } else if (type_id == QStringLiteral("int")) { - return kInt; - } else if (type_id == QStringLiteral("rational")) { - return kRational; - } else if (type_id == QStringLiteral("bool")) { - return kBoolean; - } else if (type_id == QStringLiteral("color")) { - return kColor; - } else if (type_id == QStringLiteral("matrix")) { - return kMatrix; - } else if (type_id == QStringLiteral("text")) { - return kText; - } else if (type_id == QStringLiteral("texture")) { - return kTexture; - } else if (type_id == QStringLiteral("vec2")) { - return kVec2; - } else if (type_id == QStringLiteral("vec3")) { - return kVec3; - } else if (type_id == QStringLiteral("vec4")) { - return kVec4; - } else if (type_id == QStringLiteral("combo")) { - return kCombo; - } - - return kAny; -} - template QByteArray NodeParam::ValueToBytesInternal(const QVariant &v) { diff --git a/app/node/param.h b/app/node/param.h index 40e5a9716..0157d7bdd 100644 --- a/app/node/param.h +++ b/app/node/param.h @@ -67,7 +67,7 @@ public: /** * Integer type * - * Resolves to `int` (may resolve to `long` in the future). + * Resolves to int64_t. */ kInt = 0x1, @@ -378,18 +378,13 @@ public: /** * @brief Get a human-readable translated name for a certain data type */ - static QString GetDefaultDataTypeName(const DataType &type); + static QString GetPrettyDataTypeName(const DataType &type); /** * @brief Convert a value from a NodeParam into bytes */ static QByteArray ValueToBytes(const DataType &type, const QVariant& value); - /** - * @brief Convert a string to a data type - */ - static DataType StringToDataType(const QString& s); - signals: /** * @brief Signal emitted when an edge is added to this parameter diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index d613060c8..5ba40748f 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -41,11 +41,7 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa database.Insert(input, ProcessInput(input, input_time)); } - // Insert global variables - NodeValueTable global; - global.Push(NodeParam::kFloat, range.in().toDouble(), nullptr, QStringLiteral("time_in")); - global.Push(NodeParam::kFloat, range.out().toDouble(), nullptr, QStringLiteral("time_out")); - database.Insert(QStringLiteral("global"), global); + AddGlobalsToDatabase(database, range); return database; } @@ -156,6 +152,15 @@ QVariant NodeTraverser::GetCachedFrame(const Node *node, const rational &time) return QVariant(); } +void NodeTraverser::AddGlobalsToDatabase(NodeValueDatabase &db, const TimeRange& range) +{ + // Insert global variables + NodeValueTable global; + global.Push(NodeParam::kFloat, range.in().toDouble(), nullptr, QStringLiteral("time_in")); + global.Push(NodeParam::kFloat, range.out().toDouble(), nullptr, QStringLiteral("time_out")); + db.Insert(QStringLiteral("global"), global); +} + void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, NodeValueTable &output_params) { bool got_cached_frame = false; diff --git a/app/node/traverser.h b/app/node/traverser.h index e9e1523fc..e9a377ae4 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -56,6 +56,8 @@ protected: virtual QVariant GetCachedFrame(const Node *node, const rational &time); + static void AddGlobalsToDatabase(NodeValueDatabase& db, const TimeRange &range); + private: void PostProcessTable(const Node *node, const TimeRange &range, NodeValueTable &output_params); diff --git a/app/node/value.cpp b/app/node/value.cpp index d487f0a3d..ef5ac05bd 100644 --- a/app/node/value.cpp +++ b/app/node/value.cpp @@ -44,7 +44,12 @@ void NodeValueDatabase::Insert(const NodeInput *key, const NodeValueTable &value NodeValueTable NodeValueDatabase::Merge() const { - return NodeValueTable::Merge(tables_.values()); + QHash copy = tables_; + + // Kinda hacky, but we don't need this table to slipstream + copy.remove(QStringLiteral("global")); + + return NodeValueTable::Merge(copy.values()); } NodeValue::NodeValue() : @@ -61,26 +66,11 @@ NodeValue::NodeValue(const NodeParam::DataType &type, const QVariant &data, cons { } -const NodeParam::DataType &NodeValue::type() const -{ - return type_; -} - -const QString &NodeValue::tag() const -{ - return tag_; -} - bool NodeValue::operator==(const NodeValue &rhs) const { return type_ == rhs.type_ && tag_ == rhs.tag_ && data_ == rhs.data_; } -const QVariant &NodeValue::data() const -{ - return data_; -} - QVariant NodeValueTable::Get(const NodeParam::DataType &type, const QString &tag) const { return GetWithMeta(type, tag).data(); @@ -191,7 +181,6 @@ NodeValueTable NodeValueTable::Merge(QList tables) NodeValueTable merged_table; // Slipstreams all tables together - // FIXME: I don't actually know if this is the right approach... foreach (const NodeValueTable& t, tables) { if (row >= t.Count()) { continue; diff --git a/app/node/value.h b/app/node/value.h index 387117993..448c1c54d 100644 --- a/app/node/value.h +++ b/app/node/value.h @@ -33,9 +33,25 @@ public: NodeValue(); NodeValue(const NodeParam::DataType& type, const QVariant& data, const Node* from, const QString& tag = QString()); - const NodeParam::DataType& type() const; - const QVariant& data() const; - const QString& tag() const; + const NodeParam::DataType& type() const + { + return type_; + } + + const QVariant& data() const + { + return data_; + } + + const QString& tag() const + { + return tag_; + } + + const Node* source() const + { + return from_; + } bool operator==(const NodeValue& rhs) const; @@ -90,6 +106,23 @@ public: NodeValueTable Merge() const; + using const_iterator = QHash::const_iterator; + + inline QHash::const_iterator begin() const + { + return tables_.cbegin(); + } + + inline QHash::const_iterator end() const + { + return tables_.cend(); + } + + inline bool contains(const QString& s) const + { + return tables_.contains(s); + } + private: QHash tables_; diff --git a/app/panel/CMakeLists.txt b/app/panel/CMakeLists.txt index 0c6423398..75c1c7738 100644 --- a/app/panel/CMakeLists.txt +++ b/app/panel/CMakeLists.txt @@ -23,6 +23,7 @@ add_subdirectory(pixelsampler) add_subdirectory(project) add_subdirectory(scope) add_subdirectory(sequenceviewer) +add_subdirectory(table) add_subdirectory(taskmanager) add_subdirectory(timebased) add_subdirectory(timeline) diff --git a/app/panel/curve/curve.cpp b/app/panel/curve/curve.cpp index 2c9fc0e4b..b6179d258 100644 --- a/app/panel/curve/curve.cpp +++ b/app/panel/curve/curve.cpp @@ -49,11 +49,6 @@ void CurvePanel::SetInput(NodeInput *input) Retranslate(); } -void CurvePanel::SetTimeTarget(Node *target) -{ - static_cast(GetTimeBasedWidget())->SetTimeTarget(target); -} - void CurvePanel::IncreaseTrackHeight() { CurveWidget* c = static_cast(GetTimeBasedWidget()); diff --git a/app/panel/curve/curve.h b/app/panel/curve/curve.h index 4b25b2b22..982a4d0b4 100644 --- a/app/panel/curve/curve.h +++ b/app/panel/curve/curve.h @@ -39,8 +39,6 @@ public: public slots: void SetInput(NodeInput* input); - void SetTimeTarget(Node* target); - virtual void IncreaseTrackHeight() override; virtual void DecreaseTrackHeight() override; diff --git a/app/panel/footageviewer/footageviewer.cpp b/app/panel/footageviewer/footageviewer.cpp index 073177d46..608f7f8cc 100644 --- a/app/panel/footageviewer/footageviewer.cpp +++ b/app/panel/footageviewer/footageviewer.cpp @@ -39,7 +39,14 @@ FootageViewerPanel::FootageViewerPanel(QWidget *parent) : QList FootageViewerPanel::GetSelectedFootage() const { - return {static_cast(GetTimeBasedWidget())->GetFootage()}; + QList list; + Footage* f = static_cast(GetTimeBasedWidget())->GetFootage(); + + if (f) { + list.append(f); + } + + return list; } void FootageViewerPanel::SetFootage(Footage *f) diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index 72a1c7c23..44aad38f6 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -29,7 +29,8 @@ NodePanel::NodePanel(QWidget *parent) : node_view_ = new NodeView(this); // Connect node view signals to this panel - connect(node_view_, SIGNAL(SelectionChanged(QList)), this, SIGNAL(SelectionChanged(QList))); + connect(node_view_, &NodeView::NodesSelected, this, &NodePanel::NodesSelected); + connect(node_view_, &NodeView::NodesDeselected, this, &NodePanel::NodesDeselected); // Set it as the main widget of this panel SetWidgetWithPadding(node_view_); @@ -38,64 +39,4 @@ NodePanel::NodePanel(QWidget *parent) : Retranslate(); } -void NodePanel::SetGraph(NodeGraph *graph) -{ - node_view_->SetGraph(graph); -} - -void NodePanel::SelectAll() -{ - node_view_->SelectAll(); -} - -void NodePanel::DeselectAll() -{ - node_view_->DeselectAll(); -} - -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::Duplicate() -{ - node_view_->Duplicate(); -} - -void NodePanel::Select(const QList &nodes) -{ - node_view_->Select(nodes); -} - -void NodePanel::SelectWithDependencies(const QList &nodes) -{ - node_view_->SelectWithDependencies(nodes); -} - -void NodePanel::SelectBlocks(const QList &nodes) -{ - node_view_->SelectBlocks(nodes); -} - -void NodePanel::Retranslate() -{ - SetTitle(tr("Node Editor")); -} - OLIVE_NAMESPACE_EXIT diff --git a/app/panel/node/node.h b/app/panel/node/node.h index c2b4f370e..8580b7c1e 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -35,34 +35,77 @@ class NodePanel : public PanelWidget public: NodePanel(QWidget* parent); - void SetGraph(NodeGraph* graph); + void SetGraph(NodeGraph *graph) + { + node_view_->SetGraph(graph); + } - virtual void SelectAll() override; - virtual void DeselectAll() override; + virtual void SelectAll() override + { + node_view_->SelectAll(); + } - virtual void DeleteSelected() override; + virtual void DeselectAll() override + { + node_view_->DeselectAll(); + } - virtual void CutSelected() override; - virtual void CopySelected() override; + virtual void DeleteSelected() override + { + node_view_->DeleteSelected(); + } - virtual void Paste() override; + virtual void CutSelected() override + { + node_view_->CopySelected(true); + } - virtual void Duplicate() override; + virtual void CopySelected() override + { + node_view_->CopySelected(false); + } + + virtual void Paste() override + { + node_view_->Paste(); + } + + virtual void Duplicate() override + { + node_view_->Duplicate(); + } public slots: - void Select(const QList& nodes); - void SelectWithDependencies(const QList& nodes); + void Select(const QList& nodes) + { + node_view_->Select(nodes); + } - void SelectBlocks(const QList& nodes); + void SelectWithDependencies(const QList& nodes) + { + node_view_->SelectWithDependencies(nodes); + } + + void SelectBlocks(const QList& nodes) + { + node_view_->SelectBlocks(nodes); + } + + void DeselectBlocks(const QList& nodes) + { + node_view_->DeselectBlocks(nodes); + } signals: - /** - * @brief Wrapper for NodeView::SelectionChanged() - */ - void SelectionChanged(QList selected_nodes); + void NodesSelected(const QList& nodes); + + void NodesDeselected(const QList& nodes); private: - virtual void Retranslate() override; + virtual void Retranslate() override + { + SetTitle(tr("Node Editor")); + } NodeView* node_view_; diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index 649998cdf..519250837 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -29,19 +29,23 @@ ParamPanel::ParamPanel(QWidget* parent) : { NodeParamView* view = new NodeParamView(); connect(view, &NodeParamView::InputDoubleClicked, this, &ParamPanel::CreateCurvePanel); - connect(view, &NodeParamView::TimeTargetChanged, this, &ParamPanel::TimeTargetChanged); connect(view, &NodeParamView::RequestSelectNode, this, &ParamPanel::RequestSelectNode); - connect(view, &NodeParamView::OpenedNode, this, &ParamPanel::OpeningNode); - connect(view, &NodeParamView::ClosedNode, this, &ParamPanel::ClosingNode); - connect(view, &NodeParamView::FoundGizmos, this, &ParamPanel::FoundGizmos); + //connect(view, &NodeParamView::FoundGizmos, this, &ParamPanel::FoundGizmos); SetTimeBasedWidget(view); Retranslate(); } -void ParamPanel::SetNodes(QList nodes) +void ParamPanel::SelectNodes(const QList &nodes) { - static_cast(GetTimeBasedWidget())->SetNodes(nodes); + static_cast(GetTimeBasedWidget())->SelectNodes(nodes); + + Retranslate(); +} + +void ParamPanel::DeselectNodes(const QList &nodes) +{ + static_cast(GetTimeBasedWidget())->DeselectNodes(nodes); Retranslate(); } @@ -51,13 +55,7 @@ void ParamPanel::SetTimestamp(const int64_t ×tamp) TimeBasedPanel::SetTimestamp(timestamp); // Ensure all CurvePanels are updated with this time too - QHash::const_iterator i; - - for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { - if (i.value() && i.value() != sender()) { - i.value()->SetTimestamp(timestamp); - } - } + ParamViewTimeChanged(timestamp); } void ParamPanel::DeleteSelected() @@ -71,10 +69,10 @@ void ParamPanel::Retranslate() NodeParamView* view = static_cast(GetTimeBasedWidget()); - if (view->nodes().isEmpty()) { + if (view->GetItemMap().isEmpty()) { SetSubtitle(tr("(none)")); - } else if (view->nodes().size() == 1) { - SetSubtitle(view->nodes().first()->Name()); + } else if (view->GetItemMap().size() == 1) { + SetSubtitle(view->GetItemMap().firstKey()->Name()); } else { SetSubtitle(tr("(multiple)")); } @@ -97,52 +95,52 @@ void ParamPanel::CreateCurvePanel(NodeInput *input) panel = Core::instance()->main_window()->AppendCurvePanel(); - panel->SetInput(input); - panel->SetTimebase(view->timebase()); + panel->ConnectViewerNode(view->GetConnectedNode()); panel->SetTimestamp(view->GetTimestamp()); - panel->SetTimeTarget(view->GetTimeTarget()); + panel->SetInput(input); - connect(view, &NodeParamView::TimebaseChanged, panel, &CurvePanel::SetTimebase); - connect(view, &NodeParamView::TimeChanged, panel, &CurvePanel::SetTimestamp); - connect(view, &NodeParamView::TimeTargetChanged, panel, &CurvePanel::SetTimeTarget); - connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::SetTimestamp); - connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::TimeChanged); + connect(view, &NodeParamView::TimeChanged, this, &ParamPanel::ParamViewTimeChanged); + connect(panel, &CurvePanel::TimeChanged, this, &ParamPanel::CurvePanelTimeChanged); connect(panel, &CurvePanel::CloseRequested, this, &ParamPanel::ClosingCurvePanel); open_curve_panels_.insert(input, panel); } -void ParamPanel::OpeningNode(Node *n) -{ - QList inputs = n->GetInputsIncludingArrays(); - - foreach (NodeInput* i, inputs) { - if (open_curve_panels_.contains(i)) { - // We had a CurvePanel open for this input that was closed in ClosingNode(), re-open it - CreateCurvePanel(i); - } - } -} - -void ParamPanel::ClosingNode(Node *n) -{ - QList inputs = n->GetInputsIncludingArrays(); - - foreach (NodeInput* i, inputs) { - CurvePanel* panel = open_curve_panels_.value(i); - - // Close the panel (this also destroys it), but keep a reference in the hash - if (panel) { - panel->close(); - open_curve_panels_.insert(i, nullptr); - } - } -} - void ParamPanel::ClosingCurvePanel() { CurvePanel* panel = static_cast(sender()); open_curve_panels_.remove(panel->GetInput()); } +void ParamPanel::ParamViewTimeChanged(const int64_t &time) +{ + // Ensure all CurvePanels are updated with this time too + QHash::const_iterator i; + + for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { + // If connected viewers are the same, set the timestamp + if (i.value()->GetConnectedViewer() == GetConnectedViewer()) { + i.value()->SetTimestamp(time); + } + } +} + +void ParamPanel::CurvePanelTimeChanged(const int64_t &time) +{ + GetTimeBasedWidget()->SetTimestamp(time); + emit GetTimeBasedWidget()->TimeChanged(time); + + CurvePanel* src = static_cast(sender()); + + // Ensure all CurvePanels are updated with this time too + QHash::const_iterator i; + + for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { + // If connected viewers are the same and the panel isn't the source, set the timestamp + if (i.value() != src && i.value()->GetConnectedViewer() == src->GetConnectedViewer()) { + i.value()->SetTimestamp(time); + } + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/panel/param/param.h b/app/panel/param/param.h index 0227e45ba..442c5e921 100644 --- a/app/panel/param/param.h +++ b/app/panel/param/param.h @@ -34,15 +34,14 @@ public: ParamPanel(QWidget* parent); public slots: - void SetNodes(QList nodes); + void SelectNodes(const QList& nodes); + void DeselectNodes(const QList& nodes); virtual void SetTimestamp(const int64_t& timestamp) override; virtual void DeleteSelected() override; signals: - void TimeTargetChanged(Node* node); - void RequestSelectNode(const QList& target); void FoundGizmos(Node* node); @@ -53,15 +52,16 @@ protected: private slots: void CreateCurvePanel(NodeInput* input); - void OpeningNode(Node* n); - - void ClosingNode(Node* n); - void ClosingCurvePanel(); private: QHash open_curve_panels_; +private slots: + void ParamViewTimeChanged(const int64_t& time); + + void CurvePanelTimeChanged(const int64_t& time); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/panel/table/CMakeLists.txt b/app/panel/table/CMakeLists.txt new file mode 100644 index 000000000..f332aa646 --- /dev/null +++ b/app/panel/table/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + panel/table/table.h + panel/table/table.cpp + PARENT_SCOPE +) diff --git a/app/panel/table/table.cpp b/app/panel/table/table.cpp new file mode 100644 index 000000000..0b3c5260f --- /dev/null +++ b/app/panel/table/table.cpp @@ -0,0 +1,38 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "table.h" + +OLIVE_NAMESPACE_ENTER + +NodeTablePanel::NodeTablePanel(QWidget* parent) : + TimeBasedPanel(QStringLiteral("NodeTablePanel"), parent) +{ + SetTimeBasedWidget(new NodeTableWidget()); + + Retranslate(); +} + +void NodeTablePanel::Retranslate() +{ + SetTitle(tr("Table View")); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/panel/table/table.h b/app/panel/table/table.h new file mode 100644 index 000000000..85c6ea888 --- /dev/null +++ b/app/panel/table/table.h @@ -0,0 +1,53 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLEPANEL_H +#define NODETABLEPANEL_H + +#include "panel/timebased/timebased.h" +#include "widget/nodetableview/nodetablewidget.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTablePanel : public TimeBasedPanel +{ + Q_OBJECT +public: + NodeTablePanel(QWidget* parent); + +public slots: + void SelectNodes(const QList& nodes) + { + static_cast(GetTimeBasedWidget())->SelectNodes(nodes); + } + + void DeselectNodes(const QList& nodes) + { + static_cast(GetTimeBasedWidget())->DeselectNodes(nodes); + } + +private: + virtual void Retranslate() override; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLEPANEL_H diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index 37a0959fa..5e95e944d 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -125,6 +125,10 @@ TimeRuler *TimeBasedPanel::ruler() const void TimeBasedPanel::ConnectViewerNode(ViewerOutput *node) { + if (widget_->GetConnectedNode() == node) { + return; + } + if (widget_->GetConnectedNode()) { disconnect(widget_->GetConnectedNode(), &ViewerOutput::MediaNameChanged, this, &TimeBasedPanel::SetSubtitle); } diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 4b42ea3f7..2a36987b3 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -33,7 +33,8 @@ TimelinePanel::TimelinePanel(QWidget *parent) : Retranslate(); - connect(tw, &TimelineWidget::SelectionChanged, this, &TimelinePanel::SelectionChanged); + connect(tw, &TimelineWidget::BlocksSelected, this, &TimelinePanel::BlocksSelected); + connect(tw, &TimelineWidget::BlocksDeselected, this, &TimelinePanel::BlocksDeselected); } void TimelinePanel::Clear() @@ -46,6 +47,16 @@ void TimelinePanel::SplitAtPlayhead() static_cast(GetTimeBasedWidget())->SplitAtPlayhead(); } +QByteArray TimelinePanel::SaveSplitterState() const +{ + return static_cast(GetTimeBasedWidget())->SaveSplitterState(); +} + +void TimelinePanel::RestoreSplitterState(const QByteArray &state) +{ + static_cast(GetTimeBasedWidget())->RestoreSplitterState(state); +} + void TimelinePanel::SelectAll() { static_cast(GetTimeBasedWidget())->SelectAll(); diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index bafc9b0d1..b05f7b8bc 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -39,6 +39,10 @@ public: void SplitAtPlayhead(); + QByteArray SaveSplitterState() const; + + void RestoreSplitterState(const QByteArray& state); + virtual void SelectAll() override; virtual void DeselectAll() override; @@ -87,7 +91,9 @@ protected: virtual void Retranslate() override; signals: - void SelectionChanged(const QList& selected_blocks); + void BlocksSelected(const QList& selected_blocks); + + void BlocksDeselected(const QList& deselected_blocks); }; diff --git a/app/panel/tool/tool.cpp b/app/panel/tool/tool.cpp index ac8b1162d..7822ece90 100644 --- a/app/panel/tool/tool.cpp +++ b/app/panel/tool/tool.cpp @@ -42,6 +42,7 @@ ToolPanel::ToolPanel(QWidget *parent) : connect(Core::instance(), &Core::SnappingChanged, t, &Toolbar::SetSnapping); connect(t, &Toolbar::AddableObjectChanged, Core::instance(), &Core::SetSelectedAddableObject); + connect(t, &Toolbar::SelectedTransitionChanged, Core::instance(), &Core::SetSelectedTransitionObject); Retranslate(); } diff --git a/app/panel/viewer/viewerbase.cpp b/app/panel/viewer/viewerbase.cpp index 13a8c7394..47ebbe0e0 100644 --- a/app/panel/viewer/viewerbase.cpp +++ b/app/panel/viewer/viewerbase.cpp @@ -90,6 +90,16 @@ void ViewerPanelBase::SetGizmos(Node *node) static_cast(GetTimeBasedWidget())->SetGizmos(node); } +void ViewerPanelBase::CacheEntireSequence() +{ + static_cast(GetTimeBasedWidget())->CacheEntireSequence(); +} + +void ViewerPanelBase::CacheSequenceInOut() +{ + static_cast(GetTimeBasedWidget())->CacheSequenceInOut(); +} + void ViewerPanelBase::CreateScopePanel(ScopePanel::Type type) { ViewerWidget* vw = static_cast(GetTimeBasedWidget()); diff --git a/app/panel/viewer/viewerbase.h b/app/panel/viewer/viewerbase.h index a3261da50..36ac0aa35 100644 --- a/app/panel/viewer/viewerbase.h +++ b/app/panel/viewer/viewerbase.h @@ -57,6 +57,10 @@ public: public slots: void SetGizmos(Node* node); + void CacheEntireSequence(); + + void CacheSequenceInOut(); + protected: void CreateScopePanel(ScopePanel::Type type); diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 4acb16569..56136de0f 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -21,6 +21,7 @@ #include "footage.h" #include +#include #include "codec/decoder.h" #include "common/xmlutils.h" @@ -52,6 +53,27 @@ void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const Q } } + // Validate filename + if (!QFileInfo::exists(filename_)) { + // Absolute filename does not exist, use some heuristics to try relocating the file + + if (xml_node_data.real_project_url != xml_node_data.saved_project_url) { + // Project path has changed, check if the file we're looking for is the same relative to the + // new project path + QDir saved_dir(QFileInfo(xml_node_data.saved_project_url).dir()); + QDir true_dir(QFileInfo(xml_node_data.real_project_url).dir()); + + QString relative_filename = saved_dir.relativeFilePath(filename_); + QString transformed_abs_filename = true_dir.filePath(relative_filename); + + if (QFileInfo::exists(transformed_abs_filename)) { + // Use this file instead + qInfo() << "Footage" << filename_ << "doesn't exist, using relative file" << transformed_abs_filename; + set_filename(transformed_abs_filename); + } + } + } + Decoder::ProbeMedia(this, cancelled); while (XMLReadNextStartElement(reader)) { @@ -220,38 +242,53 @@ QIcon Footage::icon() QString Footage::duration() { - if (streams_.isEmpty()) { - return QString(); + // Find longest stream duration + + StreamPtr longest_stream = nullptr; + rational longest; + + foreach (StreamPtr stream, streams_) { + if (stream->type() == Stream::kVideo || stream->type() == Stream::kAudio) { + rational this_stream_dur = Timecode::timestamp_to_time(stream->duration(), + stream->timebase()); + + if (this_stream_dur > longest) { + longest_stream = stream; + longest = this_stream_dur; + } + } } - if (streams_.first()->type() == Stream::kVideo) { - VideoStreamPtr video_stream = std::static_pointer_cast(streams_.first()); + if (longest_stream) { + if (longest_stream->type() == Stream::kVideo) { + VideoStreamPtr video_stream = std::static_pointer_cast(longest_stream); - int64_t duration = video_stream->duration(); - rational frame_rate_timebase = video_stream->frame_rate().flipped(); + int64_t duration = video_stream->duration(); + rational frame_rate_timebase = video_stream->frame_rate().flipped(); - if (video_stream->timebase() != frame_rate_timebase) { - // Convert from timebase to frame rate - rational duration_time = Timecode::timestamp_to_time(duration, video_stream->timebase()); - duration = Timecode::time_to_timestamp(duration_time, frame_rate_timebase); + if (video_stream->timebase() != frame_rate_timebase) { + // Convert from timebase to frame rate + rational duration_time = Timecode::timestamp_to_time(duration, video_stream->timebase()); + duration = Timecode::time_to_timestamp(duration_time, frame_rate_timebase); + } + + return Timecode::timestamp_to_timecode(duration, + frame_rate_timebase, + Core::instance()->GetTimecodeDisplay()); + } else if (longest_stream->type() == Stream::kAudio) { + AudioStreamPtr audio_stream = std::static_pointer_cast(longest_stream); + + // If we're showing in a timecode, we prefer showing audio in seconds instead + Timecode::Display display = Core::instance()->GetTimecodeDisplay(); + if (display == Timecode::kTimecodeDropFrame + || display == Timecode::kTimecodeNonDropFrame) { + display = Timecode::kTimecodeSeconds; + } + + return Timecode::timestamp_to_timecode(longest_stream->duration(), + longest_stream->timebase(), + display); } - - return Timecode::timestamp_to_timecode(duration, - frame_rate_timebase, - 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 = Core::instance()->GetTimecodeDisplay(); - if (display == Timecode::kTimecodeDropFrame - || display == Timecode::kTimecodeNonDropFrame) { - display = Timecode::kTimecodeSeconds; - } - - return Timecode::timestamp_to_timecode(streams_.first()->duration(), - streams_.first()->timebase(), - display); } return QString(); @@ -263,15 +300,13 @@ QString Footage::rate() return QString(); } - if (streams_.first()->type() == Stream::kVideo) { - // Return the timebase as a frame rate - VideoStreamPtr video_stream = std::static_pointer_cast(streams_.first()); - + if (HasStreamsOfType(Stream::kVideo)) { + // This is a video editor, prioritize video streams + VideoStreamPtr video_stream = std::static_pointer_cast(get_first_stream_of_type(Stream::kVideo)); return QCoreApplication::translate("Footage", "%1 FPS").arg(video_stream->frame_rate().toDouble()); - } else if (streams_.first()->type() == Stream::kAudio) { - // Return the sample rate + } else if (HasStreamsOfType(Stream::kAudio)) { + // No video streams, return audio AudioStreamPtr audio_stream = std::static_pointer_cast(streams_.first()); - return QCoreApplication::translate("Footage", "%1 Hz").arg(audio_stream->sample_rate()); } diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp index beb5f3b3b..a4a53df0a 100644 --- a/app/project/item/footage/imagestream.cpp +++ b/app/project/item/footage/imagestream.cpp @@ -28,7 +28,9 @@ OLIVE_NAMESPACE_ENTER ImageStream::ImageStream() : - premultiplied_alpha_(false) + premultiplied_alpha_(false), + interlacing_(VideoParams::kInterlaceNone), + pixel_aspect_ratio_(1) { set_type(kImage); } @@ -72,26 +74,6 @@ QString ImageStream::description() const QString::number(height())); } -const int &ImageStream::width() const -{ - return width_; -} - -void ImageStream::set_width(const int &width) -{ - width_ = width; -} - -const int &ImageStream::height() const -{ - return height_; -} - -void ImageStream::set_height(const int &height) -{ - height_ = height; -} - bool ImageStream::premultiplied_alpha() const { return premultiplied_alpha_; diff --git a/app/project/item/footage/imagestream.h b/app/project/item/footage/imagestream.h index 9e3b6f72b..dfa01bab9 100644 --- a/app/project/item/footage/imagestream.h +++ b/app/project/item/footage/imagestream.h @@ -21,6 +21,8 @@ #ifndef IMAGESTREAM_H #define IMAGESTREAM_H +#include "render/pixelformat.h" +#include "render/videoparams.h" #include "stream.h" OLIVE_NAMESPACE_ENTER @@ -36,11 +38,35 @@ public: virtual QString description() const override; - const int& width() const; - void set_width(const int& width); + const int& width() const + { + return width_; + } - const int& height() const; - void set_height(const int& height); + void set_width(const int& width) + { + width_ = width; + } + + const int& height() const + { + return height_; + } + + void set_height(const int& height) + { + height_ = height; + } + + const PixelFormat::Format& format() const + { + return format_; + } + + void set_format(const PixelFormat::Format& format) + { + format_ = format; + } bool premultiplied_alpha() const; void set_premultiplied_alpha(bool e); @@ -50,6 +76,35 @@ public: QString get_colorspace_match_string() const; + VideoParams::Interlacing interlacing() const + { + return interlacing_; + } + + void set_interlacing(VideoParams::Interlacing i) + { + interlacing_ = i; + + emit ParametersChanged(); + } + + const rational& pixel_aspect_ratio() const + { + return pixel_aspect_ratio_; + } + + void set_pixel_aspect_ratio(const rational& r) + { + // Auto-correct null aspect ratio to 1:1 + if (r.isNull()) { + pixel_aspect_ratio_ = 1; + } else { + pixel_aspect_ratio_ = r; + } + + emit ParametersChanged(); + } + protected: virtual void FootageSetEvent(Footage*) override; @@ -62,11 +117,17 @@ private: int height_; bool premultiplied_alpha_; QString colorspace_; + VideoParams::Interlacing interlacing_; + + PixelFormat::Format format_; + + rational pixel_aspect_ratio_; private slots: void ColorConfigChanged(); void DefaultColorSpaceChanged(); + }; using ImageStreamPtr = std::shared_ptr; diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index b2c5a0c22..fcf76c8dd 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -65,7 +65,8 @@ void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const if (reader->name() == QStringLiteral("video")) { int video_width = 0, video_height = 0, preview_div = 1; - rational video_timebase; + rational video_timebase, video_pixel_aspect; + VideoParams::Interlacing video_interlacing = VideoParams::kInterlaceNone; PixelFormat::Format preview_format = PixelFormat::PIX_FMT_INVALID; while (XMLReadNextStartElement(reader)) { @@ -83,12 +84,17 @@ void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, const preview_div = reader->readElementText().toInt(); } else if (reader->name() == QStringLiteral("format")) { preview_format = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("pixelaspect")) { + video_pixel_aspect = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("interlacing")) { + video_interlacing = static_cast(reader->readElementText().toInt()); } else { reader->skipCurrentElement(); } } - set_video_params(VideoParams(video_width, video_height, video_timebase, preview_format, preview_div)); + set_video_params(VideoParams(video_width, video_height, video_timebase, preview_format, + video_pixel_aspect, video_interlacing, preview_div)); } else if (reader->name() == QStringLiteral("audio")) { int rate = 0; uint64_t layout = 0; @@ -149,13 +155,15 @@ void Sequence::Save(QXmlStreamWriter *writer) const writer->writeAttribute(QStringLiteral("name"), name()); - writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(viewer_output_))); + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); writer->writeStartElement(QStringLiteral("video")); writer->writeTextElement(QStringLiteral("width"), QString::number(video_params().width())); writer->writeTextElement(QStringLiteral("height"), QString::number(video_params().height())); writer->writeTextElement(QStringLiteral("timebase"), video_params().time_base().toString()); + writer->writeTextElement(QStringLiteral("pixelaspect"), video_params().pixel_aspect_ratio().toString()); + writer->writeTextElement(QStringLiteral("interlacing"), QString::number(video_params().interlacing())); writer->writeTextElement(QStringLiteral("divider"), QString::number(video_params().divider())); writer->writeTextElement(QStringLiteral("format"), QString::number(video_params().format())); @@ -245,6 +253,8 @@ void Sequence::set_default_parameters() height, Config::Current()["DefaultSequenceFrameRate"].value(), static_cast(Config::Current()["DefaultSequencePreviewFormat"].toInt()), + Config::Current()["DefaultSequencePixelAspect"].value(), + Config::Current()["DefaultSequenceInterlacing"].value(), VideoParams::generate_auto_divider(width, height))); set_audio_params(AudioParams(Config::Current()["DefaultSequenceAudioFrequency"].toInt(), Config::Current()["DefaultSequenceAudioLayout"].toULongLong(), @@ -269,6 +279,8 @@ void Sequence::set_parameters_from_footage(const QList footage) vs->height(), vs->frame_rate().flipped(), static_cast(Config::Current()["DefaultSequencePreviewFormat"].toInt()), + vs->pixel_aspect_ratio(), + vs->interlacing(), VideoParams::generate_auto_divider(vs->width(), vs->height()))); found_video_params = true; } @@ -284,6 +296,8 @@ void Sequence::set_parameters_from_footage(const QList footage) is->height(), video_params().time_base(), static_cast(Config::Current()["DefaultSequencePreviewFormat"].toInt()), + is->pixel_aspect_ratio(), + is->interlacing(), VideoParams::generate_auto_divider(is->width(), is->height()))); } break; diff --git a/app/project/project.cpp b/app/project/project.cpp index 014489dd6..fe1e307aa 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -26,6 +26,7 @@ #include "common/xmlutils.h" #include "core.h" #include "dialog/progress/progress.h" +#include "render/diskmanager.h" #include "window/mainwindow/mainwindow.h" OLIVE_NAMESPACE_ENTER @@ -37,10 +38,13 @@ Project::Project() : root_.set_project(this); } -void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) +void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const QAtomicInt* cancelled) { XMLNodeData xml_node_data; + // Set project filename (hacky) + xml_node_data.real_project_url = static_cast(reader->device())->fileName(); + while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("folder")) { @@ -60,12 +64,29 @@ void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) } } + } else if (reader->name() == QStringLiteral("cachepath")) { + + set_cache_path(reader->readElementText()); + } else if (reader->name() == QStringLiteral("layout")) { - Core::instance()->main_window()->LoadLayout(reader, xml_node_data); + // Since the main window's functions have to occur in the GUI thread (and we're likely + // loading in a secondary thread), we load all necessary data into a separate struct so we + // can continue loading and queue it with the main window so it can handle the data + // appropriately in its own thread. + + *layout = MainWindowLayoutInfo::fromXml(reader, xml_node_data); + + } else if (reader->name() == QStringLiteral("url")) { + + // This should be read in before most other elements + xml_node_data.saved_project_url = reader->readElementText(); } else { + + // Skip this reader->skipCurrentElement(); + } } @@ -82,6 +103,8 @@ void Project::Save(QXmlStreamWriter *writer) const writer->writeTextElement("url", filename_); + writer->writeTextElement("cachepath", cache_path(false)); + root_.Save(writer); writer->writeStartElement("colormanagement"); @@ -93,7 +116,8 @@ void Project::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // colormanagement // Save main window project layout - Core::instance()->main_window()->SaveLayout(writer); + MainWindowLayoutInfo main_window_info = Core::instance()->main_window()->SaveLayout(); + main_window_info.toXml(writer); writer->writeEndElement(); // project } @@ -178,4 +202,12 @@ bool Project::is_new() const return !is_modified_ && filename_.isEmpty(); } +const QString &Project::cache_path(bool default_if_empty) const +{ + if (cache_path_.isEmpty() && default_if_empty) { + return DiskManager::instance()->GetDefaultCachePath(); + } + return cache_path_; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/project/project.h b/app/project/project.h index f61fc50da..9fa99ce48 100644 --- a/app/project/project.h +++ b/app/project/project.h @@ -26,6 +26,7 @@ #include "render/colormanager.h" #include "project/item/folder/folder.h" +#include "window/mainwindow/mainwindowlayoutinfo.h" OLIVE_NAMESPACE_ENTER @@ -46,7 +47,7 @@ class Project : public QObject public: Project(); - void Load(QXmlStreamReader* reader, const QAtomicInt* cancelled); + void Load(QXmlStreamReader* reader, MainWindowLayoutInfo *layout, const QAtomicInt* cancelled); void Save(QXmlStreamWriter* writer) const; @@ -70,11 +71,10 @@ public: bool is_new() const; - const QString& cache_path() const { - return cache_path_; - } + const QString& cache_path(bool default_if_empty = true) const; - void set_cache_path(const QString& cache_path) { + void set_cache_path(const QString& cache_path) + { cache_path_ = cache_path; } diff --git a/app/render/audioparams.cpp b/app/render/audioparams.cpp index 35c51aedf..90ab792d9 100644 --- a/app/render/audioparams.cpp +++ b/app/render/audioparams.cpp @@ -24,8 +24,31 @@ extern "C" { #include } +#include + OLIVE_NAMESPACE_ENTER +const QVector AudioParams::kSupportedSampleRates = { + 8000, // 8000 Hz + 11025, // 11025 Hz + 16000, // 16000 Hz + 22050, // 22050 Hz + 24000, // 24000 Hz + 32000, // 32000 Hz + 44100, // 44100 Hz + 48000, // 48000 Hz + 88200, // 88200 Hz + 96000 // 96000 Hz +}; + +const QVector AudioParams::kSupportedChannelLayouts = { + AV_CH_LAYOUT_MONO, + AV_CH_LAYOUT_STEREO, + AV_CH_LAYOUT_2_1, + AV_CH_LAYOUT_5POINT1, + AV_CH_LAYOUT_7POINT1 +}; + int AudioParams::time_to_bytes(const double &time) const { Q_ASSERT(is_valid()); @@ -127,4 +150,27 @@ bool AudioParams::is_valid() const && format_ != SampleFormat::SAMPLE_FMT_COUNT); } +QString AudioParams::SampleRateToString(const int &sample_rate) +{ + return QCoreApplication::translate("AudioParams", "%1 Hz").arg(sample_rate); +} + +QString AudioParams::ChannelLayoutToString(const uint64_t &layout) +{ + switch (layout) { + case AV_CH_LAYOUT_MONO: + return QCoreApplication::translate("AudioParams", "Mono"); + case AV_CH_LAYOUT_STEREO: + return QCoreApplication::translate("AudioParams", "Stereo"); + case AV_CH_LAYOUT_2_1: + return QCoreApplication::translate("AudioParams", "2.1"); + case AV_CH_LAYOUT_5POINT1: + return QCoreApplication::translate("AudioParams", "5.1"); + case AV_CH_LAYOUT_7POINT1: + return QCoreApplication::translate("AudioParams", "7.1"); + default: + return QCoreApplication::translate("AudioParams", "Unknown (0x%1)").arg(layout, 1, 16); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/render/audioparams.h b/app/render/audioparams.h index 617685a5e..56028a579 100644 --- a/app/render/audioparams.h +++ b/app/render/audioparams.h @@ -80,6 +80,19 @@ public: bool operator==(const AudioParams& other) const; bool operator!=(const AudioParams& other) const; + static const QVector kSupportedChannelLayouts; + static const QVector kSupportedSampleRates; + + /** + * @brief Convert integer sample rate to a user-friendly string + */ + static QString SampleRateToString(const int &sample_rate); + + /** + * @brief Convert channel layout to a user-friendly string + */ + static QString ChannelLayoutToString(const uint64_t &layout); + private: int sample_rate_; diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 98503223e..9b9e8897b 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -28,7 +28,8 @@ OLIVE_NAMESPACE_ENTER -AudioPlaybackCache::AudioPlaybackCache() +AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : + PlaybackCache(parent) { quint32 r = std::rand(); UpdateFilename(QString::number(r)); @@ -36,8 +37,6 @@ AudioPlaybackCache::AudioPlaybackCache() void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) { - QMutexLocker locker(lock()); - if (params_ == params) { return; } @@ -51,22 +50,17 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) } // Our current audio cache is unusable, so we truncate it automatically - TimeRange invalidate_range(0, NoLockGetLength()); + TimeRange invalidate_range(0, GetLength()); if (invalidate_range.in() != invalidate_range.out()) { - NoLockInvalidate(invalidate_range); + Invalidate(invalidate_range); } - locker.unlock(); - emit ParametersChanged(); - emit Invalidated(invalidate_range); } void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const qint64 &job_time) { - QMutexLocker locker(lock()); - - QList valid_ranges = NoLockGetValidRanges(range, job_time); + QList valid_ranges = GetValidRanges(range, job_time); if (valid_ranges.isEmpty()) { return; } @@ -101,11 +95,7 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample f.close(); - NoLockValidate(range); - - locker.unlock(); - - emit Validated(range); + Validate(range); } else { qWarning() << "Failed to write PCM data to" << filename_; } @@ -113,8 +103,6 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample void AudioPlaybackCache::WriteSilence(const TimeRange &range) { - QMutexLocker locker(lock()); - QFile f(filename_); if (f.open(QFile::ReadWrite)) { qint64 start_offset = params_.time_to_bytes(range.in()); @@ -131,6 +119,8 @@ void AudioPlaybackCache::WriteSilence(const TimeRange &range) f.write(a); f.close(); + + Validate(range); } else { qWarning() << "Failed to write PCM data to" << filename_; } @@ -231,7 +221,7 @@ void AudioPlaybackCache::LengthChangedEvent(const rational& old, const rational& } } -QList AudioPlaybackCache::NoLockGetValidRanges(const TimeRange& range, const qint64& job_time) +QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, const qint64& job_time) { QList valid_ranges; @@ -248,11 +238,11 @@ QList AudioPlaybackCache::NoLockGetValidRanges(const TimeRange& range void AudioPlaybackCache::UpdateFilename(const QString &s) { - filename_ = QDir(FileFunctions::GetMediaCacheLocation()).filePath(s); + filename_ = QDir(GetCacheDirectory()).filePath(s); filename_.append(QStringLiteral(".pcm")); } -const QString &AudioPlaybackCache::GetCacheFilename() const +const QString &AudioPlaybackCache::GetPCMFilename() const { return filename_; } diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index 9e1328fbc..facba48d3 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -31,10 +31,10 @@ class AudioPlaybackCache : public PlaybackCache { Q_OBJECT public: - AudioPlaybackCache(); + AudioPlaybackCache(QObject* parent = nullptr); - AudioParams GetParameters() { - QMutexLocker locker(lock()); + AudioParams GetParameters() + { return params_; } @@ -46,14 +46,9 @@ public: //void SetUuid(const QUuid& id); - const QString& GetCacheFilename() const; + const QString& GetPCMFilename() const; - QList GetValidRanges(const TimeRange &range, const qint64 &job_time) - { - QMutexLocker locker(lock()); - - return NoLockGetValidRanges(range, job_time); - } + QList GetValidRanges(const TimeRange &range, const qint64 &job_time); signals: void ParametersChanged(); @@ -64,8 +59,6 @@ protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; private: - QList NoLockGetValidRanges(const TimeRange &range, const qint64 &job_time); - void UpdateFilename(const QString& s); QString filename_; diff --git a/app/render/backend/opengl/openglbackend.cpp b/app/render/backend/opengl/openglbackend.cpp index a66fa8928..60e81f914 100644 --- a/app/render/backend/opengl/openglbackend.cpp +++ b/app/render/backend/opengl/openglbackend.cpp @@ -27,38 +27,17 @@ OLIVE_NAMESPACE_ENTER OpenGLBackend::OpenGLBackend(QObject* parent) : RenderBackend(parent) { - proxy_ = new OpenGLProxy(); - QThread* proxy_thread = new QThread(); - proxy_thread->start(QThread::IdlePriority); - proxy_->moveToThread(proxy_thread); - - if (!proxy_->Init()) { - ClearProxy(); - } } OpenGLBackend::~OpenGLBackend() { Close(); - - ClearProxy(); } RenderWorker *OpenGLBackend::CreateNewWorker() { - return new OpenGLWorker(this, proxy_); -} - -void OpenGLBackend::ClearProxy() -{ - if (proxy_) { - proxy_->thread()->quit(); - proxy_->thread()->wait(); - proxy_->thread()->deleteLater(); - proxy_->deleteLater(); - proxy_ = nullptr; - } + return new OpenGLWorker(this); } OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglbackend.h b/app/render/backend/opengl/openglbackend.h index a1df6dcab..7d88611f3 100644 --- a/app/render/backend/opengl/openglbackend.h +++ b/app/render/backend/opengl/openglbackend.h @@ -36,11 +36,6 @@ public: protected: virtual RenderWorker* CreateNewWorker() override; -private: - void ClearProxy(); - - OpenGLProxy* proxy_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index aff4dac42..224f52c41 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -33,6 +33,8 @@ OLIVE_NAMESPACE_ENTER +OpenGLProxy* OpenGLProxy::instance_ = nullptr; + OpenGLProxy::OpenGLProxy(QObject *parent) : QObject(parent), ctx_(nullptr), @@ -48,6 +50,30 @@ OpenGLProxy::~OpenGLProxy() surface_.destroy(); } +void OpenGLProxy::CreateInstance() +{ + instance_ = new OpenGLProxy(); + + QThread* proxy_thread = new QThread(); + proxy_thread->start(QThread::IdlePriority); + instance_->moveToThread(proxy_thread); + + if (!instance_->Init()) { + DestroyInstance(); + } +} + +void OpenGLProxy::DestroyInstance() +{ + if (instance_) { + instance_->thread()->quit(); + instance_->thread()->wait(); + instance_->thread()->deleteLater(); + instance_->deleteLater(); + instance_ = nullptr; + } +} + bool OpenGLProxy::Init() { // Create context object @@ -123,22 +149,26 @@ QVariant OpenGLProxy::FrameToValue(FramePtr frame, StreamPtr stream, const Video VideoParams frame_params = frame->video_params(); // Check frame aspect ratio - if (frame->sample_aspect_ratio() != 1 && frame->sample_aspect_ratio() != 0) { + rational true_pixel_aspect_ratio = frame_params.pixel_aspect_ratio() / params.pixel_aspect_ratio(); + + if (true_pixel_aspect_ratio != 1) { int new_width = frame_params.width(); int new_height = frame_params.height(); // Scale the frame in a way that does not reduce the resolution - if (frame->sample_aspect_ratio() > 1) { + if (frame_params.pixel_aspect_ratio() > 1) { // Make wider - new_width = qRound(static_cast(new_width) * frame->sample_aspect_ratio().toDouble()); + new_width = qRound(static_cast(new_width) * frame_params.pixel_aspect_ratio().toDouble()); } else { // Make taller - new_height = qRound(static_cast(new_height) / frame->sample_aspect_ratio().toDouble()); + new_height = qRound(static_cast(new_height) / frame_params.pixel_aspect_ratio().toDouble()); } frame_params = VideoParams(new_width, new_height, frame_params.format(), + frame_params.pixel_aspect_ratio(), + frame_params.interlacing(), frame_params.divider()); } @@ -152,6 +182,8 @@ QVariant OpenGLProxy::FrameToValue(FramePtr frame, StreamPtr stream, const Video VideoParams dest_params(frame_params.width(), frame_params.height(), texture_fmt, + frame_params.pixel_aspect_ratio(), + frame_params.interlacing(), frame_params.divider()); // Create destination texture @@ -250,31 +282,37 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->bind(); - NodeValueMap::const_iterator i; - for (i=job.GetValues().constBegin(); i!=job.GetValues().constEnd(); i++) { + NodeValueMap::const_iterator it; + for (it=job.GetValues().constBegin(); it!=job.GetValues().constEnd(); it++) { // See if the shader has takes this parameter as an input - int variable_location = shader->uniformLocation(i.key()->id()); + int variable_location = shader->uniformLocation(it.key()); if (variable_location == -1) { continue; } - // This variable is used in the shader, let's set it - const QVariant& value = i.value().data(); + // See if this value corresponds to an input (NOTE: it may not and this may be null) + NodeInput* corresponding_input = node->GetInputWithID(it.key()); - const NodeParam::DataType& data_type = (i.value().type() != NodeParam::kNone) - ? i.value().type() - : i.key()->data_type(); + // This variable is used in the shader, let's set it + const QVariant& value = it.value().data(); + + NodeParam::DataType data_type = (it.value().type() != NodeParam::kNone) + ? it.value().type() + : corresponding_input->data_type(); switch (data_type) { case NodeInput::kInt: + // kInt technically specifies a LongLong, but OpenGL doesn't support those. This may lead to + // over/underflows if the number is large enough, but the likelihood of that is quite low. shader->setUniformValue(variable_location, value.toInt()); break; case NodeInput::kFloat: + // kFloat technically specifies a double but as above, OpenGL doesn't support those. shader->setUniformValue(variable_location, value.toFloat()); break; case NodeInput::kVec2: - if (i.key()->IsArray()) { + if (corresponding_input && corresponding_input->IsArray()) { QVector nv = value.value< QVector >(); QVector a(nv.size()); @@ -284,7 +322,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValueArray(variable_location, a.constData(), a.size()); - int count_location = shader->uniformLocation(QStringLiteral("%1_count").arg(i.key()->id())); + int count_location = shader->uniformLocation(QStringLiteral("%1_count").arg(it.key())); if (count_location > -1) { shader->setUniformValue(count_location, a.size()); } @@ -314,6 +352,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, case NodeInput::kBoolean: shader->setUniformValue(variable_location, value.toBool()); break; + case NodeInput::kBuffer: case NodeInput::kTexture: { OpenGLTextureCache::ReferencePtr texture = value.value(); @@ -328,7 +367,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValue(variable_location, textures_to_bind.size()); // If this texture binding is the iterative input, set it here - if (i.key() == job.GetIterativeInput()) { + if (corresponding_input && corresponding_input == job.GetIterativeInput()) { iterative_input = textures_to_bind.size(); } @@ -336,7 +375,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, textures_to_bind.append(tex_id); // Set enable flag if shader wants it - int enable_param_location = shader->uniformLocation(QStringLiteral("%1_enabled").arg(i.key()->id())); + int enable_param_location = shader->uniformLocation(QStringLiteral("%1_enabled").arg(it.key())); if (enable_param_location > -1) { shader->setUniformValue(enable_param_location, tex_id > 0); @@ -344,7 +383,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, if (tex_id > 0) { // Set texture resolution if shader wants it - int res_param_location = shader->uniformLocation(QStringLiteral("%1_resolution").arg(i.key()->id())); + int res_param_location = shader->uniformLocation(QStringLiteral("%1_resolution").arg(it.key())); if (res_param_location > -1) { shader->setUniformValue(res_param_location, static_cast(texture->texture()->width() * texture->texture()->divider()), @@ -366,7 +405,6 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, case NodeInput::kSampleJob: case NodeInput::kGenerateJob: case NodeInput::kFootage: - case NodeInput::kBuffer: case NodeInput::kNone: case NodeInput::kAny: break; @@ -378,19 +416,6 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, static_cast(params.width()), static_cast(params.height())); - if (node->IsBlock() && static_cast(node)->type() == Block::kTransition) { - const TransitionBlock* transition_node = static_cast(node); - - // Provides total transition progress from 0.0 (start) - 1.0 (end) - shader->setUniformValue("ove_tprog_all", static_cast(transition_node->GetTotalProgress(range.in()))); - - // Provides progress of out section from 1.0 (start) - 0.0 (end) - shader->setUniformValue("ove_tprog_out", static_cast(transition_node->GetOutProgress(range.in()))); - - // Provides progress of in section from 0.0 (start) - 1.0 (end) - shader->setUniformValue("ove_tprog_in", static_cast(transition_node->GetInProgress(range.in()))); - } - shader->release(); // Create the output textures @@ -401,6 +426,8 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, params.height(), params.time_base(), output_format, + params.pixel_aspect_ratio(), + params.interlacing(), params.divider()); int real_iteration_count; diff --git a/app/render/backend/opengl/openglproxy.h b/app/render/backend/opengl/openglproxy.h index 7c6878626..ff59f76b9 100644 --- a/app/render/backend/opengl/openglproxy.h +++ b/app/render/backend/opengl/openglproxy.h @@ -41,6 +41,15 @@ public: virtual ~OpenGLProxy() override; + static void CreateInstance(); + + static void DestroyInstance(); + + static OpenGLProxy* instance() + { + return instance_; + } + /** * @brief Initialize OpenGL instance in whatever thread this object is a part of * @@ -101,6 +110,8 @@ private: OpenGLTextureCache texture_cache_; + static OpenGLProxy* instance_; + private slots: void FinishInit(); diff --git a/app/render/backend/opengl/openglshader.cpp b/app/render/backend/opengl/openglshader.cpp index ff75ad189..7e76f619a 100644 --- a/app/render/backend/opengl/openglshader.cpp +++ b/app/render/backend/opengl/openglshader.cpp @@ -146,8 +146,9 @@ OpenGLShaderPtr OpenGLShader::CreateOCIO(QOpenGLContext* ctx, return shader; } -QString OpenGLShader::CodeDefaultFragment(const QString &function_name, const QString &shader_code) +QString OpenGLShader::CodeDefaultFragment(QString function_name, const QString &shader_code) { + // Create shader header QString frag_code = QStringLiteral("#version 150\n" "\n" "#ifdef GL_ES\n" @@ -156,48 +157,47 @@ QString OpenGLShader::CodeDefaultFragment(const QString &function_name, const QS "#endif\n" "\n" "uniform sampler2D ove_maintex;\n" - "uniform bool color_only;\n" - "uniform vec4 color_only_color;\n" + "uniform vec2 ove_resolution;\n" + "uniform bool ove_deinterlace;\n" "\n" "in vec2 ove_texcoord;\n" "\n" "out vec4 fragColor;\n" "\n"); - // Finish the function with the main function - // Check if additional code was passed to this function, add it here - if (shader_code.isEmpty()) { - - // If not, just add a pure main() function - - frag_code.append(QStringLiteral("\n" - "void main() {\n" - " if (color_only) {\n" - " fragColor = color_only_color;" - " } else {\n" - " vec4 color = texture(ove_maintex, ove_texcoord);\n" - " fragColor = color;\n" - " }\n" - "}\n")); - - } else { + if (!function_name.isEmpty() && !shader_code.isEmpty()) { // If additional code was passed, add it and reference it in main(). // - // The function in the additional code is expected to be `vec4 function_name(vec4 color)`. The texture coordinate - // can be acquired through `ove_texcoord`. + // The function in the additional code is expected to be `vec4 function_name(vec4 color)`. + // The texture coordinate can be acquired through `ove_texcoord`. frag_code.append(shader_code); - frag_code.append(QStringLiteral("\n" - "void main() {\n" - " vec4 color = %1(texture(ove_maintex, ove_texcoord));\n" - " fragColor = color;\n" - "}\n").arg(function_name)); + } else { + + // No function to call + function_name = QString(); } + // Our function_name arg will either resolve to the function added to this or to nothing, in + // which case they'll just be benign brackets. + frag_code.append(QStringLiteral("\n" + "void main() {\n" + " vec2 using_texcoord = ove_texcoord;\n" + " if (ove_deinterlace) {\n" + " // A very basic deinterlace that halves the vertical\n" + " // resolution and linearly interpolates the two fields\n" + " // by reading the texture coord between them.\n" + " float half_vert = round(ove_resolution.y / 2.0);\n" + " using_texcoord.y = (round(using_texcoord.y * half_vert) + 0.25) / half_vert;\n" + " }\n" + " vec4 color = %1(texture(ove_maintex, using_texcoord));\n" + " fragColor = color;\n" + "}\n").arg(function_name)); + return frag_code; } diff --git a/app/render/backend/opengl/openglshader.h b/app/render/backend/opengl/openglshader.h index 3adab00d4..452dc3c3d 100644 --- a/app/render/backend/opengl/openglshader.h +++ b/app/render/backend/opengl/openglshader.h @@ -51,7 +51,7 @@ public: OCIO::ConstProcessorRcPtr processor, bool alpha_is_associated); - static QString CodeDefaultFragment(const QString &function_name = QString(), + static QString CodeDefaultFragment(QString function_name = QString(), const QString &shader_code = QString()); static QString CodeDefaultVertex(); static QString CodeAlphaDisassociate(const QString& function_name); diff --git a/app/render/backend/opengl/openglworker.cpp b/app/render/backend/opengl/openglworker.cpp index b3e10b71c..ec518ffc7 100644 --- a/app/render/backend/opengl/openglworker.cpp +++ b/app/render/backend/opengl/openglworker.cpp @@ -22,15 +22,14 @@ OLIVE_NAMESPACE_ENTER -OpenGLWorker::OpenGLWorker(RenderBackend *parent, OpenGLProxy* proxy) : - RenderWorker(parent), - proxy_(proxy) +OpenGLWorker::OpenGLWorker(RenderBackend *parent) : + RenderWorker(parent) { } void OpenGLWorker::TextureToFrame(const QVariant &texture, FramePtr frame, const QMatrix4x4& mat) const { - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "TextureToBuffer", Qt::BlockingQueuedConnection, Q_ARG(const QVariant&, texture), @@ -42,7 +41,7 @@ QVariant OpenGLWorker::FootageFrameToTexture(StreamPtr stream, FramePtr frame) c { QVariant value; - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "FrameToValue", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, value), @@ -58,7 +57,7 @@ QVariant OpenGLWorker::CachedFrameToTexture(FramePtr frame) const { QVariant value; - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "PreCachedFrameToValue", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, value), @@ -71,7 +70,7 @@ QVariant OpenGLWorker::ProcessShader(const Node *node, const TimeRange &range, c { QVariant value; - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "RunNodeAccelerated", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, value), diff --git a/app/render/backend/opengl/openglworker.h b/app/render/backend/opengl/openglworker.h index 23b5e787f..75eed65f8 100644 --- a/app/render/backend/opengl/openglworker.h +++ b/app/render/backend/opengl/openglworker.h @@ -29,7 +29,7 @@ OLIVE_NAMESPACE_ENTER class OpenGLWorker : public RenderWorker { public: - OpenGLWorker(RenderBackend* parent, OpenGLProxy* proxy); + OpenGLWorker(RenderBackend* parent); protected: virtual void TextureToFrame(const QVariant& texture, FramePtr frame, const QMatrix4x4 &mat) const override; @@ -42,9 +42,6 @@ protected: virtual bool TextureHasAlpha(const QVariant& v) const override; -private: - OpenGLProxy* proxy_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index df96c9efc..01d97451d 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -31,13 +31,27 @@ OLIVE_NAMESPACE_ENTER +QVector RenderBackend::instances_; +QMutex RenderBackend::instance_lock_; +RenderBackend* RenderBackend::active_instance_ = nullptr; +QThreadPool RenderBackend::thread_pool_; + RenderBackend::RenderBackend(QObject *parent) : QObject(parent), viewer_node_(nullptr), - update_with_graph_(false), - preview_job_time_(0), - render_mode_(RenderMode::kOnline) + autocache_enabled_(false), + autocache_paused_(false), + generate_audio_previews_(false), + render_mode_(RenderMode::kOnline), + autocache_has_changed_(false), + use_custom_autocache_range_(false) { + instance_lock_.lock(); + instances_.append(this); + instance_lock_.unlock(); + + // Set default autocache range + SetAutoCachePlayhead(rational()); } RenderBackend::~RenderBackend() @@ -53,22 +67,53 @@ void RenderBackend::SetViewerNode(ViewerOutput *viewer_node) ViewerOutput* old_viewer = viewer_node_; if (!viewer_node) { - // If setting to null, set it here before we wait for jobs to finish - viewer_node_ = viewer_node; + // If setting to null, set it here before we wait for jobs to finish to prevent WorkerFinished() + // from calling RunNextJob() again and preventing us from finishing + viewer_node_ = nullptr; } if (old_viewer) { - // Delete all of our copied nodes - pool_.clear(); - pool_.waitForDone(); + // Cancel any remaining tickets + ClearQueue(); - // Cancel all tickets - foreach (RenderTicketPtr t, render_queue_) { - t->Cancel(); + // Wait for any currently running jobs to finish + foreach (RenderTicketPtr ticket, running_tickets_) { + ticket->WaitForFinished(); } - render_queue_.clear(); - // Delete all the nodes + // Clear autocache lists + { + // This can be cleared normally (hashes will be discarded and need to be calculated again) + autocache_hash_tasks_.clear(); + + // We need to wait for these since they work directly on the FrameHashCache. Most of the time + // this is fine, but not if the FrameHashCache gets deleted after this function. + foreach (QFutureWatcher* watcher, autocache_hash_process_tasks_) { + watcher->waitForFinished(); + } + autocache_hash_process_tasks_.clear(); + + // This can be cleared normally (frames will be discarded and need to be rendered again) + autocache_video_tasks_.clear(); + + // This can be cleared normally (PCM data will be discarded and need to be rendered again) + autocache_audio_tasks_.clear(); + + // We'll need to wait for these since they work directly on the FrameHashCache. Frames will + // be in the cache for later use. + { + QMap*, QByteArray>::const_iterator i; + for (i=autocache_video_download_tasks_.constBegin(); i!=autocache_video_download_tasks_.constEnd(); i++) { + i.key()->waitForFinished(); + } + autocache_video_download_tasks_.clear(); + } + + // No longer caching any hashes + autocache_currently_caching_hashes_.clear(); + } + + // Delete all of our copied nodes foreach (Node* c, copy_map_) { c->deleteLater(); } @@ -76,18 +121,27 @@ void RenderBackend::SetViewerNode(ViewerOutput *viewer_node) copied_viewer_node_ = nullptr; graph_update_queue_.clear(); + // Disconnect signal (will be a no-op if the signal was never connected) disconnect(old_viewer, &ViewerOutput::GraphChangedFrom, this, &RenderBackend::NodeGraphChanged); + + disconnect(old_viewer->video_frame_cache(), + &PlaybackCache::Invalidated, + this, + &RenderBackend::AutoCacheVideoInvalidated); + + disconnect(old_viewer->audio_playback_cache(), + &PlaybackCache::Invalidated, + this, + &RenderBackend::AutoCacheAudioInvalidated); } if (viewer_node) { // If setting to non-null, set it now viewer_node_ = viewer_node; - } - if (viewer_node_) { // Copy graph copied_viewer_node_ = static_cast(viewer_node_->copy()); copy_map_.insert(viewer_node_, copied_viewer_node_); @@ -96,66 +150,94 @@ void RenderBackend::SetViewerNode(ViewerOutput *viewer_node) NodeGraphChanged(viewer_node_->samples_input()); ProcessUpdateQueue(); - if (update_with_graph_) { + if (autocache_enabled_) { connect(viewer_node_, &ViewerOutput::GraphChangedFrom, this, &RenderBackend::NodeGraphChanged); + + connect(viewer_node_->video_frame_cache(), + &PlaybackCache::Invalidated, + this, + &RenderBackend::AutoCacheVideoInvalidated); + + connect(viewer_node_->audio_playback_cache(), + &PlaybackCache::Invalidated, + this, + &RenderBackend::AutoCacheAudioInvalidated); } } } -void RenderBackend::ClearVideoQueue() +void RenderBackend::AutoCacheRange(const TimeRange &range) { - foreach (RenderTicketPtr t, render_queue_) { - t->Cancel(); - } - render_queue_.clear(); + Q_ASSERT(autocache_enabled_); + + autocache_has_changed_ = true; + use_custom_autocache_range_ = true; + custom_autocache_range_ = range; + + AutoCacheRequeueFrames(); } -QFuture > RenderBackend::Hash(const QVector ×) +RenderTicketPtr RenderBackend::Hash(const QVector ×, bool prioritize) { - return QtConcurrent::run(&pool_, [this](const QVector ×){ - QVector hashes(times.size()); + Q_ASSERT(viewer_node_); - for (int i=0;itexture_input()->get_connected_node(), - video_params_, - times.at(i)); - } + SetActiveInstance(); - return hashes; - }, times); -} + RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeHash, + QVariant::fromValue(times)); -RenderTicketPtr RenderBackend::RenderFrame(const rational &time) -{ - if (!viewer_node_) { - return nullptr; + if (prioritize) { + render_queue_.push_front(ticket); + } else { + render_queue_.push_back(ticket); } - RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeVideo, - TimeRange(time, time)); - - render_queue_.push_back(ticket); - - RunNextJob(); + QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); return ticket; } -RenderTicketPtr RenderBackend::RenderAudio(const TimeRange &r) +RenderTicketPtr RenderBackend::RenderFrame(const rational &time, bool prioritize, const QByteArray& hash) { - if (!viewer_node_) { - return nullptr; + Q_ASSERT(viewer_node_); + + SetActiveInstance(); + + RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeVideo, + QVariant::fromValue(time)); + + ticket->setProperty("hash", hash); + + if (prioritize) { + render_queue_.push_front(ticket); + } else { + render_queue_.push_back(ticket); } + QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); + + return ticket; +} + +RenderTicketPtr RenderBackend::RenderAudio(const TimeRange &r, bool prioritize) +{ + Q_ASSERT(viewer_node_); + + SetActiveInstance(); + RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeAudio, - r); + QVariant::fromValue(r)); - render_queue_.push_back(ticket); + if (prioritize) { + render_queue_.push_front(ticket); + } else { + render_queue_.push_back(ticket); + } - RunNextJob(); + QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); return ticket; } @@ -193,29 +275,72 @@ std::list RenderBackend::SplitRangeIntoChunks(const TimeRange &r) return split_ranges; } +void RenderBackend::ClearVideoQueue() +{ + ClearQueueOfType(RenderTicket::kTypeVideo); + + autocache_has_changed_ = true; + use_custom_autocache_range_ = false; +} + +void RenderBackend::ClearAudioQueue() +{ + ClearQueueOfType(RenderTicket::kTypeAudio); +} + +void RenderBackend::ClearQueue() +{ + foreach (RenderTicketPtr t, render_queue_) { + t->Cancel(); + } + render_queue_.clear(); +} + void RenderBackend::NodeGraphChanged(NodeInput *source) { - if (!graph_update_queue_.isEmpty()) { - // First, check if anything in our queue is a dependency of this input. If so, we should remove - // it and just update this input. + // We need to determine: + // - If we don't have this input, assume that it's coming soon and ignore it + // - If we do, is this input a child of another input we're already copying? + // - Or are any of the queued inputs children of this one? - // First we need to find our copy of the input being queued - Node* our_copy_node = copy_map_.value(source->parentNode()); + // First we need to find our copy of the input being queued + Node* our_copy_node = copy_map_.value(source->parentNode()); - if (our_copy_node) { - NodeInput* our_copy = our_copy_node->GetInputWithID(source->id()); - QList our_copy_deps = our_copy->GetDependencies(our_copy); + // If we don't have this node yet, assume it's coming in a later copy in which case it'll be + // copied then + if (!our_copy_node) { + // Assert that there are updates coming + Q_ASSERT(!graph_update_queue_.isEmpty()); + return; + } - for (int i=0;iparentNode()); + // If we're here, we must have this node. Determine if we're already copying a "parent" of this + for (int i=0; iparentNode()->OutputsTo(queued_input, true, true)) { + // In which case, no further copy is necessary + return; + } + + // Check if the source is a member of this array, in which case it'll be copied eventually anyway + if (queued_input->IsArray() + && static_cast(queued_input)->sub_params().contains(source)) { + return; + } + + // Check if this input supersedes an already queued input + if (queued_input->parentNode()->OutputsTo(source, true, true) + || (source->IsArray() && static_cast(source)->sub_params().contains(queued_input))) { + // In which case, we don't need to queue it and can queue our own + graph_update_queue_.removeAt(i); + i--; } } @@ -236,6 +361,14 @@ void RenderBackend::RunNextJob() { // If queue is empty, nothing to be done if (render_queue_.empty()) { + + // If we're the active instance, unset it + instance_lock_.lock(); + if (active_instance_ == this) { + active_instance_ = nullptr; + } + instance_lock_.unlock(); + return; } @@ -247,7 +380,7 @@ void RenderBackend::RunNextJob() } // If we have a value update queued, check if all workers are available and proceed from there - if (update_with_graph_ && !graph_update_queue_.isEmpty()) { + if (autocache_enabled_ && !graph_update_queue_.isEmpty()) { bool all_workers_available = true; foreach (const WorkerData& data, workers_) { @@ -268,11 +401,12 @@ void RenderBackend::RunNextJob() // If we have no workers allocated, allocate them now if (workers_.isEmpty()) { // Allocate workers here - workers_.resize(pool_.maxThreadCount()); + workers_.resize(thread_pool_.maxThreadCount()); for (int i=0;iSetAudioParams(audio_params_); worker->SetVideoDownloadMatrix(video_download_matrix_); worker->SetRenderMode(render_mode_); - if (preview_job_time_) { - worker->EnablePreviewGeneration(viewer_node_->audio_playback_cache(), preview_job_time_); - } + worker->SetPreviewGenerationEnabled(generate_audio_previews_); worker->SetCopyMap(©_map_); + worker->SetViewerNode(viewer_node_); + // Move ticket from queue to running list RenderTicketPtr ticket = render_queue_.front(); render_queue_.pop_front(); + running_tickets_.push_back(ticket); + + // Create watcher to remove from running list + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::TicketFinished); + watcher->SetTicket(ticket); + + // Set job time to now + ticket->SetJobTime(); switch (ticket->GetType()) { + case RenderTicket::kTypeHash: + QtConcurrent::run(&thread_pool_, + worker, + &RenderWorker::Hash, + ticket, + copied_viewer_node_, + ticket->GetTime().value >()); + break; case RenderTicket::kTypeVideo: - QtConcurrent::run(&pool_, + { + rational frame = ticket->GetTime().value(); + + QtConcurrent::run(&thread_pool_, worker, &RenderWorker::RenderFrame, ticket, copied_viewer_node_, - ticket->GetTime().in()); + frame); + + QByteArray frame_hash = ticket->property("hash").toByteArray(); + if (!frame_hash.isEmpty()) { + autocache_currently_caching_hashes_.append(frame_hash); + } break; + } case RenderTicket::kTypeAudio: - QtConcurrent::run(&pool_, + QtConcurrent::run(&thread_pool_, worker, &RenderWorker::RenderAudio, ticket, copied_viewer_node_, - ticket->GetTime()); + ticket->GetTime().value()); break; } @@ -327,33 +487,202 @@ void RenderBackend::RunNextJob() } } -void RenderBackend::ProcessUpdateQueue() +void RenderBackend::TicketFinished() { - /* - while (!graph_update_queue_.isEmpty()) { - CopyNodeInputValue(graph_update_queue_.takeFirst()); - } - */ + RenderTicketPtr ticket = static_cast(sender())->GetTicket(); + delete sender(); - // FIXME: SLOW DEBUGGING CODE - CopyNodeInputValue(viewer_node_->texture_input()); - CopyNodeInputValue(viewer_node_->samples_input()); + running_tickets_.remove(ticket); } -QByteArray RenderBackend::HashNode(const Node *n, const VideoParams ¶ms, const rational &time) +void RenderBackend::WorkerGeneratedWaveform(RenderTicketPtr ticket, TrackOutput *track, AudioVisualWaveform samples, TimeRange range) { - QCryptographicHash hasher(QCryptographicHash::Sha1); + QList valid_ranges = viewer_node_->audio_playback_cache()->GetValidRanges(range, + ticket->GetJobTime()); + if (!valid_ranges.isEmpty()) { + // Generate visual waveform in this background thread + track->waveform_lock()->lock(); - // Embed video parameters into this hash - hasher.addData(reinterpret_cast(¶ms.effective_width()), sizeof(int)); - hasher.addData(reinterpret_cast(¶ms.effective_height()), sizeof(int)); - hasher.addData(reinterpret_cast(¶ms.format()), sizeof(PixelFormat::Format)); + track->waveform().set_channel_count(audio_params_.channel_count()); - if (n) { - n->Hash(hasher, time); + foreach (const TimeRange& r, valid_ranges) { + track->waveform().OverwriteSums(samples, r.in(), r.in() - range.in(), r.length()); + } + + track->waveform_lock()->unlock(); + + emit track->PreviewChanged(); + } +} + +void RenderBackend::AutoCacheVideoInvalidated(const TimeRange &range) +{ + ClearVideoQueue(); + + // Hash these frames since that should be relatively quick. + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + QVector frames = viewer_node_->video_frame_cache()->GetFrameListFromTimeRange({range}); + autocache_hash_tasks_.insert(watcher, frames); + connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::AutoCacheHashesGenerated); + watcher->SetTicket(Hash(frames)); +} + +void RenderBackend::AutoCacheAudioInvalidated(const TimeRange &range) +{ + // Start a task to re-render the audio at this range + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + autocache_audio_tasks_.insert(watcher, range); + connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::AutoCacheAudioRendered); + watcher->SetTicket(RenderAudio(range, true)); +} + +void RenderBackend::SetHashes(FrameHashCache* cache, const QVector& times, const QVector& hashes, qint64 job_time) +{ + std::vector existing_hashes; + + for (int i=0; iCachePathName(hash)); + + if (hash_exists) { + existing_hashes.push_back(hash); + } + } + + QMetaObject::invokeMethod(cache, "SetHash", Qt::QueuedConnection, + OLIVE_NS_ARG(rational, time), + Q_ARG(QByteArray, hash), + Q_ARG(qint64, job_time), + Q_ARG(bool, hash_exists)); + } +} + +void RenderBackend::AutoCacheHashesGenerated() +{ + RenderTicketWatcher* watcher = static_cast(sender()); + + if (autocache_hash_tasks_.contains(watcher)) { + if (!watcher->WasCancelled()) { + QFutureWatcher* hw = new QFutureWatcher(); + connect(hw, &QFutureWatcher::finished, this, &RenderBackend::AutoCacheHashesProcessed); + autocache_hash_process_tasks_.append(hw); + hw->setFuture(QtConcurrent::run(this, + &RenderBackend::SetHashes, + viewer_node_->video_frame_cache(), + autocache_hash_tasks_.value(watcher), + watcher->Get().value >(), + watcher->GetTicket()->GetJobTime())); + } + + autocache_hash_tasks_.remove(watcher); } - return hasher.result(); + delete watcher; +} + +void RenderBackend::AutoCacheHashesProcessed() +{ + QFutureWatcher* watcher = static_cast*>(sender()); + + if (autocache_hash_process_tasks_.contains(watcher)) { + autocache_hash_process_tasks_.removeOne(watcher); + + AutoCacheRequeueFrames(); + } + + delete watcher; +} + +void RenderBackend::AutoCacheAudioRendered() +{ + RenderTicketWatcher* watcher = static_cast(sender()); + + if (autocache_audio_tasks_.contains(watcher)) { + if (!watcher->WasCancelled()) { + viewer_node_->audio_playback_cache()->WritePCM(autocache_audio_tasks_.value(watcher), + watcher->Get().value(), + watcher->GetTicket()->GetJobTime()); + } + + autocache_audio_tasks_.remove(watcher); + } + + delete watcher; +} + +void RenderBackend::AutoCacheVideoRendered() +{ + RenderTicketWatcher* watcher = static_cast(sender()); + + if (autocache_video_tasks_.contains(watcher)) { + if (!watcher->WasCancelled()) { + const QByteArray& hash = autocache_video_tasks_.value(watcher); + + // Download frame in another thread + QFutureWatcher* w = new QFutureWatcher(); + autocache_video_download_tasks_.insert(w, hash); + connect(w, &QFutureWatcher::finished, this, &RenderBackend::AutoCacheVideoDownloaded); + w->setFuture(QtConcurrent::run(viewer_node_->video_frame_cache(), + &FrameHashCache::SaveCacheFrame, + hash, + watcher->Get().value())); + } + + autocache_video_tasks_.remove(watcher); + } + + delete watcher; +} + +void RenderBackend::AutoCacheVideoDownloaded() +{ + QFutureWatcher* watcher = static_cast*>(sender()); + + if (autocache_video_download_tasks_.contains(watcher)) { + if (!watcher->isCanceled()) { + if (watcher->result()) { + const QByteArray& hash = autocache_video_download_tasks_.value(watcher); + + autocache_currently_caching_hashes_.removeOne(hash); + + viewer_node_->video_frame_cache()->ValidateFramesWithHash(hash); + } else { + qCritical() << "Failed to download video frame"; + } + } + + autocache_video_download_tasks_.remove(watcher); + } + + delete watcher; +} + +//#define PRINT_UPDATE_QUEUE_INFO +void RenderBackend::ProcessUpdateQueue() +{ +#ifdef PRINT_UPDATE_QUEUE_INFO + qint64 t = QDateTime::currentMSecsSinceEpoch(); + qDebug() << "Processing update queue of" << graph_update_queue_.size() << "elements:"; +#endif + + while (!graph_update_queue_.isEmpty()) { + NodeInput* i = graph_update_queue_.takeFirst(); +#ifdef PRINT_UPDATE_QUEUE_INFO + qDebug() << " " << i->parentNode()->id() << i->id(); +#endif + CopyNodeInputValue(i); + } + +#ifdef PRINT_UPDATE_QUEUE_INFO + qDebug() << "Update queue took:" << (QDateTime::currentMSecsSinceEpoch() - t); +#endif } void RenderBackend::WorkerFinished() @@ -377,11 +706,13 @@ void RenderBackend::CopyNodeInputValue(NodeInput *input) { // Find our copy of this parameter Node* our_copy_node = copy_map_.value(input->parentNode()); + Q_ASSERT(our_copy_node); NodeInput* our_copy = our_copy_node->GetInputWithID(input->id()); // Copy the standard/keyframe values between these two inputs NodeInput::CopyValues(input, our_copy, + false, false); // Handle connections @@ -457,4 +788,76 @@ void RenderBackend::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_ } } +void RenderBackend::ClearQueueOfType(RenderTicket::Type type) +{ + std::list::iterator i = render_queue_.begin(); + + while (i != render_queue_.end()) { + if ((*i)->GetType() == type) { + (*i)->Cancel(); + i = render_queue_.erase(i); + } else { + i++; + } + } +} + +void RenderBackend::SetActiveInstance() +{ + QMutexLocker locker(&instance_lock_); + + if (active_instance_ != this) { + // Signal active instance to stop + QMetaObject::invokeMethod(active_instance_, "ClearVideoQueue", Qt::QueuedConnection); + + active_instance_ = this; + } +} + +void RenderBackend::AutoCacheRequeueFrames() +{ + if (viewer_node_ + && viewer_node_->video_frame_cache()->HasInvalidatedRanges() + && autocache_hash_tasks_.isEmpty() + && autocache_hash_process_tasks_.isEmpty() + && autocache_has_changed_ + && (!autocache_paused_ || use_custom_autocache_range_)) { + TimeRange using_range; + + if (use_custom_autocache_range_) { + using_range = custom_autocache_range_; + use_custom_autocache_range_ = false; + } else { + using_range = autocache_range_; + } + + QVector invalidated_ranges = viewer_node_->video_frame_cache()->GetInvalidatedFrames(using_range); + + ClearVideoQueue(); + + // QMaps are automatically sorted by time which is always best for rendering + QList queued_hashes; + + foreach (const rational& t, invalidated_ranges) { + const QByteArray& hash = viewer_node_->video_frame_cache()->GetHash(t); + + if (t >= using_range.in() + && t < using_range.out() + && !queued_hashes.contains(hash) + && !autocache_currently_caching_hashes_.contains(hash)) { + // Don't render any hash more than once + queued_hashes.append(hash); + + RenderTicketWatcher* watcher = new RenderTicketWatcher(); + connect(watcher, &RenderTicketWatcher::Finished, this, &RenderBackend::AutoCacheVideoRendered); + autocache_video_tasks_.insert(watcher, hash); + + watcher->SetTicket(RenderFrame(t, false, hash)); + } + } + + autocache_has_changed_ = false; + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/renderbackend.h b/app/render/backend/renderbackend.h index 8f9339691..3ec853bbc 100644 --- a/app/render/backend/renderbackend.h +++ b/app/render/backend/renderbackend.h @@ -23,12 +23,14 @@ #include +#include "config/config.h" #include "dialog/rendercancel/rendercancel.h" #include "decodercache.h" #include "node/graph.h" #include "node/output/viewer/viewer.h" #include "render/backend/colorprocessorcache.h" #include "renderticket.h" +#include "renderticketwatcher.h" #include "renderworker.h" OLIVE_NAMESPACE_ENTER @@ -43,11 +45,49 @@ public: void Close(); + ViewerOutput* GetViewerNode() const + { + return viewer_node_; + } + void SetViewerNode(ViewerOutput* viewer_node); - void SetUpdateWithGraph(bool e) + void SetAutoCacheEnabled(bool e) { - update_with_graph_ = e; + autocache_enabled_ = e; + } + + bool IsAutoCachePaused() const + { + return autocache_paused_; + } + + void SetAutoCachePaused(bool paused) + { + autocache_paused_ = paused; + + if (autocache_paused_) { + // Pause the autocache + ClearVideoQueue(); + } else { + // Unpause the cache + AutoCacheRequeueFrames(); + } + } + + void AutoCacheRange(const TimeRange& range); + + void AutoCacheRequeueFrames(); + + void SetAutoCachePlayhead(const rational& playhead) + { + autocache_range_ = TimeRange(playhead - Config::Current()["DiskCacheBehind"].value(), + playhead + Config::Current()["DiskCacheAhead"].value()); + + autocache_has_changed_ = true; + use_custom_autocache_range_ = false; + + AutoCacheRequeueFrames(); } void SetRenderMode(RenderMode::Mode e) @@ -55,31 +95,37 @@ public: render_mode_ = e; } - void EnablePreviewGeneration(qint64 job_time) + void SetPreviewGenerationEnabled(bool e) { - preview_job_time_ = job_time; + generate_audio_previews_ = e; } - void ClearVideoQueue(); - void ProcessUpdateQueue(); - static QByteArray HashNode(const Node* n, const VideoParams& params, const rational& time); - /** * @brief Asynchronously generate a hash at a given time */ - QFuture > Hash(const QVector ×); + RenderTicketPtr Hash(const QVector ×, bool prioritize = false); /** * @brief Asynchronously generate a frame at a given time */ - RenderTicketPtr RenderFrame(const rational& time); + RenderTicketPtr RenderFrame(const rational& time, bool prioritize = false, const QByteArray& hash = QByteArray()); /** * @brief Asynchronously generate a chunk of audio */ - RenderTicketPtr RenderAudio(const TimeRange& r); + RenderTicketPtr RenderAudio(const TimeRange& r, bool prioritize = false); + + const VideoParams& GetVideoParams() const + { + return video_params_; + } + + const AudioParams& GetAudioParams() const + { + return audio_params_; + } void SetVideoParams(const VideoParams& params); @@ -92,6 +138,14 @@ public: public slots: void NodeGraphChanged(NodeInput *source); + void ClearVideoQueue(); + + void ClearAudioQueue(); + + void ClearQueue(); + +signals: + protected: virtual RenderWorker* CreateNewWorker() = 0; @@ -100,7 +154,9 @@ private: Node *CopyNodeConnections(Node *src_node); void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input); - void RunNextJob(); + void ClearQueueOfType(RenderTicket::Type type); + + void SetHashes(FrameHashCache* cache, const QVector& times, const QVector& hashes, qint64 job_time); ViewerOutput* viewer_node_; @@ -115,10 +171,10 @@ private: QHash copy_map_; ViewerOutput* copied_viewer_node_; - QThreadPool pool_; - std::list render_queue_; + std::list running_tickets_; + struct WorkerData { RenderWorker* worker; bool busy; @@ -126,15 +182,61 @@ private: QVector workers_; - bool update_with_graph_; + bool autocache_enabled_; + bool autocache_paused_; - qint64 preview_job_time_; + bool generate_audio_previews_; RenderMode::Mode render_mode_; + TimeRange autocache_range_; + + bool autocache_has_changed_; + + bool use_custom_autocache_range_; + TimeRange custom_autocache_range_; + + static QVector instances_; + static QMutex instance_lock_; + static RenderBackend* active_instance_; + static QThreadPool thread_pool_; + void SetActiveInstance(); + + QMap > autocache_hash_tasks_; + + QList*> autocache_hash_process_tasks_; + + QMap autocache_audio_tasks_; + + QMap autocache_video_tasks_; + + QMap*, QByteArray> autocache_video_download_tasks_; + + QVector autocache_currently_caching_hashes_; + private slots: void WorkerFinished(); + void RunNextJob(); + + void TicketFinished(); + + void WorkerGeneratedWaveform(OLIVE_NAMESPACE::RenderTicketPtr ticket, OLIVE_NAMESPACE::TrackOutput* track, OLIVE_NAMESPACE::AudioVisualWaveform samples, OLIVE_NAMESPACE::TimeRange range); + + void AutoCacheVideoInvalidated(const OLIVE_NAMESPACE::TimeRange &range); + + void AutoCacheAudioInvalidated(const OLIVE_NAMESPACE::TimeRange &range); + + void AutoCacheHashesGenerated(); + + void AutoCacheHashesProcessed(); + + void AutoCacheAudioRendered(); + + void AutoCacheVideoRendered(); + + void AutoCacheVideoDownloaded(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/renderticket.cpp b/app/render/backend/renderticket.cpp index 85d0d46d8..027683cd1 100644 --- a/app/render/backend/renderticket.cpp +++ b/app/render/backend/renderticket.cpp @@ -22,11 +22,12 @@ OLIVE_NAMESPACE_ENTER -RenderTicket::RenderTicket(Type type, const TimeRange &time) : +RenderTicket::RenderTicket(Type type, const QVariant &time) : finished_(false), cancelled_(false), time_(time), - type_(type) + type_(type), + job_time_(0) { } diff --git a/app/render/backend/renderticket.h b/app/render/backend/renderticket.h index 3432b0df5..f4fca0c6b 100644 --- a/app/render/backend/renderticket.h +++ b/app/render/backend/renderticket.h @@ -21,6 +21,7 @@ #ifndef RENDERTICKET_H #define RENDERTICKET_H +#include #include #include @@ -35,13 +36,24 @@ class RenderTicket : public QObject Q_OBJECT public: enum Type { + kTypeHash, kTypeVideo, kTypeAudio }; - RenderTicket(Type type, const TimeRange& time); + RenderTicket(Type type, const QVariant& time); - const TimeRange& GetTime() const + qint64 GetJobTime() const + { + return job_time_; + } + + void SetJobTime() + { + job_time_ = QDateTime::currentMSecsSinceEpoch(); + } + + const QVariant& GetTime() const { return time_; } @@ -82,14 +94,18 @@ private: QWaitCondition wait_; - TimeRange time_; + QVariant time_; Type type_; + qint64 job_time_; + }; using RenderTicketPtr = std::shared_ptr; OLIVE_NAMESPACE_EXIT +Q_DECLARE_METATYPE(OLIVE_NAMESPACE::RenderTicketPtr) + #endif // RENDERTICKET_H diff --git a/app/render/backend/renderticketwatcher.h b/app/render/backend/renderticketwatcher.h index e25c488fc..fba301a3b 100644 --- a/app/render/backend/renderticketwatcher.h +++ b/app/render/backend/renderticketwatcher.h @@ -31,6 +31,11 @@ class RenderTicketWatcher : public QObject public: RenderTicketWatcher(QObject* parent = nullptr); + RenderTicketPtr GetTicket() const + { + return ticket_; + } + void SetTicket(RenderTicketPtr ticket); bool WasCancelled(); diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 7142eae07..1f7902190 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -21,27 +21,95 @@ #include "renderworker.h" #include +#include +#include #include "audio/audiovisualwaveform.h" #include "common/functiontimer.h" #include "config/config.h" #include "node/block/clip/clip.h" #include "task/conform/conform.h" -#include "renderbackend.h" OLIVE_NAMESPACE_ENTER +// FIXME: Hardcoded value. It seems to work fine, but is there a possibility we should make +// this a dynamic value somehow or a configurable value? +const int RenderWorker::kMaxDecoderLife = 6000; + RenderWorker::RenderWorker(RenderBackend* parent) : parent_(parent), available_(true), - audio_mode_is_preview_(false), - preview_cache_(nullptr), + generate_audio_previews_(false), render_mode_(RenderMode::kOnline) { + cleanup_timer_ = new QTimer(); + cleanup_timer_->setInterval(kMaxDecoderLife); + connect(cleanup_timer_, &QTimer::timeout, this, &RenderWorker::ClearOldDecoders, Qt::DirectConnection); + cleanup_timer_->moveToThread(qApp->thread()); + QMetaObject::invokeMethod(cleanup_timer_, "start", Qt::QueuedConnection); +} + +RenderWorker::~RenderWorker() +{ + QMetaObject::invokeMethod(cleanup_timer_, "stop", Qt::QueuedConnection); + cleanup_timer_->deleteLater(); +} + +void RenderWorker::Hash(RenderTicketPtr ticket, ViewerOutput *viewer, const QVector ×) +{ + ticket_ = ticket; + + QVector hashes(times.size()); + + for (int i=0;itexture_input()->get_connected_node(), + video_params_, + times.at(i)); + } + + ticket->Finish(QVariant::fromValue(hashes)); + + emit FinishedJob(); +} + +QByteArray RenderWorker::HashNode(const Node *n, const VideoParams ¶ms, const rational &time) +{ + QCryptographicHash hasher(QCryptographicHash::Sha1); + + // Embed video parameters into this hash + hasher.addData(reinterpret_cast(¶ms.effective_width()), sizeof(int)); + hasher.addData(reinterpret_cast(¶ms.effective_height()), sizeof(int)); + hasher.addData(reinterpret_cast(¶ms.format()), sizeof(PixelFormat::Format)); + + if (n) { + n->Hash(hasher, time); + } + + return hasher.result(); +} + +void RenderWorker::ClearOldDecoders() +{ + QMutexLocker locker(&decoder_lock_); + + QHash::iterator i = decoder_age_.begin(); + + while (i != decoder_age_.end()) { + if (i.value() < QDateTime::currentMSecsSinceEpoch() - kMaxDecoderLife) { + // This decoder is old, remove it + decoder_cache_.remove(i.key()); + + i = decoder_age_.erase(i); + } else { + i++; + } + } } void RenderWorker::RenderFrame(RenderTicketPtr ticket, ViewerOutput* viewer, const rational &time) { + ticket_ = ticket; + NodeValueTable table = ProcessInput(viewer->texture_input(), TimeRange(time, time + video_params_.time_base())); @@ -59,6 +127,8 @@ void RenderWorker::RenderFrame(RenderTicketPtr ticket, ViewerOutput* viewer, con video_params_.height(), video_params_.time_base(), output_format, + video_params_.pixel_aspect_ratio(), + video_params_.interlacing(), video_params_.divider())); frame->set_timestamp(time); frame->allocate(); @@ -78,6 +148,8 @@ void RenderWorker::RenderFrame(RenderTicketPtr ticket, ViewerOutput* viewer, con void RenderWorker::RenderAudio(RenderTicketPtr ticket, ViewerOutput* viewer, const TimeRange &range) { + ticket_ = ticket; + NodeValueTable table = ProcessInput(viewer->samples_input(), range); QVariant samples = table.Get(NodeParam::kSamples); @@ -137,38 +209,26 @@ NodeValueTable RenderWorker::GenerateBlockTable(const TrackOutput *track, const NodeValueTable::Merge({merged_table, table}); } - if (preview_cache_) { + if (generate_audio_previews_) { // Find original track object TrackOutput* original_track = nullptr; - QList valid_ranges = preview_cache_->GetValidRanges(range, preview_job_time_); - if (!valid_ranges.isEmpty()) { - QHash::const_iterator i; - for (i=copy_map_->constBegin(); i!=copy_map_->constEnd(); i++) { - if (i.value() == track) { - original_track = static_cast(i.key()); - break; - } + // Have to do a manual loop since our track is const and QHash won't take it + QHash::const_iterator i; + for (i=copy_map_->constBegin(); i!=copy_map_->constEnd(); i++) { + if (i.value() == track) { + original_track = static_cast(i.key()); + break; } + } - // Generate visual waveform in this background thread - if (original_track) { - AudioVisualWaveform visual_waveform; - visual_waveform.set_channel_count(audio_params_.channel_count()); - visual_waveform.OverwriteSamples(block_range_buffer, audio_params_.sample_rate()); + if (original_track) { + // Generate a visual waveform and send it back to the main thread + AudioVisualWaveform visual_waveform; + visual_waveform.set_channel_count(audio_params_.channel_count()); + visual_waveform.OverwriteSamples(block_range_buffer, audio_params_.sample_rate()); - original_track->waveform_lock()->lock(); - - original_track->waveform().set_channel_count(audio_params_.channel_count()); - - foreach (const TimeRange& r, valid_ranges) { - original_track->waveform().OverwriteSums(visual_waveform, r.in(), r.in() - range.in(), r.length()); - } - - original_track->waveform_lock()->unlock(); - - emit original_track->PreviewChanged(); - } + emit WaveformGenerated(ticket_, original_track, visual_waveform, range); } } @@ -199,9 +259,20 @@ QVariant RenderWorker::ProcessSamples(const Node *node, const TimeRange &range, // Update all non-sample and non-footage inputs NodeValueMap::const_iterator j; for (j=job.GetValues().constBegin(); j!=job.GetValues().constEnd(); j++) { - value_db.Insert(j.key(), ProcessInput(j.key(), TimeRange(this_sample_time, this_sample_time))); + NodeValueTable value; + NodeInput* corresponding_input = node->GetInputWithID(j.key()); + + if (corresponding_input) { + value = ProcessInput(corresponding_input, TimeRange(this_sample_time, this_sample_time)); + } else { + value.Push(j.value()); + } + + value_db.Insert(j.key(), value); } + AddGlobalsToDatabase(value_db, TimeRange(this_sample_time, this_sample_time)); + node->ProcessSamples(value_db, job.samples(), output_buffer, @@ -226,6 +297,8 @@ QVariant RenderWorker::ProcessFrameGeneration(const Node* node, const GenerateJo video_params_.height(), video_params_.time_base(), output_fmt, + video_params_.pixel_aspect_ratio(), + video_params_.interlacing(), video_params_.divider())); frame->allocate(); @@ -237,18 +310,18 @@ QVariant RenderWorker::ProcessFrameGeneration(const Node* node, const GenerateJo QVariant RenderWorker::GetCachedFrame(const Node* node, const rational& time) { if (node->id() == QStringLiteral("org.olivevideoeditor.Olive.videoinput")) { - QByteArray hash = RenderBackend::HashNode(node, video_params(), time); + QByteArray hash = HashNode(node, video_params(), time); - QString fn = FrameHashCache::CachePathName(hash); - - if (QFileInfo::exists(fn)) { - FramePtr f = FrameHashCache::LoadCacheFrame(hash); + FramePtr f = viewer_->video_frame_cache()->LoadCacheFrame(hash); + if (f) { // The cached frame won't load with the correct divider by default, so we enforce it here f->set_video_params(VideoParams(f->width() * video_params_.divider(), f->height() * video_params_.divider(), f->video_params().time_base(), f->video_params().format(), + f->video_params().pixel_aspect_ratio(), + f->video_params().interlacing(), video_params_.divider())); return CachedFrameToTexture(f); @@ -261,6 +334,7 @@ QVariant RenderWorker::GetCachedFrame(const Node* node, const rational& time) DecoderPtr RenderWorker::ResolveDecoderFromInput(StreamPtr stream) { // Access a map of Node inputs and decoder instances and retrieve a frame! + QMutexLocker locker(&decoder_lock_); DecoderPtr decoder = decoder_cache_.value(stream.get()); @@ -278,6 +352,8 @@ DecoderPtr RenderWorker::ResolveDecoderFromInput(StreamPtr stream) } } + decoder_age_.insert(stream.get(), QDateTime::currentMSecsSinceEpoch()); + return decoder; } @@ -316,9 +392,7 @@ QVariant RenderWorker::ProcessVideoFootage(StreamPtr stream, const rational &inp // Return a texture from the derived class value = FootageFrameToTexture(stream, frame); - if (value.isNull()) { - qDebug() << "Texture from derivative was blank"; - } else { + if (!value.isNull()) { // Put this into the image cache instead still_image_cache_.insert(stream.get(), {value, colorspace_match, @@ -326,8 +400,6 @@ QVariant RenderWorker::ProcessVideoFootage(StreamPtr stream, const rational &inp video_params_.divider(), time_match}); } - } else { - qDebug() << "Frame from decoder was blank"; } } @@ -346,37 +418,24 @@ QVariant RenderWorker::ProcessAudioFootage(StreamPtr stream, const TimeRange &in // See if we have a conformed version of this audio if (!decoder->HasConformedVersion(audio_params())) { - // If not, check what audio mode we're in - if (audio_mode_is_preview_) { + // If not, the audio needs to be conformed + // For online rendering/export, it's a waste of time to render the audio until we have + // all we need, so we try to handle the conform ourselves + AudioStreamPtr as = std::static_pointer_cast(stream); - // For preview, we report the conform is missing and finish the render without it - // temporarily. The backend that picks up this signal will recache this section once the - // conform is available. - emit AudioConformUnavailable(decoder->stream(), - audio_render_time_, - input_time.out(), - audio_params()); + // Check if any other threads are conforming this audio + if (as->try_start_conforming(audio_params())) { + + // If not, conform it ourselves + decoder->ConformAudio(&IsCancelled(), audio_params()); } else { - // For online rendering/export, it's a waste of time to render the audio until we have - // all we need, so we try to handle the conform ourselves - AudioStreamPtr as = std::static_pointer_cast(stream); + // If another thread is conforming already, hackily try to wait until it's done. + do { + QThread::msleep(1000); + } while (!as->has_conformed_version(audio_params()) && !IsCancelled()); - // Check if any other threads are conforming this audio - if (as->try_start_conforming(audio_params())) { - - // If not, conform it ourselves - decoder->ConformAudio(&IsCancelled(), audio_params()); - - } else { - - // If another thread is conforming already, hackily try to wait until it's done. - do { - QThread::msleep(1000); - } while (!as->has_conformed_version(audio_params()) && !IsCancelled()); - - } } } diff --git a/app/render/backend/renderworker.h b/app/render/backend/renderworker.h index a8848c643..6d4729cb9 100644 --- a/app/render/backend/renderworker.h +++ b/app/render/backend/renderworker.h @@ -38,6 +38,8 @@ class RenderWorker : public QObject, public NodeTraverser public: RenderWorker(RenderBackend* parent); + virtual ~RenderWorker() override; + bool IsAvailable() const { return available_; @@ -48,6 +50,11 @@ public: available_ = a; } + void SetViewerNode(ViewerOutput* viewer) + { + viewer_ = viewer; + } + void SetVideoParams(const VideoParams& params) { video_params_ = params; @@ -63,11 +70,6 @@ public: video_download_matrix_ = mat; } - void SetAudioModeIsPreview(bool audio_mode_is_preview) - { - audio_mode_is_preview_ = audio_mode_is_preview; - } - void SetCopyMap(QHash* copy_map) { copy_map_ = copy_map; @@ -78,23 +80,12 @@ public: render_mode_ = mode; } - void EnablePreviewGeneration(AudioPlaybackCache* cache, qint64 job_time) + void SetPreviewGenerationEnabled(bool e) { - preview_cache_ = cache; - preview_job_time_ = job_time; + generate_audio_previews_ = e; } - /** - * @brief Return a unique ID for the image generated at this time - * - * This hash should always be unique to this image and can therefore be used to match existing - * cached frames. - * - * @return - * - * SHA-1 hash or empty QByteArray if no viewer node is set. - */ - void Hash(RenderTicketPtr ticket, ViewerOutput *viewer, const QList& times); + void Hash(RenderTicketPtr ticket, ViewerOutput* viewer, const QVector& times); /** * @brief Render the frame at this time @@ -153,13 +144,17 @@ signals: void FinishedJob(); - void WaveformGenerated(OLIVE_NAMESPACE::TrackOutput* track, OLIVE_NAMESPACE::AudioVisualWaveform samples, OLIVE_NAMESPACE::TimeRange start); + void WaveformGenerated(OLIVE_NAMESPACE::RenderTicketPtr ticket, OLIVE_NAMESPACE::TrackOutput* track, OLIVE_NAMESPACE::AudioVisualWaveform samples, OLIVE_NAMESPACE::TimeRange range); private: DecoderPtr ResolveDecoderFromInput(StreamPtr stream); + static QByteArray HashNode(const Node* n, const VideoParams& params, const rational& time); + RenderBackend* parent_; + RenderTicketPtr ticket_; + VideoParams video_params_; AudioParams audio_params_; @@ -176,20 +171,28 @@ private: QMatrix4x4 video_download_matrix_; + QMutex decoder_lock_; DecoderCache decoder_cache_; + QHash decoder_age_; TimeRange audio_render_time_; bool available_; - bool audio_mode_is_preview_; + bool generate_audio_previews_; - AudioPlaybackCache* preview_cache_; - qint64 preview_job_time_; + ViewerOutput* viewer_; QHash* copy_map_; RenderMode::Mode render_mode_; + QTimer* cleanup_timer_; + + static const int kMaxDecoderLife; + +private slots: + void ClearOldDecoders(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/diskmanager.cpp b/app/render/diskmanager.cpp index 2c609f357..bd75bf077 100644 --- a/app/render/diskmanager.cpp +++ b/app/render/diskmanager.cpp @@ -25,61 +25,66 @@ #include #include #include +#include #include #include "common/filefunctions.h" #include "config/config.h" +#include "core.h" +#include "dialog/diskcache/diskcachedialog.h" OLIVE_NAMESPACE_ENTER DiskManager* DiskManager::instance_ = nullptr; -DiskManager::DiskManager() : - consumption_(0) +DiskManager::DiskManager() { - // Try to load any current cache index from file - QFile cache_index_file(GetCacheIndexFilename()); + // Add default cache location + QFile default_disk_cache_file(GetDefaultDiskCacheConfigFile()); + if (default_disk_cache_file.open(QFile::ReadOnly)) { + QString default_dir = default_disk_cache_file.readAll(); - if (cache_index_file.open(QFile::ReadOnly)) { - QDataStream ds(&cache_index_file); - - while (!cache_index_file.atEnd()) { - HashTime h; - - ds >> h.file_name; - ds >> h.hash; - ds >> h.access_time; - ds >> h.file_size; - - if (QFileInfo::exists(h.file_name)) { - consumption_ += h.file_size; - disk_data_.append(h); + if (!default_dir.isEmpty()) { + if (FileFunctions::DirectoryIsValid(default_dir, true)) { + GetOpenFolder(default_dir); + } else { + QMessageBox::warning(nullptr, + tr("Disk Cache Error"), + tr("Unable to set custom application disk cache. Using default instead.")); } } + + default_disk_cache_file.close(); + } + + // If no custom default was loaded, load default + if (open_folders_.isEmpty()) { + GetOpenFolder(GetDefaultDiskCachePath()); + } + + QFile disk_cache_index(QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("diskcache"))); + + if (disk_cache_index.open(QFile::ReadOnly)) { + QTextStream stream(&disk_cache_index); + + QString line; + while (stream.readLineInto(&line)) { + GetOpenFolder(line); + } + + disk_cache_index.close(); } } DiskManager::~DiskManager() { - if (Config::Current()["ClearDiskCacheOnClose"].toBool()) { - // Clear all cache data - ClearDiskCache(true); - } else { - // Save current cache index - QFile cache_index_file(GetCacheIndexFilename()); - - if (cache_index_file.open(QFile::WriteOnly)) { - QDataStream ds(&cache_index_file); - - foreach (const HashTime& h, disk_data_) { - ds << h.file_name; - ds << h.hash; - ds << h.access_time; - ds << h.file_size; - } - } else { - qWarning() << "Failed to write cache index:" << GetCacheIndexFilename(); + QFile default_disk_cache_file(GetDefaultDiskCacheConfigFile()); + if (default_disk_cache_file.open(QFile::WriteOnly)) { + if (GetDefaultDiskCachePath() != GetDefaultCachePath()) { + default_disk_cache_file.write(GetDefaultCachePath().toUtf8()); } + + default_disk_cache_file.close(); } } @@ -99,107 +104,220 @@ DiskManager *DiskManager::instance() return instance_; } -void DiskManager::Accessed(const QByteArray &hash) +void DiskManager::Accessed(const QString &cache_folder, const QByteArray &hash) { - lock_.lock(); + DiskCacheFolder* f = GetOpenFolder(cache_folder); - for (int i=disk_data_.size()-1;i>=0;i--) { - const HashTime& h = disk_data_.at(i); + f->Accessed(hash); +} - if (h.hash == hash) { - HashTime moved_hash = h; +void DiskManager::CreatedFile(const QString &cache_folder, const QString &file_name, const QByteArray &hash) +{ + DiskCacheFolder* f = GetOpenFolder(cache_folder); - moved_hash.access_time = QDateTime::currentMSecsSinceEpoch(); + f->CreatedFile(file_name, hash); +} - disk_data_.removeAt(i); - disk_data_.append(moved_hash); - break; +bool DiskManager::ClearDiskCache(const QString &cache_folder) +{ + DiskCacheFolder* f = GetOpenFolder(cache_folder); + + return f->ClearCache(); +} + +DiskCacheFolder *DiskManager::GetOpenFolder(const QString &path) +{ + // If path is empty, this must mean default + if (path.isEmpty()) { + return GetDefaultCacheFolder(); + } + + // See if we have an existing path with this name + foreach (DiskCacheFolder* f, open_folders_) { + if (f->GetPath() == path) { + return f; } } - lock_.unlock(); + // We must have to open this folder + DiskCacheFolder* f = new DiskCacheFolder(path, this); + connect(f, &DiskCacheFolder::DeletedFrame, this, &DiskManager::DeletedFrame); + open_folders_.append(f); + + return f; } -void DiskManager::Accessed(const QString &filename) +bool DiskManager::ShowDiskCacheChangeConfirmationDialog(QWidget *parent) { - lock_.lock(); + return (QMessageBox::question(parent, + tr("Disk Cache"), + tr("You've chosen to change the default disk cache location. This " + "will invalidate your current cache. Would you like to continue?"), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok); +} - for (int i=disk_data_.size()-1;i>=0;i--) { - const HashTime& h = disk_data_.at(i); +QString DiskManager::GetDefaultDiskCacheConfigFile() +{ + return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("defaultdiskcache")); +} - if (h.file_name == filename) { - HashTime moved_hash = h; +QString DiskManager::GetDefaultDiskCachePath() +{ + return QDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("mediacache"); +} - moved_hash.access_time = QDateTime::currentMSecsSinceEpoch(); +void DiskManager::ShowDiskCacheSettingsDialog(DiskCacheFolder *folder, QWidget *parent) +{ + DiskCacheDialog d(folder, parent); + d.exec(); +} - disk_data_.removeAt(i); - disk_data_.append(moved_hash); - break; +void DiskManager::ShowDiskCacheSettingsDialog(const QString &path, QWidget *parent) +{ + if (!FileFunctions::DirectoryIsValid(path, true)) { + QMessageBox::critical(parent, tr("Disk Cache Error"), + tr("Failed to open disk cache at \"%1\". Try a different folder.").arg(path)); + return; + } + + DiskCacheFolder* folder = GetOpenFolder(path); + + ShowDiskCacheSettingsDialog(folder, parent); +} + +DiskCacheFolder::DiskCacheFolder(const QString &path, QObject *parent) : + QObject(parent) +{ + SetPath(path); + + save_timer_.setInterval(Config::Current()[QStringLiteral("DiskCacheSaveInterval")].toInt()); + connect(&save_timer_, &QTimer::timeout, this, &DiskCacheFolder::SaveDiskCacheIndex); + save_timer_.start(); +} + +DiskCacheFolder::~DiskCacheFolder() +{ + CloseCacheFolder(); +} + +bool DiskCacheFolder::ClearCache() +{ + bool deleted_files = true; + + std::list::iterator i = disk_data_.begin(); + + while (i != disk_data_.end()) { + // We return a false result if any of the files fail to delete, but still try to delete as many as we can + if (QFile::remove(i->file_name) || !QFileInfo::exists(i->file_name)) { + emit DeletedFrame(path_, i->hash); + i = disk_data_.erase(i); + } else { + qWarning() << "Failed to delete" << i->file_name; + deleted_files = false; + i++; } } - lock_.unlock(); + return deleted_files; } -void DiskManager::CreatedFile(const QString &file_name, const QByteArray &hash) +void DiskCacheFolder::Accessed(const QByteArray &hash) { - lock_.lock(); + std::list::iterator i = disk_data_.begin(); + while (i != disk_data_.end()) { + if (i->hash == hash) { + // Copy access data and erase from list + HashTime accessed_hash = *i; + disk_data_.erase(i); + + // Add it to the end + disk_data_.push_back(accessed_hash); + + // End loop + break; + } else { + i++; + } + } +} + +void DiskCacheFolder::CreatedFile(const QString &file_name, const QByteArray &hash) +{ qint64 file_size = QFile(file_name).size(); - disk_data_.append({file_name, hash, QDateTime::currentMSecsSinceEpoch(), file_size}); + disk_data_.push_back({file_name, hash, file_size}); consumption_ += file_size; QList deleted_hashes; - while (consumption_ > DiskLimit()) { + while (consumption_ > limit_) { deleted_hashes.append(DeleteLeastRecent()); } - lock_.unlock(); - foreach (const QByteArray& h, deleted_hashes) { - emit DeletedFrame(h); + emit DeletedFrame(path_, h); } } -bool DiskManager::ClearDiskCache(bool quick_delete) +void DiskCacheFolder::SetPath(const QString &path) { - bool deleted_files; - - lock_.lock(); - - if (quick_delete) { - deleted_files = QDir(FileFunctions::GetMediaCacheLocation()).removeRecursively(); + // If this is currently set to a folder, close it out now + CloseCacheFolder(); + // Signal that disk cache is gone + if (!disk_data_.empty()) { + foreach (const HashTime& h, disk_data_) { + emit DeletedFrame(path_, h.hash); + } disk_data_.clear(); - } else { - deleted_files = true; + } - for (int i=0;i> limit_; + ds >> clear_on_close_; + + while (!cache_index_file.atEnd()) { + HashTime h; + + ds >> h.file_name; + ds >> h.hash; + ds >> h.file_size; + + if (QFileInfo::exists(h.file_name)) { + consumption_ += h.file_size; + disk_data_.push_back(h); } } + + cache_index_file.close(); } - - lock_.unlock(); - - return deleted_files; } -QByteArray DiskManager::DeleteLeastRecent() +QByteArray DiskCacheFolder::DeleteLeastRecent() { - HashTime h = disk_data_.takeFirst(); + HashTime h = disk_data_.front(); + disk_data_.pop_front(); QFile::remove(h.file_name); @@ -208,19 +326,42 @@ QByteArray DiskManager::DeleteLeastRecent() return h.hash; } -qint64 DiskManager::DiskLimit() +void DiskCacheFolder::CloseCacheFolder() { - double gigabytes = Config::Current()["DiskCacheSize"].toDouble(); + if (path_.isEmpty()) { + return; + } - // Convert gigabytes to bytes - return qRound64(gigabytes * 1073741824); + if (clear_on_close_) { + // If we're not moving to new and we're set to clear on close, clear now or else it'll never + // get cleared later + ClearCache(); + } + + // Save current cache index + SaveDiskCacheIndex(); } -QString DiskManager::GetCacheIndexFilename() +void DiskCacheFolder::SaveDiskCacheIndex() { - QDir d(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); - d.mkpath("."); - return d.filePath("diskindex"); + QFile cache_index_file(index_path_); + + if (cache_index_file.open(QFile::WriteOnly)) { + QDataStream ds(&cache_index_file); + + ds << limit_; + ds << clear_on_close_; + + foreach (const HashTime& h, disk_data_) { + ds << h.file_name; + ds << h.hash; + ds << h.file_size; + } + + cache_index_file.close(); + } else { + qWarning() << "Failed to write cache index:" << index_path_; + } } OLIVE_NAMESPACE_EXIT diff --git a/app/render/diskmanager.h b/app/render/diskmanager.h index 4df5f6d36..9064a0d33 100644 --- a/app/render/diskmanager.h +++ b/app/render/diskmanager.h @@ -21,13 +21,90 @@ #ifndef DISKMANAGER_H #define DISKMANAGER_H +#include #include #include +#include #include "common/define.h" +#include "project/project.h" OLIVE_NAMESPACE_ENTER +class DiskCacheFolder : public QObject +{ + Q_OBJECT +public: + DiskCacheFolder(const QString& path, QObject* parent = nullptr); + + virtual ~DiskCacheFolder() override; + + bool ClearCache(); + + void Accessed(const QByteArray& hash); + + void CreatedFile(const QString& file_name, const QByteArray& hash); + + const QString& GetPath() const + { + return path_; + } + + void SetPath(const QString& path); + + qint64 GetLimit() const + { + return limit_; + } + + bool GetClearOnClose() const + { + return clear_on_close_; + } + + void SetLimit(qint64 l) + { + limit_ = l; + } + + void SetClearOnClose(bool e) + { + clear_on_close_ = e; + } + +signals: + void DeletedFrame(const QString& path, const QByteArray& hash); + +private: + QByteArray DeleteLeastRecent(); + + void CloseCacheFolder(); + + QString path_; + + QString index_path_; + + struct HashTime { + QString file_name; + QByteArray hash; + qint64 file_size; + }; + + std::list disk_data_; + + qint64 consumption_; + + qint64 limit_; + + bool clear_on_close_; + + QTimer save_timer_; + +private slots: + void SaveDiskCacheIndex(); + +}; + class DiskManager : public QObject { Q_OBJECT @@ -38,16 +115,44 @@ public: static DiskManager* instance(); - void Accessed(const QByteArray& hash); + bool ClearDiskCache(const QString& cache_folder); - void Accessed(const QString& filename); + DiskCacheFolder* GetDefaultCacheFolder() const + { + // The first folder will always be the default + return open_folders_.first(); + } - void CreatedFile(const QString& file_name, const QByteArray& hash); + const QString& GetDefaultCachePath() const + { + return GetDefaultCacheFolder()->GetPath(); + } - bool ClearDiskCache(bool quick_delete); + DiskCacheFolder* GetOpenFolder(const QString& path); + + const QVector& GetOpenFolders() const + { + return open_folders_; + } + + static bool ShowDiskCacheChangeConfirmationDialog(QWidget* parent); + + static QString GetDefaultDiskCacheConfigFile(); + + static QString GetDefaultDiskCachePath(); + + void ShowDiskCacheSettingsDialog(DiskCacheFolder* folder, QWidget* parent); + void ShowDiskCacheSettingsDialog(const QString& path, QWidget* parent); + +public slots: + void Accessed(const QString& cache_folder, const QByteArray& hash); + + void CreatedFile(const QString& cache_folder, const QString& file_name, const QByteArray& hash); signals: - void DeletedFrame(const QByteArray& hash); + void DeletedFrame(const QString& path, const QByteArray& hash); + + void InvalidateProject(Project* p); private: DiskManager(); @@ -56,24 +161,7 @@ private: static DiskManager* instance_; - QByteArray DeleteLeastRecent(); - - qint64 DiskLimit(); - - static QString GetCacheIndexFilename(); - - struct HashTime { - QString file_name; - QByteArray hash; - qint64 access_time; - qint64 file_size; - }; - - QList disk_data_; - - qint64 consumption_; - - QMutex lock_; + QVector open_folders_; }; diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 0bde0581a..7ed84d274 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -34,17 +34,22 @@ OLIVE_NAMESPACE_ENTER +FrameHashCache::FrameHashCache(QObject *parent) : + PlaybackCache(parent) +{ + if (DiskManager::instance()) { + connect(DiskManager::instance(), &DiskManager::DeletedFrame, this, &FrameHashCache::HashDeleted); + connect(DiskManager::instance(), &DiskManager::InvalidateProject, this, &FrameHashCache::ProjectInvalidated); + } +} + QByteArray FrameHashCache::GetHash(const rational &time) { - QMutexLocker locker(lock()); - return time_hash_map_.value(time); } -void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const qint64& job_time) +void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const qint64& job_time, bool frame_exists) { - QMutexLocker locker(lock()); - bool is_current = false; for (int i=jobs_.size()-1; i>=0; i--) { @@ -63,26 +68,37 @@ void FrameHashCache::SetHash(const rational &time, const QByteArray &hash, const time_hash_map_.insert(time, hash); - TimeRange validated_range(time, time + timebase_); - - NoLockValidate(validated_range); - - locker.unlock(); - - emit Validated(validated_range); + TimeRange validated_range; + if (frame_exists) { + validated_range = TimeRange(time, time + timebase_); + Validate(validated_range); + } } void FrameHashCache::SetTimebase(const rational &tb) { - QMutexLocker locker(lock()); - timebase_ = tb; } +void FrameHashCache::ValidateFramesWithHash(const QByteArray &hash) +{ + QMap::const_iterator iterator; + + const TimeRangeList& invalidated_ranges = GetInvalidatedRanges(); + + for (iterator=time_hash_map_.begin();iterator!=time_hash_map_.end();iterator++) { + if (iterator.value() == hash) { + TimeRange frame_range(iterator.key(), iterator.key() + timebase_); + + if (invalidated_ranges.ContainsTimeRange(frame_range)) { + Validate(frame_range); + } + } + } +} + QList FrameHashCache::GetFramesWithHash(const QByteArray &hash) { - QMutexLocker locker(lock()); - QList times; QMap::const_iterator iterator; @@ -98,8 +114,6 @@ QList FrameHashCache::GetFramesWithHash(const QByteArray &hash) QList FrameHashCache::TakeFramesWithHash(const QByteArray &hash) { - QMutexLocker locker(lock()); - QList times; QMap::iterator iterator = time_hash_map_.begin(); @@ -115,13 +129,7 @@ QList FrameHashCache::TakeFramesWithHash(const QByteArray &hash) } foreach (const rational& r, times) { - NoLockInvalidate(TimeRange(r, r + timebase_)); - } - - locker.unlock(); - - foreach (const rational& r, times) { - emit Invalidated(TimeRange(r, r + timebase_)); + Invalidate(TimeRange(r, r + timebase_)); } return times; @@ -129,8 +137,6 @@ QList FrameHashCache::TakeFramesWithHash(const QByteArray &hash) QMap FrameHashCache::time_hash_map() { - QMutexLocker locker(lock()); - return time_hash_map_; } @@ -166,74 +172,106 @@ QVector FrameHashCache::GetFrameListFromTimeRange(TimeRangeList range_ QVector FrameHashCache::GetFrameListFromTimeRange(const TimeRangeList &range) { - QMutexLocker locker(lock()); - return GetFrameListFromTimeRange(range, timebase_); } QVector FrameHashCache::GetInvalidatedFrames() { - QMutexLocker locker(lock()); - - return GetFrameListFromTimeRange(NoLockGetInvalidatedRanges()); + return GetFrameListFromTimeRange(GetInvalidatedRanges()); } -void FrameHashCache::SaveCacheFrame(const QByteArray& hash, +QVector FrameHashCache::GetInvalidatedFrames(const TimeRange &intersecting) +{ + return GetFrameListFromTimeRange(GetInvalidatedRanges().Intersects(intersecting)); +} + +bool FrameHashCache::SaveCacheFrame(const QByteArray& hash, char* data, const VideoParams& vparam, - int linesize_bytes) + int linesize_bytes) const { QString fn = CachePathName(hash); if (SaveCacheFrame(fn, data, vparam, linesize_bytes)) { // Register frame with the disk manager - DiskManager::instance()->CreatedFile(fn, hash); + QMetaObject::invokeMethod(DiskManager::instance(), + "CreatedFile", + Qt::QueuedConnection, + Q_ARG(QString, GetCacheDirectory()), + Q_ARG(QString, fn), + Q_ARG(QByteArray, hash)); + + return true; + } else { + return false; } } -void FrameHashCache::SaveCacheFrame(const QByteArray &hash, FramePtr frame) +bool FrameHashCache::SaveCacheFrame(const QByteArray &hash, FramePtr frame) const { - SaveCacheFrame(hash, frame->data(), frame->video_params(), frame->linesize_bytes()); + if (frame) { + return SaveCacheFrame(hash, frame->data(), frame->video_params(), frame->linesize_bytes()); + } else { + qWarning() << "Attempted to save a NULL frame to the cache. This may or may not be desirable."; + return false; + } } -FramePtr FrameHashCache::LoadCacheFrame(const QByteArray &hash) +FramePtr FrameHashCache::LoadCacheFrame(const QByteArray &hash) const { return LoadCacheFrame(CachePathName(hash)); } -FramePtr FrameHashCache::LoadCacheFrame(const QString &fn) +FramePtr FrameHashCache::LoadCacheFrame(const QString &fn) const { FramePtr frame = nullptr; if (!fn.isEmpty() && QFileInfo::exists(fn)) { - auto input = OIIO::ImageInput::open(fn.toStdString()); + Imf::InputFile file(fn.toUtf8(), 0); - if (input) { - - PixelFormat::Format image_format = PixelFormat::OIIOFormatToOliveFormat(input->spec().format, - input->spec().nchannels == kRGBAChannels); - - frame = Frame::Create(); - frame->set_video_params(VideoParams(input->spec().width, - input->spec().height, - image_format)); - - frame->allocate(); - - input->read_image(input->spec().format, - frame->data(), - OIIO::AutoStride, - frame->linesize_bytes()); - - input->close(); - -#if OIIO_VERSION < 10903 - OIIO::ImageInput::destroy(input); -#endif + Imath::Box2i dw = file.header().dataWindow(); + Imf::PixelType pix_type = file.header().channels().begin().channel().type; + int width = dw.max.x - dw.min.x + 1; + int height = dw.max.y - dw.min.y + 1; + bool has_alpha = file.header().channels().findChannel("A"); + PixelFormat::Format image_format; + if (pix_type == Imf::HALF) { + if (has_alpha) { + image_format = PixelFormat::PIX_FMT_RGBA16F; + } else { + image_format = PixelFormat::PIX_FMT_RGB16F; + } } else { - qWarning() << "OIIO Error:" << OIIO::geterror().c_str(); + if (has_alpha) { + image_format = PixelFormat::PIX_FMT_RGBA32F; + } else { + image_format = PixelFormat::PIX_FMT_RGB32F; + } } + + frame = Frame::Create(); + frame->set_video_params(VideoParams(width, + height, + image_format)); + + frame->allocate(); + + int bpc = PixelFormat::BytesPerChannel(image_format); + + size_t xs = PixelFormat::ChannelCount(image_format) * bpc; + size_t ys = frame->linesize_bytes(); + + Imf::FrameBuffer framebuffer; + framebuffer.insert("R", Imf::Slice(pix_type, frame->data(), xs, ys)); + framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc, xs, ys)); + framebuffer.insert("B", Imf::Slice(pix_type, frame->data() + 2*bpc, xs, ys)); + if (has_alpha) { + framebuffer.insert("A", Imf::Slice(pix_type, frame->data() + 3*bpc, xs, ys)); + } + + file.setFrameBuffer(framebuffer); + file.readPixels(dw.min.y, dw.max.y); } return frame; @@ -254,19 +292,6 @@ void FrameHashCache::LengthChangedEvent(const rational &old, const rational &new } } -void FrameHashCache::InvalidateEvent(const TimeRange &r) -{ - QMap::iterator i = time_hash_map_.begin(); - - while (i != time_hash_map_.end()) { - if (i.key() >= r.in() && i.key() < r.out()) { - i = time_hash_map_.erase(i); - } else { - i++; - } - } -} - struct HashTimePair { rational time; QByteArray hash; @@ -308,19 +333,50 @@ void FrameHashCache::ShiftEvent(const rational &from, const rational &to) } } -QString FrameHashCache::CachePathName(const QByteArray& hash) +void FrameHashCache::HashDeleted(const QString& s, const QByteArray &hash) +{ + QString cache_dir = GetCacheDirectory(); + if (cache_dir.isEmpty() || s != cache_dir) { + return; + } + + QMap::const_iterator i; + for (i=time_hash_map_.constBegin(); i!=time_hash_map_.constEnd(); i++) { + if (i.value() == hash) { + Invalidate(TimeRange(i.key(), i.key() + timebase_)); + } + } +} + +void FrameHashCache::ProjectInvalidated(Project *p) +{ + if (GetProject() == p) { + time_hash_map_.clear(); + + InvalidateAll(); + } +} + +QString FrameHashCache::CachePathName(const QByteArray& hash) const { QString ext = GetFormatExtension(); - QDir cache_dir(QDir(FileFunctions::GetMediaCacheLocation()).filePath(QString(hash.left(1).toHex()))); + QDir cache_dir(QDir(GetCacheDirectory()).filePath(QString(hash.left(1).toHex()))); cache_dir.mkpath("."); QString filename = QStringLiteral("%1%2").arg(QString(hash.mid(1).toHex()), ext); + // Register that in some way this hash has been accessed + QMetaObject::invokeMethod(DiskManager::instance(), + "Accessed", + Qt::QueuedConnection, + Q_ARG(QString, GetCacheDirectory()), + Q_ARG(QByteArray, hash)); + return cache_dir.filePath(filename); } -bool FrameHashCache::SaveCacheFrame(const QString &filename, char *data, const VideoParams &vparam, int linesize_bytes) +bool FrameHashCache::SaveCacheFrame(const QString &filename, char *data, const VideoParams &vparam, int linesize_bytes) const { Q_ASSERT(PixelFormat::FormatIsFloat(vparam.format())); diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index a31c4335c..d3314e587 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -35,14 +35,14 @@ class FrameHashCache : public PlaybackCache { Q_OBJECT public: - FrameHashCache() = default; + FrameHashCache(QObject* parent = nullptr); QByteArray GetHash(const rational& time); - void SetHash(const rational& time, const QByteArray& hash, const qint64 &job_time); - void SetTimebase(const rational& tb); + void ValidateFramesWithHash(const QByteArray& hash); + /** * @brief Returns a list of frames that use a particular hash */ @@ -58,25 +58,27 @@ public: /** * @brief Return the path of the cached image at this time */ - static QString CachePathName(const QByteArray &hash); + QString CachePathName(const QByteArray &hash) const; - static bool SaveCacheFrame(const QString& filename, char *data, const VideoParams &vparam, int linesize_bytes); - static void SaveCacheFrame(const QByteArray& hash, char *data, const VideoParams &vparam, int linesize_bytes); - static void SaveCacheFrame(const QByteArray& hash, FramePtr frame); - static FramePtr LoadCacheFrame(const QByteArray& hash); - static FramePtr LoadCacheFrame(const QString& fn); + bool SaveCacheFrame(const QString& filename, char *data, const VideoParams &vparam, int linesize_bytes) const; + bool SaveCacheFrame(const QByteArray& hash, char *data, const VideoParams &vparam, int linesize_bytes) const; + bool SaveCacheFrame(const QByteArray& hash, FramePtr frame) const; + FramePtr LoadCacheFrame(const QByteArray& hash) const; + FramePtr LoadCacheFrame(const QString& fn) const; static QString GetFormatExtension(); static QVector GetFrameListFromTimeRange(TimeRangeList range_list, const rational& timebase); QVector GetFrameListFromTimeRange(const TimeRangeList &range); QVector GetInvalidatedFrames(); + QVector GetInvalidatedFrames(const TimeRange& intersecting); + +public slots: + void SetHash(const OLIVE_NAMESPACE::rational& time, const QByteArray& hash, const qint64 &job_time, bool frame_exists); protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; - virtual void InvalidateEvent(const TimeRange& range) override; - virtual void ShiftEvent(const rational& from, const rational& to) override; private: @@ -84,6 +86,11 @@ private: rational timebase_; +private slots: + void HashDeleted(const QString &s, const QByteArray& hash); + + void ProjectInvalidated(Project* p); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/pixelformat.cpp b/app/render/pixelformat.cpp index 4c1ee999d..01abf0aff 100644 --- a/app/render/pixelformat.cpp +++ b/app/render/pixelformat.cpp @@ -263,8 +263,8 @@ FramePtr PixelFormat::ConvertPixelFormat(FramePtr frame, const PixelFormat::Form // Create a destination frame with the same parameters FramePtr converted = Frame::Create(); converted->set_video_params(VideoParams(frame->video_params().width(), - frame->video_params().height(), - dest_format)); + frame->video_params().height(), + dest_format)); converted->set_timestamp(frame->timestamp()); converted->allocate(); diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index b94ed08e0..d791e4bd9 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -22,40 +22,38 @@ #include +#include "node/output/viewer/viewer.h" +#include "project/item/sequence/sequence.h" +#include "project/project.h" + OLIVE_NAMESPACE_ENTER void PlaybackCache::Invalidate(const TimeRange &r) { - QMutexLocker locker(lock()); + Q_ASSERT(r.in() != r.out()); - NoLockInvalidate(r); + invalidated_.InsertTimeRange(r); - locker.unlock(); + RemoveRangeFromJobs(r); + qint64 job_time = QDateTime::currentMSecsSinceEpoch(); + jobs_.append({r, job_time}); + + InvalidateEvent(r); emit Invalidated(r); } void PlaybackCache::InvalidateAll() { - QMutexLocker locker(lock()); - if (length_.isNull()) { return; } - TimeRange invalidate_range(0, length_); - - NoLockInvalidate(invalidate_range); - - locker.unlock(); - - emit Invalidated(invalidate_range); + Invalidate(TimeRange(0, length_)); } void PlaybackCache::SetLength(const rational &r) { - QMutexLocker locker(lock()); - if (length_ == r) { // Same length - do nothing return; @@ -71,6 +69,7 @@ void PlaybackCache::SetLength(const rational &r) } else if (r > length_) { // If new length is greater, simply extend the invalidated range for now invalidated_.InsertTimeRange(range_diff); + jobs_.append({range_diff, QDateTime::currentMSecsSinceEpoch()}); } else { // If new length is smaller, removed hashes invalidated_.RemoveTimeRange(range_diff); @@ -80,8 +79,6 @@ void PlaybackCache::SetLength(const rational &r) rational old_length = length_; length_ = r; - locker.unlock(); - if (r > old_length) { emit Invalidated(range_diff); } else { @@ -95,21 +92,19 @@ void PlaybackCache::Shift(const rational &from, const rational &to) return; } - QMutexLocker locker(lock()); - // An region between `from` and `to` will be inserted or spliced out TimeRangeList ranges_to_shift = invalidated_.Intersects(TimeRange(from, RATIONAL_MAX)); // Remove everything from the minimum point TimeRange remove_range = TimeRange(qMin(from, to), RATIONAL_MAX); - NoLockValidate(remove_range); RemoveRangeFromJobs(remove_range); + Validate(remove_range); // Shift everything in our ranges to shift list // (`diff` is POSITIVE when moving forward -> and NEGATIVE when moving backward <-) rational diff = to - from; foreach (const TimeRange& r, ranges_to_shift) { - NoLockInvalidate(r + diff); + Invalidate(r + diff); } ShiftEvent(from, to); @@ -118,38 +113,18 @@ void PlaybackCache::Shift(const rational &from, const rational &to) if (diff > rational()) { // If shifting forward, add this section to the invalidated region - NoLockInvalidate(TimeRange(from, to)); + Invalidate(TimeRange(from, to)); } - locker.unlock(); - // Emit signals - emit Validated(remove_range); - foreach (const TimeRange& r, ranges_to_shift) { - emit Invalidated(r + diff); - } - if (diff > rational()) { - emit Invalidated(TimeRange(from, to)); - } emit Shifted(from, to); } -void PlaybackCache::NoLockInvalidate(const TimeRange &r) -{ - Q_ASSERT(r.in() != r.out()); - - invalidated_.InsertTimeRange(r); - - RemoveRangeFromJobs(r); - qint64 job_time = QDateTime::currentMSecsSinceEpoch(); - jobs_.append({r, job_time}); - - InvalidateEvent(r); -} - -void PlaybackCache::NoLockValidate(const TimeRange &r) +void PlaybackCache::Validate(const TimeRange &r) { invalidated_.RemoveTimeRange(r); + + emit Validated(r); } void PlaybackCache::LengthChangedEvent(const rational &, const rational &) @@ -164,6 +139,22 @@ void PlaybackCache::ShiftEvent(const rational &, const rational &) { } +Project *PlaybackCache::GetProject() const +{ + // NOTE: A lot of assumptions in this behavior + ViewerOutput* viewer = static_cast(parent()); + if (!viewer) { + return nullptr; + } + + Sequence* sequence = static_cast(viewer->parent()); + if (!sequence) { + return nullptr; + } + + return sequence->project(); +} + void PlaybackCache::RemoveRangeFromJobs(const TimeRange &remove) { // Code shamelessly copied from TimeRangeList::RemoveTimeRange @@ -189,4 +180,15 @@ void PlaybackCache::RemoveRangeFromJobs(const TimeRange &remove) } } +QString PlaybackCache::GetCacheDirectory() const +{ + Project* project = GetProject(); + + if (project) { + return project->cache_path(); + } else { + return QString(); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index f73cefb9f..2f857d4c1 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -28,11 +28,16 @@ OLIVE_NAMESPACE_ENTER +class Project; + class PlaybackCache : public QObject { Q_OBJECT public: - PlaybackCache() = default; + PlaybackCache(QObject* parent = nullptr) : + QObject(parent) + { + } void Invalidate(const TimeRange& r); @@ -40,17 +45,13 @@ public: const rational& GetLength() { - QMutexLocker locker(lock()); - - return NoLockGetLength(); + return length_; } void SetLength(const rational& r); bool IsFullyValidated() { - QMutexLocker locker(lock()); - return invalidated_.isEmpty(); } @@ -58,15 +59,11 @@ public: const TimeRangeList& GetInvalidatedRanges() { - QMutexLocker locker(lock()); - - return NoLockGetInvalidatedRanges(); + return invalidated_; } bool HasInvalidatedRanges() { - QMutexLocker locker(lock()); - return !invalidated_.isEmpty(); } @@ -80,19 +77,7 @@ signals: void LengthChanged(const OLIVE_NAMESPACE::rational& r); protected: - void NoLockInvalidate(const TimeRange& r); - - void NoLockValidate(const TimeRange& r); - - const rational& NoLockGetLength() const - { - return length_; - } - - const TimeRangeList& NoLockGetInvalidatedRanges() - { - return invalidated_; - } + void Validate(const TimeRange& r); virtual void LengthChangedEvent(const rational& old, const rational& newlen); @@ -100,10 +85,9 @@ protected: virtual void ShiftEvent(const rational& from, const rational& to); - QMutex* lock() - { - return &lock_; - } + Project* GetProject() const; + + QString GetCacheDirectory() const; struct JobIdentifier { TimeRange range; @@ -115,8 +99,6 @@ protected: private: void RemoveRangeFromJobs(const TimeRange& remove); - QMutex lock_; - TimeRangeList invalidated_; rational length_; diff --git a/app/render/shaderinfo.h b/app/render/shaderinfo.h index 9db03d9e0..1fa685e2b 100644 --- a/app/render/shaderinfo.h +++ b/app/render/shaderinfo.h @@ -8,13 +8,18 @@ OLIVE_NAMESPACE_ENTER -using NodeValueMap = QHash; +using NodeValueMap = QHash; class AcceleratedJob { public: AcceleratedJob() = default; NodeValue GetValue(NodeInput* input) const + { + return value_map_.value(input->id()); + } + + NodeValue GetValue(const QString& input) const { return value_map_.value(input); } @@ -31,15 +36,20 @@ public: values[j] = value[subparam].TakeWithMeta(subparam->data_type()); } - value_map_.insert(input, NodeValue(NodeParam::kVec2, QVariant::fromValue(values), input->parentNode())); + InsertValue(input->id(), NodeValue(NodeParam::kVec2, QVariant::fromValue(values), input->parentNode())); } else { - value_map_.insert(input, value[input].TakeWithMeta(input->data_type())); + InsertValue(input->id(), value[input].TakeWithMeta(input->data_type())); } } + void InsertValue(const QString& input, const NodeValue& value) + { + value_map_.insert(input, value); + } + void InsertValue(NodeInput* input, const NodeValue& value) { - value_map_.insert(input, value); + value_map_.insert(input->id(), value); } const NodeValueMap &GetValues() const diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index 8f59f41bd..8e20a717e 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -26,28 +26,70 @@ OLIVE_NAMESPACE_ENTER +const rational VideoParams::kPixelAspectSquare(1); +const rational VideoParams::kPixelAspectNTSCStandard(8, 9); +const rational VideoParams::kPixelAspectNTSCWidescreen(32, 27); +const rational VideoParams::kPixelAspectPALStandard(16, 15); +const rational VideoParams::kPixelAspectPALWidescreen(64, 45); +const rational VideoParams::kPixelAspect1080Anamorphic(4, 3); + +const QVector VideoParams::kSupportedFrameRates = { + rational(10, 1), // 10 FPS + rational(15, 1), // 15 FPS + rational(24000, 1001), // 23.976 FPS + rational(24, 1), // 24 FPS + rational(25, 1), // 25 FPS + rational(30000, 1001), // 29.97 FPS + rational(30, 1), // 30 FPS + rational(48000, 1001), // 47.952 FPS + rational(48, 1), // 48 FPS + rational(50, 1), // 50 FPS + rational(60000, 1001), // 59.94 FPS + rational(60, 1) // 60 FPS +}; + +const QVector VideoParams::kSupportedDividers = {1, 2, 3, 4, 6, 8, 12, 16}; + +const QVector VideoParams::kStandardPixelAspects = { + VideoParams::kPixelAspectSquare, + VideoParams::kPixelAspectNTSCStandard, + VideoParams::kPixelAspectNTSCWidescreen, + VideoParams::kPixelAspectPALStandard, + VideoParams::kPixelAspectPALWidescreen, + VideoParams::kPixelAspect1080Anamorphic +}; + VideoParams::VideoParams() : - format_(PixelFormat::PIX_FMT_INVALID) + width_(0), + height_(0), + format_(PixelFormat::PIX_FMT_INVALID), + interlacing_(Interlacing::kInterlaceNone) { } -VideoParams::VideoParams(const int &width, const int &height, const PixelFormat::Format &format, const int& divider) : +VideoParams::VideoParams(const int &width, const int &height, const PixelFormat::Format &format, const rational& pixel_aspect_ratio, const Interlacing &interlacing, const int& divider) : width_(width), height_(height), format_(format), + pixel_aspect_ratio_(pixel_aspect_ratio), + interlacing_(interlacing), divider_(divider) { calculate_effective_size(); + validate_pixel_aspect_ratio(); } -VideoParams::VideoParams(const int &width, const int &height, const rational &time_base, const PixelFormat::Format &format, const int ÷r) : +VideoParams::VideoParams(const int &width, const int &height, const rational &time_base, const PixelFormat::Format &format, const rational& pixel_aspect_ratio, const Interlacing &interlacing, const int ÷r) : width_(width), height_(height), time_base_(time_base), format_(format), + pixel_aspect_ratio_(pixel_aspect_ratio), + interlacing_(interlacing), divider_(divider) { calculate_effective_size(); + validate_pixel_aspect_ratio(); } int VideoParams::generate_auto_divider(qint64 width, qint64 height) @@ -60,16 +102,14 @@ int VideoParams::generate_auto_divider(qint64 width, qint64 height) double squared_divider = double(megapixels) / double(target_res); double divider = qSqrt(squared_divider); - QList supported_dividers = Core::SupportedDividers(); - - if (divider <= supported_dividers.first()) { - return supported_dividers.first(); - } else if (divider >= supported_dividers.last()) { - return supported_dividers.last(); + if (divider <= kSupportedDividers.first()) { + return kSupportedDividers.first(); + } else if (divider >= kSupportedDividers.last()) { + return kSupportedDividers.last(); } else { - for (int i=1; i= prev_divider && divider <= next_divider) { double prev_diff = qAbs(prev_divider - divider); @@ -94,6 +134,7 @@ bool VideoParams::operator==(const VideoParams &rhs) const && height() == rhs.height() && time_base() == rhs.time_base() && format() == rhs.format() + && pixel_aspect_ratio() == rhs.pixel_aspect_ratio() && divider() == rhs.divider(); } @@ -109,12 +150,49 @@ void VideoParams::calculate_effective_size() effective_height_ = qCeil(height() / divider_ * 0.5) * 2; } +void VideoParams::validate_pixel_aspect_ratio() +{ + if (pixel_aspect_ratio_.isNull()) { + pixel_aspect_ratio_ = 1; + } +} + bool VideoParams::is_valid() const { return (width() > 0 && height() > 0 + && !pixel_aspect_ratio_.isNull() && format_ != PixelFormat::PIX_FMT_INVALID && format_ != PixelFormat::PIX_FMT_COUNT); } +QString VideoParams::FrameRateToString(const rational &frame_rate) +{ + return QCoreApplication::translate("VideoParams", "%1 FPS").arg(frame_rate.toDouble()); +} + +QStringList VideoParams::GetStandardPixelAspectRatioNames() +{ + QStringList strings = { + QCoreApplication::translate("VideoParams", "Square Pixels (%1)"), + QCoreApplication::translate("VideoParams", "NTSC Standard (%1)"), + QCoreApplication::translate("VideoParams", "NTSC Widescreen (%1)"), + QCoreApplication::translate("VideoParams", "PAL Standard (%1)"), + QCoreApplication::translate("VideoParams", "PAL Widescreen (%1)"), + QCoreApplication::translate("VideoParams", "HD Anamorphic 1080 (%1)") + }; + + // Format each + for (int i=0; i kSupportedFrameRates; + static const QVector kStandardPixelAspects; + static const QVector kSupportedDividers; + + /** + * @brief Convert rational frame rate (i.e. flipped timebase) to a user-friendly string + */ + static QString FrameRateToString(const rational& frame_rate); + + static QStringList GetStandardPixelAspectRatioNames(); + static QString FormatPixelAspectRatioString(const QString& format, const rational& ratio); + private: void calculate_effective_size(); + void validate_pixel_aspect_ratio(); + int width_; int height_; rational time_base_; PixelFormat::Format format_; + rational pixel_aspect_ratio_; + + Interlacing interlacing_; + int divider_; int effective_width_; int effective_height_; @@ -91,4 +136,7 @@ private: OLIVE_NAMESPACE_EXIT +Q_DECLARE_METATYPE(OLIVE_NAMESPACE::VideoParams) +Q_DECLARE_METATYPE(OLIVE_NAMESPACE::VideoParams::Interlacing) + #endif // VIDEOPARAMS_H diff --git a/app/shaders/crossdissolve.frag b/app/shaders/crossdissolve.frag index 22eb8e54f..e9461244c 100644 --- a/app/shaders/crossdissolve.frag +++ b/app/shaders/crossdissolve.frag @@ -1,9 +1,14 @@ #version 150 +#define LINEAR_CURVE 0 +#define EXPONENTIAL_CURVE 1 +#define LOGARITHMIC_CURVE 2 + uniform sampler2D out_block_in; uniform sampler2D in_block_in; uniform bool out_block_in_enabled; uniform bool in_block_in_enabled; +uniform int curve_in; uniform float ove_tprog_all; @@ -11,16 +16,25 @@ in vec2 ove_texcoord; out vec4 fragColor; +float TransformCurve(float linear) { + if (curve_in == EXPONENTIAL_CURVE) { + return linear * linear; + } else if (curve_in == LOGARITHMIC_CURVE) { + return sqrt(linear); + } else { + return linear; + } +} + void main(void) { vec4 composite = vec4(0.0); if (out_block_in_enabled) { - composite += texture(out_block_in, ove_texcoord) * (1.0 - ove_tprog_all); + composite += texture(out_block_in, ove_texcoord) * TransformCurve(1.0 - ove_tprog_all); } if (in_block_in_enabled) { - vec4 in_block_col = texture(in_block_in, ove_texcoord) * ove_tprog_all; - composite += in_block_col; + composite += texture(in_block_in, ove_texcoord) * TransformCurve(ove_tprog_all); } fragColor = composite; diff --git a/app/shaders/diptoblack.frag b/app/shaders/diptoblack.frag index 064264506..b6e89dad3 100644 --- a/app/shaders/diptoblack.frag +++ b/app/shaders/diptoblack.frag @@ -4,7 +4,9 @@ uniform sampler2D out_block_in; uniform sampler2D in_block_in; uniform bool out_block_in_enabled; uniform bool in_block_in_enabled; +uniform vec4 color_in; +uniform float ove_tprog_all; uniform float ove_tprog_out; uniform float ove_tprog_in; @@ -13,20 +15,16 @@ in vec2 ove_texcoord; out vec4 fragColor; void main(void) { - vec4 out_block_col; - vec4 in_block_col; + if (out_block_in_enabled && in_block_in_enabled) { + vec4 out_block_col = mix(texture(out_block_in, ove_texcoord), color_in, ove_tprog_out); + vec4 in_block_col = mix(texture(in_block_in, ove_texcoord), color_in, 1.0 - ove_tprog_in); - if (out_block_in_enabled) { - out_block_col = texture(out_block_in, ove_texcoord) * pow(ove_tprog_out, 2.0); + fragColor = out_block_col + in_block_col; + } else if (out_block_in_enabled) { + fragColor = mix(texture(out_block_in, ove_texcoord), color_in, ove_tprog_all); + } else if (in_block_in_enabled) { + fragColor = mix(texture(in_block_in, ove_texcoord), color_in, 1.0 - ove_tprog_all); } else { - out_block_col = vec4(0.0); + fragColor = vec4(0.0); } - - if (in_block_in_enabled) { - in_block_col = texture(in_block_in, ove_texcoord) * pow(ove_tprog_in, 2.0); - } else { - in_block_col = vec4(0.0); - } - - fragColor = out_block_col + in_block_col; } diff --git a/app/shaders/rgbhistogram.frag b/app/shaders/rgbhistogram.frag new file mode 100644 index 000000000..435949138 --- /dev/null +++ b/app/shaders/rgbhistogram.frag @@ -0,0 +1,37 @@ +#version 150 + +uniform sampler2D ove_maintex; +uniform vec2 ove_resolution; +uniform vec2 ove_viewport; + +uniform float histogram_scale; + +in vec2 ove_texcoord; + +out vec4 fragColor; + +void main(void) { + float histogram_width = ceil(histogram_scale * ove_viewport.y); + float quantisation = 1.0 / (histogram_width - 1.0); + vec3 cur_col = vec3(0.0); + vec3 sum = vec3(0.0); + float ratio = 0.0; + + for (int i = 0; i < histogram_width; i++) { + ratio = float(i) / float(histogram_width - 1); + cur_col = texture( + ove_maintex, + vec2(ove_texcoord.y, ratio) + ).rgb; + + sum += step(vec3(ove_texcoord.x - quantisation), cur_col) * + step(cur_col, vec3(ove_texcoord.x + quantisation)) + + ( + // Account for values beyond the upper x limit. + step(1.0 - quantisation, ove_texcoord.x) * + step(vec3(1.0 - quantisation), cur_col) + ); + } + + fragColor = vec4(sum, 1.0); +} diff --git a/app/shaders/rgbhistogram.vert b/app/shaders/rgbhistogram.vert new file mode 100644 index 000000000..92536144c --- /dev/null +++ b/app/shaders/rgbhistogram.vert @@ -0,0 +1,29 @@ +#version 150 + +uniform float histogram_scale; +uniform vec2 ove_resolution; + +in vec4 a_position; +in vec2 a_texcoord; + +out vec2 ove_texcoord; + +mat4 scale_mat4(vec3 scale) { + return mat4( + scale.x, 0.0, 0.0, 0.0, + 0.0, scale.y, 0.0, 0.0, + 0.0, 0.0, scale.z, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} + +void main() { + // Create identity matrix + mat4 transform = mat4(1.0); + + // Scale the scope + transform *= scale_mat4(vec3(histogram_scale, histogram_scale, 1.0)); + + gl_Position = transform * a_position; + ove_texcoord = a_texcoord; +} \ No newline at end of file diff --git a/app/shaders/rgbhistogram_secondary.frag b/app/shaders/rgbhistogram_secondary.frag new file mode 100644 index 000000000..474db6b74 --- /dev/null +++ b/app/shaders/rgbhistogram_secondary.frag @@ -0,0 +1,35 @@ +#version 150 + +uniform sampler2D ove_maintex; +uniform vec2 ove_resolution; +uniform vec2 ove_viewport; + +uniform float histogram_scale; +uniform float histogram_power; + +in vec2 ove_texcoord; + +out vec4 fragColor; + +void main(void) { + vec3 col = vec3(0.0); + float histogram_height = ceil(ove_viewport.y * histogram_scale); + vec3 histogram_ratio = vec3(0.0); + vec3 sum = vec3(0.0); + float ratio = 0.0; + vec3 total_pixels = vec3(ceil(ove_viewport.x * ove_resolution.y * + histogram_scale)); + + for (int i = 0; i < histogram_height; i++) { + ratio = float(i) / float(histogram_height - 1.0); + sum += texture( + ove_maintex, + vec2(ove_texcoord.x, ratio) + ).rgb; + } + + histogram_ratio = pow(sum / total_pixels, vec3(histogram_power)); + col = step(vec3(ove_texcoord.y), histogram_ratio); + + fragColor = vec4(col, 1.0); +} diff --git a/app/shaders/rgbwaveform.frag b/app/shaders/rgbwaveform.frag index 4e6b13f08..e568b37cd 100644 --- a/app/shaders/rgbwaveform.frag +++ b/app/shaders/rgbwaveform.frag @@ -1,6 +1,3 @@ -// Adapted from "RGB Waveform" by lebek -// https://www.shadertoy.com/view/4dK3Wc - #version 150 uniform sampler2D ove_maintex; @@ -9,64 +6,36 @@ uniform vec2 ove_viewport; uniform vec3 luma_coeffs; uniform float waveform_scale; -uniform vec2 waveform_dims; -uniform vec4 waveform_region; -uniform vec4 waveform_uv; in vec2 ove_texcoord; out vec4 fragColor; void main(void) { - vec3 col = vec3(0.0); - // Set an increment default to 10 bit encodings. This would likely be - // better served as a UI control, as waveforms will change their combing - // based on how granular the increment is set. For example, it can be - // challenging to spot 8 bit combing with an increment of 1. / 2.^8 - 1. - float increment = 1.0 / (pow(2, 10) - 1.0); - float maxb = waveform_dims.y + increment; - float minb = waveform_dims.y - increment; - - // Intensity would make sense to also expose via the UI, as a density - // slider allows you to peek past certain values or reveal very low - // values. Hard coding it for now, as there isn't a clear way to have - // the various bit depth / code values always display at a consistent - // emission output strength. + float waveform_height = ceil(waveform_scale * ove_viewport.y); + float quantisation = 1.0 / (waveform_height - 1.0); float intensity = 0.10; + vec4 col = vec4(0.0); + vec4 cur_col = vec4(0.0); + float ratio = 0.0; - int y_lim = int(waveform_dims.y); + for (int i = 0; i < waveform_height; i++) { + ratio = float(i) / float(waveform_height - 1.0); + cur_col.rgb = texture( + ove_maintex, + vec2(ove_texcoord.x, ratio) + ).rgb; - vec3 cur_col = vec3(0.0); - vec3 cur_lum = vec3(0.0); + cur_col.w = dot(cur_col.rgb, luma_coeffs); - if ( - (gl_FragCoord.x >= waveform_region.x) && - (gl_FragCoord.y >= waveform_region.y) && - (gl_FragCoord.x < waveform_region.z) && - (gl_FragCoord.y < waveform_region.w) - ) { - // col = vec3(0.5, 0.5, 0.0); - // int start = int(waveform_region.y); - int stop = int(waveform_dims.y); - float ratio = 0.0; - float waveform_x = (ove_texcoord.x - waveform_uv.x) / waveform_scale; - float waveform_y = (ove_texcoord.y - waveform_uv.y) / waveform_scale; - for (int i = 0; i < waveform_dims.y; i++) { - ratio = float(i) / float(waveform_dims.y - 1); - cur_col = texture( - ove_maintex, - vec2(waveform_x, ratio) - ).rgb; - - col += step(vec3(waveform_y - increment), cur_col) * - step(cur_col, vec3(waveform_y + increment)) * intensity; - - cur_lum = vec3(dot(cur_col, luma_coeffs)); - - col += step(vec3(waveform_y - increment), cur_lum) * - step(cur_lum, vec3(waveform_y + increment)) * intensity; - } + col += ( + step(vec4(ove_texcoord.y - quantisation), cur_col) * + step(cur_col, vec4(ove_texcoord.y + quantisation)) * + intensity) + + (step(1.0 - quantisation, ove_texcoord.y) * + step(vec4(1.0 - quantisation), cur_col) * intensity); } - fragColor = vec4(col, 1.0); + col.rgb += vec3(col.w); + fragColor = vec4(col.rgb, 1.0); } diff --git a/app/shaders/rgbwaveform.vert b/app/shaders/rgbwaveform.vert new file mode 100644 index 000000000..88fde9934 --- /dev/null +++ b/app/shaders/rgbwaveform.vert @@ -0,0 +1,29 @@ +#version 150 + +uniform float waveform_scale; +uniform vec2 ove_resolution; + +in vec4 a_position; +in vec2 a_texcoord; + +out vec2 ove_texcoord; + +mat4 scale_mat4(vec3 scale) { + return mat4( + scale.x, 0.0, 0.0, 0.0, + 0.0, scale.y, 0.0, 0.0, + 0.0, 0.0, scale.z, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} + +void main() { + // Create identity matrix + mat4 transform = mat4(1.0); + + // Scale the scope + transform *= scale_mat4(vec3(waveform_scale, waveform_scale, 1.0)); + + gl_Position = transform * a_position; + ove_texcoord = a_texcoord; +} \ No newline at end of file diff --git a/app/task/CMakeLists.txt b/app/task/CMakeLists.txt index 02eaab8e4..3498e2910 100644 --- a/app/task/CMakeLists.txt +++ b/app/task/CMakeLists.txt @@ -14,9 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -add_subdirectory(cache) add_subdirectory(conform) add_subdirectory(export) +add_subdirectory(precache) add_subdirectory(project) add_subdirectory(render) diff --git a/app/task/cache/cache.cpp b/app/task/cache/cache.cpp deleted file mode 100644 index 563dec1b3..000000000 --- a/app/task/cache/cache.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "cache.h" - -#include -#include - -#include "project/item/sequence/sequence.h" - -OLIVE_NAMESPACE_ENTER - -CacheTask::CacheTask(ViewerOutput* viewer, const VideoParams& vparams, const AudioParams &aparams, bool in_out_only) : - RenderTask(viewer, vparams, aparams), - in_out_only_(in_out_only) -{ - SetTitle(tr("Caching \"%1\"").arg(viewer->media_name())); - - backend()->EnablePreviewGeneration(job_time()); - - // Render fastest quality - backend()->SetRenderMode(RenderMode::kOffline); -} - -bool CacheTask::Run() -{ - // Get list of invalidated ranges - TimeRangeList video_range = viewer()->video_frame_cache()->GetInvalidatedRanges(); - TimeRangeList audio_range = viewer()->audio_playback_cache()->GetInvalidatedRanges(); - - // If we're caching only in-out, limit the range to that - if (in_out_only_) { - Sequence* s = static_cast(viewer()->parent()); - - if (s->workarea()->enabled()) { - video_range = video_range.Intersects(s->workarea()->range()); - audio_range = audio_range.Intersects(s->workarea()->range()); - } - } - - Render(video_range, audio_range, QMatrix4x4(), true); - - download_threads_.waitForDone(); - - return true; -} - -QFuture CacheTask::DownloadFrame(FramePtr frame, const QByteArray &hash) -{ - return QtConcurrent::run(&download_threads_, FrameHashCache::SaveCacheFrame, hash, frame); -} - -void CacheTask::FrameDownloaded(const QByteArray &hash, const std::list ×) -{ - foreach (const rational& t, times) { - viewer()->video_frame_cache()->SetHash(t, hash, job_time()); - } -} - -void CacheTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples) -{ - if (samples) { - viewer()->audio_playback_cache()->WritePCM(range, samples, job_time()); - } else { - viewer()->audio_playback_cache()->WriteSilence(range); - } -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/task/cache/footagecache.cpp b/app/task/cache/footagecache.cpp deleted file mode 100644 index 9a21bc9f8..000000000 --- a/app/task/cache/footagecache.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "footagecache.h" - -#include "common/timecodefunctions.h" - -OLIVE_NAMESPACE_ENTER - -FootageCacheTask::FootageCacheTask(VideoStreamPtr footage, Sequence *sequence) : - CacheTask(new ViewerOutput(), sequence->video_params(), sequence->audio_params(), false), - footage_(footage) -{ - viewer()->set_video_params(sequence->video_params()); - viewer()->set_audio_params(sequence->audio_params()); - backend()->SetVideoParams(sequence->video_params()); - backend()->SetAudioParams(sequence->audio_params()); - - video_node_ = new VideoInput(); - video_node_->SetFootage(footage); - - NodeParam::ConnectEdge(video_node_->output(), viewer()->texture_input()); - - SetTitle(tr("Pre-caching %1:%2").arg(footage->footage()->filename(), - QString::number(footage->index()))); - - backend()->ProcessUpdateQueue(); -} - -FootageCacheTask::~FootageCacheTask() -{ - delete viewer(); - delete video_node_; -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 25b65aa27..69cdfe502 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -64,17 +64,17 @@ bool ExportTask::Run() frame_time_ = Timecode::time_to_timestamp(range.in(), viewer()->video_params().time_base()); - QMatrix4x4 mat; - if (params_.video_enabled()) { // If a transformation matrix is applied to this video, create it here if (params_.video_scaling_method() != ExportParams::kStretch) { - mat = ExportParams::GenerateMatrix(params_.video_scaling_method(), - viewer()->video_params().width(), - viewer()->video_params().height(), - params_.video_params().width(), - params_.video_params().height()); + QMatrix4x4 mat = ExportParams::GenerateMatrix(params_.video_scaling_method(), + viewer()->video_params().width(), + viewer()->video_params().height(), + params_.video_params().width(), + params_.video_params().height()); + + backend()->SetVideoDownloadMatrix(mat); } // Create color processor @@ -96,24 +96,16 @@ bool ExportTask::Run() if (params_.audio_enabled()) { audio_range.append(range); + audio_data_.SetLength(range.length()); } - Render(video_range, audio_range, mat, false); + Render(video_range, audio_range, false); bool success = true; - foreach (QFuture f, write_frame_futures_) { - f.waitForFinished(); - - if (!f.result()) { - SetError(tr("Failed to write AVFrame")); - success = false; - } - } - if (params_.audio_enabled()) { // Write audio data now - encoder_->WriteAudio(audio_params(), audio_data_.GetCacheFilename()); + encoder_->WriteAudio(audio_params(), audio_data_.GetPCMFilename()); } encoder_->Close(); @@ -126,8 +118,17 @@ bool ExportTask::Run() void FrameColorConvert(ColorProcessorPtr processor, FramePtr frame) { // OCIO conversion requires a frame in 32F format - if (frame->format() != PixelFormat::PIX_FMT_RGBA32F) { - frame = PixelFormat::ConvertPixelFormat(frame, PixelFormat::PIX_FMT_RGBA32F); + if (frame->format() != PixelFormat::PIX_FMT_RGBA32F + && frame->format() != PixelFormat::PIX_FMT_RGB32F) { + PixelFormat::Format dst; + + if (PixelFormat::FormatHasAlphaChannel(frame->format())) { + dst = PixelFormat::PIX_FMT_RGBA32F; + } else { + dst = PixelFormat::PIX_FMT_RGB32F; + } + + frame = PixelFormat::ConvertPixelFormat(frame, dst); } // Color conversion must be done with unassociated alpha, and the pipeline is always associated @@ -147,8 +148,10 @@ QFuture ExportTask::DownloadFrame(FramePtr frame, const QByteArray &hash) return QtConcurrent::run(FrameColorConvert, color_processor_, frame); } -void ExportTask::FrameDownloaded(const QByteArray &hash, const std::list ×) +void ExportTask::FrameDownloaded(const QByteArray &hash, const std::list ×, qint64 job_time) { + Q_UNUSED(job_time) + FramePtr f = rendered_frame_.value(hash); foreach (const rational& t, times) { @@ -165,22 +168,24 @@ void ExportTask::FrameDownloaded(const QByteArray &hash, const std::listWriteFrame(time_map_.value(real_time), real_time); + encoder_->WriteFrame(time_map_.take(real_time), real_time); frame_time_++; } } -void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples) +void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples, qint64 job_time) { + Q_UNUSED(job_time) + TimeRange adjusted_range = range; if (params_.has_custom_range()) { adjusted_range -= params_.custom_range().in(); } - audio_data_.WritePCM(adjusted_range, samples, job_time()); + audio_data_.WritePCM(adjusted_range, samples, QDateTime::currentMSecsSinceEpoch()); } OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/export.h b/app/task/export/export.h index dcbae30f0..58e392ad5 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -40,17 +40,15 @@ protected: virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) override; - virtual void FrameDownloaded(const QByteArray& hash, const std::list& times) override; + virtual void FrameDownloaded(const QByteArray& hash, const std::list& times, qint64 job_time) override; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) override; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; private: QHash rendered_frame_; QHash time_map_; - QList< QFuture > write_frame_futures_; - ColorManager* color_manager_; ExportParams params_; diff --git a/app/task/export/exportparams.cpp b/app/task/export/exportparams.cpp index a603735ee..299c1fca3 100644 --- a/app/task/export/exportparams.cpp +++ b/app/task/export/exportparams.cpp @@ -100,4 +100,26 @@ QMatrix4x4 ExportParams::GenerateMatrix(ExportParams::VideoScalingMethod method, return preview_matrix; } +void ExportParams::Save(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("export")); + + writer->writeTextElement(QStringLiteral("encoder"), encoder_id_); + + writer->writeTextElement(QStringLiteral("vscale"), QString::number(video_scaling_method_)); + + writer->writeTextElement(QStringLiteral("range"), QString::number(has_custom_range_)); + + writer->writeTextElement(QStringLiteral("customrangein"), custom_range_.in().toString()); + + writer->writeTextElement(QStringLiteral("customrangeout"), custom_range_.out().toString()); + + // FIXME: Change this when color chains are implemented + writer->writeTextElement(QStringLiteral("color"), color_transform_.output()); + + EncodingParams::Save(writer); + + writer->writeEndElement(); // export +} + OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/exportparams.h b/app/task/export/exportparams.h index a74ea3c5d..ed6106d67 100644 --- a/app/task/export/exportparams.h +++ b/app/task/export/exportparams.h @@ -56,6 +56,8 @@ public: int source_width, int source_height, int dest_width, int dest_height); + virtual void Save(QXmlStreamWriter* writer) const override; + private: QString encoder_id_; diff --git a/app/task/precache/CMakeLists.txt b/app/task/precache/CMakeLists.txt new file mode 100644 index 000000000..872149bae --- /dev/null +++ b/app/task/precache/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + task/precache/precachetask.h + task/precache/precachetask.cpp + PARENT_SCOPE +) diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp new file mode 100644 index 000000000..4829affb1 --- /dev/null +++ b/app/task/precache/precachetask.cpp @@ -0,0 +1,100 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "precachetask.h" + +OLIVE_NAMESPACE_ENTER + +PreCacheTask::PreCacheTask(VideoStreamPtr footage, Sequence* sequence) : + RenderTask(new ViewerOutput(), sequence->video_params(), sequence->audio_params()), + footage_(footage) +{ + viewer()->set_video_params(sequence->video_params()); + viewer()->set_audio_params(sequence->audio_params()); + + // Render fastest quality + backend()->SetRenderMode(RenderMode::kOffline); + + video_node_ = new VideoInput(); + video_node_->SetFootage(footage); + + NodeParam::ConnectEdge(video_node_->output(), viewer()->texture_input()); + + SetTitle(tr("Pre-caching %1:%2").arg(footage->footage()->filename(), + QString::number(footage->index()))); + + backend()->NodeGraphChanged(viewer()->texture_input()); + backend()->ProcessUpdateQueue(); +} + +PreCacheTask::~PreCacheTask() +{ + delete viewer(); + delete video_node_; +} + +bool PreCacheTask::Run() +{ + // Get list of invalidated ranges + TimeRangeList video_range = viewer()->video_frame_cache()->GetInvalidatedRanges(); + + // If we're caching only in-out, limit the range to that + /* + if (in_out_only_) { + Sequence* s = static_cast(viewer()->parent()); + + if (s->workarea()->enabled()) { + video_range = video_range.Intersects(s->workarea()->range()); + } + } + */ + + Render(video_range, TimeRangeList(), true); + + download_threads_.waitForDone(); + + return true; +} + +QFuture PreCacheTask::DownloadFrame(FramePtr frame, const QByteArray &hash) +{ + return QtConcurrent::run(&download_threads_, viewer()->video_frame_cache(), &FrameHashCache::SaveCacheFrame, hash, frame); +} + +void PreCacheTask::FrameDownloaded(const QByteArray &hash, const std::list ×, qint64 job_time) +{ + // Do nothing. Pre-cache essentially just creates more frames in the cache, it doesn't need to do + // anything else. + + Q_UNUSED(hash) + Q_UNUSED(times) + Q_UNUSED(job_time) +} + +void PreCacheTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples, qint64 job_time) +{ + // Pre-cache doesn't cache any audio + + Q_UNUSED(range) + Q_UNUSED(samples) + Q_UNUSED(job_time) +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/task/cache/cache.h b/app/task/precache/precachetask.h similarity index 69% rename from app/task/cache/cache.h rename to app/task/precache/precachetask.h index bab3cffc8..090fedd44 100644 --- a/app/task/cache/cache.h +++ b/app/task/precache/precachetask.h @@ -18,35 +18,36 @@ ***/ -#ifndef CACHETASK_H -#define CACHETASK_H - -#include +#ifndef PRECACHETASK_H +#define PRECACHETASK_H +#include "node/input/media/video/video.h" +#include "project/item/footage/footage.h" +#include "project/item/sequence/sequence.h" #include "task/render/render.h" OLIVE_NAMESPACE_ENTER -class CacheTask : public RenderTask +class PreCacheTask : public RenderTask { - Q_OBJECT public: - CacheTask(ViewerOutput* viewer, - const VideoParams &vparams, - const AudioParams &aparams, - bool in_out_only); + PreCacheTask(VideoStreamPtr footage, Sequence* sequence); + + virtual ~PreCacheTask() override; protected: virtual bool Run() override; virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) override; - virtual void FrameDownloaded(const QByteArray& hash, const std::list& times) override; + virtual void FrameDownloaded(const QByteArray& hash, const std::list& times, qint64 job_time) override; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) override; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override; private: - bool in_out_only_; + VideoStreamPtr footage_; + + VideoInput* video_node_; QThreadPool download_threads_; @@ -54,4 +55,4 @@ private: OLIVE_NAMESPACE_EXIT -#endif // CACHETASK_H +#endif // PRECACHETASK_H diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 20d05ce7d..3d945023a 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -51,13 +51,16 @@ bool ProjectLoadTask::Run() project->set_filename(filename_); - project->Load(&reader, &IsCancelled()); + MainWindowLayoutInfo layout; + + project->Load(&reader, &layout, &IsCancelled()); // Ensure project is in main thread - moveToThread(qApp->thread()); + project->moveToThread(qApp->thread()); if (!IsCancelled()) { projects_.append(project); + layout_info_.append(layout); } } else { reader.skipCurrentElement(); @@ -70,6 +73,8 @@ bool ProjectLoadTask::Run() project_file.close(); + emit ProgressChanged(1); + if (reader.hasError()) { SetError(reader.errorString()); return false; diff --git a/app/task/project/load/load.h b/app/task/project/load/load.h index fa203abfe..398853028 100644 --- a/app/task/project/load/load.h +++ b/app/task/project/load/load.h @@ -23,6 +23,7 @@ #include "project/project.h" #include "task/task.h" +#include "window/mainwindow/mainwindowlayoutinfo.h" OLIVE_NAMESPACE_ENTER @@ -32,17 +33,24 @@ class ProjectLoadTask : public Task public: ProjectLoadTask(const QString& filename); - const QList& GetLoadedProjects() + const QList& GetLoadedProjects() const { return projects_; } + const QList& GetLoadedLayouts() const + { + return layout_info_; + } + protected: virtual bool Run() override; private: QList projects_; + QList layout_info_; + QString filename_; }; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 133a6bf1d..abeff4120 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -24,18 +24,17 @@ OLIVE_NAMESPACE_ENTER -RenderTask::RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams) : - viewer_(viewer), - video_params_(vparams), - audio_params_(aparams) +RenderTask::RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams) { - job_time_ = QDateTime::currentMSecsSinceEpoch(); + backend_ = new OpenGLBackend(); + backend_->SetViewerNode(viewer); + backend_->SetVideoParams(vparams); + backend_->SetAudioParams(aparams); +} - // FIXME: This makes a full copy of the node graph every time it starts, there must be a better - // way. - backend_.SetViewerNode(viewer_); - backend_.SetVideoParams(video_params_); - backend_.SetAudioParams(audio_params_); +RenderTask::~RenderTask() +{ + delete backend_; } struct TimeHashFuturePair { @@ -61,18 +60,16 @@ struct RangeSampleFuturePair { struct HashDownloadFuturePair { QByteArray hash; QFuture download_future; + qint64 job_time; }; void RenderTask::Render(const TimeRangeList& video_range, const TimeRangeList &audio_range, - const QMatrix4x4& mat, bool use_disk_cache) { - backend_.SetVideoDownloadMatrix(mat); - double progress_counter = 0; double total_length = 0; - double video_frame_sz = video_params_.time_base().toDouble(); + double video_frame_sz = video_params().time_base().toDouble(); std::list audio_queue; std::list audio_lookup_table; @@ -89,21 +86,21 @@ void RenderTask::Render(const TimeRangeList& video_range, QVector times; QVector hashes; std::list frame_queue; + qint64 hash_job_time = 0; if (!video_range.isEmpty()) { - QList existing_hashes; + times = viewer()->video_frame_cache()->GetFrameListFromTimeRange(video_range); - foreach (const TimeRange& r, video_range) { - total_length += r.length().toDouble(); - } + total_length += video_frame_sz * times.size(); - times = viewer_->video_frame_cache()->GetFrameListFromTimeRange(video_range); + RenderTicketPtr hash_future = backend_->Hash(times); + hashes = hash_future->Get().value >(); + hash_job_time = hash_future->GetJobTime(); - QFuture > hash_future = backend_.Hash(times); - hashes = hash_future.result(); - - for (int i=0;iWasCancelled()) { + for (int i=0;ivideo_frame_cache()->CachePathName(p.hash)); + hash_exists = QFileInfo::exists(viewer()->video_frame_cache()->CachePathName(p.hash)); + // If so, add it to the list so we don't have to check the filesystem again later if (hash_exists) { existing_hashes.push_back(p.hash); } @@ -150,7 +151,7 @@ void RenderTask::Render(const TimeRangeList& video_range, if (hash_exists) { // Already exists, no need to render it again - FrameDownloaded(p.hash, {p.time}); + FrameDownloaded(p.hash, {p.time}, hash_job_time); progress_counter += video_frame_sz; emit ProgressChanged(progress_counter / total_length); } @@ -158,7 +159,7 @@ void RenderTask::Render(const TimeRangeList& video_range, // If no existing disk cache was found, queue it now if (!hash_exists) { - render_lookup_table.push_back({p.hash, backend_.RenderFrame(p.time)}); + render_lookup_table.push_back({p.hash, backend_->RenderFrame(p.time)}); running_hashes.push_back(p.hash); } } @@ -167,8 +168,8 @@ void RenderTask::Render(const TimeRangeList& video_range, frame_queue.pop_front(); } - if (!audio_queue.empty()) { - audio_lookup_table.push_back({audio_queue.front(), backend_.RenderAudio(audio_queue.front())}); + if (!IsCancelled() && !audio_queue.empty()) { + audio_lookup_table.push_back({audio_queue.front(), backend_->RenderAudio(audio_queue.front())}); audio_queue.pop_front(); } @@ -176,10 +177,12 @@ void RenderTask::Render(const TimeRangeList& video_range, while (!IsCancelled() && i != render_lookup_table.end()) { if (i->frame_future->IsFinished()) { - FramePtr f = i->frame_future->Get().value(); + if (!i->frame_future->WasCancelled()) { + FramePtr f = i->frame_future->Get().value(); - // Start multithreaded download here - download_futures.push_back({i->hash, DownloadFrame(f, i->hash)}); + // Start multithreaded download here + download_futures.push_back({i->hash, DownloadFrame(f, i->hash), i->frame_future->GetJobTime()}); + } i = render_lookup_table.erase(i); } else { @@ -194,13 +197,13 @@ void RenderTask::Render(const TimeRangeList& video_range, // Place it in the cache std::list times_with_hash; - for (int k=0;khash) { - times_with_hash.push_back(times.at(k)); + for (int hash_index=0;hash_indexhash) { + times_with_hash.push_back(times.at(hash_index)); } } - FrameDownloaded(j->hash, times_with_hash); + FrameDownloaded(j->hash, times_with_hash, j->job_time); existing_hashes.push_back(j->hash); @@ -219,7 +222,9 @@ void RenderTask::Render(const TimeRangeList& video_range, while (!IsCancelled() && k != audio_lookup_table.end()) { if (k->sample_future->IsFinished()) { - AudioDownloaded(k->range, k->sample_future->Get().value()); + AudioDownloaded(k->range, + k->sample_future->Get().value(), + k->sample_future->GetJobTime()); progress_counter += k->range.length().toDouble(); emit ProgressChanged(progress_counter / total_length); @@ -232,12 +237,7 @@ void RenderTask::Render(const TimeRangeList& video_range, } // `Close` will block until all jobs are done making a safe deletion - backend_.Close(); -} - -void RenderTask::SetAnchorPoint(const rational &r) -{ - anchor_point_ = r; + backend_->Close(); } OLIVE_NAMESPACE_EXIT diff --git a/app/task/render/render.h b/app/task/render/render.h index 9f9dd49e2..8571c0894 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -34,57 +34,41 @@ class RenderTask : public Task public: RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams); + virtual ~RenderTask() override; + protected: void Render(const TimeRangeList &video_range, const TimeRangeList &audio_range, - const QMatrix4x4 &mat, bool use_disk_cache); virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) = 0; - virtual void FrameDownloaded(const QByteArray& hash, const std::list& times) = 0; + virtual void FrameDownloaded(const QByteArray& hash, const std::list& times, qint64 job_time) = 0; - virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) = 0; + virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) = 0; ViewerOutput* viewer() const { - return viewer_; + return backend_->GetViewerNode(); } VideoParams video_params() const { - return video_params_; + return backend_->GetVideoParams(); } AudioParams audio_params() const { - return audio_params_; + return backend_->GetAudioParams(); } - void SetAnchorPoint(const rational& r); - - const qint64& job_time() const + RenderBackend* backend() { - return job_time_; - } - - OpenGLBackend* backend() - { - return &backend_; + return backend_; } private: - ViewerOutput* viewer_; - - VideoParams video_params_; - - AudioParams audio_params_; - - rational anchor_point_; - - OpenGLBackend backend_; - - qint64 job_time_; + RenderBackend* backend_; }; diff --git a/app/timeline/timelinecommon.h b/app/timeline/timelinecommon.h index ba059e258..8bd042de0 100644 --- a/app/timeline/timelinecommon.h +++ b/app/timeline/timelinecommon.h @@ -56,6 +56,9 @@ public: }; +// FIXME: Hardcoded (but that might be okay here) +#define PLAYHEAD_COLOR Qt::red + OLIVE_NAMESPACE_EXIT #endif // TIMELINECOMMON_H diff --git a/app/timeline/trackreference.cpp b/app/timeline/trackreference.cpp index 8360d0fd8..6c046df7a 100644 --- a/app/timeline/trackreference.cpp +++ b/app/timeline/trackreference.cpp @@ -49,4 +49,12 @@ bool TrackReference::operator==(const TrackReference &ref) const return type_ == ref.type_ && index_ == ref.index_; } +uint qHash(const TrackReference &r, uint seed) +{ + // Not super efficient, but couldn't think of any better way to ensure a different hash each time + return ::qHash(QStringLiteral("%1:%2").arg(QString::number(r.type()), + QString::number(r.index())), + seed); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/timeline/trackreference.h b/app/timeline/trackreference.h index 4aea1e3c2..842985168 100644 --- a/app/timeline/trackreference.h +++ b/app/timeline/trackreference.h @@ -36,6 +36,8 @@ public: const int& index() const; + bool operator<(const TrackReference& ref) const; + bool operator==(const TrackReference& ref) const; private: @@ -44,6 +46,8 @@ private: int index_; }; +uint qHash(const TrackReference& r, uint seed); + OLIVE_NAMESPACE_EXIT #endif // TRACKREFERENCE_H diff --git a/app/ui/icons/icons.cpp b/app/ui/icons/icons.cpp index 6c278df0f..a7e655373 100644 --- a/app/ui/icons/icons.cpp +++ b/app/ui/icons/icons.cpp @@ -73,6 +73,8 @@ QIcon icon::Error; QIcon icon::DirUp; QIcon icon::Clock; QIcon icon::Diamond; +QIcon icon::Plus; +QIcon icon::Minus; void icon::LoadAll(const QString& theme) { @@ -122,6 +124,8 @@ void icon::LoadAll(const QString& theme) DirUp = Create(theme, "dirup"); Clock = Create(theme, "clock"); Diamond = Create(theme, "diamond"); + Plus = Create(theme, "plus"); + Minus = Create(theme, "minus"); } QIcon icon::Create(const QString& theme, const QString &name) diff --git a/app/ui/icons/icons.h b/app/ui/icons/icons.h index ba26805bc..51bffe5eb 100644 --- a/app/ui/icons/icons.h +++ b/app/ui/icons/icons.h @@ -81,6 +81,8 @@ extern QIcon Error; extern QIcon DirUp; extern QIcon Clock; extern QIcon Diamond; +extern QIcon Plus; +extern QIcon Minus; /** * @brief Create an icon object loaded from file diff --git a/app/ui/style/generate-style.sh b/app/ui/style/generate-style.sh index 21192314e..327ae81c5 100755 --- a/app/ui/style/generate-style.sh +++ b/app/ui/style/generate-style.sh @@ -60,6 +60,7 @@ truncate -s 0 $QRCFILE echo "" >> $QRCFILE echo " " >> $QRCFILE echo " style.css" >> $QRCFILE +echo " palette.ini" >> $QRCFILE OutputPng() { echo Creating $2... diff --git a/app/ui/style/olive-dark/olive-dark.qrc b/app/ui/style/olive-dark/olive-dark.qrc index bb2749a2b..d8d71e21e 100644 --- a/app/ui/style/olive-dark/olive-dark.qrc +++ b/app/ui/style/olive-dark/olive-dark.qrc @@ -1,7 +1,7 @@ - palette.ini style.css + palette.ini png/add-button.16.png png/add-button.32.png png/add-button.64.png @@ -94,6 +94,10 @@ png/magnet.32.png png/magnet.64.png png/magnet.128.png + png/minus.16.png + png/minus.32.png + png/minus.64.png + png/minus.128.png png/new.16.png png/new.32.png png/new.64.png @@ -114,6 +118,10 @@ png/play.32.png png/play.64.png png/play.128.png + png/plus.16.png + png/plus.32.png + png/plus.64.png + png/plus.128.png png/prev.16.png png/prev.32.png png/prev.64.png diff --git a/app/ui/style/olive-dark/palette.ini b/app/ui/style/olive-dark/palette.ini index 7f93ffd25..778c3d8fc 100644 --- a/app/ui/style/olive-dark/palette.ini +++ b/app/ui/style/olive-dark/palette.ini @@ -6,7 +6,7 @@ Button=#353535 ButtonText=#FFFFFF Highlight=#2A82DA HighlightedText=#FFFFFF -Link=#2A82DA +Link=#E0B040 Text=#FFFFFF ToolTipBase=#191919 ToolTipText=#FFFFFF diff --git a/app/ui/style/olive-dark/png/minus.128.png b/app/ui/style/olive-dark/png/minus.128.png new file mode 100644 index 000000000..4db41bae7 Binary files /dev/null and b/app/ui/style/olive-dark/png/minus.128.png differ diff --git a/app/ui/style/olive-dark/png/minus.16.png b/app/ui/style/olive-dark/png/minus.16.png new file mode 100644 index 000000000..cb62f33a8 Binary files /dev/null and b/app/ui/style/olive-dark/png/minus.16.png differ diff --git a/app/ui/style/olive-dark/png/minus.32.png b/app/ui/style/olive-dark/png/minus.32.png new file mode 100644 index 000000000..5a694721d Binary files /dev/null and b/app/ui/style/olive-dark/png/minus.32.png differ diff --git a/app/ui/style/olive-dark/png/minus.64.png b/app/ui/style/olive-dark/png/minus.64.png new file mode 100644 index 000000000..85d534270 Binary files /dev/null and b/app/ui/style/olive-dark/png/minus.64.png differ diff --git a/app/ui/style/olive-dark/png/plus.128.png b/app/ui/style/olive-dark/png/plus.128.png new file mode 100644 index 000000000..02f7da28a Binary files /dev/null and b/app/ui/style/olive-dark/png/plus.128.png differ diff --git a/app/ui/style/olive-dark/png/plus.16.png b/app/ui/style/olive-dark/png/plus.16.png new file mode 100644 index 000000000..e8280ca59 Binary files /dev/null and b/app/ui/style/olive-dark/png/plus.16.png differ diff --git a/app/ui/style/olive-dark/png/plus.32.png b/app/ui/style/olive-dark/png/plus.32.png new file mode 100644 index 000000000..27e50d8ce Binary files /dev/null and b/app/ui/style/olive-dark/png/plus.32.png differ diff --git a/app/ui/style/olive-dark/png/plus.64.png b/app/ui/style/olive-dark/png/plus.64.png new file mode 100644 index 000000000..7e82c2fdc Binary files /dev/null and b/app/ui/style/olive-dark/png/plus.64.png differ diff --git a/app/ui/style/olive-dark/style.css b/app/ui/style/olive-dark/style.css index bf3fef323..96be8d2ef 100644 --- a/app/ui/style/olive-dark/style.css +++ b/app/ui/style/olive-dark/style.css @@ -22,15 +22,3 @@ QPushButton:checked { background: #191919; } - -/* Node styling */ -NodeViewItemWidget { - qproperty-titlebarColor: #4040a0; - qproperty-borderColor: #000000; -} - -/* Timeline playhead styling */ -TimelinePlayhead { - qproperty-playheadColor: #ff0000; - qproperty-playheadHighlightColor: rgba(255, 255, 255, 0.2); -} diff --git a/app/ui/style/olive-dark/svg/minus.svg b/app/ui/style/olive-dark/svg/minus.svg new file mode 100644 index 000000000..e0607b229 --- /dev/null +++ b/app/ui/style/olive-dark/svg/minus.svg @@ -0,0 +1,817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-dark/svg/plus.svg b/app/ui/style/olive-dark/svg/plus.svg new file mode 100644 index 000000000..7f2db112c --- /dev/null +++ b/app/ui/style/olive-dark/svg/plus.svg @@ -0,0 +1,817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/olive-light.qrc b/app/ui/style/olive-light/olive-light.qrc index a1469343d..cd2f5eec6 100644 --- a/app/ui/style/olive-light/olive-light.qrc +++ b/app/ui/style/olive-light/olive-light.qrc @@ -1,7 +1,7 @@ - palette.ini style.css + palette.ini png/add-button.16.png png/add-button.32.png png/add-button.64.png @@ -94,6 +94,10 @@ png/magnet.32.png png/magnet.64.png png/magnet.128.png + png/minus.16.png + png/minus.32.png + png/minus.64.png + png/minus.128.png png/new.16.png png/new.32.png png/new.64.png @@ -114,6 +118,10 @@ png/play.32.png png/play.64.png png/play.128.png + png/plus.16.png + png/plus.32.png + png/plus.64.png + png/plus.128.png png/prev.16.png png/prev.32.png png/prev.64.png diff --git a/app/ui/style/olive-light/png/minus.128.png b/app/ui/style/olive-light/png/minus.128.png new file mode 100644 index 000000000..e538ac430 Binary files /dev/null and b/app/ui/style/olive-light/png/minus.128.png differ diff --git a/app/ui/style/olive-light/png/minus.16.png b/app/ui/style/olive-light/png/minus.16.png new file mode 100644 index 000000000..4056ce828 Binary files /dev/null and b/app/ui/style/olive-light/png/minus.16.png differ diff --git a/app/ui/style/olive-light/png/minus.32.png b/app/ui/style/olive-light/png/minus.32.png new file mode 100644 index 000000000..f87eb8fe8 Binary files /dev/null and b/app/ui/style/olive-light/png/minus.32.png differ diff --git a/app/ui/style/olive-light/png/minus.64.png b/app/ui/style/olive-light/png/minus.64.png new file mode 100644 index 000000000..e80dc8d4d Binary files /dev/null and b/app/ui/style/olive-light/png/minus.64.png differ diff --git a/app/ui/style/olive-light/png/plus.128.png b/app/ui/style/olive-light/png/plus.128.png new file mode 100644 index 000000000..cf6563f68 Binary files /dev/null and b/app/ui/style/olive-light/png/plus.128.png differ diff --git a/app/ui/style/olive-light/png/plus.16.png b/app/ui/style/olive-light/png/plus.16.png new file mode 100644 index 000000000..8a456e85d Binary files /dev/null and b/app/ui/style/olive-light/png/plus.16.png differ diff --git a/app/ui/style/olive-light/png/plus.32.png b/app/ui/style/olive-light/png/plus.32.png new file mode 100644 index 000000000..25283189a Binary files /dev/null and b/app/ui/style/olive-light/png/plus.32.png differ diff --git a/app/ui/style/olive-light/png/plus.64.png b/app/ui/style/olive-light/png/plus.64.png new file mode 100644 index 000000000..c4f69a0c2 Binary files /dev/null and b/app/ui/style/olive-light/png/plus.64.png differ diff --git a/app/ui/style/olive-light/style.css b/app/ui/style/olive-light/style.css index 300445ac7..bab366ba1 100644 --- a/app/ui/style/olive-light/style.css +++ b/app/ui/style/olive-light/style.css @@ -17,18 +17,3 @@ along with this program. If not, see . ***/ - -/* Hack that forces checked QPushButtons to use dark color */ - - -/* Node styling */ -NodeViewItemWidget { - qproperty-titlebarColor: #a0a0ff; - qproperty-borderColor: #000000; -} - -/* Timeline playhead styling */ -TimelinePlayhead { - qproperty-playheadColor: #ff0000; - qproperty-playheadHighlightColor: rgba(0, 0, 0, 0.25); -} diff --git a/app/ui/style/olive-light/svg/minus.svg b/app/ui/style/olive-light/svg/minus.svg new file mode 100644 index 000000000..d10c9606a --- /dev/null +++ b/app/ui/style/olive-light/svg/minus.svg @@ -0,0 +1,817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/svg/plus.svg b/app/ui/style/olive-light/svg/plus.svg new file mode 100644 index 000000000..f7483d8b1 --- /dev/null +++ b/app/ui/style/olive-light/svg/plus.svg @@ -0,0 +1,817 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/style.cpp b/app/ui/style/style.cpp index 06753432e..9de184cb1 100644 --- a/app/ui/style/style.cpp +++ b/app/ui/style/style.cpp @@ -33,17 +33,9 @@ OLIVE_NAMESPACE_ENTER +const char* StyleManager::kDefaultStyle = "olive-dark"; QString StyleManager::current_style_; - -QList StyleManager::ListInternal() -{ - QList style_list; - - style_list.append(StyleDescriptor(tr("Olive Dark"), ":/style/olive-dark")); - style_list.append(StyleDescriptor(tr("Olive Light"), ":/style/olive-light")); - - return style_list; -} +QMap StyleManager::available_themes_; void StyleManager::UseOSNativeStyling(QWidget *widget) { @@ -145,9 +137,20 @@ void StyleManager::ParsePaletteColor(QSettings *ini, QPalette *palette, QPalette palette->setColor(group, role, QColor(ini->value(role_name).toString())); } -StyleDescriptor StyleManager::DefaultStyle() +void StyleManager::Init() { - return ListInternal().first(); + qApp->setStyle(QStyleFactory::create("Fusion")); + + available_themes_.insert(QStringLiteral("olive-dark"), QStringLiteral("Olive Dark")); + available_themes_.insert(QStringLiteral("olive-light"), QStringLiteral("Olive Light")); + + QString config_style = Config::Current()["Style"].toString(); + + if (config_style.isEmpty() || !available_themes_.contains(config_style)) { + SetStyle(kDefaultStyle); + } else { + SetStyle(config_style); + } } const QString &StyleManager::GetStyle() @@ -155,31 +158,17 @@ const QString &StyleManager::GetStyle() return current_style_; } -void StyleManager::SetStyleFromConfig() -{ - QString config_style = Config::Current()["Style"].toString(); - - if (config_style.isEmpty()) { - SetStyle(DefaultStyle()); - } else { - SetStyle(config_style); - } -} - -void StyleManager::SetStyle(const StyleDescriptor &style) -{ - SetStyle(style.path()); -} - void StyleManager::SetStyle(const QString &style_path) { current_style_ = style_path; + QString abs_style_path = QStringLiteral(":/style/%1").arg(style_path); + // Load all icons for this style (icons must be loaded first because the style change below triggers the icon change) - icon::LoadAll(style_path); + icon::LoadAll(abs_style_path); // Set palette for this - QString palette_file = QStringLiteral("%1/palette.ini").arg(style_path); + QString palette_file = QStringLiteral("%1/palette.ini").arg(abs_style_path); if (QFileInfo::exists(palette_file)) { qApp->setPalette(ParsePalette(palette_file)); } else { @@ -187,7 +176,7 @@ void StyleManager::SetStyle(const QString &style_path) } // Set CSS style for this - QFile css_file(QStringLiteral("%1/style.css").arg(style_path)); + QFile css_file(QStringLiteral("%1/style.css").arg(abs_style_path)); if (css_file.exists() && css_file.open(QFile::ReadOnly | QFile::Text)) { // Read in entire CSS from file and set as the application stylesheet @@ -201,20 +190,4 @@ void StyleManager::SetStyle(const QString &style_path) } } -StyleDescriptor::StyleDescriptor(const QString &name, const QString &path) : - name_(name), - path_(path) -{ -} - -const QString &StyleDescriptor::name() const -{ - return name_; -} - -const QString &StyleDescriptor::path() const -{ - return path_; -} - OLIVE_NAMESPACE_EXIT diff --git a/app/ui/style/style.h b/app/ui/style/style.h index 23dbc4e9c..dbf385db0 100644 --- a/app/ui/style/style.h +++ b/app/ui/style/style.h @@ -28,34 +28,23 @@ OLIVE_NAMESPACE_ENTER -class StyleDescriptor { -public: - StyleDescriptor(const QString& name, const QString& path); - - const QString& name() const; - const QString& path() const; - -private: - QString name_; - QString path_; -}; - class StyleManager : public QObject { public: - static StyleDescriptor DefaultStyle(); + static void Init(); static const QString& GetStyle(); - static void SetStyleFromConfig(); - - static void SetStyle(const StyleDescriptor& style); - static void SetStyle(const QString& style_path); - static QList ListInternal(); - static void UseOSNativeStyling(QWidget* widget); + static const char* kDefaultStyle; + + static const QMap& available_themes() + { + return available_themes_; + } + private: static QPalette ParsePalette(const QString& ini_path); @@ -65,6 +54,8 @@ private: static QString current_style_; + static QMap available_themes_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt index dc068e82b..1a05cbe55 100644 --- a/app/widget/CMakeLists.txt +++ b/app/widget/CMakeLists.txt @@ -31,7 +31,9 @@ add_subdirectory(nodecombobox) add_subdirectory(nodecopypaste) add_subdirectory(nodeview) add_subdirectory(nodeparamview) +add_subdirectory(nodetableview) add_subdirectory(panel) +add_subdirectory(path) add_subdirectory(pixelsampler) add_subdirectory(playbackcontrols) add_subdirectory(projectexplorer) @@ -39,6 +41,7 @@ add_subdirectory(projecttoolbar) add_subdirectory(resizablescrollbar) add_subdirectory(scope) add_subdirectory(slider) +add_subdirectory(standardcombos) add_subdirectory(taskview) add_subdirectory(timebased) add_subdirectory(timelinewidget) diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index 46a05a26e..a4cbd853d 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -90,11 +90,11 @@ void AudioMonitor::Stop() } } -void AudioMonitor::OutputPushed(const QByteArray &data) +void AudioMonitor::OutputPushed(const QByteArray &d) { QVector v(params_.channel_count(), 0); - BytesToSampleSummary(data, v); + BytesToSampleSummary(d, v); PushValue(v); @@ -275,23 +275,33 @@ void AudioMonitor::UpdateValuesFromFile(QVector& v) // Determines how many milliseconds have passed since last update qint64 current_time = QDateTime::currentMSecsSinceEpoch(); qint64 time_passed = current_time - last_time_; + int abs_speed = qAbs(playback_speed_); + + // Multiply by speed if the speed is not 1 + if (abs_speed != 1) { + time_passed *= abs_speed; + } // Convert ms to float seconds and determine how many bytes that is qint64 bytes_to_read = params_.time_to_bytes(static_cast(time_passed) * 0.001); if (playback_speed_ < 0) { + // If reversing, jump back by the amount of bytes we're going to read bytes_to_read = qMin(bytes_to_read, file_.pos()); file_.seek(file_.pos() - bytes_to_read); } + // Read bytes in from file QByteArray b = file_.read(bytes_to_read); if (playback_speed_ < 0) { + // If reversing, head back to where we were before the read so that the next read starts + // from where we left off file_.seek(file_.pos() - bytes_to_read); } - int abs_speed = qAbs(playback_speed_); + // If speed is not 1, transform it here if (abs_speed != 1) { int sample_sz = params_.samples_to_bytes(1); int in_nb_samples = params_.bytes_to_samples(b.size()); @@ -299,8 +309,8 @@ void AudioMonitor::UpdateValuesFromFile(QVector& v) QByteArray speed_adjusted(out_nb_samples * sample_sz, Qt::Uninitialized); for (int i=0;i + OLIVE_NAMESPACE_ENTER ClickableLabel::ClickableLabel(const QString &text, QWidget *parent) : @@ -32,16 +34,18 @@ ClickableLabel::ClickableLabel(QWidget *parent) : { } -void ClickableLabel::mouseReleaseEvent(QMouseEvent *) +void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) { - if (underMouse()) { + if (event->button() == Qt::LeftButton && underMouse()) { emit MouseClicked(); } } -void ClickableLabel::mouseDoubleClickEvent(QMouseEvent *) +void ClickableLabel::mouseDoubleClickEvent(QMouseEvent *event) { - emit MouseDoubleClicked(); + if (event->button() == Qt::LeftButton) { + emit MouseDoubleClicked(); + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/colorwheel/colorvalueswidget.cpp b/app/widget/colorwheel/colorvalueswidget.cpp index 1b7fd76fc..43b328949 100644 --- a/app/widget/colorwheel/colorvalueswidget.cpp +++ b/app/widget/colorwheel/colorvalueswidget.cpp @@ -203,7 +203,6 @@ FloatSlider *ColorValuesTab::CreateColorSlider() FloatSlider* fs = new FloatSlider(); fs->SetDragMultiplier(0.01); fs->SetDecimalPlaces(5); - fs->SetLadderEnabled(true); fs->SetLadderElementCount(1); connect(fs, &FloatSlider::ValueChanged, this, &ColorValuesTab::SliderChanged); return fs; diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index a78f97e13..e8b6884ef 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -20,8 +20,10 @@ #include "curveview.h" +#include #include #include +#include #include "common/qtutils.h" @@ -61,6 +63,16 @@ void CurveView::Clear() void CurveView::SetTrackCount(int count) { track_count_ = count; + + track_visible_.resize(track_count_); + track_visible_.fill(true); +} + +void CurveView::SetTrackVisible(int track, bool visible) +{ + track_visible_[track] = visible; + + SetKeyframeTrackVisible(track, visible); } void CurveView::drawBackground(QPainter *painter, const QRectF &rect) @@ -116,6 +128,10 @@ void CurveView::drawBackground(QPainter *painter, const QRectF &rect) // Draw keyframe lines for (int j=0;jsetPen(QPen(GetKeyframeColor(j), qMax(1, fontMetrics().height() / 4))); QList keys = GetKeyframesSortedByTime(j); @@ -237,21 +253,22 @@ void CurveView::VerticalScaleChangedEvent(double scale) void CurveView::wheelEvent(QWheelEvent *event) { - if (WheelEventIsAZoomEvent(event)) { - if (!event->angleDelta().isNull()) { - if (event->angleDelta().x() + event->angleDelta().y() > 0) { - emit ScaleChanged(GetScale() * 1.1); - SetYScale(GetYScale() * 1.1); - } else { - emit ScaleChanged(GetScale() * 0.9); - SetYScale(GetYScale() * 0.9); - } - } - } else { + if (!HandleZoomFromScroll(event)) { KeyframeViewBase::wheelEvent(event); } } +void CurveView::ContextMenuEvent(Menu &m) +{ + m.addSeparator(); + + // View settings + QAction* zoom_fit_action = m.addAction(tr("Zoom to Fit")); + connect(zoom_fit_action, &QAction::triggered, this, &CurveView::ZoomToFit); + + //QAction* reset_zoom_action = m.addAction(tr("Reset Zoom")); +} + QList CurveView::GetKeyframesSortedByTime(int track) { QList sorted; @@ -285,7 +302,12 @@ QList CurveView::GetKeyframesSortedByTime(int track) qreal CurveView::GetItemYFromKeyframeValue(NodeKeyframe *key) { - return -key->value().toDouble() * GetYScale(); + return GetItemYFromKeyframeValue(key->value().toDouble()); +} + +qreal CurveView::GetItemYFromKeyframeValue(double value) +{ + return -value * GetYScale(); } void CurveView::SetItemYFromKeyframeValue(NodeKeyframe *key, KeyframeViewItem *item) @@ -367,6 +389,45 @@ void CurveView::BezierControlPointDestroyed() bezier_control_points_.removeOne(item); } +void CurveView::ZoomToFit() +{ + if (item_map().isEmpty()) { + // Prevent scaling to DBL_MIN/DBL_MAX + return; + } + + QMap::const_iterator i; + + rational min_time = RATIONAL_MAX; + rational max_time = RATIONAL_MIN; + + double min_val = DBL_MAX; + double max_val = DBL_MIN; + + for (i=item_map().constBegin(); i!=item_map().constEnd(); i++) { + rational transformed_time = GetAdjustedTime(i.key()->parent()->parentNode(), + GetTimeTarget(), + i.key()->time(), + NodeParam::kOutput); + + min_time = qMin(transformed_time, min_time); + max_time = qMax(transformed_time, max_time); + + min_val = qMin(i.key()->value().toDouble(), min_val); + max_val = qMax(i.key()->value().toDouble(), max_val); + } + + double time_range = max_time.toDouble() - min_time.toDouble(); + double new_x_scale = CalculateScaleFromDimensions(this->width(), time_range); + double new_y_scale = CalculateScaleFromDimensions(this->height(), max_val - min_val); + + emit ScaleChanged(new_x_scale); + SetYScale(new_y_scale); + + horizontalScrollBar()->setValue(TimeToScene(min_time) - CalculatePaddingFromDimensionScale(this->width())); + verticalScrollBar()->setValue(GetItemYFromKeyframeValue(max_val) - CalculatePaddingFromDimensionScale(this->height())); +} + void CurveView::AddKeyframe(NodeKeyframePtr key) { KeyframeViewItem* item = AddKeyframeInternal(key); diff --git a/app/widget/curvewidget/curveview.h b/app/widget/curvewidget/curveview.h index 2c767acd6..e051f4f04 100644 --- a/app/widget/curvewidget/curveview.h +++ b/app/widget/curvewidget/curveview.h @@ -30,6 +30,7 @@ OLIVE_NAMESPACE_ENTER class CurveView : public KeyframeViewBase { + Q_OBJECT public: CurveView(QWidget* parent = nullptr); @@ -39,9 +40,13 @@ public: void SetTrackCount(int count); + void SetTrackVisible(int track, bool visible); + public slots: void AddKeyframe(NodeKeyframePtr key); + void ZoomToFit(); + protected: virtual void drawBackground(QPainter* painter, const QRectF& rect) override; @@ -53,10 +58,13 @@ protected: virtual void wheelEvent(QWheelEvent* event) override; + virtual void ContextMenuEvent(Menu &m) override; + private: QList GetKeyframesSortedByTime(int track); qreal GetItemYFromKeyframeValue(NodeKeyframe* key); + qreal GetItemYFromKeyframeValue(double value); void SetItemYFromKeyframeValue(NodeKeyframe* key, KeyframeViewItem* item); @@ -76,6 +84,8 @@ private: QList bezier_control_points_; + QVector track_visible_; + int track_count_; private slots: diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index 4cd9dba3e..da32088ec 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -117,12 +117,17 @@ void CurveWidget::SetInput(NodeInput *input) { if (bridge_) { foreach (QWidget* bridge_widget, bridge_->widgets()) { - delete bridge_widget; + bridge_widget->deleteLater(); } - delete bridge_; + bridge_->deleteLater(); bridge_ = nullptr; } + foreach (QCheckBox* box, checkboxes_) { + box->deleteLater(); + } + checkboxes_.clear(); + if (input_) { disconnect(input_, &NodeInput::KeyframeAdded, view_, &CurveView::AddKeyframe); disconnect(input_, &NodeInput::KeyframeRemoved, view_, &CurveView::RemoveKeyframe); @@ -142,7 +147,15 @@ void CurveWidget::SetInput(NodeInput *input) for (int i=0;iwidgets().size();i++) { // Insert between two stretches to center the widget - widget_bridge_layout_->insertWidget(2 + i, bridge_->widgets().at(i)); + QCheckBox* checkbox = new QCheckBox(); + checkbox->setChecked(true); + widget_bridge_layout_->insertWidget(2 + i*2, checkbox); + checkboxes_.append(checkbox); + connect(checkbox, &QCheckBox::clicked, this, [this](bool e){ + view_->SetTrackVisible(checkboxes_.indexOf(static_cast(sender())), e); + }); + + widget_bridge_layout_->insertWidget(2 + i*2 + 1, bridge_->widgets().at(i)); } connect(input_, &NodeInput::KeyframeAdded, view_, &CurveView::AddKeyframe); @@ -156,6 +169,8 @@ void CurveWidget::SetInput(NodeInput *input) } UpdateInputLabel(); + + QMetaObject::invokeMethod(view_, "ZoomToFit", Qt::QueuedConnection); } const double &CurveWidget::GetVerticalScale() @@ -205,8 +220,6 @@ void CurveWidget::ScaleChangedEvent(const double &scale) void CurveWidget::TimeTargetChangedEvent(Node *target) { - ConnectViewerNode(nullptr); - key_control_->SetTimeTarget(target); view_->SetTimeTarget(target); @@ -214,12 +227,11 @@ void CurveWidget::TimeTargetChangedEvent(Node *target) if (bridge_) { bridge_->SetTimeTarget(target); } +} - // FIXME: If a non-viewer node is ever set here, it will fail to update the length - ViewerOutput* viewer = dynamic_cast(target); - if (viewer) { - ConnectViewerNode(viewer); - } +void CurveWidget::ConnectedNodeChanged(ViewerOutput *n) +{ + SetTimeTarget(n); } void CurveWidget::UpdateInputLabel() diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index 21e0400d0..7c33b49c7 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -21,6 +21,7 @@ #ifndef CURVEWIDGET_H #define CURVEWIDGET_H +#include #include #include #include @@ -58,6 +59,8 @@ protected: virtual void TimeTargetChangedEvent(Node* target) override; + virtual void ConnectedNodeChanged(ViewerOutput* n) override; + private: void UpdateInputLabel(); @@ -87,6 +90,8 @@ private: NodeParamViewKeyframeControl* key_control_; + QList checkboxes_; + private slots: void SelectionChanged(); diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index cfbac331f..2d6b880e9 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -23,7 +23,8 @@ OLIVE_NAMESPACE_ENTER KeyframeView::KeyframeView(QWidget *parent) : - KeyframeViewBase(parent) + KeyframeViewBase(parent), + max_scroll_(0) { setAlignment(Qt::AlignLeft | Qt::AlignTop); } @@ -35,6 +36,12 @@ void KeyframeView::wheelEvent(QWheelEvent *event) } } +void KeyframeView::SceneRectUpdateEvent(QRectF &rect) +{ + rect.setY(0); + rect.setHeight(max_scroll_); +} + void KeyframeView::AddKeyframe(NodeKeyframePtr key, int y) { QPoint global_pt(0, y); diff --git a/app/widget/keyframeview/keyframeview.h b/app/widget/keyframeview/keyframeview.h index 505ea4f58..90361c8e5 100644 --- a/app/widget/keyframeview/keyframeview.h +++ b/app/widget/keyframeview/keyframeview.h @@ -31,12 +31,22 @@ class KeyframeView : public KeyframeViewBase public: KeyframeView(QWidget* parent = nullptr); + void SetMaxScroll(int i) + { + max_scroll_ = i; + } + protected: virtual void wheelEvent(QWheelEvent* event) override; + virtual void SceneRectUpdateEvent(QRectF& rect) override; + public slots: void AddKeyframe(NodeKeyframePtr key, int y); +private: + int max_scroll_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/keyframeview/keyframeviewbase.cpp b/app/widget/keyframeview/keyframeviewbase.cpp index 0bf903b39..56ba3078d 100644 --- a/app/widget/keyframeview/keyframeviewbase.cpp +++ b/app/widget/keyframeview/keyframeviewbase.cpp @@ -35,8 +35,6 @@ OLIVE_NAMESPACE_ENTER KeyframeViewBase::KeyframeViewBase(QWidget *parent) : TimelineViewBase(parent), dragging_bezier_point_(nullptr), - y_axis_enabled_(false), - y_scale_(1.0), currently_autoselecting_(false) { SetDefaultDragMode(RubberBandDrag); @@ -57,22 +55,6 @@ void KeyframeViewBase::Clear() item_map_.clear(); } -const double &KeyframeViewBase::GetYScale() const -{ - return y_scale_; -} - -void KeyframeViewBase::SetYScale(const double &y_scale) -{ - y_scale_ = y_scale; - - if (y_axis_enabled_) { - VerticalScaleChangedEvent(y_scale_); - - viewport()->update(); - } -} - void KeyframeViewBase::DeleteSelected() { QUndoCommand* command = new QUndoCommand(); @@ -92,6 +74,19 @@ void KeyframeViewBase::DeleteSelected() Core::instance()->undo_stack()->pushIfHasChildren(command); } +void KeyframeViewBase::RemoveKeyframesOfNode(Node *n) +{ + QList inputs = n->GetInputsIncludingArrays(); + + foreach (NodeInput* i, inputs) { + foreach (const NodeInput::KeyframeTrack& track, i->keyframe_tracks()) { + foreach (NodeKeyframePtr key, track) { + RemoveKeyframe(key); + } + } + } +} + void KeyframeViewBase::RemoveKeyframe(NodeKeyframePtr key) { KeyframeAboutToBeRemoved(key.get()); @@ -101,11 +96,20 @@ void KeyframeViewBase::RemoveKeyframe(NodeKeyframePtr key) KeyframeViewItem *KeyframeViewBase::AddKeyframeInternal(NodeKeyframePtr key) { - KeyframeViewItem* item = new KeyframeViewItem(key); - item->SetTimeTarget(GetTimeTarget()); - item->SetScale(GetScale()); - item_map_.insert(key.get(), item); - scene()->addItem(item); + KeyframeViewItem* item = item_map_.value(key.get()); + + if (!item) { + item = new KeyframeViewItem(key); + item->SetTimeTarget(GetTimeTarget()); + item->SetScale(GetScale()); + item_map_.insert(key.get(), item); + scene()->addItem(item); + + if (hidden_tracks_.contains(key->track())) { + item->setVisible(false); + } + } + return item; } @@ -185,7 +189,7 @@ void KeyframeViewBase::mouseMoveEvent(QMouseEvent *event) keypair.key->key()->set_time(node_time); - if (y_axis_enabled_) { + if (IsYAxisEnabled()) { keypair.key->key()->set_value(keypair.value - mouse_diff_scaled.y()); } @@ -248,7 +252,7 @@ void KeyframeViewBase::mouseReleaseEvent(QMouseEvent *event) command); // Commit value if we're setting a value - if (y_axis_enabled_) { + if (IsYAxisEnabled()) { item->key()->set_value(keypair.value); new NodeParamSetKeyframeValueCommand(item->key(), keypair.value - mouse_diff_scaled.y(), @@ -279,10 +283,6 @@ void KeyframeViewBase::ScaleChangedEvent(const double &scale) } } -void KeyframeViewBase::VerticalScaleChangedEvent(double) -{ -} - const QMap &KeyframeViewBase::item_map() const { return item_map_; @@ -301,9 +301,30 @@ void KeyframeViewBase::TimeTargetChangedEvent(Node *target) } } -void KeyframeViewBase::SetYAxisEnabled(bool e) +void KeyframeViewBase::SetKeyframeTrackVisible(int track, bool visible) { - y_axis_enabled_ = e; + if (!visible == hidden_tracks_.contains(track)) { + return; + } + + QMap::const_iterator i; + + for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { + if (i.key()->track() == track) { + i.value()->setVisible(visible); + } + } + + if (visible) { + hidden_tracks_.removeOne(track); + } else { + hidden_tracks_.append(track); + } +} + +void KeyframeViewBase::ContextMenuEvent(Menu& m) +{ + Q_UNUSED(m) } rational KeyframeViewBase::CalculateNewTimeFromScreen(const rational &old_time, double cursor_diff) @@ -403,7 +424,7 @@ void KeyframeViewBase::ProcessBezierDrag(QPointF mouse_diff_scaled, bool include QPointF KeyframeViewBase::GetScaledCursorPos(const QPoint &cursor_pos) { return QPointF(static_cast(cursor_pos.x()) / GetScale(), - static_cast(cursor_pos.y()) / y_scale_); + static_cast(cursor_pos.y()) / GetYScale()); } void KeyframeViewBase::ShowContextMenu() @@ -450,7 +471,11 @@ void KeyframeViewBase::ShowContextMenu() break; } } + } + ContextMenuEvent(m); + + if (!items.isEmpty()) { m.addSeparator(); QAction* properties_action = m.addAction(tr("P&roperties")); @@ -502,7 +527,7 @@ void KeyframeViewBase::ShowKeyframePropertiesDialog() void KeyframeViewBase::AutoSelectKeyTimeNeighbors() { - if (currently_autoselecting_ || y_axis_enabled_) { + if (currently_autoselecting_ || IsYAxisEnabled()) { return; } diff --git a/app/widget/keyframeview/keyframeviewbase.h b/app/widget/keyframeview/keyframeviewbase.h index 04e7d6096..9cbe0b01f 100644 --- a/app/widget/keyframeview/keyframeviewbase.h +++ b/app/widget/keyframeview/keyframeviewbase.h @@ -23,9 +23,10 @@ #include "keyframeviewitem.h" #include "node/keyframe.h" -#include "widget/timetarget/timetarget.h" #include "widget/curvewidget/beziercontrolpointitem.h" +#include "widget/menu/menu.h" #include "widget/timelinewidget/view/timelineviewbase.h" +#include "widget/timetarget/timetarget.h" OLIVE_NAMESPACE_ENTER @@ -37,11 +38,10 @@ public: virtual void Clear(); - const double& GetYScale() const; - void SetYScale(const double& y_scale); - void DeleteSelected(); + void RemoveKeyframesOfNode(Node* n); + public slots: void RemoveKeyframe(NodeKeyframePtr key); @@ -54,15 +54,15 @@ protected: virtual void ScaleChangedEvent(const double& scale) override; - virtual void VerticalScaleChangedEvent(double scale); - const QMap& item_map() const; virtual void KeyframeAboutToBeRemoved(NodeKeyframe* key); virtual void TimeTargetChangedEvent(Node*) override; - void SetYAxisEnabled(bool e); + void SetKeyframeTrackVisible(int track, bool visible); + + virtual void ContextMenuEvent(Menu &m); private: rational CalculateNewTimeFromScreen(const rational& old_time, double cursor_diff); @@ -94,12 +94,10 @@ private: QVector selected_keys_; - bool y_axis_enabled_; - - double y_scale_; - bool currently_autoselecting_; + QList hidden_tracks_; + private slots: void ShowContextMenu(); diff --git a/app/widget/menu/menu.cpp b/app/widget/menu/menu.cpp index b744c3d32..5d1f7321c 100644 --- a/app/widget/menu/menu.cpp +++ b/app/widget/menu/menu.cpp @@ -50,13 +50,13 @@ Menu::Menu(const QString &s, QWidget *parent) : Init(); } -QAction *Menu::AddActionWithData(const QString &text, const QVariant &data, const QVariant &compare) +QAction *Menu::AddActionWithData(const QString &text, const QVariant &d, const QVariant &compare) { QAction* a = addAction(text); - a->setData(data); + a->setData(d); a->setCheckable(true); - a->setChecked(data == compare); + a->setChecked(d == compare); return a; } diff --git a/app/widget/menu/menu.h b/app/widget/menu/menu.h index edafaf85d..777143efc 100644 --- a/app/widget/menu/menu.h +++ b/app/widget/menu/menu.h @@ -132,7 +132,7 @@ public: } QAction* AddActionWithData(const QString& text, - const QVariant& data, + const QVariant& d, const QVariant& compare); QAction *InsertAlphabetically(const QString& s); diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index eaae8d0eb..51b332fe6 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -43,18 +43,21 @@ NodeParamView::NodeParamView(QWidget *parent) : // Set up scroll area for params QScrollArea* scroll_area = new QScrollArea(); + scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll_area->setWidgetResizable(true); splitter->addWidget(scroll_area); // Param widget - QWidget* param_widget_area = new QWidget(); - scroll_area->setWidget(param_widget_area); + param_widget_area_ = new QWidget(); + scroll_area->setWidget(param_widget_area_); // Set up scroll area layout - param_layout_ = new QVBoxLayout(param_widget_area); + param_layout_ = new QVBoxLayout(param_widget_area_); param_layout_->setSpacing(0); - param_layout_->setMargin(0); + + // KeyframeView is offset by a ruler, so to stay synchronized with it, we should be too + param_layout_->setContentsMargins(0, ruler()->height(), 0, 0); // Add a stretch to allow empty space at the bottom of the layout param_layout_->addStretch(); @@ -73,7 +76,6 @@ NodeParamView::NodeParamView(QWidget *parent) : keyframe_view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ConnectTimelineView(keyframe_view_); connect(keyframe_view_, &KeyframeView::RequestCenterScrollOnPlayhead, this, &NodeParamView::CenterScrollOnPlayhead); - bottom_item_ = keyframe_view_->scene()->addRect(0, 0, 1, 1); keyframe_area_layout->addWidget(keyframe_view_); // Connect ruler and keyframe view together @@ -120,76 +122,41 @@ NodeParamView::NodeParamView(QWidget *parent) : SetMaximumScale(TimelineViewBase::kMaximumScale); } -void NodeParamView::SetNodes(QList nodes) +void NodeParamView::SelectNodes(const QList &nodes) { - ConnectViewerNode(nullptr); + foreach (Node* n, nodes) { + NodeParamViewItem* item = new NodeParamViewItem(n); - // If we already have item widgets, delete them all now - foreach (NodeParamViewItem* item, items_) { - emit ClosedNode(item->GetNode()); - emit FoundGizmos(nullptr); - delete item; + // Insert the widget before the stretch + param_layout_->insertWidget(param_layout_->count() - 1, item); + + connect(item, &NodeParamViewItem::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); + connect(item, &NodeParamViewItem::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); + connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::ItemRequestedTimeChanged); + connect(item, &NodeParamViewItem::InputDoubleClicked, this, &NodeParamView::InputDoubleClicked); + connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); + + items_.insert(n, item); } - items_.clear(); - emit TimeTargetChanged(nullptr); - // Reset keyframe view - SetTimebase(rational()); - keyframe_view_->Clear(); + UpdateItemTime(GetTimestamp()); - // Set the internal list to the one we've received - nodes_ = nodes; + // Re-arrange keyframes + QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection); +} - if (!nodes_.isEmpty()) { - // For each node, create a widget - bool found_gizmos = false; +void NodeParamView::DeselectNodes(const QList &nodes) +{ + // Remove item from map and delete the widget + foreach (Node* n, nodes) { + // Remove all keyframes from this node + keyframe_view_->RemoveKeyframesOfNode(n); - foreach (Node* node, nodes_) { - NodeParamViewItem* item = new NodeParamViewItem(node); - - // Insert the widget before the stretch - param_layout_->insertWidget(param_layout_->count() - 1, item); - - connect(item, &NodeParamViewItem::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); - connect(item, &NodeParamViewItem::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); - connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::ItemRequestedTimeChanged); - connect(item, &NodeParamViewItem::InputDoubleClicked, this, &NodeParamView::InputDoubleClicked); - connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); - - items_.append(item); - - QMetaObject::invokeMethod(item, - "SignalAllKeyframes", - Qt::QueuedConnection); - - emit OpenedNode(node); - - if (!found_gizmos && node->HasGizmos()) { - emit FoundGizmos(node); - found_gizmos = true; - } - } - - ViewerOutput* viewer = nodes_.first()->FindOutputNode(); - - ConnectViewerNode(viewer); - - if (viewer) { - SetTimebase(viewer->video_params().time_base()); - - // Set viewer as a time target - keyframe_view_->SetTimeTarget(viewer); - - foreach (NodeParamViewItem* item, items_) { - item->SetTimeTarget(viewer); - } - - emit TimeTargetChanged(viewer); - } - - // Forces the scroll to update to this time - keyframe_view_->SetTime(ruler()->GetTime()); + delete items_.take(n); } + + // Re-arrange keyframes + QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection); } void NodeParamView::resizeEvent(QResizeEvent *event) @@ -211,6 +178,8 @@ void NodeParamView::TimebaseChangedEvent(const rational &timebase) TimeBasedWidget::TimebaseChangedEvent(timebase); keyframe_view_->SetTimebase(timebase); + + UpdateItemTime(GetTimestamp()); } void NodeParamView::TimeChangedEvent(const int64_t ×tamp) @@ -222,9 +191,14 @@ void NodeParamView::TimeChangedEvent(const int64_t ×tamp) UpdateItemTime(timestamp); } -const QList &NodeParamView::nodes() +void NodeParamView::ConnectedNodeChanged(ViewerOutput *n) { - return nodes_; + // Set viewer as a time target + keyframe_view_->SetTimeTarget(n); + + foreach (NodeParamViewItem* item, items_) { + item->SetTimeTarget(n); + } } Node *NodeParamView::GetTimeTarget() const @@ -239,7 +213,7 @@ void NodeParamView::DeleteSelected() void NodeParamView::UpdateItemTime(const int64_t ×tamp) { - rational time = Timecode::timestamp_to_time(timestamp, keyframe_view_->timebase()); + rational time = Timecode::timestamp_to_time(timestamp, timebase()); foreach (NodeParamViewItem* item, items_) { item->SetTime(time); @@ -251,11 +225,16 @@ void NodeParamView::ItemRequestedTimeChanged(const rational &time) SetTimeAndSignal(Timecode::time_to_timestamp(time, keyframe_view_->timebase())); } -void NodeParamView::ForceKeyframeViewToScroll(int min, int max) +void NodeParamView::ForceKeyframeViewToScroll() { - Q_UNUSED(min) + keyframe_view_->SetMaxScroll(param_widget_area_->height() - ruler()->height()); +} - bottom_item_->setY(keyframe_view_->viewport()->height() + max); +void NodeParamView::PlaceKeyframesOnView() +{ + foreach (NodeParamViewItem* item, items_) { + QMetaObject::invokeMethod(item, "SignalAllKeyframes", Qt::QueuedConnection); + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index fd6fe4220..1e3889b0c 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -37,8 +37,13 @@ class NodeParamView : public TimeBasedWidget public: NodeParamView(QWidget* parent = nullptr); - void SetNodes(QList nodes); - const QList& nodes(); + void SelectNodes(const QList& nodes); + void DeselectNodes(const QList& nodes); + + const QMap& GetItemMap() const + { + return items_; + } Node* GetTimeTarget() const; @@ -47,16 +52,8 @@ public: signals: void InputDoubleClicked(NodeInput* input); - void TimeTargetChanged(Node* target); - void RequestSelectNode(const QList& target); - void OpenedNode(Node* n); - - void ClosedNode(Node* n); - - void FoundGizmos(Node* n); - protected: virtual void resizeEvent(QResizeEvent *event) override; @@ -64,6 +61,8 @@ protected: virtual void TimebaseChangedEvent(const rational&) override; virtual void TimeChangedEvent(const int64_t &) override; + virtual void ConnectedNodeChanged(ViewerOutput* n) override; + private: void UpdateItemTime(const int64_t ×tamp); @@ -71,20 +70,20 @@ private: KeyframeView* keyframe_view_; - QList nodes_; - - QList items_; + QMap items_; QScrollBar* vertical_scrollbar_; - QGraphicsRectItem* bottom_item_; - int last_scroll_val_; + QWidget* param_widget_area_; + private slots: void ItemRequestedTimeChanged(const rational& time); - void ForceKeyframeViewToScroll(int min, int max); + void ForceKeyframeViewToScroll(); + + void PlaceKeyframesOnView(); }; diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp index 612b14f81..43507cac6 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp @@ -23,7 +23,10 @@ #include #include "common/qtutils.h" +#include "core.h" #include "node/node.h" +#include "widget/menu/menu.h" +#include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER @@ -39,7 +42,9 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(NodeInput *input, QWidg connected_to_lbl_ = new ClickableLabel(); connected_to_lbl_->setCursor(Qt::PointingHandCursor); + connected_to_lbl_->setContextMenuPolicy(Qt::CustomContextMenu); connect(connected_to_lbl_, &ClickableLabel::MouseClicked, this, &NodeParamViewConnectedLabel::ConnectionClicked); + connect(connected_to_lbl_, &ClickableLabel::customContextMenuRequested, this, &NodeParamViewConnectedLabel::ShowLabelContextMenu); layout->addWidget(connected_to_lbl_); layout->addStretch(); @@ -69,4 +74,16 @@ void NodeParamViewConnectedLabel::UpdateConnected() connected_to_lbl_->setText(connection_str); } +void NodeParamViewConnectedLabel::ShowLabelContextMenu() +{ + Menu m(this); + + QAction* disconnect_action = m.addAction(tr("Disconnect")); + connect(disconnect_action, &QAction::triggered, this, [this](){ + Core::instance()->undo_stack()->push(new NodeEdgeRemoveCommand(input_->get_connected_output(), input_)); + }); + + m.exec(QCursor::pos()); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h index 161bf2a0d..04103cae1 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h @@ -37,6 +37,8 @@ signals: private slots: void UpdateConnected(); + void ShowLabelContextMenu(); + private: ClickableLabel* connected_to_lbl_; diff --git a/app/widget/nodeparamview/nodeparamviewrichtext.cpp b/app/widget/nodeparamview/nodeparamviewrichtext.cpp index de382f0c9..476c7cc76 100644 --- a/app/widget/nodeparamview/nodeparamviewrichtext.cpp +++ b/app/widget/nodeparamview/nodeparamviewrichtext.cpp @@ -48,8 +48,10 @@ void NodeParamViewRichText::ShowRichTextDialog() { RichTextDialog d(line_edit_->text(), this); if (d.exec() == QDialog::Accepted) { - line_edit_->setText(d.text()); - emit textEdited(d.text()); + QString s = d.text(); + + line_edit_->setText(s); + emit textEdited(s); } } diff --git a/app/widget/nodeparamview/nodeparamviewrichtext.h b/app/widget/nodeparamview/nodeparamviewrichtext.h index 88a2befef..974a742b4 100644 --- a/app/widget/nodeparamview/nodeparamviewrichtext.h +++ b/app/widget/nodeparamview/nodeparamviewrichtext.h @@ -45,6 +45,13 @@ public slots: line_edit_->setText(s); } + void setTextPreservingCursor(const QString &s) + { + int cursor_pos = line_edit_->cursorPosition(); + line_edit_->setText(s); + line_edit_->setCursorPosition(cursor_pos); + } + signals: void textEdited(const QString &); diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index dc2638ccd..456323238 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -96,7 +96,7 @@ void NodeParamViewWidgetBridge::CreateWidgets() { IntegerSlider* slider = new IntegerSlider(); slider->SetDefaultValue(input_->GetDefaultValue()); - slider->SetLadderEnabled(true); + slider->SetLadderElementCount(2); widgets_.append(slider); connect(slider, &IntegerSlider::ValueChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; @@ -163,6 +163,7 @@ void NodeParamViewWidgetBridge::CreateWidgets() { QFontComboBox* font_combobox = new QFontComboBox(); widgets_.append(font_combobox); + connect(font_combobox, &QFontComboBox::currentFontChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; } case NodeParam::kFootage: @@ -350,7 +351,7 @@ void NodeParamViewWidgetBridge::WidgetCallback() case NodeParam::kFont: { // Widget is a QFontComboBox - SetInputValue(static_cast(sender())->currentFont(), 0); + SetInputValue(static_cast(sender())->currentFont().family(), 0); break; } case NodeParam::kFootage: @@ -362,7 +363,17 @@ void NodeParamViewWidgetBridge::WidgetCallback() case NodeParam::kCombo: { // Widget is a QComboBox - SetInputValue(static_cast(widgets_.first())->currentIndex(), 0); + QComboBox* cb = static_cast(widgets_.first()); + int index = cb->currentIndex(); + + // Subtract any splitters up until this point + for (int i=index-1; i>=0; i--) { + if (cb->itemData(i, Qt::AccessibleDescriptionRole).toString() == QStringLiteral("separator")) { + index--; + } + } + + SetInputValue(index, 0); break; } } @@ -373,7 +384,7 @@ void NodeParamViewWidgetBridge::CreateSliders(int count) for (int i=0;iSetDefaultValue(input_->GetDefaultValueForTrack(i)); - fs->SetLadderEnabled(true); + fs->SetLadderElementCount(2); widgets_.append(fs); connect(fs, &FloatSlider::ValueChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); } @@ -459,7 +470,7 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() case NodeParam::kText: { NodeParamViewRichText* e = static_cast(widgets_.first()); - e->setText(input_->get_value_at_time(node_time).toString()); + e->setTextPreservingCursor(input_->get_value_at_time(node_time).toString()); break; } case NodeParam::kBoolean: @@ -467,7 +478,10 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() break; case NodeParam::kFont: { - // FIXME: Implement this + QFontComboBox* fc = static_cast(widgets_.first()); + fc->blockSignals(true); + fc->setCurrentFont(input_->get_value_at_time(node_time).toString()); + fc->blockSignals(false); break; } case NodeParam::kCombo: diff --git a/app/widget/nodetableview/CMakeLists.txt b/app/widget/nodetableview/CMakeLists.txt new file mode 100644 index 000000000..f12dff040 --- /dev/null +++ b/app/widget/nodetableview/CMakeLists.txt @@ -0,0 +1,26 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/nodetableview/nodetabletraverser.h + widget/nodetableview/nodetabletraverser.cpp + widget/nodetableview/nodetableview.h + widget/nodetableview/nodetableview.cpp + widget/nodetableview/nodetablewidget.h + widget/nodetableview/nodetablewidget.cpp + PARENT_SCOPE +) diff --git a/app/widget/nodetableview/nodetabletraverser.cpp b/app/widget/nodetableview/nodetabletraverser.cpp new file mode 100644 index 000000000..77d92afb6 --- /dev/null +++ b/app/widget/nodetableview/nodetabletraverser.cpp @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodetabletraverser.h" + +OLIVE_NAMESPACE_ENTER + +QVariant NodeTableTraverser::ProcessVideoFootage(StreamPtr stream, const rational &input_time) +{ + ImageStreamPtr video_stream = std::static_pointer_cast(stream); + + return QVariant::fromValue(VideoParams(video_stream->width(), + video_stream->height(), + video_stream->timebase(), + video_stream->format(), + video_stream->pixel_aspect_ratio())); +} + +QVariant NodeTableTraverser::ProcessAudioFootage(StreamPtr stream, const TimeRange &input_time) +{ + AudioStreamPtr audio_stream = std::static_pointer_cast(stream); + + return QVariant::fromValue(AudioParams(audio_stream->sample_rate(), + audio_stream->channel_layout(), + SampleFormat::kInternalFormat)); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetabletraverser.h b/app/widget/nodetableview/nodetabletraverser.h new file mode 100644 index 000000000..20dae8e2e --- /dev/null +++ b/app/widget/nodetableview/nodetabletraverser.h @@ -0,0 +1,42 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLETRAVERSER_H +#define NODETABLETRAVERSER_H + +#include "node/traverser.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTableTraverser : public NodeTraverser +{ +public: + NodeTableTraverser() = default; + +protected: + virtual QVariant ProcessVideoFootage(StreamPtr stream, const rational &input_time); + + virtual QVariant ProcessAudioFootage(StreamPtr stream, const TimeRange &input_time); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLETRAVERSER_H diff --git a/app/widget/nodetableview/nodetableview.cpp b/app/widget/nodetableview/nodetableview.cpp new file mode 100644 index 000000000..7696ace3b --- /dev/null +++ b/app/widget/nodetableview/nodetableview.cpp @@ -0,0 +1,266 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodetableview.h" + +#include +#include + +#include "node/param.h" +#include "nodetabletraverser.h" + +OLIVE_NAMESPACE_ENTER + +NodeTableView::NodeTableView(QWidget* parent) : + QTreeWidget(parent) +{ + setColumnCount(3); + setHeaderLabels({tr("Type"), + tr("Source"), + tr("R/X"), + tr("G/Y"), + tr("B/Z"), + tr("A/W")}); +} + +void NodeTableView::SelectNodes(const QList &nodes) +{ + foreach (Node* n, nodes) { + QTreeWidgetItem* top_item = new QTreeWidgetItem(); + top_item->setText(0, n->Name()); + top_item->setFirstColumnSpanned(true); + this->addTopLevelItem(top_item); + top_level_item_map_.insert(n, top_item); + } + + SetTime(last_time_); +} + +void NodeTableView::DeselectNodes(const QList &nodes) +{ + foreach (Node* n, nodes) { + delete top_level_item_map_.take(n); + } +} + +void NodeTableView::SetTime(const rational &time) +{ + last_time_ = time; + + NodeTableTraverser traverser; + + QMap::const_iterator i; + for (i=top_level_item_map_.constBegin(); i!=top_level_item_map_.constEnd(); i++) { + Node* node = i.key(); + QTreeWidgetItem* item = i.value(); + + // Generate a value database for this node at this time + NodeValueDatabase db = traverser.GenerateDatabase(node, TimeRange(time, time)); + + // Delete any children of this item that aren't in this database + for (int j=0; jchildCount(); j++) { + if (!db.contains(item->child(j)->data(0, Qt::UserRole).toString())) { + delete item->takeChild(j); + j--; + } + } + + // Update all inputs + NodeValueDatabase::const_iterator l; + + for (l=db.begin(); l!=db.end(); l++) { + const NodeValueTable& table = l.value(); + + NodeInput* input = node->GetInputWithID(l.key()); + if (!input) { + // Filters out table entries that aren't inputs (like "global") + continue; + } + + QTreeWidgetItem* input_item = nullptr; + + for (int j=0; jchildCount(); j++) { + QTreeWidgetItem* compare = item->child(j); + + if (compare->data(0, Qt::UserRole).toString() == input->id()) { + input_item = compare; + break; + } + } + + if (!input_item) { + input_item = new QTreeWidgetItem(); + input_item->setText(0, input->name()); + input_item->setData(0, Qt::UserRole, input->id()); + input_item->setFirstColumnSpanned(true); + item->addChild(input_item); + } + + // Create children if necessary + while (input_item->childCount() < table.Count()) { + input_item->addChild(new QTreeWidgetItem()); + } + + // Remove children if necessary + while (input_item->childCount() > table.Count()) { + delete input_item->takeChild(input_item->childCount() - 1); + } + + for (int j=0;jchild(j); + + // Set data type name + sub_item->setText(0, NodeParam::GetPrettyDataTypeName(value.type())); + + // Determine source + QString source_name; + if (value.source()) { + source_name = value.source()->Name(); + } else { + source_name = tr("(unknown)"); + } + sub_item->setText(1, source_name); + + switch (value.type()) { + case NodeParam::kTexture: + { + // NodeTableTraverser puts video params in here + VideoParams p = value.data().value(); + int channel_count = PixelFormat::ChannelCount(p.format()); + + for (int k=0;ksetItemWidget(sub_item, 2 + k, new QCheckBox()); + } + break; + } + default: + { + QVector split_values = input->split_normal_value_into_track_values(value.data()); + for (int k=0;ksetText(2 + k, NodeInput::ValueToString(value.type(), split_values.at(k), true)); + } + } + } + } + } + } +} + +/* +void NodeTableView::SetNode(Node *n, const rational &time) +{ + NodeTableTraverser traverser; + NodeValueDatabase db = traverser.GenerateDatabase(n, TimeRange(time, time)); + + // Remove top items if necessary + for (int i=0;itopLevelItemCount();i++) { + if (!db.contains(this->topLevelItem(i)->data(0, Qt::UserRole).toString())) { + delete this->takeTopLevelItem(i); + i--; + } + } + + NodeValueDatabase::const_iterator i; + + for (i=db.begin(); i!=db.end(); i++) { + const NodeValueTable& table = i.value(); + + NodeInput* input = n->GetInputWithID(i.key()); + if (!input) { + // Filters out table entries that aren't inputs (like "global") + continue; + } + + QTreeWidgetItem* top_item = nullptr; + + for (int j=0;jtopLevelItemCount();j++) { + QTreeWidgetItem* compare = this->topLevelItem(j); + + if (compare->data(0, Qt::UserRole).toString() == input->id()) { + top_item = compare; + break; + } + } + + if (!top_item) { + top_item = new QTreeWidgetItem(); + top_item->setText(0, input->name()); + top_item->setData(0, Qt::UserRole, input->id()); + top_item->setFirstColumnSpanned(true); + this->addTopLevelItem(top_item); + } + + // Create children if necessary + while (top_item->childCount() < table.Count()) { + top_item->addChild(new QTreeWidgetItem()); + } + + // Remove children if necessary + while (top_item->childCount() > table.Count()) { + delete top_item->takeChild(top_item->childCount() - 1); + } + + for (int j=0;jchild(j); + + // Set data type name + sub_item->setText(0, NodeParam::GetPrettyDataTypeName(value.type())); + + // Determine source + QString source_name; + if (value.source()) { + source_name = value.source()->Name(); + } else { + source_name = tr("(unknown)"); + } + sub_item->setText(1, source_name); + + switch (value.type()) { + case NodeParam::kTexture: + { + // NodeTableTraverser puts video params in here + VideoParams p = value.data().value(); + int channel_count = PixelFormat::ChannelCount(p.format()); + + for (int k=0;ksetItemWidget(sub_item, 2 + k, new QCheckBox()); + } + break; + } + default: + { + QVector split_values = input->split_normal_value_into_track_values(value.data()); + for (int k=0;ksetText(2 + k, NodeInput::ValueToString(value.type(), split_values.at(k))); + } + } + } + } + } +} +*/ + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetableview.h b/app/widget/nodetableview/nodetableview.h new file mode 100644 index 000000000..7aa6e3eeb --- /dev/null +++ b/app/widget/nodetableview/nodetableview.h @@ -0,0 +1,50 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLEVIEW_H +#define NODETABLEVIEW_H + +#include + +#include "node/node.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTableView : public QTreeWidget +{ +public: + NodeTableView(QWidget* parent = nullptr); + + void SelectNodes(const QList& nodes); + + void DeselectNodes(const QList& nodes); + + void SetTime(const rational& time); + +private: + QMap top_level_item_map_; + + rational last_time_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLEVIEW_H diff --git a/app/widget/nodetableview/nodetablewidget.cpp b/app/widget/nodetableview/nodetablewidget.cpp new file mode 100644 index 000000000..f06678d33 --- /dev/null +++ b/app/widget/nodetableview/nodetablewidget.cpp @@ -0,0 +1,38 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodetablewidget.h" + +#include + +OLIVE_NAMESPACE_ENTER + +NodeTableWidget::NodeTableWidget(QWidget* parent) : + TimeBasedWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setSpacing(0); + layout->setMargin(0); + + view_ = new NodeTableView(); + layout->addWidget(view_); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetablewidget.h b/app/widget/nodetableview/nodetablewidget.h new file mode 100644 index 000000000..1eeecbfcb --- /dev/null +++ b/app/widget/nodetableview/nodetablewidget.h @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLEWIDGET_H +#define NODETABLEWIDGET_H + +#include "nodetableview.h" +#include "widget/timebased/timebased.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTableWidget : public TimeBasedWidget +{ +public: + NodeTableWidget(QWidget* parent = nullptr); + + void SelectNodes(const QList& nodes) + { + view_->SelectNodes(nodes); + } + + void DeselectNodes(const QList& nodes) + { + view_->DeselectNodes(nodes); + } + +protected: + virtual void TimeChangedEvent(const int64_t& ts) override + { + UpdateView(); + } + +private: + void UpdateView() + { + view_->SetTime(GetTime()); + } + + NodeTableView* view_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLEWIDGET_H diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index e2a5ed929..b8d7e526f 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -74,6 +74,7 @@ void NodeView::SetGraph(NodeGraph *graph) if (graph_ != nullptr) { disconnect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); disconnect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); + disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::GraphNodeRemoved); disconnect(graph_, &NodeGraph::EdgeAdded, this, &NodeView::GraphEdgeAdded); disconnect(graph_, &NodeGraph::EdgeRemoved, this, &NodeView::GraphEdgeRemoved); } @@ -89,6 +90,7 @@ void NodeView::SetGraph(NodeGraph *graph) if (graph_ != nullptr) { connect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); connect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); + connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::GraphNodeRemoved); connect(graph_, &NodeGraph::EdgeAdded, this, &NodeView::GraphEdgeAdded); connect(graph_, &NodeGraph::EdgeRemoved, this, &NodeView::GraphEdgeRemoved); @@ -144,7 +146,8 @@ void NodeView::SelectAll() scene_.SelectAll(); - ReconnectSelectionChangedSignal(); + ConnectSelectionChangedSignal(); + SceneSelectionChangedSlot(); } void NodeView::DeselectAll() @@ -155,7 +158,8 @@ void NodeView::DeselectAll() scene_.DeselectAll(); - ReconnectSelectionChangedSignal(); + ConnectSelectionChangedSignal(); + SceneSelectionChangedSlot(); } void NodeView::Select(const QList &nodes) @@ -176,7 +180,8 @@ void NodeView::Select(const QList &nodes) item->setSelected(true); } - ReconnectSelectionChangedSignal(); + ConnectSelectionChangedSignal(); + SceneSelectionChangedSlot(); } void NodeView::SelectWithDependencies(QList nodes) @@ -195,10 +200,13 @@ void NodeView::SelectWithDependencies(QList nodes) void NodeView::SelectBlocks(const QList &blocks) { - if (selected_blocks_ == blocks) { - return; - } + selected_blocks_.append(blocks); + SelectBlocksInternal(); +} + +void NodeView::DeselectBlocks(const QList &blocks) +{ // Remove temporary associations foreach (Block* b, selected_blocks_) { if (!blocks.contains(b)) { @@ -206,35 +214,12 @@ void NodeView::SelectBlocks(const QList &blocks) } } - selected_blocks_ = blocks; - - // Block scene signals while our selection is changing a lot - scene_.blockSignals(true); - - if (filter_mode_ == kFilterShowSelectedBlocks) { - UpdateBlockFilter(); - } - - QList nodes; - nodes.reserve(blocks.size()); - + // Remove blocks from selected array foreach (Block* b, blocks) { - nodes.append(b); - nodes.append(b->GetDependencies()); + selected_blocks_.removeOne(b); } - SelectWithDependencies(nodes); - - // Stop blocking signals and send a change signal now that all of our processing is done - scene_.blockSignals(false); - SceneSelectionChangedSlot(); - - if (!blocks.isEmpty()) { - NodeViewItem* item = scene_.NodeToUIObject(blocks.first()); - if (item) { - centerOn(item); - } - } + SelectBlocksInternal(); } void NodeView::CopySelected(bool cut) @@ -356,30 +341,17 @@ void NodeView::mousePressEvent(QMouseEvent *event) { if (HandPress(event)) return; - if (!attached_items_.isEmpty()) { - if (attached_items_.size() == 1) { - Node* dropping_node = attached_items_.first().item->GetNode(); - - if (drop_edge_) { - NodeEdgePtr old_edge = drop_edge_->edge(); - - // We have everything we need to place the node in between - QUndoCommand* command = new QUndoCommand(); - - // Remove old edge - new NodeEdgeRemoveCommand(old_edge, command); - - // Place new edges - new NodeEdgeAddCommand(old_edge->output(), drop_input_, command); - new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); - - Core::instance()->undo_stack()->push(command); - } - - drop_edge_ = nullptr; + if (event->button() == Qt::RightButton) { + // Qt doesn't do this by default for some reason + if (!(event->modifiers() & Qt::ShiftModifier)) { + scene_.clearSelection(); } - DetachItemsFromCursor(); + // If there's an item here, select it + QGraphicsItem* item = itemAt(event->pos()); + if (item) { + item->setSelected(true); + } } super::mousePressEvent(event); @@ -456,6 +428,32 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) { if (HandRelease(event)) return; + if (!attached_items_.isEmpty()) { + if (attached_items_.size() == 1) { + Node* dropping_node = attached_items_.first().item->GetNode(); + + if (drop_edge_) { + NodeEdgePtr old_edge = drop_edge_->edge(); + + // We have everything we need to place the node in between + QUndoCommand* command = new QUndoCommand(); + + // Remove old edge + new NodeEdgeRemoveCommand(old_edge, command); + + // Place new edges + new NodeEdgeAddCommand(old_edge->output(), drop_input_, command); + new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); + + Core::instance()->undo_stack()->push(command); + } + + drop_edge_ = nullptr; + } + + DetachItemsFromCursor(); + } + super::mouseReleaseEvent(event); } @@ -478,7 +476,44 @@ void NodeView::wheelEvent(QWheelEvent *event) void NodeView::SceneSelectionChangedSlot() { - emit SelectionChanged(scene_.GetSelectedNodes()); + QList current_selection = scene_.GetSelectedNodes(); + + QList selected; + QList deselected; + + // Determine which nodes are newly selected + if (selected_nodes_.isEmpty()) { + // All nodes in the current selection have just been selected + selected = current_selection; + } else { + foreach (Node* n, current_selection) { + if (!selected_nodes_.contains(n)) { + selected.append(n); + } + } + } + + // Determine which nodes are newly deselected + if (current_selection.isEmpty()) { + // All nodes that were selected have been deselected + deselected = selected_nodes_; + } else { + foreach (Node* n, selected_nodes_) { + if (!current_selection.contains(n)) { + deselected.append(n); + } + } + } + + selected_nodes_ = current_selection; + + if (!selected.isEmpty()) { + emit NodesSelected(selected); + } + + if (!deselected.isEmpty()) { + emit NodesDeselected(deselected); + } } void NodeView::ShowContextMenu(const QPoint &pos) @@ -803,12 +838,6 @@ void NodeView::ConnectSelectionChangedSignal() connect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::SceneSelectionChangedSlot); } -void NodeView::ReconnectSelectionChangedSignal() -{ - ConnectSelectionChangedSignal(); - SceneSelectionChangedSlot(); -} - void NodeView::DisconnectSelectionChangedSignal() { disconnect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::SceneSelectionChangedSlot); @@ -913,6 +942,37 @@ void NodeView::DisassociateNode(Node *n, bool remove_from_map) disconnect(n, &Node::destroyed, this, &NodeView::AssociatedNodeDestroyed); } +void NodeView::SelectBlocksInternal() +{ + // Block scene signals while our selection is changing a lot + scene_.blockSignals(true); + + if (filter_mode_ == kFilterShowSelectedBlocks) { + UpdateBlockFilter(); + } + + QList nodes; + nodes.reserve(selected_blocks_.size()); + + foreach (Block* b, selected_blocks_) { + nodes.append(b); + nodes.append(b->GetDependencies()); + } + + SelectWithDependencies(nodes); + + // Stop blocking signals and send a change signal now that all of our processing is done + scene_.blockSignals(false); + SceneSelectionChangedSlot(); + + if (!selected_blocks_.isEmpty()) { + NodeViewItem* item = scene_.NodeToUIObject(selected_blocks_.first()); + if (item) { + centerOn(item); + } + } +} + void NodeView::ValidateFilter() { // Force auto-positioning @@ -932,6 +992,13 @@ void NodeView::AssociatedNodeDestroyed() DisassociateNode(static_cast(sender()), true); } +void NodeView::GraphNodeRemoved(Node *node) +{ + if (selected_blocks_.contains(static_cast(node))) { + DeselectBlocks({static_cast(node)}); + } +} + void NodeView::GraphEdgeAdded(NodeEdgePtr edge) { Node* input_node = edge->input()->parentNode(); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index bca1d669a..573aaa79d 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -61,18 +61,19 @@ public: void Select(const QList& nodes); void SelectWithDependencies(QList nodes); - void SelectBlocks(const QList& blocks); - void CopySelected(bool cut); void Paste(); void Duplicate(); + void SelectBlocks(const QList& blocks); + + void DeselectBlocks(const QList& blocks); + signals: - /** - * @brief Signal emitted when the selected nodes have changed - */ - void SelectionChanged(QList selected_nodes); + void NodesSelected(const QList& nodes); + + void NodesDeselected(const QList& nodes); protected: virtual void keyPressEvent(QKeyEvent *event) override; @@ -83,6 +84,8 @@ protected: virtual void wheelEvent(QWheelEvent* event) override; + //virtual void scrollContentsBy(int dx, int dy) override; + private: void PlaceNode(NodeViewItem* n, const QPointF& pos); @@ -97,7 +100,6 @@ private: void MoveAttachedNodesToCursor(const QPoint &p); void ConnectSelectionChangedSignal(); - void ReconnectSelectionChangedSignal(); void DisconnectSelectionChangedSignal(); void UpdateBlockFilter(); @@ -105,6 +107,8 @@ private: void AssociateNodeWithSelectedBlocks(Node* n); void DisassociateNode(Node* n, bool remove_from_map); + void SelectBlocksInternal(); + NodeGraph* graph_; struct AttachedItem { @@ -119,6 +123,8 @@ private: NodeViewScene scene_; + QList selected_nodes_; + QList selected_blocks_; QHash > association_map_; @@ -139,6 +145,8 @@ private slots: void AssociatedNodeDestroyed(); + void GraphNodeRemoved(Node* node); + void GraphEdgeAdded(NodeEdgePtr edge); void GraphEdgeRemoved(NodeEdgePtr edge); diff --git a/app/widget/path/CMakeLists.txt b/app/widget/path/CMakeLists.txt new file mode 100644 index 000000000..a7bd17910 --- /dev/null +++ b/app/widget/path/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/path/pathwidget.h + widget/path/pathwidget.cpp + PARENT_SCOPE +) diff --git a/app/widget/path/pathwidget.cpp b/app/widget/path/pathwidget.cpp new file mode 100644 index 000000000..b646e2d40 --- /dev/null +++ b/app/widget/path/pathwidget.cpp @@ -0,0 +1,68 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "pathwidget.h" + +#include +#include +#include + +#include "common/filefunctions.h" + +OLIVE_NAMESPACE_ENTER + +PathWidget::PathWidget(const QString &path, QWidget *parent) : + QWidget(parent) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setMargin(0); + + path_edit_ = new QLineEdit(); + path_edit_->setText(path); + layout->addWidget(path_edit_); + connect(path_edit_, &QLineEdit::textChanged, this, &PathWidget::LineEditChanged); + + browse_btn_ = new QPushButton(tr("Browse")); + layout->addWidget(browse_btn_); + + connect(browse_btn_, &QPushButton::clicked, this, &PathWidget::BrowseClicked); +} + +void PathWidget::BrowseClicked() +{ + QString dir = QFileDialog::getExistingDirectory(static_cast(parent()), + tr("Browse for path"), + path_edit_->text()); + + if (!dir.isEmpty()) { + path_edit_->setText(dir); + } +} + +void PathWidget::LineEditChanged() +{ + if (FileFunctions::DirectoryIsValid(text(), false)) { + path_edit_->setStyleSheet(QString()); + } else { + path_edit_->setStyleSheet(QStringLiteral("QLineEdit {color: red;}")); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/task/cache/footagecache.h b/app/widget/path/pathwidget.h similarity index 64% rename from app/task/cache/footagecache.h rename to app/widget/path/pathwidget.h index f48218532..c35eae30e 100644 --- a/app/task/cache/footagecache.h +++ b/app/widget/path/pathwidget.h @@ -18,31 +18,40 @@ ***/ -#ifndef FOOTAGECACHETASK_H -#define FOOTAGECACHETASK_H +#ifndef PATHWIDGET_H +#define PATHWIDGET_H -#include "cache.h" -#include "node/input/media/video/video.h" -#include "project/item/footage/footage.h" -#include "project/item/sequence/sequence.h" +#include +#include + +#include "common/define.h" OLIVE_NAMESPACE_ENTER -class FootageCacheTask : public CacheTask +class PathWidget : public QWidget { Q_OBJECT public: - FootageCacheTask(VideoStreamPtr footage, Sequence* sequence); + PathWidget(const QString& path, + QWidget* parent = nullptr); - virtual ~FootageCacheTask() override; + QString text() const + { + return path_edit_->text(); + } + +private slots: + void BrowseClicked(); + + void LineEditChanged(); private: - VideoStreamPtr footage_; + QLineEdit* path_edit_; - VideoInput* video_node_; + QPushButton* browse_btn_; }; OLIVE_NAMESPACE_EXIT -#endif // FOOTAGECACHETASK_H +#endif // PATHWIDGET_H diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index 4f1fdb64a..8a397786a 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -97,7 +97,10 @@ PlaybackControls::PlaybackControls(QWidget *parent) : // Default to showing play button playpause_stack_->setCurrentWidget(play_btn_); - playpause_stack_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); + + // Hack to conform the play/pause button size to the other buttons (QStackedWidget has a + // different size policy by default) + playpause_stack_->setSizePolicy(prev_frame_btn_->sizePolicy()); // Next Frame Button next_frame_btn_ = new QPushButton(); diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index c5b4ab690..0a1dd0ab7 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -31,7 +31,7 @@ #include "core.h" #include "dialog/footageproperties/footageproperties.h" #include "dialog/sequence/sequence.h" -#include "task/cache/footagecache.h" +#include "task/precache/precachetask.h" #include "task/taskmanager.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" @@ -427,7 +427,7 @@ void ProjectExplorer::ContextMenuStartProxy(QAction *a) // Start a background task for proxying foreach (VideoStreamPtr video_stream, video_streams) { - FootageCacheTask* proxy_task = new FootageCacheTask(video_stream, sequence); + PreCacheTask* proxy_task = new PreCacheTask(video_stream, sequence); TaskManager::instance()->AddTask(proxy_task); } } diff --git a/app/widget/scope/histogram/histogram.cpp b/app/widget/scope/histogram/histogram.cpp index 9665e32de..e89918c06 100644 --- a/app/widget/scope/histogram/histogram.cpp +++ b/app/widget/scope/histogram/histogram.cpp @@ -23,8 +23,9 @@ #include #include -#include "common/clamp.h" -#include "common/functiontimer.h" +#include "common/qtutils.h" +#include "node/node.h" +#include "render/backend/opengl/openglrenderfunctions.h" OLIVE_NAMESPACE_ENTER @@ -33,4 +34,170 @@ HistogramScope::HistogramScope(QWidget* parent) : { } +HistogramScope::~HistogramScope() +{ + CleanUp(); + + if (context()) { + disconnect(context(), &QOpenGLContext::aboutToBeDestroyed, this, + &HistogramScope::CleanUp); + } +} + +void HistogramScope::initializeGL() +{ + ScopeBase::initializeGL(); + + pipeline_secondary_ = CreateSecondaryShader(); + + connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, + &HistogramScope::CleanUp, Qt::DirectConnection); +} + +void HistogramScope::AssertAdditionalTextures() +{ + if (!texture_row_sums_.IsCreated() + || texture_row_sums_.width() != width() + || texture_row_sums_.height() != height()) { + texture_row_sums_.Destroy(); + texture_row_sums_.Create(context(), VideoParams(width(), + height(), managed_tex().format())); + } +} + +void HistogramScope::CleanUp() +{ + makeCurrent(); + + pipeline_secondary_ = nullptr; + texture_row_sums_.Destroy(); + + doneCurrent(); +} + +OpenGLShaderPtr HistogramScope::CreateShader() +{ + OpenGLShaderPtr pipeline = OpenGLShader::Create(); + + pipeline->create(); + pipeline->addShaderFromSourceCode(QOpenGLShader::Vertex, + OpenGLShader::CodeDefaultVertex()); + pipeline->addShaderFromSourceCode(QOpenGLShader::Fragment, + Node::ReadFileAsString(":/shaders/rgbhistogram.frag")); + pipeline->link(); + + return pipeline; +} + +OpenGLShaderPtr HistogramScope::CreateSecondaryShader() +{ + OpenGLShaderPtr shader = OpenGLShader::Create(); + + shader->create(); + shader->addShaderFromSourceCode(QOpenGLShader::Vertex, + Node::ReadFileAsString(":/shaders/rgbhistogram.vert")); + shader->addShaderFromSourceCode(QOpenGLShader::Fragment, + Node::ReadFileAsString(":/shaders/rgbhistogram_secondary.frag")); + shader->link(); + + return shader; +} + +void HistogramScope::DrawScope() +{ + float histogram_scale = 0.80f; + // This value is eyeballed for usefulness. Until we have a geometry + // shader approach, it is impossible to normalize against a peak + // sum of image values. + float histogram_base = 2.5f; + float histogram_power = 1.0f / histogram_base; + + pipeline()->bind(); + pipeline()->setUniformValue("ove_resolution", managed_tex().width(), + managed_tex().height()); + pipeline()->setUniformValue("ove_viewport", width(), height()); + pipeline()->setUniformValue("histogram_scale", histogram_scale); + pipeline()->release(); + + AssertAdditionalTextures(); + + framebuffer().Attach(&texture_row_sums_, true); + framebuffer().Bind(); + + managed_tex().Bind(); + + OpenGLRenderFunctions::Blit(pipeline()); + + managed_tex().Release(); + + framebuffer().Release(); + framebuffer().Detach(); + + pipeline_secondary_->bind(); + pipeline_secondary_->setUniformValue("ove_resolution", + texture_row_sums_.width(), texture_row_sums_.height()); + pipeline_secondary_->setUniformValue("ove_viewport", width(), height()); + pipeline_secondary_->setUniformValue("histogram_scale", histogram_scale); + pipeline_secondary_->setUniformValue("histogram_power", histogram_power); + pipeline_secondary_->release(); + + texture_row_sums_.Bind(); + + OpenGLRenderFunctions::Blit(pipeline_secondary_); + + texture_row_sums_.Release(); + + // Draw line overlays + QPainter p(this); + QFont font = p.font(); + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); + QString label; + std::vector histogram_increments = { + 0.00, + 0.25, + 0.50, + 1.0 + }; + + int histogram_steps = histogram_increments.size(); + QVector histogram_lines(histogram_steps + 1); + int font_x_offset = 0; + int font_y_offset = font_metrics.capHeight() / 2.0f; + + p.setCompositionMode(QPainter::CompositionMode_Plus); + + p.setPen(QColor(0.0, 0.6 * 255.0, 0.0)); + p.setFont(font); + + float histogram_dim_x = ceil((width() - 1.0) * histogram_scale); + float histogram_dim_y = ceil((height() - 1.0) * histogram_scale); + float histogram_start_dim_x = + ((width() - 1.0) - histogram_dim_x) / 2.0f; + float histogram_start_dim_y = + ((height() - 1.0) - histogram_dim_y) / 2.0f; + float histogram_end_dim_x = (width() - 1.0) - histogram_start_dim_x; + + // for (int i=0; i <= histogram_steps; i++) { + for(std::vector::iterator it = histogram_increments.begin(); + it != histogram_increments.end(); it++) { + histogram_lines[it - histogram_increments.begin()].setLine( + histogram_start_dim_x, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y, + histogram_end_dim_x, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y); + label = QString::number( + *it * 100, 'f', 1) + "%"; + font_x_offset = QFontMetricsWidth(font_metrics, label) + 4; + + p.drawText( + histogram_start_dim_x - font_x_offset, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y + font_y_offset, label); + } + p.drawLines(histogram_lines); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/scope/histogram/histogram.h b/app/widget/scope/histogram/histogram.h index 625074a89..70751355f 100644 --- a/app/widget/scope/histogram/histogram.h +++ b/app/widget/scope/histogram/histogram.h @@ -31,11 +31,24 @@ class HistogramScope : public ScopeBase public: HistogramScope(QWidget* parent = nullptr); + virtual ~HistogramScope() override; + protected: - //virtual OpenGLShaderPtr CreateShader() override; + virtual void initializeGL() override; - //virtual void DrawScope() override; + virtual OpenGLShaderPtr CreateShader() override; + OpenGLShaderPtr CreateSecondaryShader(); + void AssertAdditionalTextures(); + + virtual void DrawScope() override; + +private: + OpenGLShaderPtr pipeline_secondary_; + OpenGLTexture texture_row_sums_; + +private slots: + void CleanUp(); }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/scope/scopebase/scopebase.cpp b/app/widget/scope/scopebase/scopebase.cpp index c58b9e8bd..e3b65d48c 100644 --- a/app/widget/scope/scopebase/scopebase.cpp +++ b/app/widget/scope/scopebase/scopebase.cpp @@ -68,16 +68,6 @@ void ScopeBase::DrawScope() managed_tex().Release(); } -OpenGLShaderPtr ScopeBase::pipeline() -{ - return pipeline_; -} - -OpenGLTexture &ScopeBase::managed_tex() -{ - return managed_tex_; -} - void ScopeBase::UploadTextureFromBuffer() { if (!isVisible()) { diff --git a/app/widget/scope/scopebase/scopebase.h b/app/widget/scope/scopebase/scopebase.h index 3098af212..41e2edefc 100644 --- a/app/widget/scope/scopebase/scopebase.h +++ b/app/widget/scope/scopebase/scopebase.h @@ -51,9 +51,20 @@ protected: virtual void DrawScope(); - OpenGLShaderPtr pipeline(); + OpenGLShaderPtr pipeline() + { + return pipeline_; + } - OpenGLTexture& managed_tex(); + OpenGLTexture& managed_tex() + { + return managed_tex_; + } + + OpenGLFramebuffer& framebuffer() + { + return framebuffer_; + } private: void UploadTextureFromBuffer(); diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index daefe84e7..ff4f1296b 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -41,8 +41,10 @@ OpenGLShaderPtr WaveformScope::CreateShader() OpenGLShaderPtr pipeline = OpenGLShader::Create(); pipeline->create(); - pipeline->addShaderFromSourceCode(QOpenGLShader::Vertex, OpenGLShader::CodeDefaultVertex()); - pipeline->addShaderFromSourceCode(QOpenGLShader::Fragment, Node::ReadFileAsString(":/shaders/rgbwaveform.frag")); + pipeline->addShaderFromSourceCode(QOpenGLShader::Vertex, + Node::ReadFileAsString(":/shaders/rgbwaveform.vert")); + pipeline->addShaderFromSourceCode(QOpenGLShader::Fragment, + Node::ReadFileAsString(":/shaders/rgbwaveform.frag")); pipeline->link(); return pipeline; @@ -51,12 +53,6 @@ OpenGLShaderPtr WaveformScope::CreateShader() void WaveformScope::DrawScope() { float waveform_scale = 0.80f; - float waveform_dim_x = width() * waveform_scale; - float waveform_dim_y = height() * waveform_scale; - float waveform_start_dim_x = (width() - waveform_dim_x) / 2.0f; - float waveform_start_dim_y = (height() - waveform_dim_y) / 2.0f; - float waveform_end_dim_x = width() - waveform_start_dim_x; - float waveform_end_dim_y = height() - waveform_start_dim_y; // Draw waveform through shader pipeline()->bind(); @@ -68,22 +64,6 @@ void WaveformScope::DrawScope() // Scale of the waveform relative to the viewport surface. pipeline()->setUniformValue("waveform_scale", waveform_scale); - pipeline()->setUniformValue( - "waveform_dims", waveform_dim_x, waveform_dim_y); - - pipeline()->setUniformValue( - "waveform_region", - waveform_start_dim_x, waveform_start_dim_y, - waveform_end_dim_x, waveform_end_dim_y); - - float waveform_start_uv_x = waveform_start_dim_x / width(); - float waveform_start_uv_y = waveform_start_dim_y / height(); - float waveform_end_uv_x = waveform_end_dim_x / width(); - float waveform_end_uv_y = waveform_end_dim_y / height(); - pipeline()->setUniformValue( - "waveform_uv", - waveform_start_uv_x, waveform_start_uv_y, - waveform_end_uv_x, waveform_end_uv_y); pipeline()->release(); @@ -93,9 +73,19 @@ void WaveformScope::DrawScope() managed_tex().Release(); + float waveform_dim_x = ceil((width() - 1.0) * waveform_scale); + float waveform_dim_y = ceil((height() - 1.0) * waveform_scale); + float waveform_start_dim_x = + ((width() - 1.0) - waveform_dim_x) / 2.0f; + float waveform_start_dim_y = + ((height() - 1.0) - waveform_dim_y) / 2.0f; + float waveform_end_dim_x = (width() - 1.0) - waveform_start_dim_x; + // Draw line overlays QPainter p(this); - QFontMetrics font_metrics = QFontMetrics(QFont()); + QFont font; + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); QString label; float ire_increment = 0.1f; int ire_steps = qRound(1.0 / ire_increment); @@ -106,7 +96,7 @@ void WaveformScope::DrawScope() p.setCompositionMode(QPainter::CompositionMode_Plus); p.setPen(QColor(0.0, 0.6 * 255.0, 0.0)); - p.setFont(QFont()); + p.setFont(font); for (int i=0; i <= ire_steps; i++) { ire_lines[i].setLine( diff --git a/app/widget/slider/sliderbase.cpp b/app/widget/slider/sliderbase.cpp index 875d5f4e2..727b2aa01 100644 --- a/app/widget/slider/sliderbase.cpp +++ b/app/widget/slider/sliderbase.cpp @@ -40,8 +40,8 @@ SliderBase::SliderBase(Mode mode, QWidget *parent) : require_valid_input_(true), tristate_(false), drag_ladder_(nullptr), - enable_ladder_(false), - ladder_element_count_(2) + ladder_element_count_(0), + dragged_(false) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); @@ -52,9 +52,8 @@ SliderBase::SliderBase(Mode mode, QWidget *parent) : editor_ = new FocusableLineEdit(this); addWidget(editor_); - connect(label_, &SliderLabel::LabelMoved, this, &SliderBase::LabelDragged); - connect(label_, &SliderLabel::LabelReleased, this, &SliderBase::LabelClicked); - connect(label_, &SliderLabel::focused, this, &SliderBase::LabelClicked); + connect(label_, &SliderLabel::LabelPressed, this, &SliderBase::LabelPressed); + connect(label_, &SliderLabel::focused, this, &SliderBase::ShowEditor); connect(label_, &SliderLabel::RequestReset, this, &SliderBase::ResetValue); connect(editor_, &FocusableLineEdit::Confirmed, this, &SliderBase::LineEditConfirmed); connect(editor_, &FocusableLineEdit::Cancelled, this, &SliderBase::LineEditCancelled); @@ -201,6 +200,22 @@ QString SliderBase::GetFormat() const } } +void SliderBase::RepositionLadder() +{ + QPoint label_global_pos = label_->mapToGlobal(label_->pos()); + int text_width = QFontMetricsWidth(label_->fontMetrics(), label_->text()); + QPoint ladder_pos(label_global_pos.x(), + label_global_pos.y() + label_->height() / 2 - drag_ladder_->height() / 2); + + if (ladder_element_count_ > 0) { + ladder_pos.setX(ladder_pos.x() + text_width + QFontMetricsWidth(label_->fontMetrics(), QStringLiteral("H"))); + } else { + ladder_pos.setX(ladder_pos.x() + text_width / 2 - drag_ladder_->width() / 2); + } + + drag_ladder_->move(ladder_pos); +} + void SliderBase::UpdateLabel(const QVariant &v) { if (tristate_) { @@ -226,44 +241,21 @@ QVariant SliderBase::StringToValue(const QString &s, bool *ok) return s; } -void SliderBase::LabelClicked() +void SliderBase::ShowEditor() { - if (!drag_ladder_) { - // This was a simple click - // Load label's text into editor - editor_->setText(ValueToString(value_)); + // This was a simple click + // Load label's text into editor + editor_->setText(ValueToString(value_)); - // Show editor - setCurrentWidget(editor_); + // Show editor + setCurrentWidget(editor_); - // Select all text in the editor - editor_->setFocus(); - editor_->selectAll(); - } + // Select all text in the editor + editor_->setFocus(); + editor_->selectAll(); } -void SliderBase::LabelDragged() -{ - switch (mode_) { - case kString: - // No dragging supported for strings - break; - case kInteger: - case kFloat: - drag_ladder_ = new SliderLadder(value_.toDouble(), drag_multiplier_, enable_ladder_ ? ladder_element_count_ : 0); - drag_ladder_->show(); - - QPoint label_global_pos = label_->mapToGlobal(label_->pos()); - drag_ladder_->move(label_global_pos.x() + QFontMetricsWidth(label_->fontMetrics(), label_->text()) / 2 - drag_ladder_->width() / 2, - label_global_pos.y() + label_->height() / 2 - drag_ladder_->height() / 2); - - connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &SliderBase::LadderDragged); - connect(drag_ladder_, &SliderLadder::Released, this, &SliderBase::LadderReleased); - break; - } -} - -void SliderBase::LadderDragged(int value, double multiplier) +void SliderBase::LabelPressed() { switch (mode_) { case kString: @@ -272,7 +264,31 @@ void SliderBase::LadderDragged(int value, double multiplier) case kInteger: case kFloat: { - dragged_diff_ += static_cast(value) * drag_multiplier_ * multiplier; + drag_ladder_ = new SliderLadder(drag_multiplier_, ladder_element_count_); + drag_ladder_->SetValue(ValueToString(value_)); + drag_ladder_->show(); + + RepositionLadder(); + + connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &SliderBase::LadderDragged); + connect(drag_ladder_, &SliderLadder::Released, this, &SliderBase::LadderReleased); + break; + } + } +} + +void SliderBase::LadderDragged(int value, double multiplier) +{ + dragged_ = true; + + switch (mode_) { + case kString: + // No dragging supported for strings + break; + case kInteger: + case kFloat: + { + dragged_diff_ += value * drag_multiplier_ * multiplier; double drag_val = AdjustDragDistanceInternal(value_.toDouble(), dragged_diff_); @@ -291,7 +307,10 @@ void SliderBase::LadderDragged(int value, double multiplier) } UpdateLabel(temp_dragged_value_); - drag_ladder_->SetValue(temp_dragged_value_.toDouble()); + + drag_ladder_->SetValue(ValueToString(temp_dragged_value_)); + RepositionLadder(); + emit ValueChanged(temp_dragged_value_); break; } @@ -304,20 +323,26 @@ void SliderBase::LadderReleased() drag_ladder_ = nullptr; dragged_diff_ = 0; - // This was a drag - switch (mode_) { - case kString: - // No-op - break; - case kInteger: - SetValue(temp_dragged_value_.toInt()); - break; - case kFloat: - SetValue(temp_dragged_value_.toDouble()); - break; - } + if (dragged_) { + // This was a drag + switch (mode_) { + case kString: + // No-op + break; + case kInteger: + SetValue(temp_dragged_value_.toInt()); + break; + case kFloat: + SetValue(temp_dragged_value_.toDouble()); + break; + } - emit ValueChanged(value_); + emit ValueChanged(value_); + + dragged_ = false; + } else { + ShowEditor(); + } } void SliderBase::LineEditConfirmed() diff --git a/app/widget/slider/sliderbase.h b/app/widget/slider/sliderbase.h index fa956db22..7e8062694 100644 --- a/app/widget/slider/sliderbase.h +++ b/app/widget/slider/sliderbase.h @@ -56,11 +56,6 @@ public: void SetFormat(const QString& s); void ClearFormat(); - void SetLadderEnabled(bool e) - { - enable_ladder_ = e; - } - void SetLadderElementCount(int b) { ladder_element_count_ = b; @@ -97,6 +92,8 @@ private: QString GetFormat() const; + void RepositionLadder(); + SliderLabel* label_; FocusableLineEdit* editor_; @@ -124,14 +121,14 @@ private: SliderLadder* drag_ladder_; - bool enable_ladder_; - int ladder_element_count_; -private slots: - void LabelClicked(); + bool dragged_; - void LabelDragged(); +private slots: + void ShowEditor(); + + void LabelPressed(); void LadderDragged(int value, double multiplier); diff --git a/app/widget/slider/sliderlabel.cpp b/app/widget/slider/sliderlabel.cpp index ab80988da..d100379a5 100644 --- a/app/widget/slider/sliderlabel.cpp +++ b/app/widget/slider/sliderlabel.cpp @@ -24,15 +24,10 @@ #include #include -#ifdef Q_OS_MAC -#include -#endif - OLIVE_NAMESPACE_ENTER SliderLabel::SliderLabel(QWidget *parent) : - QLabel(parent), - dragging_(false) + QLabel(parent) { QPalette p = palette(); @@ -43,7 +38,7 @@ SliderLabel::SliderLabel(QWidget *parent) : setPalette(p); // Use highlight color as font color - setForegroundRole(QPalette::Highlight); + setForegroundRole(QPalette::Link); // Set underlined QFont f = font(); @@ -59,26 +54,10 @@ void SliderLabel::mousePressEvent(QMouseEvent *e) if (e->modifiers() & Qt::AltModifier) { emit RequestReset(); } else { - dragging_ = true; emit LabelPressed(); } } -void SliderLabel::mouseMoveEvent(QMouseEvent *) -{ - if (dragging_) { - emit LabelMoved(); - } -} - -void SliderLabel::mouseReleaseEvent(QMouseEvent *) -{ - if (dragging_) { - emit LabelReleased(); - dragging_ = false; - } -} - void SliderLabel::focusInEvent(QFocusEvent *event) { QWidget::focusInEvent(event); diff --git a/app/widget/slider/sliderlabel.h b/app/widget/slider/sliderlabel.h index 081c5eafa..68d0666e3 100644 --- a/app/widget/slider/sliderlabel.h +++ b/app/widget/slider/sliderlabel.h @@ -36,26 +36,15 @@ public: protected: virtual void mousePressEvent(QMouseEvent *ev) override; - virtual void mouseMoveEvent(QMouseEvent *ev) override; - - virtual void mouseReleaseEvent(QMouseEvent *ev) override; - virtual void focusInEvent(QFocusEvent *event) override; signals: void LabelPressed(); - void LabelMoved(); - - void LabelReleased(); - void focused(); void RequestReset(); -private: - bool dragging_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/slider/sliderladder.cpp b/app/widget/slider/sliderladder.cpp index 41bc4d031..5a9c4d235 100644 --- a/app/widget/slider/sliderladder.cpp +++ b/app/widget/slider/sliderladder.cpp @@ -26,16 +26,18 @@ #include #include +#ifdef Q_OS_MAC +#include +#endif + #include "common/clamp.h" #include "common/lerp.h" OLIVE_NAMESPACE_ENTER -SliderLadder::SliderLadder(double start_val, double drag_multiplier, int nb_outer_values, QWidget* parent) : +SliderLadder::SliderLadder(double drag_multiplier, int nb_outer_values, QWidget* parent) : QFrame(parent, Qt::Popup), - start_val_(start_val), - active_element_(nullptr), - relative_y_(-1) + y_mobility_(0) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setMargin(0); @@ -48,14 +50,17 @@ SliderLadder::SliderLadder(double start_val, double drag_multiplier, int nb_oute elements_.append(new SliderLadderElement(qPow(10, i + 1) * drag_multiplier)); } - elements_.append(new SliderLadderElement(drag_multiplier)); + // Create center entry + SliderLadderElement* start_element = new SliderLadderElement(drag_multiplier); + active_element_ = elements_.size(); + start_element->SetHighlighted(true); + elements_.append(start_element); for (int i=0;iSetValue(start_val_); layout->addWidget(e); } @@ -87,10 +92,10 @@ SliderLadder::~SliderLadder() #endif } -void SliderLadder::SetValue(double val) +void SliderLadder::SetValue(const QString &s) { foreach (SliderLadderElement* e, elements_) { - e->SetValue(val); + e->SetValue(s); } } @@ -98,6 +103,8 @@ void SliderLadder::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) + drag_timer_.stop(); + emit Released(); } @@ -105,38 +112,7 @@ void SliderLadder::showEvent(QShowEvent *event) { QWidget::showEvent(event); - QMetaObject::invokeMethod(this, "InitRelativeY", Qt::QueuedConnection); - QMetaObject::invokeMethod(&drag_timer_, "start", Qt::QueuedConnection); -} - -void SliderLadder::SetActiveElement() -{ - if (!active_element_ - || relative_y_ < active_element_->y() - || relative_y_ >= active_element_->y() + active_element_->height()) { - if (active_element_) { - // Un-highlight active element if one is set - active_element_->SetHighlighted(false); - } - - // Find new active element - foreach (SliderLadderElement* ele, elements_) { - if (relative_y_ >= ele->y() && relative_y_ < ele->y() + ele->height()) { - // This is the element! - active_element_ = ele; - active_element_->SetHighlighted(true); - relative_y_ = active_element_->y() + active_element_->height() / 2; - break; - } - } - } -} - -void SliderLadder::InitRelativeY() -{ - relative_y_ = QCursor::pos().y() - this->y(); - - SetActiveElement(); + drag_timer_.start(); } void SliderLadder::TimerUpdate() @@ -155,22 +131,40 @@ void SliderLadder::TimerUpdate() QCursor::setPos(drag_start_); #endif - int target = active_element_->y() + active_element_->height() / 2; - relative_y_ = lerp(relative_y_, static_cast(target), 0.1f); - if (!x_mvmt && !y_mvmt) { return; } - // Determine which element we're in - relative_y_ = clamp(relative_y_ + y_mvmt, - static_cast(elements_.first()->y()), - static_cast(elements_.last()->y() + elements_.last()->height() - 1)); + if (qApp->keyboardModifiers() & Qt::ControlModifier) { + // Movement is vertical + y_mobility_ += y_mvmt; - SetActiveElement(); + if (qAbs(y_mobility_) > fontMetrics().height()) { + int new_active_element; - if (qAbs(x_mvmt) > qAbs(y_mvmt)) { - emit DraggedByValue(x_mvmt, active_element_->GetMultiplier()); + if (y_mvmt < 0) { + // Movement is UP + new_active_element = active_element_ - 1; + } else { + // Movement is DOWN + new_active_element = active_element_ + 1; + } + + // Check if the proposed element is valid + if (new_active_element >= 0 && new_active_element < elements_.size()) { + elements_.at(active_element_)->SetHighlighted(false); + + active_element_ = new_active_element; + + elements_.at(active_element_)->SetHighlighted(true); + } + + y_mobility_ = 0; + } + } else { + y_mobility_ = 0; + + emit DraggedByValue(x_mvmt + y_mvmt, elements_.at(active_element_)->GetMultiplier()); } } @@ -210,7 +204,7 @@ void SliderLadderElement::SetHighlighted(bool e) UpdateLabel(); } -void SliderLadderElement::SetValue(double value) +void SliderLadderElement::SetValue(const QString &value) { value_ = value; @@ -230,13 +224,13 @@ void SliderLadderElement::UpdateLabel() QString val_text; if (highlighted_) { - val_text = QString::number(value_); + val_text = value_; } label_->setText(QStringLiteral("%1\n%2").arg(QString::number(multiplier_), val_text)); } else { - label_->setText(QString::number(value_)); + label_->setText(value_); } } diff --git a/app/widget/slider/sliderladder.h b/app/widget/slider/sliderladder.h index d404b1fb0..ce0ae6193 100644 --- a/app/widget/slider/sliderladder.h +++ b/app/widget/slider/sliderladder.h @@ -37,7 +37,7 @@ public: void SetHighlighted(bool e); - void SetValue(double value); + void SetValue(const QString& value); void SetMultiplierVisible(bool e); @@ -52,7 +52,7 @@ private: QLabel* label_; double multiplier_; - double value_; + QString value_; bool highlighted_; @@ -64,11 +64,11 @@ class SliderLadder : public QFrame { Q_OBJECT public: - SliderLadder(double start_val, double drag_multiplier, int nb_outer_values, QWidget* parent = nullptr); + SliderLadder(double drag_multiplier, int nb_outer_values, QWidget* parent = nullptr); virtual ~SliderLadder() override; - void SetValue(double val); + void SetValue(const QString& s); protected: virtual void mouseReleaseEvent(QMouseEvent *event) override; @@ -81,23 +81,17 @@ signals: void Released(); private: - void SetActiveElement(); - QPoint drag_start_; - double start_val_; - QList elements_; - SliderLadderElement* active_element_; - - float relative_y_; + int active_element_; QTimer drag_timer_; -private slots: - void InitRelativeY(); + int y_mobility_; +private slots: void TimerUpdate(); }; diff --git a/app/widget/standardcombos/CMakeLists.txt b/app/widget/standardcombos/CMakeLists.txt new file mode 100644 index 000000000..cbfac1d51 --- /dev/null +++ b/app/widget/standardcombos/CMakeLists.txt @@ -0,0 +1,28 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/standardcombos/channellayoutcombobox.h + widget/standardcombos/frameratecombobox.h + widget/standardcombos/interlacedcombobox.h + widget/standardcombos/pixelaspectratiocombobox.h + widget/standardcombos/pixelformatcombobox.h + widget/standardcombos/sampleratecombobox.h + widget/standardcombos/standardcombos.h + widget/standardcombos/videodividercombobox.h + PARENT_SCOPE +) diff --git a/app/widget/standardcombos/channellayoutcombobox.h b/app/widget/standardcombos/channellayoutcombobox.h new file mode 100644 index 000000000..7deec22e6 --- /dev/null +++ b/app/widget/standardcombos/channellayoutcombobox.h @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef CHANNELLAYOUTCOMBOBOX_H +#define CHANNELLAYOUTCOMBOBOX_H + +#include + +#include "render/audioparams.h" + +OLIVE_NAMESPACE_ENTER + +class ChannelLayoutComboBox : public QComboBox +{ + Q_OBJECT +public: + ChannelLayoutComboBox(QWidget* parent = nullptr) : + QComboBox(parent) + { + foreach (const uint64_t& ch_layout, AudioParams::kSupportedChannelLayouts) { + this->addItem(AudioParams::ChannelLayoutToString(ch_layout), + QVariant::fromValue(ch_layout)); + } + } + + uint64_t GetChannelLayout() const + { + return this->currentData().toULongLong(); + } + + void SetChannelLayout(uint64_t ch) + { + for (int i=0; icount(); i++) { + if (this->itemData(i).toULongLong() == ch) { + this->setCurrentIndex(i); + break; + } + } + } + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // CHANNELLAYOUTCOMBOBOX_H diff --git a/app/widget/standardcombos/frameratecombobox.h b/app/widget/standardcombos/frameratecombobox.h new file mode 100644 index 000000000..00eefe7aa --- /dev/null +++ b/app/widget/standardcombos/frameratecombobox.h @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef FRAMERATECOMBOBOX_H +#define FRAMERATECOMBOBOX_H + +#include + +#include "common/rational.h" +#include "render/videoparams.h" + +OLIVE_NAMESPACE_ENTER + +class FrameRateComboBox : public QComboBox +{ + Q_OBJECT +public: + FrameRateComboBox(QWidget* parent = nullptr) : + QComboBox(parent) + { + foreach (const rational& fr, VideoParams::kSupportedFrameRates) { + this->addItem(VideoParams::FrameRateToString(fr), QVariant::fromValue(fr)); + } + } + + rational GetFrameRate() const + { + return this->currentData().value(); + } + + void SetFrameRate(const rational& r) + { + for (int i=0; icount(); i++) { + if (this->itemData(i).value() == r) { + this->setCurrentIndex(i); + break; + } + } + } + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // FRAMERATECOMBOBOX_H diff --git a/app/widget/standardcombos/interlacedcombobox.h b/app/widget/standardcombos/interlacedcombobox.h new file mode 100644 index 000000000..1bf54806b --- /dev/null +++ b/app/widget/standardcombos/interlacedcombobox.h @@ -0,0 +1,57 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef INTERLACEDCOMBOBOX_H +#define INTERLACEDCOMBOBOX_H + +#include + +#include "render/videoparams.h" + +OLIVE_NAMESPACE_ENTER + +class InterlacedComboBox : public QComboBox +{ + Q_OBJECT +public: + InterlacedComboBox(QWidget* parent = nullptr) : + QComboBox(parent) + { + // These must match the Interlacing enum in VideoParams + this->addItem(tr("None (Progressive)")); + this->addItem(tr("Top-Field First")); + this->addItem(tr("Bottom-Field First")); + } + + VideoParams::Interlacing GetInterlaceMode() const + { + return static_cast(this->currentIndex()); + } + + void SetInterlaceMode(VideoParams::Interlacing mode) + { + this->setCurrentIndex(mode); + } + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // INTERLACEDCOMBOBOX_H diff --git a/app/widget/standardcombos/pixelaspectratiocombobox.h b/app/widget/standardcombos/pixelaspectratiocombobox.h new file mode 100644 index 000000000..a5e2d02b9 --- /dev/null +++ b/app/widget/standardcombos/pixelaspectratiocombobox.h @@ -0,0 +1,127 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef PIXELASPECTRATIOCOMBOBOX_H +#define PIXELASPECTRATIOCOMBOBOX_H + +#include + +#include "common/ratiodialog.h" +#include "render/videoparams.h" + +OLIVE_NAMESPACE_ENTER + +class PixelAspectRatioComboBox : public QComboBox +{ + Q_OBJECT +public: + PixelAspectRatioComboBox(QWidget* parent = nullptr) : + QComboBox(parent), + dont_prompt_custom_par_(false) + { + QStringList par_names = VideoParams::GetStandardPixelAspectRatioNames(); + for (int i=0; iaddItem(par_names.at(i), + QVariant::fromValue(ratio)); + } + + // Always add custom item last, much of the logic relies on this. Set this to the current AR so + // that if none of the above are ==, it will eventually select this item + this->addItem(QString()); + UpdateCustomItem(rational()); + + // Pick up index signal to query for custom aspect ratio if requested + connect(this, static_cast(&QComboBox::currentIndexChanged), + this, &PixelAspectRatioComboBox::IndexChanged); + } + + rational GetPixelAspectRatio() const + { + return this->currentData().value(); + } + + void SetPixelAspectRatio(const rational& r) + { + // Determine which index to select on startup + for (int i=0; icount(); i++) { + if (this->itemData(i).value() == r) { + this->setCurrentIndex(i); + return; + } + } + + // Must not have found the ratio, so it must be custom + UpdateCustomItem(r); + dont_prompt_custom_par_ = true; + this->setCurrentIndex(this->count() - 1); + dont_prompt_custom_par_ = false; + } + +private slots: + void IndexChanged(int index) + { + if (dont_prompt_custom_par_) { + return; + } + + // Detect if custom was selected, in which case query what the new AR should be + if (index == this->count() - 1) { + // Query for custom pixel aspect ratio + bool ok; + + double custom_ratio = GetFloatRatioFromUser(this, + tr("Set Custom Pixel Aspect Ratio"), + &ok); + + if (ok) { + UpdateCustomItem(rational::fromDouble(custom_ratio)); + } + } + } + +private: + void UpdateCustomItem(const rational &ratio) + { + const int custom_index = this->count() - 1; + + if (ratio.isNull()) { + this->setItemText(custom_index, + tr("Custom...")); + + // Use 1:1 to prevent any real chance of the PAR being set to 0 + this->setItemData(custom_index, + QVariant::fromValue(rational(1))); + } else { + this->setItemText(custom_index, + VideoParams::FormatPixelAspectRatioString(tr("Custom (%1)"), ratio)); + this->setItemData(custom_index, + QVariant::fromValue(ratio)); + } + } + + bool dont_prompt_custom_par_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // PIXELASPECTRATIOCOMBOBOX_H diff --git a/app/widget/standardcombos/pixelformatcombobox.h b/app/widget/standardcombos/pixelformatcombobox.h new file mode 100644 index 000000000..48638a14b --- /dev/null +++ b/app/widget/standardcombos/pixelformatcombobox.h @@ -0,0 +1,67 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef PIXELFORMATCOMBOBOX_H +#define PIXELFORMATCOMBOBOX_H + +#include + +#include "render/pixelformat.h" + +OLIVE_NAMESPACE_ENTER + +class PixelFormatComboBox : public QComboBox +{ + Q_OBJECT +public: + PixelFormatComboBox(bool alpha_only, bool float_only, QWidget* parent = nullptr) : + QComboBox(parent) + { + // Set up preview formats + for (int i=0;i(i); + + if ((!alpha_only || PixelFormat::FormatHasAlphaChannel(pix_fmt)) + && (!float_only || PixelFormat::FormatIsFloat(pix_fmt))) { + this->addItem(PixelFormat::GetName(pix_fmt), pix_fmt); + } + } + } + + PixelFormat::Format GetPixelFormat() const + { + return static_cast(this->currentData().toInt()); + } + + void SetPixelFormat(PixelFormat::Format fmt) + { + for (int i=0; icount(); i++) { + if (this->itemData(i).toInt() == fmt) { + this->setCurrentIndex(i); + break; + } + } + } + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // PIXELFORMATCOMBOBOX_H diff --git a/app/widget/standardcombos/sampleratecombobox.h b/app/widget/standardcombos/sampleratecombobox.h new file mode 100644 index 000000000..e7300517f --- /dev/null +++ b/app/widget/standardcombos/sampleratecombobox.h @@ -0,0 +1,61 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef SAMPLERATECOMBOBOX_H +#define SAMPLERATECOMBOBOX_H + +#include + +#include "render/audioparams.h" + +OLIVE_NAMESPACE_ENTER + +class SampleRateComboBox : public QComboBox +{ + Q_OBJECT +public: + SampleRateComboBox(QWidget* parent = nullptr) : + QComboBox(parent) + { + foreach (int sr, AudioParams::kSupportedSampleRates) { + this->addItem(AudioParams::SampleRateToString(sr), sr); + } + } + + int GetSampleRate() const + { + return this->currentData().toInt(); + } + + void SetSampleRate(int rate) + { + for (int i=0; icount(); i++) { + if (this->itemData(i).toInt() == rate) { + this->setCurrentIndex(i); + break; + } + } + } + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // SAMPLERATECOMBOBOX_H diff --git a/app/widget/standardcombos/standardcombos.h b/app/widget/standardcombos/standardcombos.h new file mode 100644 index 000000000..e59aedab8 --- /dev/null +++ b/app/widget/standardcombos/standardcombos.h @@ -0,0 +1,32 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef STANDARDCOMBOS_H +#define STANDARDCOMBOS_H + +#include "channellayoutcombobox.h" +#include "frameratecombobox.h" +#include "interlacedcombobox.h" +#include "pixelaspectratiocombobox.h" +#include "pixelformatcombobox.h" +#include "sampleratecombobox.h" +#include "videodividercombobox.h" + +#endif // STANDARDCOMBOS_H diff --git a/app/widget/standardcombos/videodividercombobox.h b/app/widget/standardcombos/videodividercombobox.h new file mode 100644 index 000000000..89b6c1b6a --- /dev/null +++ b/app/widget/standardcombos/videodividercombobox.h @@ -0,0 +1,69 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef VIDEODIVIDERCOMBOBOX_H +#define VIDEODIVIDERCOMBOBOX_H + +#include + +#include "render/videoparams.h" + +OLIVE_NAMESPACE_ENTER + +class VideoDividerComboBox : public QComboBox +{ + Q_OBJECT +public: + VideoDividerComboBox(QWidget* parent = nullptr) : + QComboBox(parent) + { + foreach (int d, VideoParams::kSupportedDividers) { + QString name; + + if (d == 1) { + name = tr("Full"); + } else { + name = tr("1/%1").arg(d); + } + + this->addItem(name, d); + } + } + + int GetDivider() const + { + return this->currentData().toInt(); + } + + void SetDivider(int d) + { + for (int i=0; icount(); i++) { + if (this->itemData(i).toInt() == d) { + this->setCurrentIndex(i); + break; + } + } + } + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // VIDEODIVIDERCOMBOBOX_H diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index 6c83257d2..5c83206c6 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -36,7 +36,8 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_statu viewer_node_(nullptr), auto_max_scrollbar_(false), points_(nullptr), - toggle_show_all_(false) + toggle_show_all_(false), + auto_set_timebase_(true) { ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this); connect(ruler_, &TimeRuler::TimeChanged, this, &TimeBasedWidget::SetTimeAndSignal); @@ -79,6 +80,11 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) DisconnectNodeInternal(viewer_node_); disconnect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + disconnect(viewer_node_, &ViewerOutput::TimebaseChanged, this, &TimeBasedWidget::SetTimebase); + + if (auto_set_timebase_) { + SetTimebase(rational()); + } points_ = nullptr; ruler()->ConnectTimelinePoints(nullptr); @@ -95,6 +101,18 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) ruler()->ConnectTimelinePoints(points_); } + if (auto_set_timebase_) { + if (!viewer_node_->video_params().time_base().isNull()) { + SetTimebase(viewer_node_->video_params().time_base()); + } else if (viewer_node_->audio_params().sample_rate() > 0) { + SetTimebase(viewer_node_->audio_params().time_base()); + } else { + SetTimebase(rational()); + } + + connect(viewer_node_, &ViewerOutput::TimebaseChanged, this, &TimeBasedWidget::SetTimebase); + } + ConnectNodeInternal(viewer_node_); } @@ -332,6 +350,11 @@ void TimeBasedWidget::CenterScrollOnPlayhead() scrollbar_->setValue(qRound(TimeToScene(Timecode::timestamp_to_time(ruler_->GetTime(), timebase()))) - scrollbar_->width()/2); } +void TimeBasedWidget::SetAutoSetTimebase(bool e) +{ + auto_set_timebase_ = e; +} + void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) { if (!points_) { @@ -459,12 +482,12 @@ void TimeBasedWidget::ToggleShowAll() w = timeline_views_.first()->width(); } - w = w / 10 * 9; + toggle_show_all_old_scale_ = GetScale(); toggle_show_all_old_scroll_ = scrollbar_->value(); - SetScale(w / GetConnectedNode()->GetLength().toDouble()); + SetScaleFromDimensions(w, GetConnectedNode()->GetLength().toDouble()); scrollbar_->setValue(0); // Must explicitly do this because SetScale() will automatically set this to false diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 49012af1c..008514d26 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -52,6 +52,8 @@ public: void SetScaleAndCenterOnPlayhead(const double& scale); + TimeRuler* ruler() const; + public slots: void SetTimestamp(int64_t timestamp); @@ -89,8 +91,6 @@ public slots: void GoToOut(); - TimeRuler* ruler() const; - protected slots: void SetTimeAndSignal(const int64_t& t); @@ -127,6 +127,12 @@ protected slots: */ void CenterScrollOnPlayhead(); + /** + * @brief By default, TimeBasedWidget will set the timebase to the viewer node's video timebase. + * Set this to false if you want to set your own timebase. + */ + void SetAutoSetTimebase(bool e); + signals: void TimeChanged(const int64_t&); @@ -170,6 +176,8 @@ private: double toggle_show_all_old_scale_; int toggle_show_all_old_scroll_; + bool auto_set_timebase_; + private slots: void UpdateMaximumScroll(); diff --git a/app/widget/timelinewidget/timelinescaledobject.cpp b/app/widget/timelinewidget/timelinescaledobject.cpp index 979ecf048..d12a998f8 100644 --- a/app/widget/timelinewidget/timelinescaledobject.cpp +++ b/app/widget/timelinewidget/timelinescaledobject.cpp @@ -27,6 +27,8 @@ OLIVE_NAMESPACE_ENTER +const int TimelineScaledObject::kCalculateDimensionsPadding = 10; + TimelineScaledObject::TimelineScaledObject() : scale_(1.0), min_scale_(0), @@ -112,6 +114,21 @@ void TimelineScaledObject::SetScale(const double& scale) ScaleChangedEvent(scale_); } +void TimelineScaledObject::SetScaleFromDimensions(double viewport_width, double content_width) +{ + SetScale(CalculateScaleFromDimensions(viewport_width, content_width)); +} + +double TimelineScaledObject::CalculateScaleFromDimensions(double viewport_sz, double content_sz) +{ + return static_cast(viewport_sz / kCalculateDimensionsPadding * (kCalculateDimensionsPadding-1)) / static_cast(content_sz); +} + +double TimelineScaledObject::CalculatePaddingFromDimensionScale(double viewport_sz) +{ + return (viewport_sz / (kCalculateDimensionsPadding * 2)); +} + TimelineScaledWidget::TimelineScaledWidget(QWidget *parent) : QWidget(parent) { diff --git a/app/widget/timelinewidget/timelinescaledobject.h b/app/widget/timelinewidget/timelinescaledobject.h index c55cff296..c4aec301c 100644 --- a/app/widget/timelinewidget/timelinescaledobject.h +++ b/app/widget/timelinewidget/timelinescaledobject.h @@ -44,6 +44,10 @@ public: void SetScale(const double& scale); + void SetScaleFromDimensions(double viewport_width, double content_width); + static double CalculateScaleFromDimensions(double viewport_sz, double content_sz); + static double CalculatePaddingFromDimensionScale(double viewport_sz); + protected: double TimeToScene(const rational& time); rational SceneToTime(const double &x, bool round = false); @@ -67,6 +71,8 @@ private: double max_scale_; + static const int kCalculateDimensionsPadding; + }; class TimelineScaledWidget : public QWidget, public TimelineScaledObject diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index c011fa558..07b74ca46 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -63,9 +63,9 @@ TimelineWidget::TimelineWidget(QWidget *parent) : // Create list of TimelineViews - these MUST correspond to the ViewType enum - QSplitter* view_splitter = new QSplitter(Qt::Vertical); - view_splitter->setChildrenCollapsible(false); - vert_layout->addWidget(view_splitter); + view_splitter_ = new QSplitter(Qt::Vertical); + view_splitter_->setChildrenCollapsible(false); + vert_layout->addWidget(view_splitter_); // Video view views_.append(new TimelineAndTrackView(Qt::AlignBottom)); @@ -109,7 +109,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) : view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); view->SetSnapService(this); - view_splitter->addWidget(tview); + view_splitter_->addWidget(tview); ConnectTimelineView(view); @@ -129,7 +129,6 @@ TimelineWidget::TimelineWidget(QWidget *parent) : connect(view, &TimelineView::DragMoved, this, &TimelineWidget::ViewDragMoved); connect(view, &TimelineView::DragLeft, this, &TimelineWidget::ViewDragLeft); connect(view, &TimelineView::DragDropped, this, &TimelineWidget::ViewDragDropped); - ConnectViewSelectionSignal(view); connect(tview->splitter(), &QSplitter::splitterMoved, this, &TimelineWidget::UpdateHorizontalSplitters); @@ -144,11 +143,13 @@ TimelineWidget::TimelineWidget(QWidget *parent) : } // Split viewer 50/50 - view_splitter->setSizes({INT_MAX, INT_MAX}); + view_splitter_->setSizes({INT_MAX, INT_MAX}); // FIXME: Magic number - SetMaximumScale(TimelineViewBase::kMaximumScale); SetScale(90.0); + + SetMaximumScale(TimelineViewBase::kMaximumScale); + SetAutoSetTimebase(false); } TimelineWidget::~TimelineWidget() @@ -161,21 +162,19 @@ TimelineWidget::~TimelineWidget() void TimelineWidget::Clear() { - foreach (TimelineAndTrackView* tview, views_) { - DisconnectViewSelectionSignal(tview->view()); - } + QList deselected_blocks; QMap::const_iterator iterator; for (iterator=block_items_.begin(); iterator!=block_items_.end(); iterator++) { + if (iterator.value()->isSelected()) { + deselected_blocks.append(iterator.key()); + } + delete iterator.value(); } block_items_.clear(); - foreach (TimelineAndTrackView* tview, views_) { - ConnectViewSelectionSignal(tview->view()); - } - - emit SelectionChanged(QList()); + emit BlocksDeselected(deselected_blocks); SetTimebase(0); } @@ -377,24 +376,34 @@ rational TimelineWidget::GetToolTipTimebase() const void TimelineWidget::SelectAll() { - foreach (TimelineAndTrackView* view, views_) { - DisconnectViewSelectionSignal(view->view()); - view->view()->SelectAll(); - ConnectViewSelectionSignal(view->view()); + QList blocks_selected; + + QMap::const_iterator i; + + for (i=block_items_.constBegin(); i!=block_items_.end(); i++) { + if (!i.value()->isSelected()) { + i.value()->setSelected(true); + blocks_selected.append(i.key()); + } } - ViewSelectionChanged(); + emit BlocksSelected(blocks_selected); } void TimelineWidget::DeselectAll() { - foreach (TimelineAndTrackView* view, views_) { - DisconnectViewSelectionSignal(view->view()); - view->view()->DeselectAll(); - ConnectViewSelectionSignal(view->view()); + QList blocks_deselected; + + QMap::const_iterator i; + + for (i=block_items_.constBegin(); i!=block_items_.end(); i++) { + if (i.value()->isSelected()) { + i.value()->setSelected(false); + blocks_deselected.append(i.key()); + } } - emit SelectionChanged(QList()); + emit BlocksDeselected(blocks_deselected); } void TimelineWidget::RippleToIn() @@ -470,67 +479,13 @@ void TimelineWidget::SplitAtPlayhead() } } -void TimelineWidget::DeleteSelectedInternal(const QList &blocks, - bool transition_aware, +void TimelineWidget::ReplaceBlocksWithGaps(const QList &blocks, bool remove_from_graph, QUndoCommand *command) { foreach (Block* b, blocks) { TrackOutput* original_track = TrackOutput::TrackFromBlock(b); - /*if (transition_aware && b->type() == Block::kTransition) { - // Deleting transitions restores their in/out offsets to their attached blocks - TransitionBlock* transition = static_cast(b); - - // Ripple remove transition - new TrackRippleRemoveBlockCommand(original_track, - transition, - command); - - // Resize attached blocks to make up length - if (transition->connected_in_block()) { - new BlockResizeWithMediaInCommand(transition->connected_in_block(), - transition->connected_in_block()->length() + transition->in_offset(), - command); - } - - if (transition->connected_out_block()) { - new BlockResizeCommand(transition->connected_out_block(), - transition->connected_out_block()->length() + transition->out_offset(), - command); - } - } else */ - - - /* - if (b->next()) { - - new TrackRippleRemoveBlockCommand(original_track, b, command); - - if (b->previous() && b->previous()->type() == Block::kGap - && b->next() && b->next()->type() == Block::kGap) { - - // Both previous AND next are blocks. We'll want to merge them together. - new TrackRippleRemoveBlockCommand(original_track, b->next(), command); - - } else { - - // Make new gap and replace old Block with it for now - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(b->length()); - - new NodeAddCommand(static_cast(b->parent()), - gap, - command); - - new TrackReplaceBlockCommand(original_track, - b, - gap, - command); - } - } - */ - new TrackReplaceBlockWithGapCommand(original_track, b, command); if (remove_from_graph) { @@ -564,17 +519,30 @@ void TimelineWidget::DeleteSelected(bool ripple) QUndoCommand* command = new QUndoCommand(); - // Replace blocks with gaps (effectively deleting them) - DeleteSelectedInternal(blocks_to_delete, true, true, command); + QList clips_to_delete; + QList transitions_to_delete; - /* - // Clean each track - foreach (const TrackReference& track, tracks_affected) { - new TrackCleanGapsCommand(GetConnectedNode()->track_list(track.type()), - track.index(), - command); + foreach (Block* b, blocks_to_delete) { + if (b->type() == Block::kClip) { + clips_to_delete.append(b); + } else if (b->type() == Block::kTransition) { + transitions_to_delete.append(static_cast(b)); + } } - */ + + // For transitions, remove them but extend their attached blocks to fill their place + foreach (TransitionBlock* transition, transitions_to_delete) { + new TransitionRemoveCommand(TrackOutput::TrackFromBlock(transition), + transition, + command); + + new NodeRemoveWithExclusiveDeps(static_cast(GetConnectedNode()->parent()), + transition, + command); + } + + // Replace clips with gaps (effectively deleting them) + ReplaceBlocksWithGaps(clips_to_delete, true, command); // Insert ripple command now that it's all cleaned up gaps if (ripple) { @@ -632,12 +600,16 @@ void TimelineWidget::ToggleLinksOnSelected() { QList sel = GetSelectedBlocks(); - // Prioritize unlinking - QList blocks; bool link = true; foreach (TimelineViewBlockItem* item, sel) { + // Only clips can be linked + if (item->block()->type() != Block::kClip) { + continue; + } + + // Prioritize unlinking, if any block has links, assume we're unlinking if (link && item->block()->HasLinks()) { link = false; } @@ -838,16 +810,6 @@ TrackOutput *TimelineWidget::GetTrackFromReference(const TrackReference &ref) return GetConnectedNode()->track_list(ref.type())->GetTrackAt(ref.index()); } -void TimelineWidget::ConnectViewSelectionSignal(TimelineView *view) -{ - connect(view, &TimelineView::SelectionChanged, this, &TimelineWidget::ViewSelectionChanged); -} - -void TimelineWidget::DisconnectViewSelectionSignal(TimelineView *view) -{ - disconnect(view, &TimelineView::SelectionChanged, this, &TimelineWidget::ViewSelectionChanged); -} - int TimelineWidget::GetTrackY(const TrackReference &ref) { return views_.at(ref.type())->view()->GetTrackY(ref.index()); @@ -893,6 +855,12 @@ void TimelineWidget::ViewMousePressed(TimelineViewMouseEvent *event) if (GetConnectedNode() && active_tool_ != nullptr) { active_tool_->MousePress(event); } + + if (event->GetButton() != Qt::LeftButton) { + // Suspend tool immediately if the cursor isn't the primary button + active_tool_->MouseRelease(event); + active_tool_ = nullptr; + } } void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event) @@ -976,7 +944,14 @@ void TimelineWidget::RemoveBlock(Block *block) disconnect(block, &Block::LabelChanged, this, &TimelineWidget::BlockUpdated); disconnect(block, &Block::EnabledChanged, this, &TimelineWidget::BlockUpdated); - delete block_items_.take(block); + TimelineViewBlockItem* item = block_items_.take(block); + + if (item->isSelected()) { + // Sending a list of one item all the time is not very efficient + emit BlocksDeselected({block}); + } + + delete item; } void TimelineWidget::AddTrack(TrackOutput *track, Timeline::TrackType type) @@ -1012,22 +987,6 @@ void TimelineWidget::TrackIndexChanged() } } -void TimelineWidget::ViewSelectionChanged() -{ - if (rubberband_.isVisible()) { - return; - } - - QList selected_items = GetSelectedBlocks(); - QList selected_blocks; - - foreach (TimelineViewBlockItem* item, selected_items) { - selected_blocks.append(item->block()); - } - - emit SelectionChanged(selected_blocks); -} - void TimelineWidget::BlockRefreshed() { TimelineViewRect* rect = block_items_.value(static_cast(sender())); @@ -1241,6 +1200,13 @@ void TimelineWidget::UpdateViewTimebases() } } +void TimelineWidget::SetViewBeamCursor(const TimelineCoordinate &coord) +{ + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->SetBeamCursor(coord); + } +} + void TimelineWidget::SetBlockLinksSelected(Block* block, bool selected) { TimelineViewBlockItem* link_item; @@ -1400,11 +1366,29 @@ void TimelineWidget::HideSnaps() } } +QByteArray TimelineWidget::SaveSplitterState() const +{ + return view_splitter_->saveState(); +} + +void TimelineWidget::RestoreSplitterState(const QByteArray &state) +{ + view_splitter_->restoreState(state); +} + void TimelineWidget::StartRubberBandSelect(bool enable_selecting, bool select_links) { drag_origin_ = QCursor::pos(); rubberband_.show(); + // We don't touch any blocks that are already selected. If you want these to be deselected by + // default, call DeselectAll() befoer calling StartRubberBandSelect() + foreach (TimelineViewBlockItem* block, block_items_) { + if (block->isSelected()) { + rubberband_already_selected_.append(block); + } + } + MoveRubberBandSelect(enable_selecting, select_links); } @@ -1420,10 +1404,11 @@ void TimelineWidget::MoveRubberBandSelect(bool enable_selecting, bool select_lin QList new_selected_list; + // Determine all items in the rubberband foreach (TimelineAndTrackView* tview, views_) { - // Map global mouse coordinates to viewport TimelineView* view = tview->view(); + // Map global mouse coordinates to viewport QRect mapped_rect(view->viewport()->mapFromGlobal(drag_origin_), view->viewport()->mapFromGlobal(rubberband_now)); @@ -1433,13 +1418,25 @@ void TimelineWidget::MoveRubberBandSelect(bool enable_selecting, bool select_lin new_selected_list.append(rubberband_items); } + // Filter out any items that were already selected + if (!rubberband_already_selected_.isEmpty()) { + for (int i=0; isetSelected(false); } - foreach (QGraphicsItem* item, new_selected_list) { - TimelineViewBlockItem* block_item = dynamic_cast(item); - if (!block_item || block_item->block()->type() == Block::kGap) { + // Cache limit because we append to this array in this loop and don't need to process those + int lim = new_selected_list.size(); + for (int i=0;i(new_selected_list.at(i)); + if (block_item->block()->type() == Block::kGap) { continue; } @@ -1448,18 +1445,22 @@ void TimelineWidget::MoveRubberBandSelect(bool enable_selecting, bool select_lin continue; } + // Since new_selected_list is filtered by rubberband_already_selected_, this should certainly + // be deselected by now block_item->setSelected(true); if (select_links) { // Select the block's links Block* b = block_item->block(); - SetBlockLinksSelected(b, true); // Add its links to the list TimelineViewBlockItem* link_item; foreach (Block* link, b->linked_clips()) { if ((link_item = block_items_[link]) != nullptr) { - if (!new_selected_list.contains(link_item)) { + link_item->setSelected(true); + + if (!new_selected_list.contains(link_item) + && !rubberband_already_selected_.contains(link_item)) { new_selected_list.append(link_item); } } @@ -1470,13 +1471,19 @@ void TimelineWidget::MoveRubberBandSelect(bool enable_selecting, bool select_lin rubberband_now_selected_ = new_selected_list; } -void TimelineWidget::EndRubberBandSelect(bool enable_selecting, bool select_links) +void TimelineWidget::EndRubberBandSelect() { - MoveRubberBandSelect(enable_selecting, select_links); rubberband_.hide(); - rubberband_now_selected_.clear(); - ViewSelectionChanged(); + // Emit any blocks that were newly selected + QList selected_blocks; + foreach (QGraphicsItem* item, rubberband_now_selected_) { + selected_blocks.append(static_cast(item)->block()); + } + emit BlocksSelected(selected_blocks); + + rubberband_now_selected_.clear(); + rubberband_already_selected_.clear(); } struct SnapData { @@ -1571,9 +1578,9 @@ bool TimelineWidget::SnapPoint(QList start_times, rational* movement, // Find all points at this movement QList snap_times; - foreach (const SnapData& data, potential_snaps) { - if (data.movement == *movement) { - snap_times.append(data.time); + foreach (const SnapData& d, potential_snaps) { + if (d.movement == *movement) { + snap_times.append(d.time); } } diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index d8a6c0dc5..cab78ee0f 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -26,6 +26,7 @@ #include #include "core.h" +#include "node/block/transition/transition.h" #include "node/output/viewer/viewer.h" #include "snapservice.h" #include "timeline/timelinecommon.h" @@ -98,8 +99,14 @@ public: virtual void HideSnaps() override; + QByteArray SaveSplitterState() const; + + void RestoreSplitterState(const QByteArray& state); + signals: - void SelectionChanged(const QList& selected_blocks); + void BlocksSelected(const QList& selected_blocks); + + void BlocksDeselected(const QList& deselected_blocks); protected: virtual void resizeEvent(QResizeEvent *event) override; @@ -185,18 +192,18 @@ private: * Validation is the process of ensuring that whatever movements the user is making are "valid" and "legal". This * function's validation ensures that no Ghost's in point ends up in a negative timecode. */ - rational ValidateTimeMovement(rational movement, const QVector ghosts); + rational ValidateTimeMovement(rational movement); /** * @brief Validates Ghosts that are moving vertically (track-based) * * This function's validation ensures that no Ghost's track ends up in a negative (non-existent) track. */ - int ValidateTrackMovement(int movement, const QVector ghosts); + int ValidateTrackMovement(int movement, const QVector &ghosts); - void GetGhostData(const QVector& ghosts, rational *earliest_point, rational *latest_point); + void GetGhostData(rational *earliest_point, rational *latest_point); - void InsertGapsAtGhostDestination(const QVector& ghosts, QUndoCommand* command); + void InsertGapsAtGhostDestination(QUndoCommand* command); QList snap_points_; @@ -209,6 +216,18 @@ private: }; + class BeamTool : public Tool + { + public: + BeamTool(TimelineWidget *parent); + + virtual void HoverMove(TimelineViewMouseEvent *event) override; + + protected: + TimelineCoordinate ValidatedCoordinate(TimelineCoordinate coord); + + }; + class PointerTool : public Tool { public: @@ -226,7 +245,7 @@ private: virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode); - TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode); + TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists = false); TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, const TrackReference& track, Timeline::MovementMode mode); @@ -236,7 +255,7 @@ private: * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no * Ghost's length becomes 0 or negative. */ - rational ValidateInTrimming(rational movement, const QVector ghosts, bool prevent_overwriting); + rational ValidateInTrimming(rational movement); /** * @brief Validates Ghosts that are getting their out points trimmed @@ -244,10 +263,15 @@ private: * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no * Ghost's length becomes 0 or negative. */ - rational ValidateOutTrimming(rational movement, const QVector ghosts, bool prevent_overwriting); + rational ValidateOutTrimming(rational movement); virtual void ProcessDrag(const TimelineCoordinate &mouse_pos); + void InitiateDragInternal(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + bool dont_roll_trims, + bool allow_nongap_rolling, bool slide_instead_of_moving); + const Timeline::MovementMode& drag_movement_mode() const { return drag_movement_mode_; @@ -268,11 +292,6 @@ private: track_movement_allowed_ = e; } - void SetTrimOverwriteAllowed(bool e) - { - trim_overwrite_allowed_ = e; - } - void SetGapTrimmingAllowed(bool e) { gap_trimming_allowed_ = e; @@ -287,10 +306,15 @@ private: const QList& items, const Timeline::MovementMode& mode); + void ProcessGhostsForSliding(); + + void ProcessGhostsForRolling(); + + bool AddMovingTransitionsToClipGhost(Block *block, const TrackReference &track, Timeline::MovementMode movement, const QList &selected_items); + bool movement_allowed_; bool trimming_allowed_; bool track_movement_allowed_; - bool trim_overwrite_allowed_; bool gap_trimming_allowed_; bool rubberband_selecting_; @@ -327,7 +351,7 @@ private: }; - class EditTool : public Tool + class EditTool : public BeamTool { public: EditTool(TimelineWidget* parent); @@ -337,7 +361,7 @@ private: virtual void MouseRelease(TimelineViewMouseEvent *event) override; }; - class RazorTool : public Tool + class RazorTool : public BeamTool { public: RazorTool(TimelineWidget* parent); @@ -367,8 +391,6 @@ private: RollingTool(TimelineWidget* parent); protected: - virtual void FinishDrag(TimelineViewMouseEvent *event) override; - virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) override; }; @@ -379,7 +401,6 @@ private: SlideTool(TimelineWidget* parent); protected: - virtual void FinishDrag(TimelineViewMouseEvent *event) override; virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) override; @@ -406,7 +427,7 @@ private: }; - class AddTool : public Tool + class AddTool : public BeamTool { public: AddTool(TimelineWidget* parent); @@ -439,7 +460,7 @@ private: void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command); - void DeleteSelectedInternal(const QList& blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command); + void ReplaceBlocksWithGaps(const QList& blocks, bool remove_from_graph, QUndoCommand* command); void SetBlockLinksSelected(Block *block, bool selected); @@ -455,8 +476,9 @@ private: void StartRubberBandSelect(bool enable_selecting, bool select_links); void MoveRubberBandSelect(bool enable_selecting, bool select_links); - void EndRubberBandSelect(bool enable_selecting, bool select_links); + void EndRubberBandSelect(); QRubberBand rubberband_; + QList rubberband_already_selected_; QList rubberband_now_selected_; Tool* GetActiveTool(); @@ -477,10 +499,6 @@ private: TrackOutput* GetTrackFromReference(const TrackReference& ref); - void ConnectViewSelectionSignal(TimelineView* view); - - void DisconnectViewSelectionSignal(TimelineView* view); - QList views_; TimeSlider* timecode_label_; @@ -489,6 +507,8 @@ private: bool use_audio_time_units_; + QSplitter* view_splitter_; + int GetTrackY(const TrackReference& ref); int GetTrackHeight(const TrackReference& ref); @@ -498,6 +518,8 @@ private: void UpdateViewTimebases(); + void SetViewBeamCursor(const TimelineCoordinate& coord); + private slots: void ViewMousePressed(TimelineViewMouseEvent* event); void ViewMouseMoved(TimelineViewMouseEvent* event); @@ -516,8 +538,6 @@ private slots: void RemoveTrack(TrackOutput* track); void TrackIndexChanged(); - void ViewSelectionChanged(); - /** * @brief Slot for when a Block node changes its parameters and the graphics need to update * diff --git a/app/widget/timelinewidget/tool/CMakeLists.txt b/app/widget/timelinewidget/tool/CMakeLists.txt index 53b862a84..f14d4a8de 100644 --- a/app/widget/timelinewidget/tool/CMakeLists.txt +++ b/app/widget/timelinewidget/tool/CMakeLists.txt @@ -17,6 +17,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/timelinewidget/tool/add.cpp + widget/timelinewidget/tool/beam.cpp widget/timelinewidget/tool/edit.cpp widget/timelinewidget/tool/import.cpp widget/timelinewidget/tool/pointer.cpp diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index 8f1ae9c74..f488bbfca 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -22,12 +22,14 @@ #include "core.h" #include "node/factory.h" +#include "node/generator/solid/solid.h" +#include "node/generator/text/text.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER TimelineWidget::AddTool::AddTool(TimelineWidget *parent) : - Tool(parent), + BeamTool(parent), ghost_(nullptr) { } @@ -44,7 +46,7 @@ void TimelineWidget::AddTool::MousePress(TimelineViewMouseEvent *event) Timeline::TrackType add_type = Timeline::kTrackTypeNone; - switch (Core::instance()->selected_addable_object()) { + switch (Core::instance()->GetSelectedAddableObject()) { case OLIVE_NAMESPACE::Tool::kAddableBars: case OLIVE_NAMESPACE::Tool::kAddableSolid: case OLIVE_NAMESPACE::Tool::kAddableTitle: @@ -63,15 +65,7 @@ void TimelineWidget::AddTool::MousePress(TimelineViewMouseEvent *event) if (add_type == Timeline::kTrackTypeNone || add_type == track.type()) { - drag_start_point_ = event->GetFrame(); - - if (Core::instance()->snapping()) { - rational movement; - parent()->SnapPoint({drag_start_point_}, &movement); - if (!movement.isNull()) { - drag_start_point_ += movement; - } - } + drag_start_point_ = ValidatedCoordinate(event->GetCoordinates(true)).GetFrame(); ghost_ = new TimelineViewGhostItem(); ghost_->SetIn(drag_start_point_); @@ -95,8 +89,6 @@ void TimelineWidget::AddTool::MouseMove(TimelineViewMouseEvent *event) void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) { - MouseMove(event); - const TrackReference& track = ghost_->Track(); if (ghost_) { @@ -105,7 +97,7 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) ClipBlock* clip = new ClipBlock(); clip->set_length_and_media_out(ghost_->AdjustedLength()); - clip->SetLabel(OLIVE_NAMESPACE::Tool::GetAddableObjectName(Core::instance()->selected_addable_object())); + clip->SetLabel(OLIVE_NAMESPACE::Tool::GetAddableObjectName(Core::instance()->GetSelectedAddableObject())); NodeGraph* graph = static_cast(parent()->GetConnectedNode()->parent()); @@ -119,13 +111,13 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) ghost_->GetAdjustedIn(), command); - switch (Core::instance()->selected_addable_object()) { + switch (Core::instance()->GetSelectedAddableObject()) { case OLIVE_NAMESPACE::Tool::kAddableEmpty: // Empty, nothing to be done break; case OLIVE_NAMESPACE::Tool::kAddableSolid: { - Node* solid = NodeFactory::CreateFromID(QStringLiteral("org.olivevideoeditor.Olive.solidgenerator")); + Node* solid = new SolidGenerator(); new NodeAddCommand(graph, solid, @@ -136,7 +128,7 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) } case OLIVE_NAMESPACE::Tool::kAddableTitle: { - Node* text = NodeFactory::CreateFromID(QStringLiteral("org.olivevideoeditor.Olive.textgenerator")); + Node* text = new TextGenerator(); new NodeAddCommand(graph, text, @@ -148,7 +140,7 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) case OLIVE_NAMESPACE::Tool::kAddableBars: case OLIVE_NAMESPACE::Tool::kAddableTone: // Not implemented yet - qWarning() << "Unimplemented add object:" << Core::instance()->selected_addable_object(); + qWarning() << "Unimplemented add object:" << Core::instance()->GetSelectedAddableObject(); break; case OLIVE_NAMESPACE::Tool::kAddableCount: // Invalid value, do nothing diff --git a/app/widget/timelinewidget/tool/beam.cpp b/app/widget/timelinewidget/tool/beam.cpp new file mode 100644 index 000000000..e2fe1120e --- /dev/null +++ b/app/widget/timelinewidget/tool/beam.cpp @@ -0,0 +1,48 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "widget/timelinewidget/timelinewidget.h" + +OLIVE_NAMESPACE_ENTER + +TimelineWidget::BeamTool::BeamTool(TimelineWidget *parent) : + Tool(parent) +{ +} + +void TimelineWidget::BeamTool::HoverMove(TimelineViewMouseEvent *event) +{ + parent()->SetViewBeamCursor(ValidatedCoordinate(event->GetCoordinates(true))); +} + +TimelineCoordinate TimelineWidget::BeamTool::ValidatedCoordinate(TimelineCoordinate coord) +{ + if (Core::instance()->snapping()) { + rational movement; + parent()->SnapPoint({coord.GetFrame()}, &movement); + if (!movement.isNull()) { + coord.SetFrame(coord.GetFrame() + movement); + } + } + + return coord; +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/edit.cpp b/app/widget/timelinewidget/tool/edit.cpp index d7940f0c4..cb804bebc 100644 --- a/app/widget/timelinewidget/tool/edit.cpp +++ b/app/widget/timelinewidget/tool/edit.cpp @@ -23,7 +23,7 @@ OLIVE_NAMESPACE_ENTER TimelineWidget::EditTool::EditTool(TimelineWidget* parent) : - Tool(parent) + BeamTool(parent) { } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 148129721..9850a6eab 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -123,14 +123,14 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) rational time_movement = event->GetFrame() - drag_start_.GetFrame(); int track_movement = event->GetTrack().index() - drag_start_.GetTrack().index(); - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); + time_movement = ValidateTimeMovement(time_movement); track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); // If snapping is enabled, check for snap points if (Core::instance()->snapping()) { parent()->SnapPoint(snap_points_, &time_movement); - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); + time_movement = ValidateTimeMovement(time_movement); track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); } @@ -216,7 +216,9 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi QVector track_offsets(Timeline::kTrackTypeCount); track_offsets.fill(track_start); + QVector footage_ghosts; rational footage_duration; + bool contains_image_stream = false; quint64 enabled_streams = footage.streams(); @@ -236,37 +238,48 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); if (stream->type() == Stream::kImage) { - // Stream is essentially length-less - use config's default image length - footage_duration = Config::Current()["DefaultStillLength"].value(); + // Stream is essentially length-less - we may use the default still image length in config, + // or we may use another stream's length depending on the circumstance + contains_image_stream = true; } else { // Rescale stream duration to timeline timebase // Convert to rational time if (footage.footage()->workarea()->enabled()) { - footage_duration = footage.footage()->workarea()->range().length(); + footage_duration = qMax(footage_duration, footage.footage()->workarea()->range().length()); ghost->SetMediaIn(footage.footage()->workarea()->in()); } else { int64_t stream_duration = Timecode::rescale_timestamp_ceil(stream->duration(), stream->timebase(), dest_tb); - footage_duration = Timecode::timestamp_to_time(stream_duration, dest_tb); + footage_duration = qMax(footage_duration, Timecode::timestamp_to_time(stream_duration, dest_tb)); } } - ghost->SetIn(ghost_start); - ghost->SetOut(ghost_start + footage_duration); ghost->SetTrack(TrackReference(track_type, track_offsets.at(track_type))); // Increment track count for this track type track_offsets[track_type]++; - snap_points_.append(ghost->In()); - snap_points_.append(ghost->Out()); - ghost->setData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); ghost->SetMode(Timeline::kMove); - parent()->AddGhost(ghost); + footage_ghosts.append(ghost); } + if (contains_image_stream && footage_duration.isNull()) { + // Footage must ONLY be image streams so no duration value was found, use default in config + footage_duration = Config::Current()["DefaultStillLength"].value(); + } + + foreach (TimelineViewGhostItem* ghost, footage_ghosts) { + ghost->SetIn(ghost_start); + ghost->SetOut(ghost_start + footage_duration); + + snap_points_.append(ghost->In()); + snap_points_.append(ghost->Out()); + + parent()->AddGhost(ghost); + } + // Stack each ghost one after the other ghost_start += footage_duration; @@ -385,7 +398,7 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert) // Check if we're inserting if (insert) { - InsertGapsAtGhostDestination(parent()->ghost_items_, command); + InsertGapsAtGhostDestination(command); } for (int i=0;ighost_items_.size();i++) { diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index e300daff8..9889a30a3 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -41,7 +41,6 @@ TimelineWidget::PointerTool::PointerTool(TimelineWidget *parent) : movement_allowed_(true), trimming_allowed_(true), track_movement_allowed_(true), - trim_overwrite_allowed_(false), gap_trimming_allowed_(false), rubberband_selecting_(false) { @@ -79,16 +78,23 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) // If this item is already selected, no further selection needs to be made if (clicked_item_->isSelected()) { + // Collect item deselections + QList deselected_blocks; + // If shift is held, deselect it if (event->GetModifiers() & Qt::ShiftModifier) { clicked_item_->setSelected(false); + deselected_blocks.append(clicked_item_->block()); // If not holding alt, deselect all links as well if (!(event->GetModifiers() & Qt::AltModifier)) { parent()->SetBlockLinksSelected(clicked_item_->block(), false); + deselected_blocks.append(clicked_item_->block()->linked_clips().toList()); } } + emit parent()->BlocksDeselected(deselected_blocks); + return; } } @@ -99,18 +105,29 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) } if (selectable_item) { + + // Collect item selections + QList selected_blocks; + // Select this item clicked_item_->setSelected(true); + selected_blocks.append(clicked_item_->block()); // If not holding alt, select all links as well if (!(event->GetModifiers() & Qt::AltModifier)) { parent()->SetBlockLinksSelected(clicked_item_->block(), true); + selected_blocks.append(clicked_item_->block()->linked_clips().toList()); } + + emit parent()->BlocksSelected(selected_blocks); + } else if (event->GetButton() == Qt::LeftButton) { + // Start rubberband drag parent()->StartRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier)); rubberband_selecting_ = true; + } } @@ -119,31 +136,31 @@ void TimelineWidget::PointerTool::MouseMove(TimelineViewMouseEvent *event) if (rubberband_selecting_) { // Process rubberband select parent()->MoveRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier)); - return; - } + } else { + // Process drag + if (!dragging_) { - if (!dragging_) { + // Now that the cursor has moved, we will assume the intention is to drag - // Now that the cursor has moved, we will assume the intention is to drag + // Clear snap points + snap_points_.clear(); - // Clear snap points - snap_points_.clear(); + // If we're performing an action, we can initiate ghosts + if (drag_movement_mode_ != Timeline::kNone) { + InitiateDrag(clicked_item_, drag_movement_mode_); + } + + // Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed + dragging_ = true; - // If we're performing an action, we can initiate ghosts - if (drag_movement_mode_ != Timeline::kNone) { - InitiateDrag(clicked_item_, drag_movement_mode_); } - // Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed - dragging_ = true; + if (dragging_ && !parent()->ghost_items_.isEmpty()) { - } - - if (dragging_ && !parent()->ghost_items_.isEmpty()) { - - // We're already dragging AND we have ghosts to work with - ProcessDrag(event->GetCoordinates()); + // We're already dragging AND we have ghosts to work with + ProcessDrag(event->GetCoordinates()); + } } } @@ -151,16 +168,18 @@ void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event) { if (rubberband_selecting_) { // Finish rubberband select - parent()->EndRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier)); + parent()->EndRubberBandSelect(); rubberband_selecting_ = false; return; } if (dragging_) { + // If we were dragging, process the end of the drag if (!parent()->ghost_items_.isEmpty()) { FinishDrag(event); } + // Clean up parent()->ClearGhosts(); snap_points_.clear(); @@ -193,92 +212,236 @@ void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event) } } -void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) +void SetGhostToSlideMode(TimelineViewGhostItem* g) { - QList ghosts_moving; - QList blocks_moving; - QList ghosts_trimming; - QList blocks_trimming; + g->SetCanMoveTracks(false); + g->setData(TimelineViewGhostItem::kGhostIsSliding, true); +} - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (!ghost->HasBeenAdjusted()) { - continue; +void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *clicked_item, + Timeline::MovementMode trim_mode, + bool dont_roll_trims, + bool allow_nongap_rolling, + bool slide_instead_of_moving) +{ + // Get list of selected blocks + QList clips = parent()->GetSelectedBlocks(); + + if (trim_mode == Timeline::kMove) { + + // Each block type has different behavior, so we determine the type of the block that was + // clicked and filter out any others. + Block::Type clicked_block_type = clicked_item->block()->type(); + + // Gaps are not allowed to move, and since we only allow moving one block type at a time, + // dragging a gap is a no-op + if (clicked_block_type == Block::kGap) { + return; } - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + // Determine if this move is a slide, which is determined by either + bool clips_are_sliding = (slide_instead_of_moving || clicked_block_type == Block::kTransition); - if (ghost->mode() == Timeline::kMove) { - ghosts_moving.append(ghost); - blocks_moving.append(b); - } else if (Timeline::IsATrimMode(ghost->mode())) { - ghosts_trimming.append(ghost); - blocks_trimming.append(b); - } - } + if (clips_are_sliding) { + // This is a slide. What we do here is move clips within their own track, between the clips + // that they're already next to. We don't allow changing tracks or changing the order of + // blocks. + // + // For slides to be legal, we make all blocks "contiguous". This means that only one series + // of blocks can move at a time and prevents. - if (blocks_moving.isEmpty() && blocks_trimming.isEmpty()) { - // Likely means no block was adjusted, so we can skip the rest of the processing - return; - } + QHash earliest_block_on_track; + QHash latest_block_on_track; - // See if we're duplicated because ALT is held (only moved blocks can duplicate) - bool duplicate_clips = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::AltModifier); - bool inserting = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::ControlModifier); + foreach (TimelineViewBlockItem* item, clips) { + Block* this_block = item->block(); + const TrackReference& track = item->Track(); - QUndoCommand* command = new QUndoCommand(); + Block* current_earliest = earliest_block_on_track.value(track, nullptr); + if (!current_earliest || this_block->in() < current_earliest->in()) { + earliest_block_on_track.insert(track, item->block()); + } - for (int i=0;iGetTrackFromReference(ghost->GetAdjustedTrack()), - blocks_trimming.at(i), - ghost->AdjustedLength(), - ghost->mode(), - command); - } - - if (!blocks_moving.isEmpty()) { - // If we're not duplicating, "remove" the clips and replace them with gaps - if (!duplicate_clips) { - parent()->DeleteSelectedInternal(blocks_moving, false, false, command); - } - - if (inserting) { - // If we're inserting, ripple everything at the destination with gaps - InsertGapsAtGhostDestination(parent()->ghost_items_, command); - } - - // Now we can re-add each clip - for (int i=0;icopy(); - - new NodeAddCommand(static_cast(block->parent()), - copy, - command); - - new NodeCopyInputsCommand(block, copy, true, command); - - // Place the copy instead of the original block - block = static_cast(copy); + Block* current_latest = latest_block_on_track.value(track, nullptr); + if (!current_latest || this_block->out() > current_earliest->out()) { + latest_block_on_track.insert(track, item->block()); + } } - const TrackReference& track_ref = ghost->GetAdjustedTrack(); - new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(track_ref.type()), - track_ref.index(), - block, - ghost->GetAdjustedIn(), - command); + QHash::const_iterator i; + for (i=earliest_block_on_track.constBegin(); i!=earliest_block_on_track.constEnd(); i++) { + // Make a contiguous stream + const TrackReference& track = i.key(); + Block* earliest = i.value(); + Block* latest = latest_block_on_track.value(i.key()); + + // First we add the block that's out trimming, the one prior to the earliest + TimelineViewGhostItem* earliest_ghost; + if (earliest->previous()) { + earliest_ghost = AddGhostFromBlock(earliest->previous(), track, Timeline::kTrimOut); + } else { + earliest_ghost = AddGhostFromNull(earliest->in(), earliest->in(), track, Timeline::kTrimOut); + } + SetGhostToSlideMode(earliest_ghost); + + // Then we add the block that's in trimming, the one after the latest + if (latest->next()) { + TimelineViewGhostItem* latest_ghost = AddGhostFromBlock(latest->next(), track, Timeline::kTrimIn); + SetGhostToSlideMode(latest_ghost); + } + + // Finally, we add all of the moving blocks in between + Block* b = nullptr; + do { + // On first run-through, set to earliest only. From then on, set to the next of the last + // in the loop. + if (b) { + b = b->next(); + } else { + b = earliest; + } + + TimelineViewGhostItem* between_ghost = AddGhostFromBlock(b, track, Timeline::kMove); + SetGhostToSlideMode(between_ghost); + } while (b != latest); + } + } else { + // Prepare for a standard pointer move + foreach (TimelineViewBlockItem* clip_item, clips) { + Block* block = clip_item->block(); + + if (block->type() == Block::kGap || block->type() == Block::kTransition) { + // Gaps cannot move, and we handle transitions further down + continue; + } + + // Create ghost + TimelineViewGhostItem* ghost = AddGhostFromBlock(block, + clip_item->Track(), + trim_mode); + Q_UNUSED(ghost) + + // Add transitions if this has any + TransitionBlock* opening_transition = TransitionBlock::GetBlockInTransition(block); + TransitionBlock* closing_transition = TransitionBlock::GetBlockOutTransition(block); + + if (opening_transition) { + TimelineViewGhostItem* ot_ghost = AddGhostFromBlock(opening_transition, + clip_item->Track(), + trim_mode); + Q_UNUSED(ot_ghost) + } + + if (closing_transition) { + TimelineViewGhostItem* cl_ghost = AddGhostFromBlock(closing_transition, + clip_item->Track(), + trim_mode); + Q_UNUSED(cl_ghost) + } + } } - // FIXME: Heavy optimization since MOST of the timeline does NOT change in this time - } + } else { - Core::instance()->undo_stack()->pushIfHasChildren(command); + // "Multi-trim" is trimming a clip on more than one track. Only the earliest (for in trimming) + // or latest (for out trimming) clip on each track can be trimmed. Therefore, it's only enabled + // if the clicked item is the earliest/latest on its track. + bool multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); + + // Create ghosts for trimming + foreach (TimelineViewBlockItem* clip_item, clips) { + if (clip_item != clicked_item + && (!multitrim_enabled || !IsClipTrimmable(clip_item, clips, trim_mode))) { + // Either multitrim is disabled or this clip is NOT the earliest/latest in its track. We + // won't include it. + continue; + } + + Block* block = clip_item->block(); + + // Create ghost for this block + TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), trim_mode); + + // If this side of the clip has a transition, we treat it more like a slide for that + // transition than a trim/roll + bool treat_trim_as_slide = false; + + if (block->type() == Block::kClip) { + // See if this clip has a transition attached, and move it with the trim if so + TransitionBlock* connected_transition; + + // Get appropriate transition for the side of the clip + if (trim_mode == Timeline::kTrimIn) { + connected_transition = TransitionBlock::GetBlockInTransition(block); + } else { + connected_transition = TransitionBlock::GetBlockOutTransition(block); + } + + if (connected_transition) { + // We found a transition, we'll make this a "slide" action + TimelineViewGhostItem* transition_ghost = AddGhostFromBlock(connected_transition, clip_item->Track(), Timeline::kMove); + + // This will in effect be a slide with the transition moving between two other blocks + SetGhostToSlideMode(ghost); + SetGhostToSlideMode(transition_ghost); + treat_trim_as_slide = true; + + // Further processing will apply to this transition rather than the clip + block = connected_transition; + } + } + + // Standard pointer trimming in reality is a "roll" edit with an adjacent gap (one that may + // or may not exist already) + if (!dont_roll_trims) { + Block* adjacent = nullptr; + + // Determine which block is adjacent + if (trim_mode == Timeline::kTrimIn) { + adjacent = block->previous(); + } else { + adjacent = block->next(); + } + + // See if we can roll the adjacent or if we'll need to create our own gap + if (block->type() != Block::kGap + && !allow_nongap_rolling && adjacent && adjacent->type() != Block::kGap + && !(block->type() == Block::kTransition + && ((trim_mode == Timeline::kTrimIn && static_cast(block)->connected_out_block() == adjacent) + || (trim_mode == Timeline::kTrimOut && static_cast(block)->connected_in_block() == adjacent)))) { + adjacent = nullptr; + } + + Timeline::MovementMode flipped_mode = FlipTrimMode(trim_mode); + TimelineViewGhostItem* adjacent_ghost; + + if (adjacent) { + adjacent_ghost = AddGhostFromBlock(adjacent, clip_item->Track(), flipped_mode); + } else if (trim_mode == Timeline::kTrimIn || block->next()) { + rational null_ghost_pos = (trim_mode == Timeline::kTrimIn) ? block->in() : block->out(); + + adjacent_ghost = AddGhostFromNull(null_ghost_pos, null_ghost_pos, clip_item->Track(), flipped_mode); + } else { + adjacent_ghost = nullptr; + } + + // If we have an adjacent block (for any reason), this is a roll edit and the adjacent is + // expected to fill the remaining space (no gap needs to be created) + ghost->setData(TimelineViewGhostItem::kTrimIsARollEdit, static_cast(adjacent)); + + if (adjacent_ghost) { + if (treat_trim_as_slide) { + // We're sliding a transition rather than a pure trim/roll + SetGhostToSlideMode(adjacent_ghost); + } else if (block->type() == Block::kGap) { + ghost->setData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); + } else { + adjacent_ghost->setData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); + } + } + } + } + } } void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_pos) @@ -292,17 +455,17 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po rational time_movement = mouse_pos.GetFrame() - drag_start_.GetFrame(); // Validate movement (enforce all ghosts moving in legal ways) - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); - time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); - time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); + time_movement = ValidateTimeMovement(time_movement); + time_movement = ValidateInTrimming(time_movement); + time_movement = ValidateOutTrimming(time_movement); // Perform snapping if enabled (adjusts time_movement if it's close to any potential snap points) if (Core::instance()->snapping()) { parent()->SnapPoint(snap_points_, &time_movement); - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); - time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); - time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); + time_movement = ValidateTimeMovement(time_movement); + time_movement = ValidateInTrimming(time_movement); + time_movement = ValidateOutTrimming(time_movement); } // Validate ghosts that are being moved (clips from other track types do NOT get moved) @@ -357,6 +520,165 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po parent()); } +struct GhostBlockPair { + TimelineViewGhostItem* ghost; + Block* block; +}; + +void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) +{ + QList blocks_moving; + QList blocks_sliding; + QList blocks_trimming; + + // Sort ghosts depending on which ones are trimming, which are moving, and which are sliding + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + if (ghost->HasBeenAdjusted()) { + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (ghost->data(TimelineViewGhostItem::kGhostIsSliding).toBool()) { + blocks_sliding.append({ghost, b}); + } else if (ghost->mode() == Timeline::kMove) { + blocks_moving.append({ghost, b}); + } else if (Timeline::IsATrimMode(ghost->mode())) { + blocks_trimming.append({ghost, b}); + } + } + } + + if (blocks_moving.isEmpty() + && blocks_trimming.isEmpty() + && blocks_sliding.isEmpty()) { + // No blocks were adjusted, so nothing to do + return; + } + + QUndoCommand* command = new QUndoCommand(); + + foreach (const GhostBlockPair& p, blocks_trimming) { + TimelineViewGhostItem* ghost = p.ghost; + + if (!ghost->data(TimelineViewGhostItem::kTrimShouldBeIgnored).toBool()) { + // Must be an ordinary trim/roll + BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->GetAdjustedTrack()), + p.block, + ghost->AdjustedLength(), + ghost->mode(), + command); + + c->SetTrimIsARollEdit(ghost->data(TimelineViewGhostItem::kTrimIsARollEdit).toBool()); + } + } + + if (!blocks_moving.isEmpty()) { + // See if we're duplicated because ALT is held (only moved blocks can duplicate) + bool duplicate_clips = (event->GetModifiers() & Qt::AltModifier); + bool inserting = (event->GetModifiers() & Qt::ControlModifier); + + // If we're not duplicating, "remove" the clips and replace them with gaps + if (!duplicate_clips) { + QList blocks_to_delete; + + foreach (const GhostBlockPair& p, blocks_moving) { + blocks_to_delete.append(p.block); + } + + parent()->ReplaceBlocksWithGaps(blocks_to_delete, false, command); + } + + if (inserting) { + // If we're inserting, ripple everything at the destination with gaps + InsertGapsAtGhostDestination(command); + } + + // Now we can re-add each clip + foreach (const GhostBlockPair& p, blocks_moving) { + Block* block = p.block; + + if (duplicate_clips) { + // Duplicate rather than move + Node* copy = block->copy(); + + new NodeAddCommand(static_cast(block->parent()), + copy, + command); + + new NodeCopyInputsCommand(block, copy, true, command); + + // Place the copy instead of the original block + block = static_cast(copy); + } + + const TrackReference& track_ref = p.ghost->GetAdjustedTrack(); + new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(track_ref.type()), + track_ref.index(), + block, + p.ghost->GetAdjustedIn(), + command); + } + } + + if (!blocks_sliding.isEmpty()) { + // Assume that the blocks are contiguous per track as set up in InitiateGhostsInternal() + + // All we need to do is sort them by track and order them + QHash > slide_info; + QHash in_adjacents; + QHash out_adjacents; + rational movement; + + foreach (const GhostBlockPair& p, blocks_sliding) { + const TrackReference& track = p.ghost->Track(); + + switch (p.ghost->mode()) { + case Timeline::kNone: + break; + case Timeline::kMove: + { + // These all should have moved uniformly, so as long as this is set, it should be fine + movement = p.ghost->InAdjustment(); + + QList& blocks_on_this_track = slide_info[track]; + bool inserted = false; + + for (int i=0;iin() > p.block->in()) { + blocks_on_this_track.insert(i, p.block); + inserted = true; + break; + } + } + + if (!inserted) { + blocks_on_this_track.append(p.block); + } + break; + } + case Timeline::kTrimIn: + out_adjacents.insert(track, p.block); + break; + case Timeline::kTrimOut: + in_adjacents.insert(track, p.block); + break; + } + } + + if (!movement.isNull()) { + QHash >::const_iterator i; + for (i=slide_info.constBegin(); i!=slide_info.constEnd(); i++) { + new TrackSlideCommand(parent()->GetTrackFromReference(i.key()), + i.value(), + in_adjacents.value(i.key()), + out_adjacents.value(i.key()), + movement, + command); + } + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(TimelineViewBlockItem *block, qreal cursor_x) { double kTrimHandle = QFontMetricsWidth(parent()->fontMetrics(), "H"); @@ -378,67 +700,32 @@ Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(Timelin void TimelineWidget::PointerTool::InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) { - // Get list of selected blocks - QList clips = parent()->GetSelectedBlocks(); - - if (trim_mode == Timeline::kMove) { - - // Create ghosts for moving - foreach (TimelineViewBlockItem* clip_item, clips) { - - // Gaps are not allowed to move, so we ignore those here - if (clip_item->block()->type() == Block::kGap) { - continue; - } - - AddGhostFromBlock(clip_item->block(), clip_item->Track(), trim_mode); - } - - } else { - - // "Multi-trim" is trimming a clip on more than one track. Only the earliest (for in trimming) - // or latest (for out trimming) clip on each track can be trimmed. Therefore, it's only enabled - // if the clicked item is the earliest/latest on its track. - bool multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); - - // Create ghosts for trimming - foreach (TimelineViewBlockItem* clip_item, clips) { - if (clip_item != clicked_item - && (!multitrim_enabled || !IsClipTrimmable(clip_item, clips, trim_mode))) { - // Either multitrim is disabled or this clip is NOT the earliest/latest in its track. We - // won't include it. - continue; - } - - Block* block = clip_item->block(); - Timeline::MovementMode block_mode = trim_mode; - - // Some tools interpret "gap trimming" as equivalent to resizing the adjacent block. In that - // scenario, we include the adjacent block instead. - if (block->type() == Block::kGap && !gap_trimming_allowed_) { - block = (trim_mode == Timeline::kTrimIn) ? block->previous() : block->next(); - block_mode = FlipTrimMode(trim_mode); - - // If there's no adjacent block, do nothing here - if (!block) { - continue; - } - } - - // Create ghost for this block - AddGhostFromBlock(block, clip_item->Track(), block_mode); - } - - } + InitiateDragInternal(clicked_item, trim_mode, false, false, false); } -TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode) +//#define HIDE_GAP_GHOSTS + +TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists) { + if (check_if_exists) { + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + if (Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)) == block) { + return ghost; + } + } + } + TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromBlock(block, track, parent()->GetTrackY(track), parent()->GetTrackHeight(track)); +#ifdef HIDE_GAP_GHOSTS + if (block->type() == Block::kGap) { + ghost->SetInvisible(true); + } +#endif + AddGhostInternal(ghost, mode); return ghost; @@ -453,6 +740,10 @@ TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const ratio ghost->SetTrack(track); ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); +#ifdef HIDE_GAP_GHOSTS + ghost->SetInvisible(true); +#endif + AddGhostInternal(ghost, mode); return ghost; @@ -497,63 +788,70 @@ bool TimelineWidget::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, return true; } -rational GetEarliestPointForClip(Block* block) +bool TimelineWidget::PointerTool::AddMovingTransitionsToClipGhost(Block* block, + const TrackReference& track, + Timeline::MovementMode movement, + const QList& selected_items) { - return qMax(rational(0), block->in() - block->media_in()); + // Assume block is a clip and see if it has any transitions + TransitionBlock* transitions[2]; + + if (movement == Timeline::kMove || movement == Timeline::kTrimOut) { + transitions[0] = TransitionBlock::GetBlockOutTransition(block); + } else { + transitions[0] = nullptr; + } + + if (movement == Timeline::kMove || movement == Timeline::kTrimIn) { + transitions[1] = TransitionBlock::GetBlockInTransition(block); + } else { + transitions[1] = nullptr; + } + + bool ret = false; + + for (int i=0;i<2;i++) { + if (!transitions[i]) { + continue; + } + + bool found = false; + + foreach (TimelineViewBlockItem* item, selected_items) { + if (item->block() == transitions[i]) { + // Do nothing + found = true; + break; + } + } + + if (!found) { + TimelineViewGhostItem* transition_ghost = AddGhostFromBlock(transitions[i], track, + Timeline::kMove); + + Q_UNUSED(transition_ghost) + + ret = true; + } + } + + return ret; } -rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, - const QVector ghosts, - bool prevent_overwriting) +rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement) { - foreach (TimelineViewGhostItem* ghost, ghosts) { + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { if (ghost->mode() != Timeline::kTrimIn) { continue; } - Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - rational earliest_in = RATIONAL_MIN; rational latest_in = ghost->Out(); - if (block->type() == Block::kTransition) { - // For transitions, validate with the attached block - TransitionBlock* transition = static_cast(block); - - if (transition->connected_in_block() && transition->connected_out_block()) { - // Here, we try to get the latest earliest point for both the in and out blocks, we do in here and out will - // be calculated later - earliest_in = GetEarliestPointForClip(transition->connected_in_block()); - - // We set the block to the out block since that will be before the in block and will be the one we use to - // prevent overwriting since we're trimming the in side of this transition - block = transition->connected_out_block(); - - latest_in = transition->in() + transition->out_offset(); - } else { - // Use whatever block is attached - block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); - } - } - - earliest_in = qMax(earliest_in, GetEarliestPointForClip(block)); - if (!ghost->CanHaveZeroLength()) { latest_in -= parent()->timebase(); } - if (prevent_overwriting) { - // Look for a Block in the way - Block* prev = block->previous(); - while (prev != nullptr) { - if (prev->type() == Block::kClip) { - earliest_in = qMax(earliest_in, prev->out()); - break; - } - prev = prev->previous(); - } - } - // Clamp adjusted value between the earliest and latest values rational adjusted = ghost->In() + movement; rational clamped = clamp(adjusted, earliest_in, latest_in); @@ -566,17 +864,13 @@ rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, return movement; } -rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement, - const QVector ghosts, - bool prevent_overwriting) +rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement) { - foreach (TimelineViewGhostItem* ghost, ghosts) { + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { if (ghost->mode() != Timeline::kTrimOut) { continue; } - Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - // Determine earliest and latest out points rational earliest_out = ghost->In(); @@ -586,36 +880,6 @@ rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement, rational latest_out = RATIONAL_MAX; - if (block->type() == Block::kTransition) { - // For transitions, validate with the attached block - TransitionBlock* transition = static_cast(block); - - if (transition->connected_in_block() && transition->connected_out_block()) { - // We set the block to the out block since that will be before the in block and will be the one we use to - // prevent overwriting since we're trimming the in side of this transition - - // FIXME: At some point we may add some better logic to `latest_out` akin to the logic in ValidateInTrimming - // which is why this hasn't yet been collapsed into the ternary below. - block = transition->connected_in_block(); - - earliest_out = transition->out() - transition->in_offset(); - } else { - block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); - } - } - - if (prevent_overwriting) { - // Determine if there's a block in the way - Block* next = block->next(); - while (next != nullptr) { - if (next->type() == Block::kClip) { - latest_out = qMin(latest_out, next->in()); - break; - } - next = next->next(); - } - } - // Clamp adjusted value between the earliest and latest values rational adjusted = ghost->Out() + movement; rational clamped = clamp(adjusted, earliest_out, latest_out); diff --git a/app/widget/timelinewidget/tool/razor.cpp b/app/widget/timelinewidget/tool/razor.cpp index d89e9125d..49177ce8a 100644 --- a/app/widget/timelinewidget/tool/razor.cpp +++ b/app/widget/timelinewidget/tool/razor.cpp @@ -23,7 +23,7 @@ OLIVE_NAMESPACE_ENTER TimelineWidget::RazorTool::RazorTool(TimelineWidget* parent) : - Tool(parent) + BeamTool(parent) { } @@ -37,7 +37,7 @@ void TimelineWidget::RazorTool::MousePress(TimelineViewMouseEvent *event) void TimelineWidget::RazorTool::MouseMove(TimelineViewMouseEvent *event) { if (!dragging_) { - drag_start_ = event->GetCoordinates(true); + drag_start_ = ValidatedCoordinate(event->GetCoordinates(true)); dragging_ = true; } diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index ed441b5de..919c0d908 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -29,14 +29,13 @@ TimelineWidget::RippleTool::RippleTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); - SetTrimOverwriteAllowed(true); SetGapTrimmingAllowed(true); } void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - PointerTool::InitiateDrag(clicked_item, trim_mode); + InitiateDragInternal(clicked_item, trim_mode, true, true, false); if (parent()->ghost_items_.isEmpty()) { return; @@ -87,13 +86,18 @@ void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_ite if (block_before_ripple->type() == Block::kGap) { // If this Block is already a Gap, ghost it now ghost = AddGhostFromBlock(block_before_ripple, track_ref, trim_mode); - } else { - // If there's no gap here, we'll need to create one - ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode); - ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); - } + } else if (block_before_ripple->next()) { + // Assuming this block is NOT at the end of the track (i.e. next != null) - ghost->SetInvisible(true); + // We're going to create a gap after it. If next is a gap, we can just use that + if (block_before_ripple->next()->type() == Block::kGap) { + ghost = AddGhostFromBlock(block_before_ripple->next(), track_ref, trim_mode); + } else { + // If next is NOT a gap, we'll need to create one, for which we'll use a null ghost + ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode); + ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); + } + } } } } diff --git a/app/widget/timelinewidget/tool/rolling.cpp b/app/widget/timelinewidget/tool/rolling.cpp index 6baf25316..2aa55e43e 100644 --- a/app/widget/timelinewidget/tool/rolling.cpp +++ b/app/widget/timelinewidget/tool/rolling.cpp @@ -29,47 +29,13 @@ TimelineWidget::RollingTool::RollingTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); - SetTrimOverwriteAllowed(true); SetGapTrimmingAllowed(true); } void TimelineWidget::RollingTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - PointerTool::InitiateDrag(clicked_item, trim_mode); - - // For each ghost, we make an equivalent Ghost on the next/previous block - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (ghost->mode() == Timeline::kTrimIn && ghost_block->previous()) { - // Add an extra Ghost for the previous block - AddGhostFromBlock(ghost_block->previous(), ghost->Track(), Timeline::kTrimOut); - } else if (ghost->mode() == Timeline::kTrimOut && ghost_block->next()) { - AddGhostFromBlock(ghost_block->next(), ghost->Track(), Timeline::kTrimIn); - } - } -} - -void TimelineWidget::RollingTool::FinishDrag(TimelineViewMouseEvent *event) -{ - QUndoCommand* command = new QUndoCommand(); - - // Find earliest point to ripple around - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (ghost->mode() == drag_movement_mode()) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->Track()), - b, - ghost->AdjustedLength(), - drag_movement_mode(), - command); - c->SetAllowNonGapTrimming(true); - } - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); + InitiateDragInternal(clicked_item, trim_mode, false, true, false); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/slide.cpp b/app/widget/timelinewidget/tool/slide.cpp index 26423f335..c64d9ec43 100644 --- a/app/widget/timelinewidget/tool/slide.cpp +++ b/app/widget/timelinewidget/tool/slide.cpp @@ -30,105 +30,13 @@ TimelineWidget::SlideTool::SlideTool(TimelineWidget* parent) : { SetTrimmingAllowed(false); SetTrackMovementAllowed(false); - SetTrimOverwriteAllowed(true); SetGapTrimmingAllowed(true); } -struct TrackBlockListPair { - TrackReference track; - QList blocks; -}; - void TimelineWidget::SlideTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - PointerTool::InitiateDrag(clicked_item, trim_mode); - - // Sort blocks into tracks - QList blocks_per_track; - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - bool found = false; - - for (int i=0;iTrack()) { - blocks_per_track[i].blocks.append(b); - found = true; - break; - } - } - - if (!found) { - blocks_per_track.append({ghost->Track(), {b}}); - } - } - - // Make contiguous runs of blocks per each track - foreach (const TrackBlockListPair& p, blocks_per_track) { - // Blocks must be merged if any are non-adjacent - const TrackReference& track = p.track; - const QList& blocks = p.blocks; - - Block* earliest_block = blocks.first(); - Block* latest_block = blocks.first(); - - // Find the earliest and latest selected blocks - for (int j=1;jin() < earliest_block->in()) { - earliest_block = compare; - } - - if (compare->in() > latest_block->in()) { - latest_block = compare; - } - } - - // Add any blocks between these blocks that aren't already in the list - if (earliest_block != latest_block) { - Block* b = earliest_block; - while ((b = b->next()) != latest_block) { - if (!blocks.contains(b)) { - AddGhostFromBlock(b, track, Timeline::kMove); - } - } - } - - // Add surrounding blocks that will be trimming instead of moving - if (earliest_block->previous()) { - AddGhostFromBlock(earliest_block->previous(), track, Timeline::kTrimOut); - } - - if (latest_block->next()) { - AddGhostFromBlock(latest_block->next(), track, Timeline::kTrimIn); - } - } -} - -void TimelineWidget::SlideTool::FinishDrag(TimelineViewMouseEvent *event) -{ - Q_UNUSED(event) - - QVector info; - - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (!ghost->HasBeenAdjusted()) { - continue; - } - - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - info.append({parent()->GetTrackFromReference(ghost->Track()), - b, - ghost->mode(), - ghost->mode() == Timeline::kMove ? ghost->GetAdjustedIn() : ghost->AdjustedLength(), - ghost->mode() == Timeline::kMove ? ghost->In() : ghost->Length()}); - } - - if (!info.isEmpty()) { - Core::instance()->undo_stack()->push(new TrackSlideCommand(info)); - } + InitiateDragInternal(clicked_item, trim_mode, false, true, true); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 51f9425ce..04ce25b32 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -73,30 +73,13 @@ TimelineViewBlockItem *TimelineWidget::Tool::GetItemAtScenePos(const TimelineCoo return nullptr; } -rational TimelineWidget::Tool::ValidateTimeMovement(rational movement, const QVector ghosts) +rational TimelineWidget::Tool::ValidateTimeMovement(rational movement) { - foreach (TimelineViewGhostItem* ghost, ghosts) { + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { if (ghost->mode() != Timeline::kMove) { continue; } - Block* block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (block && block->type() == Block::kTransition) { - TransitionBlock* transition = static_cast(block); - - // Dual transitions are only allowed to move so that neither of their offsets are < 0 - if (transition->connected_in_block() && transition->connected_out_block()) { - if (movement > transition->out_offset()) { - movement = transition->out_offset(); - } - - if (movement < -transition->in_offset()) { - movement = -transition->in_offset(); - } - } - } - // Prevents any ghosts from going below 0:00:00 time if (ghost->In() + movement < 0) { movement = -ghost->In(); @@ -106,7 +89,7 @@ rational TimelineWidget::Tool::ValidateTimeMovement(rational movement, const QVe return movement; } -int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector ghosts) +int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector& ghosts) { foreach (TimelineViewGhostItem* ghost, ghosts) { if (ghost->mode() != Timeline::kMove) { @@ -115,7 +98,7 @@ int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector