diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc82036f6..539a5831f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ on: env: DOWNLOAD_TOOL: curl -fLOSs --retry 2 --retry-delay 60 UPLOAD_TOOL: curl -X POST --retry 2 --retry-delay 60 + CMAKE_ARGS: -DUSE_WERROR=ON -DBUILD_TESTS=ON jobs: linux: @@ -31,12 +32,12 @@ jobs: fail-fast: false matrix: include: - - build-type: RelWithDebInfo - cc-compiler: gcc - cxx-compiler: g++ - compiler-name: GCC 9.3.1 - cmake-gen: Ninja - os-name: Linux (CentOS 7) + #- build-type: RelWithDebInfo + # cc-compiler: gcc + # cxx-compiler: g++ + # compiler-name: GCC 9.3.1 + # cmake-gen: Ninja + # os-name: Linux (CentOS 7) - build-type: RelWithDebInfo cc-compiler: clang cxx-compiler: clang++ @@ -73,7 +74,8 @@ jobs: cmake .. -G "${{ matrix.cmake-gen }}" \ -DCMAKE_BUILD_TYPE="${{ matrix.build-type }}" \ -DCMAKE_C_COMPILER="${{ matrix.cc-compiler }}" \ - -DCMAKE_CXX_COMPILER="${{ matrix.cxx-compiler }}" + -DCMAKE_CXX_COMPILER="${{ matrix.cxx-compiler }}" \ + $CMAKE_ARGS - name: Build working-directory: build @@ -204,7 +206,8 @@ jobs: working-directory: ${{ runner.workspace }}/build run: | cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} -G "${{ matrix.cmake-gen }}" \ - -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \ + $CMAKE_ARGS - name: Build working-directory: ${{ runner.workspace }}/build @@ -356,7 +359,8 @@ jobs: PATH=$DEP_LOCATION:$DEP_LOCATION/bin:$DEP_LOCATION/include:$DEP_LOCATION/lib:$DEP_LOCATION/crashpad:$PATH \ cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DCMAKE_OSX_DEPLOYMENT_TARGET=${{ matrix.min-deploy }} -G "${{ matrix.cmake-gen }}" \ - -DCMAKE_OSX_ARCHITECTURES="${{ matrix.os-arch }}" + -DCMAKE_OSX_ARCHITECTURES="${{ matrix.os-arch }}" \ + $CMAKE_ARGS - name: Build working-directory: ${{ runner.workspace }}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index b46d11f6f..61f4adef4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,8 +19,8 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) project(olive-editor VERSION 0.2.0 LANGUAGES CXX) option(BUILD_DOXYGEN "Build Doxygen documentation" OFF) -option(BUILD_TESTS "Build unit tests" ON) -option(USE_WERROR "Error on compile warning" ON) +option(BUILD_TESTS "Build unit tests" OFF) +option(USE_WERROR "Error on compile warning" OFF) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -100,7 +100,6 @@ set(QT_LIBRARIES Gui Widgets OpenGL - Svg LinguistTools Concurrent ) @@ -121,7 +120,6 @@ list(APPEND OLIVE_LIBRARIES Qt5::Gui Qt5::Widgets Qt5::OpenGL - Qt5::Svg Qt5::Concurrent ) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index d438dc135..35a818bc0 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -35,7 +35,6 @@ add_subdirectory(panel) add_subdirectory(render) add_subdirectory(shaders) add_subdirectory(task) -add_subdirectory(threading) add_subdirectory(timeline) add_subdirectory(ts) add_subdirectory(tool) @@ -99,7 +98,7 @@ if (WIN32) # Set Windows application icon target_sources(olive-editor PRIVATE packaging/windows/resources.rc) - # Preserve folder structure in visual studio + # Preserve folder structure in visual studio source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${OLIVE_SOURCES}) elseif(APPLE) diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index 0fb9c215a..a64b1298f 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -280,19 +280,19 @@ AudioVisualWaveform::Sample AudioVisualWaveform::GetSummaryFromTime(const ration return AudioVisualWaveform::Sample(channel_count(), {0, 0}); } -void ExpandMinMaxChannel(const float *a, int start, int length, float &min_val, float &max_val) +void ExpandMinMaxChannel(const float *a, size_t length, float &min_val, float &max_val) { #if defined(Q_PROCESSOR_X86) || defined(Q_PROCESSOR_ARM) // SSE optimized // load the first 4 elements of 'a' into min and max (they are 4 * 32 = 128 bits) - __m128 max = _mm_loadu_ps(a + start); - __m128 min = _mm_loadu_ps(a + start); + __m128 max = _mm_loadu_ps(a); + __m128 min = _mm_loadu_ps(a); // loop over 'a' and compare current elements with min and max 4 by 4. // we need to make sure we don't read out of boundaries should 'a' length be not mod. 4 - for(int i = 4; i < length-4; i+=4) { - __m128 cur = _mm_loadu_ps(a + start + i); + for(size_t i = 4; i < length-4; i+=4) { + __m128 cur = _mm_loadu_ps(a + i); max = _mm_max_ps(max, cur); min = _mm_min_ps(min, cur); } @@ -316,8 +316,7 @@ void ExpandMinMaxChannel(const float *a, int start, int length, float &min_val, // I bet you don't find annotated low level code very often. #else // Standard unoptimized function - int end = start + length; - for (int i=start; iIsCancelled()) { return nullptr; } - return RetrieveVideoInternal(renderer, timecode, divider, cancelled); + if (cached_texture_ && cached_time_ == p.time) { + return cached_texture_; + } + + cached_texture_ = RetrieveVideoInternal(p); + cached_time_ = p.time; + + return cached_texture_; } -Decoder::RetrieveAudioStatus Decoder::RetrieveAudio(SampleBuffer &dest, const TimeRange &range, const AudioParams ¶ms, const QString& cache_path, Footage::LoopMode loop_mode, RenderMode::Mode mode) +Decoder::RetrieveAudioStatus Decoder::RetrieveAudio(SampleBuffer &dest, const TimeRange &range, const AudioParams ¶ms, const QString& cache_path, LoopMode loop_mode, RenderMode::Mode mode) { QMutexLocker locker(&mutex_); @@ -152,6 +166,8 @@ void Decoder::Close() UpdateLastAccessed(); + cached_texture_ = nullptr; + if (stream_.IsValid()) { CloseInternal(); stream_.Reset(); @@ -160,7 +176,7 @@ void Decoder::Close() } } -bool Decoder::ConformAudio(const QVector &output_filenames, const AudioParams ¶ms, const QAtomicInt *cancelled) +bool Decoder::ConformAudio(const QVector &output_filenames, const AudioParams ¶ms, CancelAtom *cancelled) { return ConformAudioInternal(output_filenames, params, cancelled); } @@ -264,15 +280,13 @@ int64_t Decoder::GetImageSequenceIndex(const QString &filename) return number_only.toLongLong(); } -TexturePtr Decoder::RetrieveVideoInternal(Renderer *renderer, const rational &timecode, const RetrieveVideoParams ÷r, const QAtomicInt *cancelled) +TexturePtr Decoder::RetrieveVideoInternal(const RetrieveVideoParams &p) { - Q_UNUSED(timecode) - Q_UNUSED(divider) - Q_UNUSED(cancelled) + Q_UNUSED(p) return nullptr; } -bool Decoder::ConformAudioInternal(const QVector &filenames, const AudioParams ¶ms, const QAtomicInt* cancelled) +bool Decoder::ConformAudioInternal(const QVector &filenames, const AudioParams ¶ms, CancelAtom *cancelled) { Q_UNUSED(filenames) Q_UNUSED(cancelled) @@ -280,7 +294,7 @@ bool Decoder::ConformAudioInternal(const QVector &filenames, const Audi return false; } -bool Decoder::RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, const TimeRange& range, Footage::LoopMode loop_mode, const AudioParams &input_params) +bool Decoder::RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, const TimeRange& range, LoopMode loop_mode, const AudioParams &input_params) { PlanarFileDevice input; if (input.open(conform_filenames, QFile::ReadOnly)) { @@ -290,7 +304,7 @@ bool Decoder::RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVecto const qint64 buffer_length_in_bytes = sample_buffer.sample_count() * input_params.bytes_per_sample_per_channel(); while (write_index < buffer_length_in_bytes) { - if (loop_mode == Footage::kLoopModeLoop) { + if (loop_mode == kLoopModeLoop) { while (read_index >= input.size()) { read_index -= input.size(); } @@ -335,7 +349,7 @@ void Decoder::UpdateLastAccessed() uint qHash(Decoder::CodecStream stream, uint seed) { - return qHash(stream.filename(), seed) ^ qHash(stream.stream(), seed); + return qHash(stream.filename(), seed) ^ qHash(stream.stream(), seed) ^ qHash(stream.block(), seed); } } diff --git a/app/codec/decoder.h b/app/codec/decoder.h index 061378be0..983efb753 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -34,7 +34,7 @@ extern "C" { #include "codec/frame.h" #include "codec/samplebuffer.h" #include "common/rational.h" -#include "node/project/footage/footage.h" +#include "node/block/block.h" #include "node/project/footage/footagedescription.h" #include "task/task.h" @@ -71,6 +71,12 @@ public: kIndexUnavailable }; + enum LoopMode { + kLoopModeOff, + kLoopModeLoop, + kLoopModeClamp + }; + Decoder(); /** @@ -81,17 +87,21 @@ public: virtual bool SupportsVideo(){return false;} virtual bool SupportsAudio(){return false;} + void IncrementAccessTime(qint64 t); + class CodecStream { public: CodecStream() : - stream_(-1) + stream_(-1), + block_(nullptr) { } - CodecStream(const QString& filename, int stream) : + CodecStream(const QString& filename, int stream, Block *block) : filename_(filename), - stream_(stream) + stream_(stream), + block_(block) { } @@ -125,11 +135,18 @@ public: return stream_; } + Block *block() const + { + return block_; + } + private: QString filename_; int stream_; + Block *block_; + }; /** @@ -147,29 +164,13 @@ public: struct RetrieveVideoParams { - RetrieveVideoParams() - { - divider = 1; - maximum_format = VideoParams::kFormatInvalid; - } - - int divider; - VideoParams::Format maximum_format; - - void reset() - { - *this = RetrieveVideoParams(); - } - - bool operator==(const RetrieveVideoParams& rhs) const - { - return divider == rhs.divider && maximum_format == rhs.maximum_format; - } - - bool operator!=(const RetrieveVideoParams& rhs) const - { - return !(*this == rhs); - } + Renderer *renderer = nullptr; + rational time; + int divider = 1; + VideoParams::Format maximum_format = VideoParams::kFormatInvalid; + CancelAtom *cancelled = nullptr; + VideoParams::ColorRange force_range = VideoParams::kColorRangeDefault; + VideoParams::Interlacing src_interlacing = VideoParams::kInterlaceNone; }; /** @@ -182,7 +183,7 @@ public: * * This function is thread safe and can only run while the decoder is open. \see Open() */ - TexturePtr RetrieveVideo(Renderer *renderer, const rational& timecode, const RetrieveVideoParams& params, const QAtomicInt *cancelled = nullptr); + TexturePtr RetrieveVideo(const RetrieveVideoParams& p); enum RetrieveAudioStatus { kInvalid = -1, @@ -199,7 +200,7 @@ public: * * This function is thread safe and can only run while the decoder is open. \see Open() */ - RetrieveAudioStatus RetrieveAudio(SampleBuffer &dest, const TimeRange& range, const AudioParams& params, const QString &cache_path, Footage::LoopMode loop_mode, RenderMode::Mode mode); + RetrieveAudioStatus RetrieveAudio(SampleBuffer &dest, const TimeRange& range, const AudioParams& params, const QString &cache_path, LoopMode loop_mode, RenderMode::Mode mode); /** * @brief Determine the last time this decoder instance was used in any way @@ -217,7 +218,7 @@ public: * * This function is re-entrant. */ - virtual FootageDescription Probe(const QString& filename, const QAtomicInt* cancelled) const = 0; + virtual FootageDescription Probe(const QString& filename, CancelAtom *cancelled) const = 0; /** * @brief Closes media/deallocates memory @@ -229,7 +230,7 @@ public: /** * @brief Conform audio stream */ - bool ConformAudio(const QVector &output_filenames, const AudioParams ¶ms, const QAtomicInt *cancelled = nullptr); + bool ConformAudio(const QVector &output_filenames, const AudioParams ¶ms, CancelAtom *cancelled = nullptr); /** * @brief Create a Decoder instance using a Decoder ID @@ -277,9 +278,9 @@ protected: * Sub-classes must override this function IF they support video. Function is already mutexed * so sub-classes don't need to worry about thread safety. */ - virtual TexturePtr RetrieveVideoInternal(Renderer *renderer, const rational& timecode, const RetrieveVideoParams& params, const QAtomicInt *cancelled); + virtual TexturePtr RetrieveVideoInternal(const RetrieveVideoParams& p); - virtual bool ConformAudioInternal(const QVector& filenames, const AudioParams ¶ms, const QAtomicInt* cancelled); + virtual bool ConformAudioInternal(const QVector& filenames, const AudioParams ¶ms, CancelAtom *cancelled); void SignalProcessingProgress(int64_t ts, int64_t duration); @@ -306,7 +307,7 @@ signals: private: void UpdateLastAccessed(); - bool RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, const TimeRange &range, Footage::LoopMode loop_mode, const AudioParams ¶ms); + bool RetrieveAudioFromConform(SampleBuffer &sample_buffer, const QVector &conform_filenames, const TimeRange &range, LoopMode loop_mode, const AudioParams ¶ms); CodecStream stream_; @@ -314,6 +315,9 @@ private: qint64 last_accessed_; + TexturePtr cached_texture_; + rational cached_time_; + }; uint qHash(Decoder::CodecStream stream, uint seed = 0); diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 458c71d8e..cb5080402 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -23,6 +23,7 @@ #include #include "common/timecodefunctions.h" +#include "common/xmlutils.h" #include "ffmpeg/ffmpegencoder.h" #include "oiio/oiioencoder.h" @@ -92,13 +93,22 @@ EncodingParams::EncodingParams() : video_is_image_sequence_(false), audio_enabled_(false), audio_bit_rate_(0), - subtitles_enabled_(false) + subtitles_enabled_(false), + subtitles_are_sidecar_(false), + video_scaling_method_(kStretch), + has_custom_range_(false) { } -void EncodingParams::SetFilename(const QString &filename) +QDir EncodingParams::GetPresetPath() { - filename_ = filename; + return QDir(FileFunctions::GetConfigurationLocation()).filePath(QStringLiteral("exportpresets")); +} + +QStringList EncodingParams::GetListOfPresets() +{ + QDir d = EncodingParams::GetPresetPath(); + return d.entryList(QDir::Files); } void EncodingParams::EnableVideo(const VideoParams &video_params, const ExportCodec::Codec &vcodec) @@ -121,134 +131,79 @@ void EncodingParams::EnableSubtitles(const ExportCodec::Codec &scodec) subtitles_codec_ = scodec; } -void EncodingParams::set_video_option(const QString &key, const QString &value) +void EncodingParams::EnableSidecarSubtitles(const ExportFormat::Format &sfmt, const ExportCodec::Codec &scodec) { - video_opts_.insert(key, value); + subtitles_enabled_ = true; + subtitles_are_sidecar_ = true; + subtitle_sidecar_fmt_ = sfmt; + subtitles_codec_ = scodec; } -void EncodingParams::set_video_bit_rate(const int64_t &rate) +void EncodingParams::DisableVideo() { - video_bit_rate_ = rate; + video_enabled_ = false; } -void EncodingParams::set_video_min_bit_rate(const int64_t &rate) +void EncodingParams::DisableAudio() { - video_min_bit_rate_ = rate; + audio_enabled_ = false; } -void EncodingParams::set_video_max_bit_rate(const int64_t &rate) +void EncodingParams::DisableSubtitles() { - video_max_bit_rate_ = rate; + subtitles_enabled_ = false; } -void EncodingParams::set_video_buffer_size(const int64_t &sz) +bool EncodingParams::Load(QXmlStreamReader *reader) { - video_buffer_size_ = sz; + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("export")) { + int version = 0; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("version")) { + version = attr.value().toInt(); + } + } + + switch (version) { + case 1: + return LoadV1(reader); + } + } else { + reader->skipCurrentElement(); + } + } + + return false; } -void EncodingParams::set_video_threads(const int &threads) +bool EncodingParams::Load(QIODevice *device) { - video_threads_ = threads; + QXmlStreamReader reader(device); + return Load(&reader); } -void EncodingParams::set_video_pix_fmt(const QString &s) +void EncodingParams::Save(QIODevice *device) const { - video_pix_fmt_ = s; -} - -const QString &EncodingParams::filename() const -{ - return filename_; -} - -bool EncodingParams::video_enabled() const -{ - return video_enabled_; -} - -const ExportCodec::Codec &EncodingParams::video_codec() const -{ - return video_codec_; -} - -const VideoParams &EncodingParams::video_params() const -{ - return video_params_; -} - -const QHash &EncodingParams::video_opts() const -{ - return video_opts_; -} - -const int64_t &EncodingParams::video_bit_rate() const -{ - return video_bit_rate_; -} - -const int64_t &EncodingParams::video_min_bit_rate() const -{ - return video_min_bit_rate_; -} - -const int64_t &EncodingParams::video_max_bit_rate() const -{ - return video_max_bit_rate_; -} - -const int64_t &EncodingParams::video_buffer_size() const -{ - return video_buffer_size_; -} - -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_; -} - -const ExportCodec::Codec &EncodingParams::audio_codec() const -{ - return audio_codec_; -} - -const AudioParams &EncodingParams::audio_params() const -{ - return audio_params_; -} - -bool EncodingParams::subtitles_enabled() const -{ - return subtitles_enabled_; -} - -ExportCodec::Codec EncodingParams::subtitles_codec() const -{ - return subtitles_codec_; -} - -const rational &EncodingParams::GetExportLength() const -{ - return export_length_; -} - -void EncodingParams::SetExportLength(const rational &export_length) -{ - export_length_ = export_length; + QXmlStreamWriter writer(device); + Save(&writer); } void EncodingParams::Save(QXmlStreamWriter *writer) const { + writer->writeStartDocument(); + + writer->writeStartElement(QStringLiteral("export")); + + writer->writeAttribute(QStringLiteral("version"), QString::number(kEncoderParamsVersion)); + writer->writeTextElement(QStringLiteral("filename"), filename_); + writer->writeTextElement(QStringLiteral("format"), QString::number(format_)); + + 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()); writer->writeStartElement(QStringLiteral("video")); @@ -262,10 +217,18 @@ void EncodingParams::Save(QXmlStreamWriter *writer) const 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("minbitrate"), QString::number(video_max_bit_rate_)); + writer->writeTextElement(QStringLiteral("minbitrate"), QString::number(video_min_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_)); + writer->writeTextElement(QStringLiteral("pixfmt"), video_pix_fmt_); + writer->writeTextElement(QStringLiteral("imgseq"), QString::number(video_is_image_sequence_)); + + writer->writeStartElement(QStringLiteral("color")); + writer->writeTextElement(QStringLiteral("output"), color_transform_.output()); + writer->writeEndElement(); // colortransform + + writer->writeTextElement(QStringLiteral("vscale"), QString::number(video_scaling_method_)); if (!video_opts_.isEmpty()) { writer->writeStartElement(QStringLiteral("opts")); @@ -297,7 +260,24 @@ void EncodingParams::Save(QXmlStreamWriter *writer) const writer->writeTextElement(QStringLiteral("format"), QString::number(audio_params_.format())); } + writer->writeStartElement(QStringLiteral("subtitles")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(subtitles_enabled_)); + + if (subtitles_enabled_) { + writer->writeTextElement(QStringLiteral("sidecar"), QString::number(subtitles_are_sidecar_)); + writer->writeTextElement(QStringLiteral("sidecarformat"), QString::number(subtitle_sidecar_fmt_)); + + writer->writeTextElement(QStringLiteral("codec"), QString::number(subtitles_codec_)); + } + + writer->writeEndElement(); // subtitles + writer->writeEndElement(); // audio + + writer->writeEndElement(); // export + + writer->writeEndDocument(); } Encoder* Encoder::CreateFromID(Type id, const EncodingParams& params) @@ -346,6 +326,11 @@ Encoder *Encoder::CreateFromFormat(ExportFormat::Format f, const EncodingParams return CreateFromID(GetTypeFromFormat(f), params); } +Encoder *Encoder::CreateFromParams(const EncodingParams ¶ms) +{ + return CreateFromFormat(params.format(), params); +} + QStringList Encoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const { return QStringList(); @@ -356,4 +341,157 @@ std::vector Encoder::GetSampleFormatsForCodec(ExportCodec:: return std::vector(); } +QMatrix4x4 EncodingParams::GenerateMatrix(EncodingParams::VideoScalingMethod method, + int source_width, int source_height, + int dest_width, int dest_height) +{ + QMatrix4x4 preview_matrix; + + if (method == EncodingParams::kStretch) { + return preview_matrix; + } + + float export_ar = static_cast(dest_width) / static_cast(dest_height); + float source_ar = static_cast(source_width) / static_cast(source_height); + + if (qFuzzyCompare(export_ar, source_ar)) { + return preview_matrix; + } + + if ((export_ar > source_ar) == (method == EncodingParams::kFit)) { + preview_matrix.scale(source_ar / export_ar, 1.0F); + } else { + preview_matrix.scale(1.0F, export_ar / source_ar); + } + + return preview_matrix; +} + +bool EncodingParams::LoadV1(QXmlStreamReader *reader) +{ + rational custom_range_in, custom_range_out; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("filename")) { + filename_ = reader->readElementText(); + } else if (reader->name() == QStringLiteral("format")) { + format_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("range")) { + has_custom_range_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("customrangein")) { + custom_range_in = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("customrangeout")) { + custom_range_out = rational::fromString(reader->readElementText()); + } else if (reader->name() == QStringLiteral("video")) { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("enabled")) { + video_enabled_ = attr.value().toInt(); + } + } + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("codec")) { + video_codec_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("width")) { + video_params_.set_width(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("height")) { + video_params_.set_height(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("format")) { + video_params_.set_format(static_cast(reader->readElementText().toInt())); + } else if (reader->name() == QStringLiteral("timebase")) { + video_params_.set_time_base(rational::fromString(reader->readElementText())); + } else if (reader->name() == QStringLiteral("divider")) { + video_params_.set_divider(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("bitrate")) { + video_bit_rate_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("minbitrate")) { + video_min_bit_rate_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("maxbitrate")) { + video_max_bit_rate_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("bufsize")) { + video_buffer_size_ = reader->readElementText().toLongLong(); + } else if (reader->name() == QStringLiteral("threads")) { + video_threads_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("pixfmt")) { + video_pix_fmt_ = reader->readElementText(); + } else if (reader->name() == QStringLiteral("imgseq")) { + video_is_image_sequence_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("color")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("output")) { + color_transform_ = reader->readElementText(); + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("vscale")) { + video_scaling_method_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("opts")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("entry")) { + QString key, value; + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("key")) { + key = reader->readElementText(); + } else if (reader->name() == QStringLiteral("value")) { + value = reader->readElementText(); + } else { + reader->skipCurrentElement(); + } + } + set_video_option(key, value); + } else { + reader->skipCurrentElement(); + } + } + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("audio")) { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("enabled")) { + audio_enabled_ = attr.value().toInt(); + } + } + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("codec")) { + audio_codec_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("samplerate")) { + audio_params_.set_sample_rate(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("channellayout")) { + audio_params_.set_channel_layout(reader->readElementText().toULongLong()); + } else if (reader->name() == QStringLiteral("format")) { + audio_params_.set_format(static_cast(reader->readElementText().toInt())); + } else { + reader->skipCurrentElement(); + } + } + } else if (reader->name() == QStringLiteral("subtitles")) { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("enabled")) { + subtitles_enabled_ = attr.value().toInt(); + } + } + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("sidecar")) { + subtitles_are_sidecar_ = reader->readElementText().toInt(); + } else if (reader->name() == QStringLiteral("sidecarformat")) { + subtitle_sidecar_fmt_ = static_cast(reader->readElementText().toInt()); + } else if (reader->name() == QStringLiteral("codec")) { + subtitles_codec_ = static_cast(reader->readElementText().toInt()); + } else { + reader->skipCurrentElement(); + } + } + } else { + reader->skipCurrentElement(); + } + } + + return true; +} + } diff --git a/app/codec/encoder.h b/app/codec/encoder.h index e1535290b..e33f1de9a 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -41,69 +41,109 @@ namespace olive { class Encoder; using EncoderPtr = std::shared_ptr; -class EncodingParams { +class EncodingParams +{ public: + enum VideoScalingMethod { + kFit, + kStretch, + kCrop + }; + EncodingParams(); - void SetFilename(const QString& filename); + static QDir GetPresetPath(); + static QStringList GetListOfPresets(); + + bool IsValid() const + { + return video_enabled_ || audio_enabled_ || subtitles_enabled_; + } + + void SetFilename(const QString& filename) { filename_ = filename; } void EnableVideo(const VideoParams& video_params, const ExportCodec::Codec& vcodec); void EnableAudio(const AudioParams& audio_params, const ExportCodec::Codec &acodec); void EnableSubtitles(const ExportCodec::Codec &scodec); + void EnableSidecarSubtitles(const ExportFormat::Format &sfmt, const ExportCodec::Codec &scodec); - void set_video_option(const QString& key, const QString& value); - void set_video_bit_rate(const int64_t& rate); - void set_video_min_bit_rate(const int64_t& rate); - void set_video_max_bit_rate(const int64_t& rate); - void set_video_buffer_size(const int64_t& sz); - void set_video_threads(const int& threads); - void set_video_pix_fmt(const QString& s); - void set_video_is_image_sequence(bool s) + void DisableVideo(); + void DisableAudio(); + void DisableSubtitles(); + + const ExportFormat::Format &format() const { return format_; } + void set_format(const ExportFormat::Format &format) { format_ = format; } + + void set_video_option(const QString& key, const QString& value) { video_opts_.insert(key, value); } + void set_video_bit_rate(const int64_t& rate) { video_bit_rate_ = rate; } + void set_video_min_bit_rate(const int64_t& rate) { video_min_bit_rate_ = rate; } + void set_video_max_bit_rate(const int64_t& rate) { video_max_bit_rate_ = rate; } + void set_video_buffer_size(const int64_t& sz) { video_buffer_size_ = sz; } + void set_video_threads(const int& threads) { video_threads_ = threads; } + void set_video_pix_fmt(const QString& s) { video_pix_fmt_ = s; } + void set_video_is_image_sequence(bool s) { video_is_image_sequence_ = s; } + void set_color_transform(const ColorTransform& color_transform) { color_transform_ = color_transform; } + + const QString& filename() const { return filename_; } + + bool video_enabled() const { return video_enabled_; } + const ExportCodec::Codec& video_codec() const { return video_codec_; } + const VideoParams& video_params() const { return video_params_; } + const QHash& video_opts() const { return video_opts_; } + QString video_option(const QString &key) const { return video_opts_.value(key); } + bool has_video_opt(const QString &key) const { return video_opts_.contains(key); } + const int64_t& video_bit_rate() const { return video_bit_rate_; } + const int64_t& video_min_bit_rate() const { return video_min_bit_rate_; } + const int64_t& video_max_bit_rate() const { return video_max_bit_rate_; } + const int64_t& video_buffer_size() const { return video_buffer_size_; } + const int& video_threads() const { return video_threads_; } + const QString& video_pix_fmt() const { return video_pix_fmt_; } + bool video_is_image_sequence() const { return video_is_image_sequence_; } + const ColorTransform& color_transform() const { return color_transform_; } + + bool audio_enabled() const { return audio_enabled_; } + const ExportCodec::Codec &audio_codec() const { return audio_codec_; } + const AudioParams& audio_params() const { return audio_params_; } + const int64_t& audio_bit_rate() const { return audio_bit_rate_; } + + void set_audio_bit_rate(const int64_t& b) { audio_bit_rate_ = b; } + + bool subtitles_enabled() const { return subtitles_enabled_; } + bool subtitles_are_sidecar() const { return subtitles_are_sidecar_; } + ExportFormat::Format subtitle_sidecar_fmt() const { return subtitle_sidecar_fmt_; } + ExportCodec::Codec subtitles_codec() const { return subtitles_codec_; } + + const rational& GetExportLength() const { return export_length_; } + void SetExportLength(const rational& export_length) { export_length_ = export_length; } + + bool Load(QIODevice *device); + bool Load(QXmlStreamReader *reader); + + void Save(QIODevice *device) const; + void Save(QXmlStreamWriter* writer) const; + + bool has_custom_range() const { return has_custom_range_; } + const TimeRange& custom_range() const { return custom_range_; } + void set_custom_range(const TimeRange& custom_range) { - video_is_image_sequence_ = s; + has_custom_range_ = true; + custom_range_ = custom_range; } - const QString& filename() const; + const VideoScalingMethod& video_scaling_method() const { return video_scaling_method_; } + void set_video_scaling_method(const VideoScalingMethod& video_scaling_method) { video_scaling_method_ = video_scaling_method; } - bool video_enabled() const; - const ExportCodec::Codec& video_codec() const; - const VideoParams& video_params() const; - const QHash& video_opts() const; - const int64_t& video_bit_rate() const; - const int64_t& video_min_bit_rate() const; - 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 video_is_image_sequence() const - { - return video_is_image_sequence_; - } - - bool audio_enabled() const; - const ExportCodec::Codec &audio_codec() const; - const AudioParams& audio_params() const; - - const int64_t& audio_bit_rate() const - { - return audio_bit_rate_; - } - - void set_audio_bit_rate(const int64_t& b) - { - audio_bit_rate_ = b; - } - - bool subtitles_enabled() const; - ExportCodec::Codec subtitles_codec() const; - - const rational& GetExportLength() const; - void SetExportLength(const rational& GetExportLength); - - virtual void Save(QXmlStreamWriter* writer) const; + static QMatrix4x4 GenerateMatrix(VideoScalingMethod method, + int source_width, int source_height, + int dest_width, int dest_height); private: + static const int kEncoderParamsVersion = 1; + + bool LoadV1(QXmlStreamReader *reader); + QString filename_; + ExportFormat::Format format_; bool video_enabled_; ExportCodec::Codec video_codec_; @@ -116,6 +156,7 @@ private: int video_threads_; QString video_pix_fmt_; bool video_is_image_sequence_; + ColorTransform color_transform_; bool audio_enabled_; ExportCodec::Codec audio_codec_; @@ -123,9 +164,15 @@ private: int64_t audio_bit_rate_; bool subtitles_enabled_; + bool subtitles_are_sidecar_; + ExportFormat::Format subtitle_sidecar_fmt_; ExportCodec::Codec subtitles_codec_; rational export_length_; + VideoScalingMethod video_scaling_method_; + + bool has_custom_range_; + TimeRange custom_range_; }; @@ -154,6 +201,8 @@ public: static Encoder *CreateFromFormat(ExportFormat::Format f, const EncodingParams ¶ms); + static Encoder *CreateFromParams(const EncodingParams ¶ms); + virtual QStringList GetPixelFormatsForCodec(ExportCodec::Codec c) const; virtual std::vector GetSampleFormatsForCodec(ExportCodec::Codec c) const; diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 977f1c251..3cb61d8f1 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -63,7 +63,6 @@ FFmpegDecoder::FFmpegDecoder() : native_output_pix_fmt_(VideoParams::kFormatInvalid), working_frame_(nullptr), working_packet_(nullptr), - is_working_(false), cache_at_zero_(false), cache_at_eof_(false) { @@ -80,6 +79,7 @@ bool FFmpegDecoder::OpenInternal() working_frame_ = av_frame_alloc(); working_packet_ = av_packet_alloc(); + frame_rate_tb_ = rational::NaN; return true; } @@ -142,28 +142,32 @@ bool FFmpegDecoder::OpenInternal() return output_frame; }*/ -TexturePtr FFmpegDecoder::RetrieveVideoInternal(Renderer *renderer, const rational &timecode, const RetrieveVideoParams ¶ms, const QAtomicInt *cancelled) +TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) { - if (AVFramePtr f = RetrieveFrame(timecode, cancelled)) { - if (cancelled && *cancelled) { + if (AVFramePtr f = RetrieveFrame(p.time, p.cancelled)) { + if (p.cancelled && p.cancelled->IsCancelled()) { return nullptr; } - if (InitScaler(f.get(), params)) { + int &src_fmt = f.get()->format; + src_fmt = FFmpegUtils::ConvertJPEGSpaceToRegularSpace(static_cast(src_fmt)); + + f->color_range = p.force_range == VideoParams::kColorRangeFull ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; + + if (InitScaler(f.get(), p)) { VideoParams vp(instance_.avstream()->codecpar->width, instance_.avstream()->codecpar->height, native_output_pix_fmt_, native_channel_count_, av_guess_sample_aspect_ratio(instance_.fmt_ctx(), instance_.avstream(), nullptr), VideoParams::kInterlaceNone, - params.divider); + p.divider); TexturePtr tex = nullptr; - const bool hwscale = true; + bool hwscale = true; // Attempt to use GLSL shader for faster YUV to RGB conversion if (hwscale) { - AVPixelFormat src_fmt = AVPixelFormat(f.get()->format); if (src_fmt == AV_PIX_FMT_YUV420P || src_fmt == AV_PIX_FMT_YUV422P || src_fmt == AV_PIX_FMT_YUV444P @@ -175,7 +179,7 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(Renderer *renderer, const ration || src_fmt == AV_PIX_FMT_YUV444P12LE) { if (Yuv2RgbShader.isNull()) { // Compile shader - Yuv2RgbShader = renderer->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/yuv2rgb.frag")))); + Yuv2RgbShader = p.renderer->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/yuv2rgb.frag")))); } if (!Yuv2RgbShader.isNull()) { @@ -207,7 +211,7 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(Renderer *renderer, const ration plane_params.set_channel_count(1); plane_params.set_divider(1); plane_params.set_format(native_internal_pix_fmt_); - TexturePtr y_plane = renderer->CreateTexture(plane_params, f->data[0], f->linesize[0] / px_size); + TexturePtr y_plane = p.renderer->CreateTexture(plane_params, f->data[0], f->linesize[0] / px_size); if (src_fmt == AV_PIX_FMT_YUV420P || src_fmt == AV_PIX_FMT_YUV422P @@ -224,17 +228,47 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(Renderer *renderer, const ration plane_params.set_height(plane_params.height()/2); } - TexturePtr u_plane = renderer->CreateTexture(plane_params, f->data[1], f->linesize[1] / px_size); - TexturePtr v_plane = renderer->CreateTexture(plane_params, f->data[2], f->linesize[2] / px_size); + TexturePtr u_plane = p.renderer->CreateTexture(plane_params, f->data[1], f->linesize[1] / px_size); + TexturePtr v_plane = p.renderer->CreateTexture(plane_params, f->data[2], f->linesize[2] / px_size); ShaderJob job; job.Insert(QStringLiteral("y_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(y_plane))); job.Insert(QStringLiteral("u_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(u_plane))); job.Insert(QStringLiteral("v_channel"), NodeValue(NodeValue::kTexture, QVariant::fromValue(v_plane))); job.Insert(QStringLiteral("bits_per_pixel"), NodeValue(NodeValue::kInt, bits_per_pixel)); + job.Insert(QStringLiteral("full_range"), NodeValue(NodeValue::kBoolean, f->color_range == AVCOL_RANGE_JPEG)); - tex = renderer->CreateTexture(vp); - renderer->BlitToTexture(Yuv2RgbShader, job, tex.get(), false); + const int *yuv_coeffs = sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(f.get()->colorspace)); + job.Insert(QStringLiteral("yuv_crv"), NodeValue(NodeValue::kInt, yuv_coeffs[0])); + job.Insert(QStringLiteral("yuv_cgu"), NodeValue(NodeValue::kInt, yuv_coeffs[2])); + job.Insert(QStringLiteral("yuv_cgv"), NodeValue(NodeValue::kInt, yuv_coeffs[3])); + job.Insert(QStringLiteral("yuv_cbu"), NodeValue(NodeValue::kInt, yuv_coeffs[1])); + + int interlacing = 0; + if (p.src_interlacing != VideoParams::kInterlaceNone) { + if (frame_rate_tb_.isNull()) { + frame_rate_tb_ = av_guess_frame_rate(instance_.fmt_ctx(), instance_.avstream(), f.get()); + + // Double frame rate for interlaced fields + frame_rate_tb_ *= 2; + + // Flip frame rate so it can be used as a timebase + frame_rate_tb_.flip(); + } + + int64_t req = Timecode::time_to_timestamp(p.time, frame_rate_tb_); + int64_t frm = Timecode::rescale_timestamp(f->pts - instance_.avstream()->start_time, instance_.avstream()->time_base, frame_rate_tb_); + + bool first = (req == frm); + bool top_first = (p.src_interlacing == VideoParams::kInterlacedTopFirst); + + interlacing = (first == top_first) ? 1 : 2; + } + job.Insert(QStringLiteral("interlacing"), NodeValue(NodeValue::kInt, interlacing)); + job.Insert(QStringLiteral("pixel_height"), NodeValue(NodeValue::kInt, f->height)); + + tex = p.renderer->CreateTexture(vp); + p.renderer->BlitToTexture(Yuv2RgbShader, job, tex.get(), false); } } } @@ -242,6 +276,7 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(Renderer *renderer, const ration if (!tex) { // Fallback to software pixel format conversion int r; + r = av_buffersrc_add_frame_flags(buffersrc_ctx_, f.get(), AV_BUFFERSRC_FLAG_KEEP_REF); if (r < 0) { return nullptr; @@ -251,7 +286,7 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(Renderer *renderer, const ration return nullptr; } - tex = renderer->CreateTexture(vp, working_frame_->data[0], working_frame_->linesize[0] / vp.GetBytesPerPixel()); + tex = p.renderer->CreateTexture(vp, working_frame_->data[0], working_frame_->linesize[0] / vp.GetBytesPerPixel()); av_frame_unref(working_frame_); } @@ -290,7 +325,7 @@ QString FFmpegDecoder::id() const return QStringLiteral("ffmpeg"); } -FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicInt *cancelled) const +FootageDescription FFmpegDecoder::Probe(const QString &filename, CancelAtom *cancelled) const { // Return value FootageDescription desc(id()); @@ -416,6 +451,7 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn stream.set_start_time(avstream->start_time); stream.set_time_base(avstream->time_base); stream.set_duration(avstream->duration); + stream.set_color_range(avstream->codecpar->color_range == AVCOL_RANGE_JPEG ? VideoParams::kColorRangeFull : VideoParams::kColorRangeLimited); // Defaults to false, requires user intervention if incorrect stream.set_premultiplied_alpha(false); @@ -500,6 +536,8 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn } + desc.SetStreamCount(fmt_ctx->nb_streams); + } // Free all memory @@ -515,7 +553,7 @@ QString FFmpegDecoder::FFmpegError(int error_code) return QStringLiteral("%1 %2").arg(QString::number(error_code), err); } -bool FFmpegDecoder::ConformAudioInternal(const QVector &filenames, const AudioParams ¶ms, const QAtomicInt *cancelled) +bool FFmpegDecoder::ConformAudioInternal(const QVector &filenames, const AudioParams ¶ms, CancelAtom *cancelled) { // Iterate through each audio frame and extract the PCM data @@ -565,7 +603,7 @@ bool FFmpegDecoder::ConformAudioInternal(const QVector &filenames, cons while (true) { // Check if we have a `cancelled` ptr and its value - if (cancelled && *cancelled) { + if (cancelled && cancelled->IsCancelled()) { break; } @@ -745,12 +783,12 @@ void FFmpegDecoder::ClearFrameCache() } } -AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, const QAtomicInt *cancelled) +AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, CancelAtom *cancelled) { int64_t target_ts = GetTimeInTimebaseUnits(time, instance_.avstream()->time_base, instance_.avstream()->start_time); const int64_t min_seek = -instance_.avstream()->start_time; - int64_t seek_ts = target_ts; + int64_t seek_ts = std::max(min_seek, target_ts - MaximumQueueSize()); bool still_seeking = false; if (time != kAnyTimecode) { @@ -783,7 +821,7 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, const QAtomicInt * while (true) { // Break out of loop if we've cancelled - if (cancelled && *cancelled) { + if (cancelled && cancelled->IsCancelled()) { break; } @@ -794,7 +832,7 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, const QAtomicInt * // Pull from the decoder ret = instance_.GetFrame(working_packet_, filtered.get()); - if (cancelled && *cancelled) { + if (cancelled && cancelled->IsCancelled()) { break; } @@ -839,7 +877,7 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, const QAtomicInt * } else { // Cut down to thread count - 1 before we acquire a new frame - if (cached_frames_.size() == size_t(QThread::idealThreadCount())) { + if (cached_frames_.size() > size_t(MaximumQueueSize())) { RemoveFirstFrame(); } @@ -879,7 +917,12 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, const QAtomicInt * bool FFmpegDecoder::InitScaler(AVFrame *input, const RetrieveVideoParams& params) { - if (params == filter_params_ && filter_graph_ && input_fmt_ == input->format) { + if (params.divider == filter_params_.divider + && params.force_range == filter_params_.force_range + && params.maximum_format == filter_params_.maximum_format + && params.src_interlacing == filter_params_.src_interlacing + && filter_graph_ + && input_fmt_ == input->format) { // We have an appropriate filter for these parameters, just return true return true; } @@ -944,6 +987,20 @@ bool FFmpegDecoder::InitScaler(AVFrame *input, const RetrieveVideoParams& params // Link filters as necessary AVFilterContext *last_filter = buffersrc_ctx_; + // Add deinterlace filter if necessary + if (filter_params_.src_interlacing != VideoParams::kInterlaceNone) { + AVFilterContext* deint_filter; + + snprintf(filter_args, kFilterArgSz, "mode=1:parity=%s", + filter_params_.src_interlacing == VideoParams::kInterlacedTopFirst ? "0" : "1"); + + avfilter_graph_create_filter(&deint_filter, avfilter_get_by_name("yadif"), "deint", filter_args, nullptr, filter_graph_); + + avfilter_link(last_filter, 0, deint_filter, 0); + + last_filter = deint_filter; + } + // Add scale filter if necessary int dst_width, dst_height; if (filter_params_.divider > 1) { @@ -1040,6 +1097,15 @@ void FFmpegDecoder::RemoveFirstFrame() cache_at_zero_ = false; } +int FFmpegDecoder::MaximumQueueSize() +{ + // Fairly arbitrary size. This used to need to be the number of current threads to ensure any + // thread that arrived would have its frame available, but if we only have one render thread, + // that's no longer a concern. Now, this value could technically be 1, but some memory cache + // may be useful for reversing. This value may be tweaked over time. + return 2; +} + FFmpegDecoder::Instance::Instance() : fmt_ctx_(nullptr), codec_ctx_(nullptr), diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index 80c46fb1f..8df5a67bc 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -25,27 +25,22 @@ #include extern "C" { +#include #include #include #include #include } -#include #include #include #include #include "codec/decoder.h" +#include "common/ffmpegutils.h" namespace olive { -using AVFramePtr = std::shared_ptr; -inline AVFramePtr CreateAVFramePtr(AVFrame *f) -{ - return std::shared_ptr(f, [](AVFrame *g){ av_frame_free(&g); }); -} - /** * @brief A Decoder derivative that wraps FFmpeg functions as on Olive decoder */ @@ -64,12 +59,12 @@ public: virtual bool SupportsVideo() override{return true;} virtual bool SupportsAudio() override{return true;} - virtual FootageDescription Probe(const QString &filename, const QAtomicInt *cancelled) const override; + virtual FootageDescription Probe(const QString &filename, CancelAtom *cancelled) const override; protected: virtual bool OpenInternal() override; - virtual TexturePtr RetrieveVideoInternal(Renderer *renderer, const rational& timecode, const RetrieveVideoParams& params, const QAtomicInt *cancelled) override; - virtual bool ConformAudioInternal(const QVector& filenames, const AudioParams ¶ms, const QAtomicInt* cancelled) override; + virtual TexturePtr RetrieveVideoInternal(const RetrieveVideoParams& p) override; + virtual bool ConformAudioInternal(const QVector& filenames, const AudioParams ¶ms, CancelAtom *cancelled) override; virtual void CloseInternal() override; private: @@ -151,10 +146,12 @@ private: void ClearFrameCache(); - AVFramePtr RetrieveFrame(const rational &time, const QAtomicInt *cancelled); + AVFramePtr RetrieveFrame(const rational &time, CancelAtom *cancelled); void RemoveFirstFrame(); + static int MaximumQueueSize(); + RetrieveVideoParams filter_params_; AVFilterGraph* filter_graph_; AVFilterContext* buffersrc_ctx_; @@ -163,6 +160,7 @@ private: VideoParams::Format native_internal_pix_fmt_; VideoParams::Format native_output_pix_fmt_; int native_channel_count_; + rational frame_rate_tb_; AVFrame *working_frame_; AVPacket *working_packet_; @@ -171,9 +169,6 @@ private: std::list cached_frames_; - bool is_working_; - QMutex is_working_mutex_; - bool cache_at_zero_; bool cache_at_eof_; diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index d5dba8c6d..192e03305 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -21,6 +21,8 @@ #include "ffmpegencoder.h" extern "C" { +#include +#include #include } @@ -36,8 +38,9 @@ FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms) : fmt_ctx_(nullptr), video_stream_(nullptr), video_codec_ctx_(nullptr), - video_alpha_scale_ctx_(nullptr), - video_noalpha_scale_ctx_(nullptr), + video_scale_ctx_(nullptr), + video_buffersrc_ctx_(nullptr), + video_buffersink_ctx_(nullptr), audio_stream_(nullptr), audio_codec_ctx_(nullptr), audio_resample_ctx_(nullptr), @@ -54,6 +57,11 @@ QStringList FFmpegEncoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const if (codec_info) { for (int i=0; codec_info->pix_fmts[i]!=-1; i++) { + if (FFmpegUtils::ConvertJPEGSpaceToRegularSpace(codec_info->pix_fmts[i]) != codec_info->pix_fmts[i]) { + // This is a deprecated "JPEG" space, skip it + continue; + } + const char* pix_fmt_name = av_get_pix_fmt_name(codec_info->pix_fmts[i]); pix_fmts.append(pix_fmt_name); } @@ -142,29 +150,59 @@ bool FFmpegEncoder::Open() // This is the pixel format the encoder wants to encode to AVPixelFormat encoder_pix_fmt = video_codec_ctx_->pix_fmt; - // Set up a scaling context - if the native pixel format is not equal to the encoder's, we'll need to convert it - // before encoding. Even if we don't, this may be useful for converting between linesizes, etc. - video_alpha_scale_ctx_ = sws_getContext(params().video_params().width(), - params().video_params().height(), - src_alpha_pix_fmt, - params().video_params().width(), - params().video_params().height(), - encoder_pix_fmt, - 0, - nullptr, - nullptr, - nullptr); + video_scale_ctx_ = avfilter_graph_alloc(); + if (!video_scale_ctx_) { + return false; + } - video_noalpha_scale_ctx_ = sws_getContext(params().video_params().width(), - params().video_params().height(), - src_noalpha_pix_fmt, - params().video_params().width(), - params().video_params().height(), - encoder_pix_fmt, - 0, - nullptr, - nullptr, - nullptr); + static const int FILTER_ARG_SZ = 1024; + char filter_args[FILTER_ARG_SZ]; + + snprintf(filter_args, FILTER_ARG_SZ, "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", + params().video_params().effective_width(), + params().video_params().effective_height(), + src_alpha_pix_fmt, + params().video_params().time_base().numerator(), + params().video_params().time_base().denominator(), + params().video_params().pixel_aspect_ratio().numerator(), + params().video_params().pixel_aspect_ratio().denominator()); + + avfilter_graph_create_filter(&video_buffersrc_ctx_, avfilter_get_by_name("buffer"), "in", filter_args, nullptr, video_scale_ctx_); + avfilter_graph_create_filter(&video_buffersink_ctx_, avfilter_get_by_name("buffersink"), "out", nullptr, nullptr, video_scale_ctx_); + + AVFilterContext *last_filter = video_buffersrc_ctx_; + + { + // Set color range + AVFilterContext* range_filter; + + snprintf(filter_args, FILTER_ARG_SZ, "in_range=full:out_range=%s", + params().video_params().color_range() == VideoParams::kColorRangeFull ? "full" : "limited"); + + avfilter_graph_create_filter(&range_filter, avfilter_get_by_name("scale"), "range", filter_args, nullptr, video_scale_ctx_); + + avfilter_link(last_filter, 0, range_filter, 0); + last_filter = range_filter; + } + + if (src_alpha_pix_fmt != encoder_pix_fmt) { + // Transform pixel format + AVFilterContext* format_filter; + + snprintf(filter_args, FILTER_ARG_SZ, "pix_fmts=%u", encoder_pix_fmt); + + avfilter_graph_create_filter(&format_filter, avfilter_get_by_name("format"), "format", filter_args, nullptr, video_scale_ctx_); + + avfilter_link(last_filter, 0, format_filter, 0); + last_filter = format_filter; + } + + avfilter_link(last_filter, 0, video_buffersink_ctx_, 0); + + if (avfilter_graph_config(video_scale_ctx_, nullptr) < 0) { + SetError(tr("Failed to configure filter graph")); + return false; + } } // Initialize an audio stream if it's enabled @@ -203,67 +241,41 @@ bool FFmpegEncoder::Open() bool FFmpegEncoder::WriteFrame(FramePtr frame, rational time) { - bool success = false; - - AVFrame* encoded_frame = av_frame_alloc(); - - int error_code; - const char* input_data; - int input_linesize; - - // Frame must be video - encoded_frame->width = frame->width(); - 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(tr("Failed to create AVFrame buffer"), error_code); - goto fail; - } - // We may need to convert this frame to a frame that swscale will understand if (frame->format() != video_conversion_fmt_) { frame = frame->convert(video_conversion_fmt_); } // Use swscale context to convert formats/linesizes - input_data = frame->const_data(); - input_linesize = frame->linesize_bytes(); + AVFramePtr input_frame = CreateAVFramePtr(av_frame_alloc()); + input_frame->width = frame->width(); + input_frame->height = frame->height(); + input_frame->format = FFmpegUtils::GetFFmpegPixelFormat(frame->format(), frame->channel_count()); + input_frame->data[0] = reinterpret_cast(frame->data()); + input_frame->linesize[0] = frame->linesize_bytes(); - error_code = sws_scale((frame->channel_count() == VideoParams::kRGBAChannelCount) ? video_alpha_scale_ctx_ : video_noalpha_scale_ctx_, - reinterpret_cast(&input_data), - &input_linesize, - 0, - frame->height(), - encoded_frame->data, - encoded_frame->linesize); + input_frame->color_primaries = video_codec_ctx_->color_primaries; + input_frame->color_trc = video_codec_ctx_->color_trc; + input_frame->colorspace = video_codec_ctx_->colorspace; + input_frame->color_range = video_codec_ctx_->color_range; + int r; + r = av_buffersrc_add_frame_flags(video_buffersrc_ctx_, input_frame.get(), AV_BUFFERSRC_FLAG_KEEP_REF); + if (r < 0) { + FFmpegError(tr("Failed to add frame to filter graph"), r); + return false; + } - if (error_code < 0) { - FFmpegError(tr("Failed to scale frame"), error_code); - goto fail; + AVFramePtr encoded_frame = CreateAVFramePtr(av_frame_alloc()); + r = av_buffersink_get_frame(video_buffersink_ctx_, encoded_frame.get()); + if (r < 0) { + FFmpegError(tr("Failed to retrieve frame from buffer sink"), r); + return false; } encoded_frame->pts = qRound64(time.toDouble() / av_q2d(video_codec_ctx_->time_base)); - success = WriteAVFrame(encoded_frame, video_codec_ctx_, video_stream_); - -fail: - av_frame_free(&encoded_frame); - - return success; + return WriteAVFrame(encoded_frame.get(), video_codec_ctx_, video_stream_); } bool FFmpegEncoder::WriteAudio(const SampleBuffer &audio) @@ -484,14 +496,11 @@ void FFmpegEncoder::Close() audio_frame_ = nullptr; } - if (video_alpha_scale_ctx_) { - sws_freeContext(video_alpha_scale_ctx_); - video_alpha_scale_ctx_ = nullptr; - } - - if (video_noalpha_scale_ctx_) { - sws_freeContext(video_noalpha_scale_ctx_); - video_noalpha_scale_ctx_ = nullptr; + if (video_scale_ctx_) { + avfilter_graph_free(&video_scale_ctx_); + video_scale_ctx_ = nullptr; + video_buffersrc_ctx_ = nullptr; + video_buffersink_ctx_ = nullptr; } if (video_codec_ctx_) { @@ -606,6 +615,7 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV codec_ctx->time_base = params().video_params().frame_rate_as_time_base().toAVRational(); codec_ctx->framerate = params().video_params().frame_rate().toAVRational(); codec_ctx->pix_fmt = av_get_pix_fmt(params().video_pix_fmt().toUtf8()); + codec_ctx->color_range = params().video_params().color_range() == VideoParams::kColorRangeFull ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; if (params().video_params().interlacing() != VideoParams::kInterlaceNone) { // FIXME: I actually don't know what these flags do, the documentation helpfully doesn't @@ -628,7 +638,9 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV // Set custom options { for (auto i=params().video_opts().begin();i!=params().video_opts().end();i++) { - av_opt_set(codec_ctx->priv_data, i.key().toUtf8(), i.value().toUtf8(), AV_OPT_SEARCH_CHILDREN); + if (!i.key().startsWith(QStringLiteral("ove_"))) { + av_opt_set(codec_ctx->priv_data, i.key().toUtf8(), i.value().toUtf8(), AV_OPT_SEARCH_CHILDREN); + } } if (params().video_bit_rate() > 0) { @@ -646,6 +658,18 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV if (params().video_buffer_size() > 0) { codec_ctx->rc_buffer_size = static_cast(params().video_buffer_size()); } + + // nclc tags. See https://ffmpeg.org/doxygen/4.0/pixfmt_8h.html#ad384ee5a840bafd73daef08e6d9cafe7 + // ffprobe -v error -show_format -show_streams "C:\Users\Tom\Documents\srgb correct tags.mov" + if (params().color_transform().output().contains(QStringLiteral("sRGB"), Qt::CaseInsensitive)) { + codec_ctx->color_primaries = AVCOL_PRI_BT709; + codec_ctx->color_trc = AVCOL_TRC_IEC61966_2_1; + codec_ctx->colorspace = AVCOL_SPC_BT709; + } else { // Assume Rec.709 + codec_ctx->color_primaries = AVCOL_PRI_BT709; + codec_ctx->color_trc = AVCOL_TRC_BT709; + codec_ctx->colorspace = AVCOL_SPC_BT709; + } } } else if (type == AVMEDIA_TYPE_AUDIO) { diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index 28786b9ce..9a9f6cae3 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -23,8 +23,8 @@ extern "C" { #include +#include #include -#include #include #include } @@ -88,8 +88,9 @@ private: AVStream* video_stream_; AVCodecContext* video_codec_ctx_; - SwsContext* video_alpha_scale_ctx_; - SwsContext* video_noalpha_scale_ctx_; + AVFilterGraph *video_scale_ctx_; + AVFilterContext *video_buffersrc_ctx_; + AVFilterContext *video_buffersink_ctx_; VideoParams::Format video_conversion_fmt_; AVStream* audio_stream_; diff --git a/app/codec/footagemeta.h b/app/codec/footagemeta.h deleted file mode 100644 index 4d23025f1..000000000 --- a/app/codec/footagemeta.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef FOOTAGEMETA_H -#define FOOTAGEMETA_H - -struct FootageData { - struct StreamData { - - }; -}; - -#endif // FOOTAGEMETA_H diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index 9eea81d18..5593bbbe1 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -45,7 +45,7 @@ QString OIIODecoder::id() const return QStringLiteral("oiio"); } -FootageDescription OIIODecoder::Probe(const QString &filename, const QAtomicInt* cancelled) const +FootageDescription OIIODecoder::Probe(const QString &filename, CancelAtom *cancelled) const { Q_UNUSED(cancelled) @@ -73,7 +73,8 @@ FootageDescription OIIODecoder::Probe(const QString &filename, const QAtomicInt* bool stream_enabled = true; - for (int i=0; in->seek_subimage(i, 0); i++) { + int i; + for (i=0; in->seek_subimage(i, 0); i++) { OIIO::ImageSpec spec = in->spec(); VideoParams video_params = GetVideoParamsFromImageSpec(spec); @@ -104,6 +105,8 @@ FootageDescription OIIODecoder::Probe(const QString &filename, const QAtomicInt* desc.AddVideoStream(video_params); } + desc.SetStreamCount(i); + // If we're here, we have a successful image open in->close(); @@ -116,22 +119,20 @@ bool OIIODecoder::OpenInternal() return OpenImageHandler(stream().filename(), stream().stream()); } -TexturePtr OIIODecoder::RetrieveVideoInternal(Renderer *renderer, const rational &timecode, const RetrieveVideoParams ¶ms, const QAtomicInt *cancelled) +TexturePtr OIIODecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) { - Q_UNUSED(timecode) - Q_UNUSED(cancelled) - VideoParams vp = GetVideoParamsFromImageSpec(image_->spec()); - vp.set_divider(params.divider); + vp.set_divider(p.divider); - if (!buffer_.is_allocated() || last_params_ != params) { - last_params_ = params; + if (!buffer_.is_allocated() + || last_params_.divider != p.divider) { + last_params_ = p; buffer_.destroy(); buffer_.set_video_params(vp); buffer_.allocate(); - if (params.divider == 1) { + if (p.divider == 1) { // Just upload straight to the buffer image_->read_image(oiio_pix_fmt_, buffer_.data(), OIIO::AutoStride, buffer_.linesize_bytes()); } else { @@ -153,7 +154,7 @@ TexturePtr OIIODecoder::RetrieveVideoInternal(Renderer *renderer, const rational } } - return renderer->CreateTexture(vp, buffer_.data(), buffer_.linesize_pixels()); + return p.renderer->CreateTexture(vp, buffer_.data(), buffer_.linesize_pixels()); } void OIIODecoder::CloseInternal() diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index 5233678af..f16e33692 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -40,11 +40,11 @@ public: virtual bool SupportsVideo() override{return true;} - virtual FootageDescription Probe(const QString& filename, const QAtomicInt* cancelled) const override; + virtual FootageDescription Probe(const QString& filename, CancelAtom *cancelled) const override; protected: virtual bool OpenInternal() override; - virtual TexturePtr RetrieveVideoInternal(Renderer *renderer, const rational& timecode, const RetrieveVideoParams& params, const QAtomicInt *cancelled) override; + virtual TexturePtr RetrieveVideoInternal(const RetrieveVideoParams& p) override; virtual void CloseInternal() override; private: diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index b4bdea348..0b7ff983a 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -55,8 +55,6 @@ set(OLIVE_SOURCES common/ratiodialog.h common/rational.cpp common/rational.h - common/threadedobject.cpp - common/threadedobject.h common/threadsafemap.h common/timecodefunctions.cpp common/timecodefunctions.h diff --git a/app/common/cancelableobject.h b/app/common/cancelableobject.h index d7a1fe5d8..eef63d375 100644 --- a/app/common/cancelableobject.h +++ b/app/common/cancelableobject.h @@ -21,35 +21,38 @@ #ifndef CANCELABLEOBJECT_H #define CANCELABLEOBJECT_H -#include - #include "common/define.h" +#include "render/cancelatom.h" namespace olive { class CancelableObject { public: - CancelableObject() : - cancelled_(false) + CancelableObject() { } void Cancel() { - cancelled_ = true; + cancel_.Cancel(); CancelEvent(); } - const QAtomicInt& IsCancelled() const + CancelAtom *GetCancelAtom() { - return cancelled_; + return &cancel_; + } + + bool IsCancelled() + { + return cancel_.IsCancelled(); } protected: virtual void CancelEvent(){} private: - QAtomicInt cancelled_; + CancelAtom cancel_; }; diff --git a/app/common/ffmpegutils.cpp b/app/common/ffmpegutils.cpp index 94a314f3a..565f0269c 100644 --- a/app/common/ffmpegutils.cpp +++ b/app/common/ffmpegutils.cpp @@ -111,6 +111,43 @@ AVSampleFormat FFmpegUtils::GetFFmpegSampleFormat(const AudioParams::Format &smp return AV_SAMPLE_FMT_NONE; } +int FFmpegUtils::GetSwsColorspaceFromAVColorSpace(AVColorSpace cs) +{ + switch (cs) { + case AVCOL_SPC_BT709: + return SWS_CS_ITU709; + case AVCOL_SPC_FCC: + return SWS_CS_FCC; + case AVCOL_SPC_BT470BG: + return SWS_CS_ITU624; + case AVCOL_SPC_SMPTE170M: + return SWS_CS_SMPTE170M; + case AVCOL_SPC_SMPTE240M: + return SWS_CS_SMPTE240M; + case AVCOL_SPC_BT2020_NCL: + return SWS_CS_BT2020; + default: + break; + } + + return SWS_CS_DEFAULT; +} + +AVPixelFormat FFmpegUtils::ConvertJPEGSpaceToRegularSpace(AVPixelFormat f) +{ + switch (f) { + case AV_PIX_FMT_YUVJ420P: return AV_PIX_FMT_YUV420P; + case AV_PIX_FMT_YUVJ422P: return AV_PIX_FMT_YUV422P; + case AV_PIX_FMT_YUVJ444P: return AV_PIX_FMT_YUV444P; + case AV_PIX_FMT_YUVJ440P: return AV_PIX_FMT_YUV440P; + case AV_PIX_FMT_YUVJ411P: return AV_PIX_FMT_YUV411P; + default: + break; + } + + return f; +} + AVPixelFormat FFmpegUtils::GetFFmpegPixelFormat(const VideoParams::Format &pix_fmt, int channel_layout) { if (channel_layout == VideoParams::kRGBChannelCount) { diff --git a/app/common/ffmpegutils.h b/app/common/ffmpegutils.h index cc74d8c48..7d13b8642 100644 --- a/app/common/ffmpegutils.h +++ b/app/common/ffmpegutils.h @@ -24,6 +24,7 @@ extern "C" { #include #include +#include } #include "render/audioparams.h" @@ -57,8 +58,31 @@ public: * @brief Returns an FFmpeg sample format type for a given native type */ static AVSampleFormat GetFFmpegSampleFormat(const AudioParams::Format &smp_fmt); + + /** + * @brief Returns an SWS_CS_* macro from an AVColorSpace enum member + * + * Why aren't these the same thing anyway? And for that matter, why doesn't FFmpeg provide a + * convenience function to do this conversion for us? Who knows, but here we are. + */ + static int GetSwsColorspaceFromAVColorSpace(AVColorSpace cs); + + /** + * @brief Convert "JPEG"/full-range colorspace to its regular counterpart + * + * "JPEG "spaces are deprecated in favor of the regular space and setting `color_range`. For the + * time being, FFmpeg still uses these JPEG spaces, so for simplicity (since we *are* color_range + * aware), we use this function. + */ + static AVPixelFormat ConvertJPEGSpaceToRegularSpace(AVPixelFormat f); }; +using AVFramePtr = std::shared_ptr; +inline AVFramePtr CreateAVFramePtr(AVFrame *f) +{ + return std::shared_ptr(f, [](AVFrame *g){ av_frame_free(&g); }); +} + } #endif // FFMPEGABSTRACTION_H diff --git a/app/common/html.cpp b/app/common/html.cpp index 1b58418fa..d9ad84a28 100644 --- a/app/common/html.cpp +++ b/app/common/html.cpp @@ -142,7 +142,7 @@ void Html::WriteBlock(QXmlStreamWriter *writer, const QTextBlock &block) if (!(fmt.alignment() & Qt::AlignLeft)) { if (fmt.alignment() & Qt::AlignRight) { writer->writeAttribute(QStringLiteral("align"), QStringLiteral("right")); - } else if (fmt.alignment() & Qt::AlignCenter) { + } else if (fmt.alignment() & Qt::AlignHCenter) { writer->writeAttribute(QStringLiteral("align"), QStringLiteral("center")); } else if (fmt.alignment() & Qt::AlignJustify) { writer->writeAttribute(QStringLiteral("align"), QStringLiteral("justify")); @@ -161,7 +161,7 @@ void Html::WriteBlock(QXmlStreamWriter *writer, const QTextBlock &block) WriteCSSProperty(&style, QStringLiteral("line-height"), QStringLiteral("%1%").arg(fmt.lineHeight())); } - //WriteCharFormat(&style, block.charFormat()); + WriteCharFormat(&style, block.charFormat()); if (!style.isEmpty()) { writer->writeAttribute(QStringLiteral("style"), style); @@ -169,12 +169,7 @@ void Html::WriteBlock(QXmlStreamWriter *writer, const QTextBlock &block) auto it = block.begin(); - if (it == block.end()) { - // FIXME: Might not be necessary with our custom HTML implementation - QString s; - s.append(QChar::Nbsp); - writer->writeCharacters(s); - } else { + if (it != block.end()) { for (; it!=block.end(); it++) { WriteFragment(writer, it.fragment()); } @@ -374,7 +369,7 @@ QTextBlockFormat Html::ReadBlockFormat(const QXmlStreamAttributes &attributes) if (StrEquals(attr.value(), QStringLiteral("right"))) { block_fmt.setAlignment(Qt::AlignRight); } else if (StrEquals(attr.value(), QStringLiteral("center"))) { - block_fmt.setAlignment(Qt::AlignCenter); + block_fmt.setAlignment(Qt::AlignHCenter); } else if (StrEquals(attr.value(), QStringLiteral("justify"))) { block_fmt.setAlignment(Qt::AlignJustify); } diff --git a/app/common/qtutils.cpp b/app/common/qtutils.cpp index e4cc56c28..0a750810c 100644 --- a/app/common/qtutils.cpp +++ b/app/common/qtutils.cpp @@ -20,6 +20,8 @@ #include "qtutils.h" +#include + namespace olive { int QtUtils::QFontMetricsWidth(QFontMetrics fm, const QString& s) { @@ -41,11 +43,11 @@ QFrame *QtUtils::CreateHorizontalLine() QFrame *QtUtils::CreateVerticalLine() { QFrame *l = CreateHorizontalLine(); - l->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); + l->setFrameShape(QFrame::VLine); return l; } -int QtUtils::MessageBox(QWidget *parent, QMessageBox::Icon icon, const QString &title, const QString &message, QMessageBox::StandardButtons buttons) +int QtUtils::MsgBox(QWidget *parent, QMessageBox::Icon icon, const QString &title, const QString &message, QMessageBox::StandardButtons buttons) { QMessageBox b(parent); b.setIcon(icon); @@ -70,7 +72,11 @@ QDateTime QtUtils::GetCreationDate(const QFileInfo &info) #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) return info.created(); #else - return info.birthTime(); + QDateTime t = info.birthTime(); + if (!t.isValid()) { + t = info.metadataChangeTime(); + } + return t; #endif } @@ -90,19 +96,43 @@ QStringList QtUtils::WordWrapString(const QString &s, const QFontMetrics &fm, in QString this_line = lines.at(i); while (this_line.size() > 1 && QFontMetricsWidth(fm, this_line) >= bounding_width) { + int old_size = this_line.size(); + int hard_break = -1; + for (int j=this_line.size()-1; j>=0; j--) { - if (this_line.at(j).isSpace()) { - QString chopped = this_line.left(j); - if (QFontMetricsWidth(fm, chopped) < bounding_width) { + const QChar &char_test = this_line.at(j); + + if (char_test.isSpace() + || char_test == '-') { + if (QFontMetricsWidth(fm, this_line.left(j)) < bounding_width) { + if (!char_test.isSpace()) { + j++; + } + + QString chopped = this_line.left(j); + list.append(chopped); - int k = j+1; - while (k < this_line.size() && this_line.at(k).isSpace()) { - k++; + while (j < this_line.size() && this_line.at(j).isSpace()) { + j++; } - this_line.remove(0, k); + this_line.remove(0, j); break; } + } else if (hard_break == -1 && QFontMetricsWidth(fm, this_line.left(j)) < bounding_width) { + // In case we can't find a better place to split, split at the earliest time the line + // goes under the width limit + hard_break = j; + } + } + + if (old_size == this_line.size()) { + if (hard_break != -1) { + list.append(this_line.left(hard_break)); + this_line.remove(0, hard_break); + } else { + qWarning() << "Failed to find anywhere to wrap. Returning full line."; + break; } } } @@ -115,4 +145,14 @@ QStringList QtUtils::WordWrapString(const QString &s, const QFontMetrics &fm, in return list; } +void QtUtils::SetComboBoxData(QComboBox *cb, int data) +{ + for (int i=0; icount(); i++) { + if (cb->itemData(i).toInt() == data) { + cb->setCurrentIndex(i); + break; + } + } +} + } diff --git a/app/common/qtutils.h b/app/common/qtutils.h index 122a5022c..74ff4fcf8 100644 --- a/app/common/qtutils.h +++ b/app/common/qtutils.h @@ -21,24 +21,13 @@ #ifndef QTVERSIONABSTRACTION_H #define QTVERSIONABSTRACTION_H -/** - * - * A fairly simple header for reducing the amount of Qt version checks necessary throughout the code - * - */ - +#include #include #include #include #include #include -#include "common/define.h" - -#ifdef MessageBox -#undef MessageBox -#endif - namespace olive { class QtUtils { @@ -56,7 +45,7 @@ public: static QFrame* CreateVerticalLine(); - static int MessageBox(QWidget *parent, QMessageBox::Icon icon, const QString& title, const QString& message, QMessageBox::StandardButtons buttons = QMessageBox::Ok); + static int MsgBox(QWidget *parent, QMessageBox::Icon icon, const QString& title, const QString& message, QMessageBox::StandardButtons buttons = QMessageBox::Ok); static QDateTime GetCreationDate(const QFileInfo &info); @@ -64,6 +53,23 @@ public: static QStringList WordWrapString(const QString &s, const QFontMetrics &fm, int bounding_width); + static void SetComboBoxData(QComboBox *cb, int data); + + template + static T *GetParentOfType(const QObject *child) + { + QObject *t = child->parent(); + + while (t) { + if (T *p = dynamic_cast(t)) { + return p; + } + t = t->parent(); + } + + return nullptr; + } + }; } diff --git a/app/common/threadedobject.cpp b/app/common/threadedobject.cpp deleted file mode 100644 index fccb2cd4d..000000000 --- a/app/common/threadedobject.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 "threadedobject.h" - -namespace olive { - -void ThreadedObject::LockDeletes() -{ - threadobj_delete_lock_++; -} - -void ThreadedObject::UnlockDeletes() -{ - Q_ASSERT(AreDeletesLocked()); - - threadobj_delete_lock_--; -} - -bool ThreadedObject::AreDeletesLocked() -{ - return (threadobj_delete_lock_ > 0); -} - -void ThreadedObject::LockMutex() -{ - threadobj_main_lock_.lock(); -} - -void ThreadedObject::UnlockMutex() -{ - threadobj_main_lock_.unlock(); -} - -bool ThreadedObject::TryLockMutex(int timeout) -{ - return threadobj_main_lock_.tryLock(timeout); -} - -} diff --git a/app/common/threadedobject.h b/app/common/threadedobject.h deleted file mode 100644 index c42b4faa5..000000000 --- a/app/common/threadedobject.h +++ /dev/null @@ -1,50 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 THREADEDOBJECT_H -#define THREADEDOBJECT_H - -#include - -#include "common/define.h" - -namespace olive { - -class ThreadedObject -{ -public: - - void LockMutex(); - void UnlockMutex(); - bool TryLockMutex(int timeout = 0); - - void LockDeletes(); - void UnlockDeletes(); - bool AreDeletesLocked(); - -private: - QMutex threadobj_main_lock_; - - QAtomicInt threadobj_delete_lock_; -}; - -} - -#endif // THREADEDOBJECT_H diff --git a/app/config/config.cpp b/app/config/config.cpp index 43668c467..1e0ee70a2 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -104,6 +104,15 @@ void Config::SetDefaults() SetEntryInternal(QStringLiteral("ReassocLinToNonLin"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("PreviewNonFloatDontAskAgain"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("DefaultVideoTransition"), NodeValue::kText, QStringLiteral("org.olivevideoeditor.Olive.crossdissolve")); + SetEntryInternal(QStringLiteral("DefaultAudioTransition"), NodeValue::kText, QStringLiteral("org.olivevideoeditor.Olive.crossdissolve")); + SetEntryInternal(QStringLiteral("DefaultTransitionLength"), NodeValue::kRational, QVariant::fromValue(rational(1))); + + SetEntryInternal(QStringLiteral("DefaultSubtitleSize"), NodeValue::kInt, 48); + SetEntryInternal(QStringLiteral("DefaultSubtitleFamily"), NodeValue::kText, QString()); + SetEntryInternal(QStringLiteral("DefaultSubtitleWeight"), NodeValue::kInt, QFont::Bold); + SetEntryInternal(QStringLiteral("AntialiasSubtitles"), NodeValue::kBoolean, true); + SetEntryInternal(QStringLiteral("AutoCacheDelay"), NodeValue::kInt, 1000); SetEntryInternal(QStringLiteral("CatColor0"), NodeValue::kInt, ColorCoding::kRed); @@ -141,7 +150,7 @@ void Config::SetDefaults() SetEntryInternal(QStringLiteral("DefaultSequencePixelAspect"), NodeValue::kRational, QVariant::fromValue(rational(1))); SetEntryInternal(QStringLiteral("DefaultSequenceFrameRate"), NodeValue::kRational, QVariant::fromValue(rational(1001, 30000))); SetEntryInternal(QStringLiteral("DefaultSequenceInterlacing"), NodeValue::kInt, VideoParams::kInterlaceNone); - SetEntryInternal(QStringLiteral("DefaultSequenceAutoCache"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("DefaultSequenceAutoCache2"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("DefaultSequenceAudioFrequency"), NodeValue::kInt, 48000); SetEntryInternal(QStringLiteral("DefaultSequenceAudioLayout"), NodeValue::kInt, QVariant::fromValue(static_cast(AV_CH_LAYOUT_STEREO))); diff --git a/app/core.cpp b/app/core.cpp index f6f18e2fe..d6989319a 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -522,6 +522,9 @@ bool Core::AddOpenProjectFromTask(Task *task) return true; } else { delete project; + if (open_projects_.empty()) { + CreateNewProject(); + } } } @@ -583,6 +586,8 @@ void Core::ImportTaskComplete(Task* task) } undo_stack_.pushIfHasChildren(command); + + main_window_->SelectFootage(import_task->GetImportedFootage()); } bool Core::ConfirmImageSequence(const QString& filename) diff --git a/app/crashhandler/crashhandler.cpp b/app/crashhandler/crashhandler.cpp index eac036c27..5fe8cfb75 100644 --- a/app/crashhandler/crashhandler.cpp +++ b/app/crashhandler/crashhandler.cpp @@ -153,7 +153,7 @@ void CrashHandlerDialog::ReplyFinished(QNetworkReply* reply) b.setIcon(QMessageBox::Critical); b.setWindowModality(Qt::WindowModal); b.setWindowTitle(tr("Upload Failed")); - b.setText(tr("Failed to send error report. Please try again later.")); + b.setText(tr("Failed to send error report (%1). Please try again later.").arg(QString::number(reply->error()))); b.addButton(QMessageBox::Ok); b.exec(); @@ -161,6 +161,22 @@ void CrashHandlerDialog::ReplyFinished(QNetworkReply* reply) } } +void CrashHandlerDialog::HandleSslErrors(QNetworkReply *reply, const QList &se) +{ + QStringList errors; + for (const QSslError &err : se) { + errors.append(err.errorString()); + } + + QMessageBox b(this); + b.setIcon(QMessageBox::Critical); + b.setWindowModality(Qt::WindowModal); + b.setWindowTitle(tr("SSL Error")); + b.setText(tr("Encountered the following SSL errors:\n\n%1").arg(errors.join('\n'))); + b.addButton(QMessageBox::Ok); + b.exec(); +} + void CrashHandlerDialog::AttemptToFindReport() { // If we found it, use it, otherwise wait a second and try again @@ -198,6 +214,7 @@ void CrashHandlerDialog::SendErrorReport() QNetworkAccessManager* manager = new QNetworkAccessManager(); connect(manager, &QNetworkAccessManager::finished, this, &CrashHandlerDialog::ReplyFinished); + connect(manager, &QNetworkAccessManager::sslErrors, this, &CrashHandlerDialog::HandleSslErrors); QNetworkRequest request; request.setSslConfiguration(QSslConfiguration::defaultConfiguration()); diff --git a/app/crashhandler/crashhandler.h b/app/crashhandler/crashhandler.h index d4e37d583..d377061e2 100644 --- a/app/crashhandler/crashhandler.h +++ b/app/crashhandler/crashhandler.h @@ -65,6 +65,8 @@ protected: private slots: void ReplyFinished(QNetworkReply *reply); + void HandleSslErrors(QNetworkReply *reply, const QList &errors); + void AttemptToFindReport(); void ReadProcessHasData(); diff --git a/app/dialog/export/CMakeLists.txt b/app/dialog/export/CMakeLists.txt index 2a309a6fc..8847c4ba4 100644 --- a/app/dialog/export/CMakeLists.txt +++ b/app/dialog/export/CMakeLists.txt @@ -26,6 +26,8 @@ set(OLIVE_SOURCES dialog/export/exportaudiotab.h dialog/export/exportformatcombobox.cpp dialog/export/exportformatcombobox.h + dialog/export/exportsavepresetdialog.cpp + dialog/export/exportsavepresetdialog.h dialog/export/exportsubtitlestab.cpp dialog/export/exportsubtitlestab.h dialog/export/exportvideotab.cpp diff --git a/app/dialog/export/codec/cineformsection.cpp b/app/dialog/export/codec/cineformsection.cpp index ba0071e6d..da83eabed 100644 --- a/app/dialog/export/codec/cineformsection.cpp +++ b/app/dialog/export/codec/cineformsection.cpp @@ -82,4 +82,9 @@ void CineformSection::AddOpts(EncodingParams *params) params->set_video_option(QStringLiteral("quality"), QString::number(quality_combobox_->currentIndex())); } +void CineformSection::SetOpts(const EncodingParams *p) +{ + quality_combobox_->setCurrentIndex(p->video_option(QStringLiteral("quality")).toInt()); +} + } diff --git a/app/dialog/export/codec/cineformsection.h b/app/dialog/export/codec/cineformsection.h index edbe8813a..8a3a08491 100644 --- a/app/dialog/export/codec/cineformsection.h +++ b/app/dialog/export/codec/cineformsection.h @@ -35,6 +35,8 @@ public: virtual void AddOpts(EncodingParams* params) override; + virtual void SetOpts(const EncodingParams *p) override; + private: QComboBox *quality_combobox_; diff --git a/app/dialog/export/codec/codecsection.h b/app/dialog/export/codec/codecsection.h index 24be8f1f8..93eebaa1d 100644 --- a/app/dialog/export/codec/codecsection.h +++ b/app/dialog/export/codec/codecsection.h @@ -35,6 +35,8 @@ public: virtual void AddOpts(EncodingParams* params){Q_UNUSED(params)} + virtual void SetOpts(const EncodingParams *p){Q_UNUSED(p)} + }; } diff --git a/app/dialog/export/codec/h264section.cpp b/app/dialog/export/codec/h264section.cpp index 8c015e0f8..732aef346 100644 --- a/app/dialog/export/codec/h264section.cpp +++ b/app/dialog/export/codec/h264section.cpp @@ -60,7 +60,7 @@ H264Section::H264Section(int default_crf, QWidget *parent) : preset_combobox_->addItem(tr("Slow")); preset_combobox_->addItem(tr("Slower")); preset_combobox_->addItem(tr("Very Slow")); - + //Default to "medium" preset_combobox_->setCurrentIndex(5); @@ -105,6 +105,10 @@ void H264Section::AddOpts(EncodingParams *params) CompressionMethod method = static_cast(compression_method_stack_->currentIndex()); + // This option is not used by the encoder (nor is anything with the ove_ prefix), it's to help us + // identify which option was chosen when params are restored + params->set_video_option(QStringLiteral("ove_compressionmethod"), QString::number(method)); + if (method == kConstantRateFactor) { // Simply set CRF value @@ -121,9 +125,12 @@ void H264Section::AddOpts(EncodingParams *params) max_rate = bitrate_section_->GetMaximumBitRate(); } else { // Calculate the bit rate from the file size divided by the sequence length in seconds (bits per second) - target_rate = qRound64(static_cast(filesize_section_->GetFileSize()) / params->GetExportLength().toDouble()); + int64_t target_fs = filesize_section_->GetFileSize(); + target_rate = qRound64(static_cast(target_fs) / params->GetExportLength().toDouble()); min_rate = target_rate; max_rate = target_rate; + + params->set_video_option(QStringLiteral("ove_targetfilesize"), QString::number(target_fs)); } // Disable CRF encoding @@ -135,10 +142,33 @@ void H264Section::AddOpts(EncodingParams *params) params->set_video_buffer_size(2000000); } - + params->set_video_option(QStringLiteral("preset"), QString::number(preset_combobox_->currentIndex())); } +void H264Section::SetOpts(const EncodingParams *p) +{ + CompressionMethod method = static_cast(p->video_option(QStringLiteral("ove_compressionmethod")).toInt()); + + compression_method_stack_->setCurrentIndex(method); + + if (method == kConstantRateFactor) { + crf_section_->SetValue(p->video_option(QStringLiteral("crf")).toInt()); + } else { + int64_t target_rate = p->video_bit_rate(); + int64_t max_rate = p->video_max_bit_rate(); + + if (method == kTargetBitRate) { + // Use user-supplied values for the bit rate + bitrate_section_->SetTargetBitRate(target_rate); + bitrate_section_->SetMaximumBitRate(max_rate); + } else { + // Calculate the bit rate from the file size divided by the sequence length in seconds (bits per second) + filesize_section_->SetFileSize(p->video_option(QStringLiteral("ove_targetfilesize")).toLongLong()); + } + } +} + H264CRFSection::H264CRFSection(int default_crf, QWidget *parent) : QWidget(parent) { @@ -168,6 +198,11 @@ int H264CRFSection::GetValue() const return crf_slider_->value(); } +void H264CRFSection::SetValue(int c) +{ + crf_slider_->setValue(c); +} + H264BitRateSection::H264BitRateSection(QWidget *parent) : QWidget(parent) { @@ -207,11 +242,21 @@ int64_t H264BitRateSection::GetTargetBitRate() const return qRound64(target_rate_->GetValue() * 1000000.0); } +void H264BitRateSection::SetTargetBitRate(int64_t b) +{ + target_rate_->SetValue(double(b) * 0.000001); +} + int64_t H264BitRateSection::GetMaximumBitRate() const { return qRound64(max_rate_->GetValue() * 1000000.0); } +void H264BitRateSection::SetMaximumBitRate(int64_t b) +{ + max_rate_->SetValue(double(b) * 0.000001); +} + H264FileSizeSection::H264FileSizeSection(QWidget *parent) : QWidget(parent) { @@ -243,6 +288,12 @@ int64_t H264FileSizeSection::GetFileSize() const return qRound64(file_size_->GetValue() * 1024.0 * 1024.0 * 8.0); } +void H264FileSizeSection::SetFileSize(int64_t f) +{ + // Convert bits back to megabytes + file_size_->SetValue(double(f) / 8.0 / 1024.0 / 1024.0); +} + H265Section::H265Section(QWidget *parent) : H264Section(H264CRFSection::kDefaultH265CRF, parent) { diff --git a/app/dialog/export/codec/h264section.h b/app/dialog/export/codec/h264section.h index 2ca0eaa59..9fd984ae5 100644 --- a/app/dialog/export/codec/h264section.h +++ b/app/dialog/export/codec/h264section.h @@ -37,6 +37,7 @@ public: H264CRFSection(int default_crf, QWidget* parent = nullptr); int GetValue() const; + void SetValue(int c); static const int kDefaultH264CRF = 18; static const int kDefaultH265CRF = 23; @@ -59,11 +60,13 @@ public: * @brief Get user-selected target bit rate (returns in BITS) */ int64_t GetTargetBitRate() const; + void SetTargetBitRate(int64_t b); /** * @brief Get user-selected maximum bit rate (returns in BITS) */ int64_t GetMaximumBitRate() const; + void SetMaximumBitRate(int64_t b); private: FloatSlider* target_rate_; @@ -82,6 +85,7 @@ public: * @brief Returns file size in BITS */ int64_t GetFileSize() const; + void SetFileSize(int64_t f); private: FloatSlider* file_size_; @@ -103,6 +107,8 @@ public: virtual void AddOpts(EncodingParams* params) override; + virtual void SetOpts(const EncodingParams *p) override; + private: QStackedWidget* compression_method_stack_; diff --git a/app/dialog/export/codec/imagesection.h b/app/dialog/export/codec/imagesection.h index b93f733a0..3575ea5a2 100644 --- a/app/dialog/export/codec/imagesection.h +++ b/app/dialog/export/codec/imagesection.h @@ -39,6 +39,11 @@ public: return image_sequence_checkbox_->isChecked(); } + void SetImageSequenceChecked(bool e) + { + image_sequence_checkbox_->setChecked(e); + } + void SetTimebase(const rational& r) { frame_slider_->SetTimebase(r); diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index d805f8726..54a1403a8 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -32,8 +32,8 @@ #include "common/digit.h" #include "common/qtutils.h" -#include "core.h" #include "dialog/task/task.h" +#include "exportsavepresetdialog.h" #include "node/project/project.h" #include "node/project/sequence/sequence.h" #include "task/taskmanager.h" @@ -42,8 +42,10 @@ namespace olive { +#define super QDialog + ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : - QDialog(parent), + super(parent), viewer_node_(viewer_node) { QHBoxLayout* layout = new QHBoxLayout(this); @@ -80,21 +82,21 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : QLabel* preset_lbl = new QLabel(tr("Preset:")); preset_lbl->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); preferences_layout->addWidget(preset_lbl, row, 0); - QComboBox* preset_combobox = new QComboBox(); - preset_combobox->addItem(tr("Same As Source - High Quality")); - preset_combobox->addItem(tr("Same As Source - Medium Quality")); - preset_combobox->addItem(tr("Same As Source - Low Quality")); - preferences_layout->addWidget(preset_combobox, row, 1); + preset_combobox_ = new QComboBox(); + LoadPresets(); + connect(preset_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &ExportDialog::PresetComboBoxChanged); + preferences_layout->addWidget(preset_combobox_, row, 1, 1, 2); - QPushButton* preset_load_btn = new QPushButton(); + /*QPushButton* preset_load_btn = new QPushButton(); preset_load_btn->setIcon(icon::Open); preset_load_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); - preferences_layout->addWidget(preset_load_btn, row, 2); + preferences_layout->addWidget(preset_load_btn, row, 2);*/ QPushButton* preset_save_btn = new QPushButton(); preset_save_btn->setIcon(icon::Save); preset_save_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); preferences_layout->addWidget(preset_save_btn, row, 3); + connect(preset_save_btn, &QPushButton::clicked, this, &ExportDialog::SavePreset); row++; @@ -107,7 +109,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : range_combobox_ = new QComboBox(); range_combobox_->addItem(tr("Entire Sequence")); range_combobox_->addItem(tr("In to Out")); - range_combobox_->setEnabled(viewer_node_->GetTimelinePoints()->workarea()->enabled()); + range_combobox_->setEnabled(viewer_node_->GetWorkArea()->enabled()); preferences_layout->addWidget(range_combobox_, row, 1, 1, 3); @@ -131,7 +133,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : audio_enabled_ = new QCheckBox(tr("Export Audio")); av_enabled_layout->addWidget(audio_enabled_); - subtitles_enabled_ = new QCheckBox(tr("Export Subtitle")); + subtitles_enabled_ = new QCheckBox(tr("Export Subtitles")); av_enabled_layout->addWidget(subtitles_enabled_); preferences_layout->addLayout(av_enabled_layout, row, 0, 1, 4); @@ -182,6 +184,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : QVBoxLayout* preview_layout = new QVBoxLayout(preview_area); preview_layout->addWidget(new QLabel(tr("Preview"))); preview_viewer_ = new ViewerWidget(); + preview_viewer_->ruler()->SetMarkerEditingEnabled(false); preview_viewer_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); connect(preview_viewer_, &ViewerWidget::TimeChanged, video_tab_, &ExportVideoTab::SetTime); preview_layout->addWidget(preview_viewer_); @@ -195,25 +198,9 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : // Set defaults previously_selected_format_ = ExportFormat::kFormatMPEG4Video; - format_combobox_->SetFormat(ExportFormat::kFormatMPEG4Video); connect(format_combobox_, &ExportFormatComboBox::FormatChanged, this, &ExportDialog::FormatChanged); - FormatChanged(format_combobox_->GetFormat()); VideoParams vp = viewer_node_->GetVideoParams(); - AudioParams ap = viewer_node_->GetAudioParams(); - - video_tab_->width_slider()->SetValue(vp.width()); - video_tab_->width_slider()->SetDefaultValue(vp.width()); - video_tab_->height_slider()->SetValue(vp.height()); - video_tab_->height_slider()->SetDefaultValue(vp.height()); - video_tab_->SetSelectedFrameRate(vp.frame_rate()); - video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(vp.pixel_aspect_ratio()); - video_tab_->pixel_format_field()->SetPixelFormat(static_cast(OLIVE_CONFIG("OnlinePixelFormat").toInt())); - video_tab_->interlaced_combobox()->SetInterlaceMode(vp.interlacing()); - audio_tab_->sample_rate_combobox()->SetSampleRate(ap.sample_rate()); - audio_tab_->sample_format_combobox()->SetAttemptToRestoreFormat(false); - audio_tab_->channel_layout_combobox()->SetChannelLayout(ap.channel_layout()); - video_aspect_ratio_ = static_cast(vp.width()) / static_cast(vp.height()); connect(video_tab_->width_slider(), @@ -245,11 +232,31 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : this, &ExportDialog::ImageSequenceCheckBoxChanged); - // Set viewer to view the node + // We don't check if the codec supports subtitles because we can always export to a sidecar file + bool has_subtitle_tracks = SequenceHasSubtitles(); + connect(subtitles_enabled_, &QCheckBox::toggled, subtitle_tab_, &QWidget::setEnabled); + subtitles_enabled_->setEnabled(has_subtitle_tracks); + + // If the viewer already has cached params, use them + if (viewer_node_->GetLastUsedEncodingParams().IsValid()) { + SetParams(viewer_node_->GetLastUsedEncodingParams()); + } else { + SetDefaults(); + } + + // Set viewer to view the node and set its colorspace preview_viewer_->ConnectViewerNode(viewer_node_); - preview_viewer_->ruler()->ConnectTimelinePoints(viewer_node_->GetTimelinePoints()); preview_viewer_->SetColorMenuEnabled(false); preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); + + qApp->installEventFilter(this); + + connect(video_enabled_, &QCheckBox::toggled, video_tab_, &QWidget::setEnabled); + video_tab_->setEnabled(video_enabled_->isChecked()); + connect(audio_enabled_, &QCheckBox::toggled, audio_tab_, &QWidget::setEnabled); + audio_tab_->setEnabled(audio_enabled_->isChecked()); + connect(subtitles_enabled_, &QCheckBox::toggled, subtitle_tab_, &QWidget::setEnabled); + subtitle_tab_->setEnabled(subtitles_enabled_->isChecked()); } rational ExportDialog::GetSelectedTimebase() const @@ -257,10 +264,15 @@ rational ExportDialog::GetSelectedTimebase() const return video_tab_->GetSelectedFrameRate().flipped(); } +void ExportDialog::SetSelectedTimebase(const rational &r) +{ + video_tab_->SetSelectedFrameRate(r.flipped()); +} + void ExportDialog::StartExport() { if (!video_enabled_->isChecked() && !audio_enabled_->isChecked() && !subtitles_enabled_->isChecked()) { - QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid parameters"), + QtUtils::MsgBox(this, QMessageBox::Critical, tr("Invalid parameters"), tr("Video, audio, and subtitles are disabled. There's nothing to export.")); return; } @@ -272,7 +284,7 @@ void ExportDialog::StartExport() // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. if (!proposed_filename.endsWith(necessary_ext, Qt::CaseInsensitive)) { - if (QtUtils::MessageBox(this, QMessageBox::Warning, tr("Invalid filename"), + if (QtUtils::MsgBox(this, QMessageBox::Warning, tr("Invalid filename"), tr("The filename must contain the extension \"%1\". Would you like to append it " "automatically?").arg(necessary_ext), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { @@ -289,7 +301,7 @@ void ExportDialog::StartExport() // If the directory does not exist, try to create it QDir dest_dir(file_info.path()); if (!FileFunctions::DirectoryIsValid(dest_dir)) { - QtUtils::MessageBox(this, QMessageBox::Critical, tr("Failed to create output directory"), + QtUtils::MsgBox(this, QMessageBox::Critical, tr("Failed to create output directory"), tr("The intended output directory doesn't exist and Olive couldn't create it. " "Please choose a different filename.")); return; @@ -299,7 +311,7 @@ void ExportDialog::StartExport() if (video_tab_->IsImageSequenceSet()) { // Ensure filename contains digits if (!Encoder::FilenameContainsDigitPlaceholder(proposed_filename)) { - QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid filename"), + QtUtils::MsgBox(this, QMessageBox::Critical, tr("Invalid filename"), tr("Export is set to an image sequence, but the filename does not have a section for digits " "(formatted as [#####] where the amount of # is the amount of digits).")); return; @@ -309,7 +321,7 @@ void ExportDialog::StartExport() int64_t needed_digit_count = GetDigitCount(frame_count); int current_digit_count = Encoder::GetImageSequencePlaceholderDigitCount(proposed_filename); if (current_digit_count < needed_digit_count) { - QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid filename"), + QtUtils::MsgBox(this, QMessageBox::Critical, tr("Invalid filename"), tr("Filename doesn't contain enough digits for the amount of frames " "this export will need (need %1 for %n frame(s)).", nullptr, frame_count) .arg(QString::number(needed_digit_count))); @@ -319,7 +331,7 @@ void ExportDialog::StartExport() // Validate if the file exists and whether the user wishes to overwrite it if (file_info.exists()) { - if (QtUtils::MessageBox(this, QMessageBox::Warning, tr("Confirm Overwrite"), + if (QtUtils::MsgBox(this, QMessageBox::Warning, tr("Confirm Overwrite"), tr("The file \"%1\" already exists. Do you want to overwrite it?") .arg(proposed_filename), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { @@ -331,7 +343,7 @@ void ExportDialog::StartExport() if (video_enabled_->isChecked() && (video_tab_->GetSelectedCodec() == ExportCodec::kCodecH264 || video_tab_->GetSelectedCodec() == ExportCodec::kCodecH265) && (video_tab_->width_slider()->GetValue()%2 != 0 || video_tab_->height_slider()->GetValue()%2 != 0)) { - QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid Parameters"), + QtUtils::MsgBox(this, QMessageBox::Critical, tr("Invalid Parameters"), tr("Width and height must be multiples of 2.")); return; } @@ -385,11 +397,27 @@ void ExportDialog::ImageSequenceCheckBoxChanged(bool e) filename_edit_->setText(current_fileinfo.dir().filePath(basename)); } -void ExportDialog::closeEvent(QCloseEvent *e) +void ExportDialog::SavePreset() { - preview_viewer_->ConnectViewerNode(nullptr); + ExportSavePresetDialog d(GenerateParams(), this); + if (d.exec() == QDialog::Accepted) { + LoadPresets(); + preset_combobox_->setCurrentText(d.GetSelectedPresetName()); + } +} - QDialog::closeEvent(e); +void ExportDialog::PresetComboBoxChanged() +{ + QComboBox *c = static_cast(sender()); + + int preset_number = c->currentData().toInt(); + if (preset_number == kPresetDefault) { + SetDefaults(); + } else if (preset_number == kPresetLastUsed) { + SetParams(viewer_node_->GetLastUsedEncodingParams()); + } else { + SetParams(presets_.at(preset_number)); + } } void ExportDialog::AddPreferencesTab(QWidget *inner_widget, const QString &title) @@ -445,9 +473,9 @@ void ExportDialog::FormatChanged(ExportFormat::Format current_format) audio_enabled_->setChecked(has_audio_codecs); audio_enabled_->setEnabled(has_audio_codecs); - bool has_subtitle_codecs = subtitle_tab_->SetFormat(current_format); - subtitles_enabled_->setChecked(has_subtitle_codecs); - subtitles_enabled_->setEnabled(has_subtitle_codecs); + if (subtitles_enabled_->isEnabled()) { + subtitle_tab_->SetFormat(current_format); + } } void ExportDialog::ResolutionChanged() @@ -484,7 +512,32 @@ void ExportDialog::ResolutionChanged() void ExportDialog::LoadPresets() { + preset_combobox_->clear(); + presets_.clear(); + preset_combobox_->addItem(tr("Default"), kPresetDefault); + + if (viewer_node_->GetLastUsedEncodingParams().IsValid()) { + preset_combobox_->addItem(tr("Last Used"), kPresetLastUsed); + } + + preset_combobox_->insertSeparator(preset_combobox_->count()); + + QStringList l = EncodingParams::GetListOfPresets(); + presets_.reserve(l.size()); + + for (const QString &preset : l) { + EncodingParams p; + + QFile f(EncodingParams::GetPresetPath().filePath(preset)); + if (f.open(QFile::ReadOnly)) { + if (p.Load(&f)) { + preset_combobox_->addItem(preset, int(presets_.size())); + presets_.push_back(p); + } + f.close(); + } + } } void ExportDialog::SetDefaultFilename() @@ -503,7 +556,43 @@ void ExportDialog::SetDefaultFilename() filename_edit_->setText(file_location); } -ExportParams ExportDialog::GenerateParams() const +bool ExportDialog::SequenceHasSubtitles() const +{ + if (Sequence *s = dynamic_cast(viewer_node_)) { + TrackList *tl = s->track_list(Track::kSubtitle); + for (Track *t : tl->GetTracks()) { + if (!t->IsMuted() && !t->Blocks().empty()) { + return true; + } + } + } + + return false; +} + +void ExportDialog::SetDefaults() +{ + format_combobox_->SetFormat(ExportFormat::kFormatMPEG4Video); + FormatChanged(format_combobox_->GetFormat()); + + VideoParams vp = viewer_node_->GetVideoParams(); + AudioParams ap = viewer_node_->GetAudioParams(); + + video_tab_->width_slider()->SetValue(vp.width()); + video_tab_->width_slider()->SetDefaultValue(vp.width()); + video_tab_->height_slider()->SetValue(vp.height()); + video_tab_->height_slider()->SetDefaultValue(vp.height()); + video_tab_->SetSelectedFrameRate(vp.frame_rate()); + video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(vp.pixel_aspect_ratio()); + video_tab_->pixel_format_field()->SetPixelFormat(static_cast(OLIVE_CONFIG("OnlinePixelFormat").toInt())); + video_tab_->interlaced_combobox()->SetInterlaceMode(vp.interlacing()); + audio_tab_->sample_rate_combobox()->SetSampleRate(ap.sample_rate()); + audio_tab_->sample_format_combobox()->SetAttemptToRestoreFormat(false); + audio_tab_->channel_layout_combobox()->SetChannelLayout(ap.channel_layout()); + subtitles_enabled_->setChecked(SequenceHasSubtitles()); +} + +EncodingParams ExportDialog::GenerateParams() const { VideoParams video_render_params(static_cast(video_tab_->width_slider()->GetValue()), static_cast(video_tab_->height_slider()->GetValue()), @@ -518,8 +607,8 @@ ExportParams ExportDialog::GenerateParams() const audio_tab_->channel_layout_combobox()->GetChannelLayout(), audio_tab_->sample_format_combobox()->GetSampleFormat()); - ExportParams params; - params.set_encoder(Encoder::GetTypeFromFormat(format_combobox_->GetFormat())); + EncodingParams params; + params.set_format(format_combobox_->GetFormat()); params.SetFilename(filename_edit_->text().trimmed()); params.SetExportLength(viewer_node_->GetLength()); @@ -529,15 +618,18 @@ ExportParams ExportDialog::GenerateParams() const params.set_custom_range(TimeRange(export_time, export_time + GetSelectedTimebase())); } else if (range_combobox_->currentIndex() == kRangeInToOut) { // Assume if this combobox is enabled, workarea is enabled - a check that we make in this dialog's constructor - params.set_custom_range(viewer_node_->GetTimelinePoints()->workarea()->range()); + params.set_custom_range(viewer_node_->GetWorkArea()->range()); } if (video_tab_->scaling_method_combobox()->isEnabled()) { - params.set_video_scaling_method(static_cast(video_tab_->scaling_method_combobox()->currentData().toInt())); + params.set_video_scaling_method(static_cast(video_tab_->scaling_method_combobox()->currentData().toInt())); } if (video_enabled_->isChecked()) { ExportCodec::Codec video_codec = video_tab_->GetSelectedCodec(); + + video_render_params.set_color_range(video_tab_->color_range()); + params.EnableVideo(video_render_params, video_codec); params.set_video_threads(video_tab_->threads()); @@ -560,17 +652,108 @@ ExportParams ExportDialog::GenerateParams() const params.set_audio_bit_rate(audio_tab_->bit_rate_slider()->GetValue() * 1000); } - if (subtitles_enabled_->isChecked()) { - params.EnableSubtitles(subtitle_tab_->GetSubtitleCodec()); + if (subtitles_enabled_->isEnabled() + && subtitles_enabled_->isChecked()) { + if (!subtitle_tab_->GetSidecarEnabled()) { + // Export subtitles embedded in container + params.EnableSubtitles(subtitle_tab_->GetSubtitleCodec()); + } else { + // Export subtitles to a sidecar file + params.EnableSidecarSubtitles(subtitle_tab_->GetSidecarFormat(), subtitle_tab_->GetSubtitleCodec()); + } } return params; } +void ExportDialog::SetParams(const EncodingParams &e) +{ + format_combobox_->SetFormat(e.format()); + + if (e.has_custom_range() && viewer_node_->GetWorkArea()->enabled()) { + range_combobox_->setCurrentIndex(kRangeInToOut); + } + + QtUtils::SetComboBoxData(video_tab_->scaling_method_combobox(), e.video_scaling_method()); + + video_enabled_->setChecked(e.video_enabled()); + if (e.video_enabled()) { + video_tab_->width_slider()->SetValue(e.video_params().width()); + video_tab_->height_slider()->SetValue(e.video_params().height()); + SetSelectedTimebase(e.video_params().time_base()); + video_tab_->pixel_format_field()->SetPixelFormat(e.video_params().format()); + video_tab_->pixel_aspect_combobox()->SetPixelAspectRatio(e.video_params().pixel_aspect_ratio()); + video_tab_->interlaced_combobox()->SetInterlaceMode(e.video_params().interlacing()); + + video_tab_->SetSelectedCodec(e.video_codec()); + + video_tab_->SetColorRange(e.video_params().color_range()); + + video_tab_->SetThreads(e.video_threads()); + + if (video_tab_->isVisible()) { + video_tab_->GetCodecSection()->SetOpts(&e); + } + + video_tab_->SetOCIOColorSpace(e.color_transform().output()); + + video_tab_->SetPixFmt(e.video_pix_fmt()); + + video_tab_->SetImageSequence(e.video_is_image_sequence()); + } + + audio_enabled_->setChecked(e.audio_enabled()); + if (e.audio_enabled()) { + audio_tab_->sample_rate_combobox()->SetSampleRate(e.audio_params().sample_rate()); + audio_tab_->channel_layout_combobox()->SetChannelLayout(e.audio_params().channel_layout()); + audio_tab_->sample_format_combobox()->SetSampleFormat(e.audio_params().format()); + + audio_tab_->SetCodec(e.audio_codec()); + + audio_tab_->bit_rate_slider()->SetValue(e.audio_bit_rate() / 1000); + } + + if (subtitles_enabled_->isEnabled()) { + subtitles_enabled_->setChecked(e.subtitles_enabled()); + subtitle_tab_->SetSidecarEnabled(e.subtitles_are_sidecar()); + if (e.subtitles_enabled()) { + subtitle_tab_->SetSubtitleCodec(e.subtitles_codec()); + if (e.subtitles_are_sidecar()) { + subtitle_tab_->SetSidecarFormat(e.subtitle_sidecar_fmt()); + } + } + } +} + +bool ExportDialog::eventFilter(QObject *o, QEvent *e) +{ + // Any parameters in scrollable areas, ignore wheel events so the user doesn't unwittingly change + // them while trying to scroll through the pages + if (e->type() == QEvent::Wheel) { + while ((o = o->parent())) { + if (o == video_tab_ || o == audio_tab_ || o == subtitle_tab_) { + e->ignore(); + return true; + } + } + } + + return super::eventFilter(o, e); +} + +void ExportDialog::done(int r) +{ + preview_viewer_->ConnectViewerNode(nullptr); + + viewer_node_->SetLastUsedEncodingParams(GenerateParams()); + + super::done(r); +} + rational ExportDialog::GetExportLength() const { if (range_combobox_->currentIndex() == kRangeInToOut) { - return viewer_node_->GetTimelinePoints()->workarea()->range().length(); + return viewer_node_->GetWorkArea()->range().length(); } else { return viewer_node_->GetLength(); } @@ -588,8 +771,8 @@ void ExportDialog::UpdateViewerDimensions() VideoParams vp = viewer_node_->GetVideoParams(); - QMatrix4x4 transform = ExportParams::GenerateMatrix( - static_cast(video_tab_->scaling_method_combobox()->currentData().toInt()), + QMatrix4x4 transform = EncodingParams::GenerateMatrix( + static_cast(video_tab_->scaling_method_combobox()->currentData().toInt()), vp.width(), vp.height(), static_cast(video_tab_->width_slider()->GetValue()), diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index df16c4850..b9cd62b73 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -34,6 +34,7 @@ #include "exportsubtitlestab.h" #include "exportvideotab.h" #include "task/export/export.h" +#include "widget/nodeparamview/nodeparamviewwidgetbridge.h" #include "widget/viewer/viewer.h" namespace olive { @@ -45,6 +46,7 @@ public: ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr); rational GetSelectedTimebase() const; + void SetSelectedTimebase(const rational &r); void SetTime(const rational &time) { @@ -54,8 +56,13 @@ public: preview_viewer_->SetAudioScrubbingEnabled(true); } -protected: - virtual void closeEvent(QCloseEvent *e) override; + EncodingParams GenerateParams() const; + void SetParams(const EncodingParams &e); + + virtual bool eventFilter(QObject *o, QEvent *e) override; + +public slots: + virtual void done(int r) override; private: void AddPreferencesTab(QWidget *inner_widget, const QString &title); @@ -63,7 +70,9 @@ private: void LoadPresets(); void SetDefaultFilename(); - ExportParams GenerateParams() const; + bool SequenceHasSubtitles() const; + + void SetDefaults(); ViewerOutput* viewer_node_; @@ -77,9 +86,16 @@ private: kRangeInToOut }; + enum AutoPreset { + kPresetDefault = -1, + kPresetLastUsed = -2, + }; + QTabWidget* preferences_tabs_; + QComboBox* preset_combobox_; QComboBox* range_combobox_; + std::vector presets_; QCheckBox* video_enabled_; QCheckBox* audio_enabled_; @@ -115,6 +131,10 @@ private slots: void ImageSequenceCheckBoxChanged(bool e); + void SavePreset(); + + void PresetComboBoxChanged(); + }; } diff --git a/app/dialog/export/exportadvancedvideodialog.cpp b/app/dialog/export/exportadvancedvideodialog.cpp index 71143b50b..2d08ceacd 100644 --- a/app/dialog/export/exportadvancedvideodialog.cpp +++ b/app/dialog/export/exportadvancedvideodialog.cpp @@ -31,6 +31,12 @@ ExportAdvancedVideoDialog::ExportAdvancedVideoDialog(const QList &pix_f pixel_layout->addWidget(pixel_format_combobox_, row, 1); row++; + + pixel_layout->addWidget(new QLabel(tr("YUV Color Range:")), row, 0); + + yuv_color_range_combobox_ = new QComboBox(); + yuv_color_range_combobox_->addItems({tr("Limited (16-235)"), tr("Full (0-255)")}); + pixel_layout->addWidget(yuv_color_range_combobox_, row, 1); } { diff --git a/app/dialog/export/exportadvancedvideodialog.h b/app/dialog/export/exportadvancedvideodialog.h index 275ae44f9..f001a476d 100644 --- a/app/dialog/export/exportadvancedvideodialog.h +++ b/app/dialog/export/exportadvancedvideodialog.h @@ -4,6 +4,7 @@ #include #include +#include "codec/encoder.h" #include "widget/slider/integerslider.h" namespace olive { @@ -35,11 +36,23 @@ public: pixel_format_combobox_->setCurrentText(s); } + VideoParams::ColorRange yuv_range() const + { + return static_cast(yuv_color_range_combobox_->currentIndex()); + } + + void set_yuv_range(VideoParams::ColorRange i) + { + yuv_color_range_combobox_->setCurrentIndex(i); + } + private: IntegerSlider* thread_slider_; QComboBox* pixel_format_combobox_; + QComboBox* yuv_color_range_combobox_; + }; } diff --git a/app/dialog/export/exportformatcombobox.cpp b/app/dialog/export/exportformatcombobox.cpp index c185b27a8..dcbcdd01d 100644 --- a/app/dialog/export/exportformatcombobox.cpp +++ b/app/dialog/export/exportformatcombobox.cpp @@ -46,6 +46,13 @@ ExportFormatComboBox::ExportFormatComboBox(Mode mode, QWidget *parent) : continue; } break; + case kShowSubtitlesOnly: + if (!ExportFormat::GetVideoCodecs(f).isEmpty() + || ExportFormat::GetSubtitleCodecs(f).isEmpty() + || !ExportFormat::GetAudioCodecs(f).isEmpty()) { + continue; + } + break; } QString format_name = ExportFormat::GetName(f); diff --git a/app/dialog/export/exportformatcombobox.h b/app/dialog/export/exportformatcombobox.h index 2f60867af..c90479e72 100644 --- a/app/dialog/export/exportformatcombobox.h +++ b/app/dialog/export/exportformatcombobox.h @@ -34,7 +34,8 @@ public: enum Mode { kShowAllFormats, kShowAudioOnly, - kShowVideoOnly + kShowVideoOnly, + kShowSubtitlesOnly }; ExportFormatComboBox(Mode mode, QWidget *parent = nullptr); diff --git a/app/dialog/export/exportsavepresetdialog.cpp b/app/dialog/export/exportsavepresetdialog.cpp new file mode 100644 index 000000000..03052c4ec --- /dev/null +++ b/app/dialog/export/exportsavepresetdialog.cpp @@ -0,0 +1,97 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 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 "exportsavepresetdialog.h" + +#include +#include +#include +#include + +namespace olive { + +ExportSavePresetDialog::ExportSavePresetDialog(const EncodingParams &p, QWidget *parent) : + QDialog(parent), + params_(p) +{ + auto layout = new QVBoxLayout(this); + + name_edit_ = new QLineEdit(); + + // Populate existing list + QStringList l = EncodingParams::GetListOfPresets(); + if (!l.empty()) { + auto list_widget_ = new QListWidget(); + for (const QString &f : l) { + list_widget_->addItem(f); + } + connect(list_widget_, &QListWidget::currentTextChanged, name_edit_, &QLineEdit::setText); + layout->addWidget(list_widget_); + } + + auto name_layout = new QHBoxLayout(); + layout->addLayout(name_layout); + + name_layout->addWidget(new QLabel(tr("Name:"))); + + name_edit_->setFocus(); + name_layout->addWidget(name_edit_); + + auto btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(btns, &QDialogButtonBox::accepted, this, &ExportSavePresetDialog::accept); + connect(btns, &QDialogButtonBox::rejected, this, &ExportSavePresetDialog::reject); + layout->addWidget(btns); + + setWindowTitle(tr("Save Export Preset")); +} + +void ExportSavePresetDialog::accept() +{ + if (name_edit_->text().isEmpty()) { + QMessageBox::critical(this, tr("Invalid Name"), tr("You must enter a name to save an export preset.")); + return; + } + + QDir d(EncodingParams::GetPresetPath()); + if (!d.exists()) { + d.mkpath(QStringLiteral(".")); + } + + QFile f(d.filePath(name_edit_->text())); + if (f.exists()) { + if (QMessageBox::question(this, tr("Overwrite Preset"), tr("A preset with the name \"%1\" already exists. Do you wish to overwrite it?").arg(name_edit_->text()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + return; + } + } + + if (!f.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Write Error"), tr("Failed to open file \"%1\" for writing.").arg(f.fileName())); + return; + } + + params_.Save(&f); + + f.close(); + + QDialog::accept(); +} + +} diff --git a/app/timeline/timelinepoints.h b/app/dialog/export/exportsavepresetdialog.h similarity index 61% rename from app/timeline/timelinepoints.h rename to app/dialog/export/exportsavepresetdialog.h index 02ea804ce..1f8ca09b9 100644 --- a/app/timeline/timelinepoints.h +++ b/app/dialog/export/exportsavepresetdialog.h @@ -18,36 +18,38 @@ ***/ -#ifndef TIMELINEPOINTS_H -#define TIMELINEPOINTS_H +#ifndef EXPORTSAVEPRESETDIALOG_H +#define EXPORTSAVEPRESETDIALOG_H -#include -#include +#include +#include +#include -#include "timelinemarker.h" -#include "timelineworkarea.h" +#include "codec/encoder.h" namespace olive { -class TimelinePoints : public QObject +class ExportSavePresetDialog : public QDialog { Q_OBJECT public: - TimelinePoints(QObject *parent = nullptr); + ExportSavePresetDialog(const EncodingParams &p, QWidget *parent = nullptr); - TimelineMarkerList* markers(); - const TimelineMarkerList* markers() const; + QString GetSelectedPresetName() const + { + return name_edit_->text(); + } - TimelineWorkArea* workarea(); - const TimelineWorkArea* workarea() const; +public slots: + virtual void accept() override; private: - TimelineMarkerList *markers_; + QLineEdit *name_edit_; - TimelineWorkArea *workarea_; + EncodingParams params_; }; } -#endif // TIMELINEPOINTS_H +#endif // EXPORTSAVEPRESETDIALOG_H diff --git a/app/dialog/export/exportsubtitlestab.cpp b/app/dialog/export/exportsubtitlestab.cpp index 27c40fc16..330942810 100644 --- a/app/dialog/export/exportsubtitlestab.cpp +++ b/app/dialog/export/exportsubtitlestab.cpp @@ -1,7 +1,6 @@ #include "exportsubtitlestab.h" #include -#include namespace olive { @@ -15,22 +14,46 @@ ExportSubtitlesTab::ExportSubtitlesTab(QWidget *parent) : int row = 0; + sidecar_checkbox_ = new QCheckBox(tr("Export to sidecar file")); + layout->addWidget(sidecar_checkbox_, row, 0, 1, 2); + + row++; + + sidecar_format_label_ = new QLabel(tr("Sidecar Format:")); + sidecar_format_label_->setVisible(false); + layout->addWidget(sidecar_format_label_, row, 0); + + sidecar_format_combobox_ = new ExportFormatComboBox(ExportFormatComboBox::kShowSubtitlesOnly); + sidecar_format_combobox_->setVisible(false); + layout->addWidget(sidecar_format_combobox_, row, 1); + + row++; + layout->addWidget(new QLabel(tr("Codec:")), row, 0); codec_combobox_ = new QComboBox(); layout->addWidget(codec_combobox_, row, 1); outer_layout->addStretch(); + + connect(sidecar_checkbox_, &QCheckBox::toggled, sidecar_format_label_, &QWidget::setVisible); + connect(sidecar_checkbox_, &QCheckBox::toggled, sidecar_format_combobox_, &QWidget::setVisible); } int ExportSubtitlesTab::SetFormat(ExportFormat::Format format) { auto scodecs = ExportFormat::GetSubtitleCodecs(format); - setEnabled(!scodecs.isEmpty()); + + sidecar_checkbox_->setChecked(scodecs.empty()); + sidecar_checkbox_->setEnabled(!scodecs.empty()); + + scodecs = ExportFormat::GetSubtitleCodecs(sidecar_format_combobox_->GetFormat()); + codec_combobox_->clear(); foreach (ExportCodec::Codec scodec, scodecs) { codec_combobox_->addItem(ExportCodec::GetCodecName(scodec), scodec); } + return scodecs.size(); } diff --git a/app/dialog/export/exportsubtitlestab.h b/app/dialog/export/exportsubtitlestab.h index a88f4d904..dee62f93e 100644 --- a/app/dialog/export/exportsubtitlestab.h +++ b/app/dialog/export/exportsubtitlestab.h @@ -21,10 +21,13 @@ #ifndef EXPORTSUBTITLESTAB_H #define EXPORTSUBTITLESTAB_H +#include #include +#include #include "codec/exportformat.h" -#include "render/subtitleparams.h" +#include "common/qtutils.h" +#include "dialog/export/exportformatcombobox.h" namespace olive { @@ -34,6 +37,12 @@ class ExportSubtitlesTab : public QWidget public: ExportSubtitlesTab(QWidget *parent = nullptr); + bool GetSidecarEnabled() const { return sidecar_checkbox_->isChecked(); } + void SetSidecarEnabled(bool e) { sidecar_checkbox_->setEnabled(e); } + + ExportFormat::Format GetSidecarFormat() const { return sidecar_format_combobox_->GetFormat(); } + void SetSidecarFormat(ExportFormat::Format f) { sidecar_format_combobox_->SetFormat(f); } + int SetFormat(ExportFormat::Format format); ExportCodec::Codec GetSubtitleCodec() @@ -41,7 +50,17 @@ public: return static_cast(codec_combobox_->currentData().toInt()); } + void SetSubtitleCodec(ExportCodec::Codec c) + { + QtUtils::SetComboBoxData(codec_combobox_, c); + } + private: + QCheckBox *sidecar_checkbox_; + + QLabel *sidecar_format_label_; + ExportFormatComboBox *sidecar_format_combobox_; + QComboBox *codec_combobox_; }; diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index 9fe74ae7e..8eefefee1 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -29,14 +29,14 @@ #include "core.h" #include "exportadvancedvideodialog.h" #include "node/color/colormanager/colormanager.h" -#include "task/export/exportparams.h" namespace olive { ExportVideoTab::ExportVideoTab(ColorManager* color_manager, QWidget *parent) : QWidget(parent), color_manager_(color_manager), - threads_(0) + threads_(0), + color_range_(VideoParams::kColorRangeDefault) { QVBoxLayout* outer_layout = new QVBoxLayout(this); @@ -69,6 +69,13 @@ bool ExportVideoTab::IsImageSequenceSet() const return (img_section && img_section->IsImageSequenceChecked()); } +void ExportVideoTab::SetImageSequence(bool e) const +{ + if (ImageSection* img_section = dynamic_cast(codec_stack_->currentWidget())) { + img_section->SetImageSequenceChecked(e); + } +} + QWidget* ExportVideoTab::SetupResolutionSection() { int row = 0; @@ -106,9 +113,9 @@ QWidget* ExportVideoTab::SetupResolutionSection() scaling_method_combobox_ = new QComboBox(); scaling_method_combobox_->setEnabled(false); - scaling_method_combobox_->addItem(tr("Fit"), ExportParams::kFit); - scaling_method_combobox_->addItem(tr("Stretch"), ExportParams::kStretch); - scaling_method_combobox_->addItem(tr("Crop"), ExportParams::kCrop); + scaling_method_combobox_->addItem(tr("Fit"), EncodingParams::kFit); + scaling_method_combobox_->addItem(tr("Stretch"), EncodingParams::kStretch); + scaling_method_combobox_->addItem(tr("Crop"), EncodingParams::kCrop); layout->addWidget(scaling_method_combobox_, row, 1); // Automatically enable/disable the scaling method depending on maintain aspect ratio @@ -214,10 +221,12 @@ void ExportVideoTab::OpenAdvancedDialog() d.set_threads(threads_); d.set_pix_fmt(pix_fmt_); + d.set_yuv_range(color_range_); if (d.exec() == QDialog::Accepted) { threads_ = d.threads(); pix_fmt_ = d.pix_fmt(); + color_range_ = d.yuv_range(); } } diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index 79fcb5207..67d0cfef0 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -25,6 +25,7 @@ #include #include +#include "common/qtutils.h" #include "common/rational.h" #include "dialog/export/codec/av1section.h" #include "dialog/export/codec/cineformsection.h" @@ -47,6 +48,7 @@ public: int SetFormat(ExportFormat::Format format); bool IsImageSequenceSet() const; + void SetImageSequence(bool e) const; rational GetStillImageTime() const { @@ -58,6 +60,11 @@ public: return static_cast(codec_combobox()->currentData().toInt()); } + void SetSelectedCodec(ExportCodec::Codec c) + { + QtUtils::SetComboBoxData(codec_combobox(), c); + } + QComboBox* codec_combobox() const { return codec_combobox_; @@ -99,6 +106,11 @@ public: return color_space_chooser_->input(); } + void SetOCIOColorSpace(const QString &s) + { + color_space_chooser_->set_input(s); + } + CodecSection* GetCodecSection() const { return static_cast(codec_stack_->currentWidget()); @@ -134,10 +146,17 @@ public: return threads_; } - const QString& pix_fmt() const { - return pix_fmt_; + void SetThreads(int t) + { + threads_ = t; } + const QString& pix_fmt() const { return pix_fmt_; } + void SetPixFmt(const QString &s) { pix_fmt_ = s; } + + VideoParams::ColorRange color_range() const { return color_range_; } + void SetColorRange(VideoParams::ColorRange c) { color_range_ = c; } + public slots: void VideoCodecChanged(); @@ -179,6 +198,7 @@ private: int threads_; QString pix_fmt_; + VideoParams::ColorRange color_range_; ExportFormat::Format format_; diff --git a/app/dialog/footageproperties/footageproperties.h b/app/dialog/footageproperties/footageproperties.h index aa71fa01e..e25898381 100644 --- a/app/dialog/footageproperties/footageproperties.h +++ b/app/dialog/footageproperties/footageproperties.h @@ -83,11 +83,6 @@ private: */ QStackedWidget* stacked_widget_; - /** - * @brief ComboBox for interlacing setting - */ - QComboBox* interlacing_box; - /** * @brief Media name text field */ diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 6fcbdd914..6ed1e486f 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -79,6 +79,17 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) video_layout->addWidget(video_color_space_, row, 1); + row++; + + video_layout->addWidget(new QLabel(tr("Color Range:")), row, 0); + + color_range_combo_ = new QComboBox(); + color_range_combo_->addItem(tr("Limited (16-235)"), VideoParams::kColorRangeLimited); + color_range_combo_->addItem(tr("Full (0-255)"), VideoParams::kColorRangeFull); + color_range_combo_->setCurrentIndex(vp.color_range()); + + video_layout->addWidget(color_range_combo_, row, 1); + if (vp.channel_count() == VideoParams::kRGBAChannelCount) { row++; @@ -136,14 +147,16 @@ void VideoStreamProperties::Accept(MultiUndoCommand *parent) if ((video_premultiply_alpha_ && video_premultiply_alpha_->isChecked() != vp.premultiplied_alpha()) || set_colorspace != vp.colorspace() || static_cast(video_interlace_combo_->currentIndex()) != vp.interlacing() - || pixel_aspect_combo_->GetPixelAspectRatio() != vp.pixel_aspect_ratio()) { + || pixel_aspect_combo_->GetPixelAspectRatio() != vp.pixel_aspect_ratio() + || color_range_combo_->currentData().toInt() != vp.color_range()) { parent->add_child(new VideoStreamChangeCommand(footage_, video_index_, video_premultiply_alpha_ ? video_premultiply_alpha_->isChecked() : vp.premultiplied_alpha(), set_colorspace, static_cast(video_interlace_combo_->currentIndex()), - pixel_aspect_combo_->GetPixelAspectRatio())); + pixel_aspect_combo_->GetPixelAspectRatio(), + static_cast(color_range_combo_->currentData().toInt()))); } if (vp.video_type() == VideoParams::kVideoTypeImageSequence) { @@ -181,13 +194,14 @@ VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(Footag bool premultiplied, QString colorspace, VideoParams::Interlacing interlacing, - const rational &pixel_ar) : + const rational &pixel_ar, VideoParams::ColorRange range) : footage_(footage), video_index_(video_index), new_premultiplied_(premultiplied), new_colorspace_(colorspace), new_interlacing_(interlacing), - new_pixel_ar_(pixel_ar) + new_pixel_ar_(pixel_ar), + new_range_(range) { } @@ -204,11 +218,13 @@ void VideoStreamProperties::VideoStreamChangeCommand::redo() old_colorspace_ = vp.colorspace(); old_interlacing_ = vp.interlacing(); old_pixel_ar_ = vp.pixel_aspect_ratio(); + old_range_ = vp.color_range(); vp.set_premultiplied_alpha(new_premultiplied_); vp.set_colorspace(new_colorspace_); vp.set_interlacing(new_interlacing_); vp.set_pixel_aspect_ratio(new_pixel_ar_); + vp.set_color_range(new_range_); footage_->SetVideoParams(vp, video_index_); } @@ -221,6 +237,7 @@ void VideoStreamProperties::VideoStreamChangeCommand::undo() vp.set_colorspace(old_colorspace_); vp.set_interlacing(old_interlacing_); vp.set_pixel_aspect_ratio(old_pixel_ar_); + vp.set_color_range(old_range_); footage_->SetVideoParams(vp, video_index_); } diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index ea689e2f0..dec410c97 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -56,6 +56,11 @@ private: */ QComboBox* video_color_space_; + /** + * @brief Setting for this streams's color range + */ + QComboBox *color_range_combo_; + /** * @brief Setting for video interlacing */ @@ -88,7 +93,8 @@ private: bool premultiplied, QString colorspace, VideoParams::Interlacing interlacing, - const rational& pixel_ar); + const rational& pixel_ar, + VideoParams::ColorRange range); virtual Project* GetRelevantProject() const override; @@ -104,11 +110,13 @@ private: QString new_colorspace_; VideoParams::Interlacing new_interlacing_; rational new_pixel_ar_; + VideoParams::ColorRange new_range_; bool old_premultiplied_; QString old_colorspace_; VideoParams::Interlacing old_interlacing_; rational old_pixel_ar_; + VideoParams::ColorRange old_range_; }; diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp index a081260fe..df9917a84 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp @@ -44,6 +44,11 @@ PreferencesBehaviorTab::PreferencesBehaviorTab() AddItem(tr("Enable slider ladder"), QStringLiteral("UseSliderLadders"), general_group); + AddItem(tr("Scrolling zooms by default"), + QStringLiteral("ScrollZooms"), + tr("By default, scrolling will move the view around, and holding Ctrl/Cmd will make it zoom instead. " + "Enabling this will switch those, scrolling will zoom by default, and holding Ctrl/Cmd will move the view instead."), + general_group); QTreeWidgetItem* audio_group = AddParent(tr("Audio")); AddItem(tr("Enable audio scrubbing"), diff --git a/app/dialog/sequence/sequence.cpp b/app/dialog/sequence/sequence.cpp index 28f8f98e6..35b8f311e 100644 --- a/app/dialog/sequence/sequence.cpp +++ b/app/dialog/sequence/sequence.cpp @@ -102,7 +102,7 @@ void SequenceDialog::SetNameIsEditable(bool e) void SequenceDialog::accept() { if (name_field_->isEnabled() && name_field_->text().isEmpty()) { - QtUtils::MessageBox(this, QMessageBox::Critical, tr("Error editing Sequence"), tr("Please enter a name for this Sequence.")); + QtUtils::MsgBox(this, QMessageBox::Critical, tr("Error editing Sequence"), tr("Please enter a name for this Sequence.")); return; } @@ -167,7 +167,7 @@ void SequenceDialog::accept() void SequenceDialog::SetAsDefaultClicked() { - if (QtUtils::MessageBox(this, QMessageBox::Question, tr("Confirm Set As Default"), + if (QtUtils::MsgBox(this, QMessageBox::Question, tr("Confirm Set As Default"), tr("Are you sure you want to set the current parameters as defaults?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { // Maybe replace with Preset system diff --git a/app/dialog/sequence/sequencedialogpresettab.cpp b/app/dialog/sequence/sequencedialogpresettab.cpp index b8a94fbe7..776832906 100644 --- a/app/dialog/sequence/sequencedialogpresettab.cpp +++ b/app/dialog/sequence/sequencedialogpresettab.cpp @@ -63,9 +63,9 @@ SequenceDialogPresetTab::SequenceDialogPresetTab(QWidget* parent) : preset_tree_->addTopLevelItem(my_presets_folder_); // Add presets - preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("4K UHD"), 3840, 2160, 6)); - preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("1080p"), 1920, 1080, 3)); - preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("720p"), 1280, 720, 2)); + preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("4K UHD"), 3840, 2160, 2)); + preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("1080p"), 1920, 1080, 1)); + preset_tree_->addTopLevelItem(CreateHDPresetFolder(tr("720p"), 1280, 720, 1)); preset_tree_->addTopLevelItem(CreateSDPresetFolder(tr("NTSC"), 720, 480, rational(30000, 1001), VideoParams::kPixelAspectNTSCStandard, diff --git a/app/dialog/speedduration/speeddurationdialog.cpp b/app/dialog/speedduration/speeddurationdialog.cpp index 580da35d8..7214ca27c 100644 --- a/app/dialog/speedduration/speeddurationdialog.cpp +++ b/app/dialog/speedduration/speeddurationdialog.cpp @@ -38,12 +38,12 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, cons clips_(clips), timebase_(timebase) { - setWindowTitle(tr("Speed/Duration")); + setWindowTitle(tr("Clip Properties")); QVBoxLayout *layout = new QVBoxLayout(this); { - QGroupBox *speed_group = new QGroupBox(); + QGroupBox *speed_group = new QGroupBox(tr("Speed/Duration")); layout->addWidget(speed_group); QGridLayout *speed_layout = new QGridLayout(speed_group); @@ -72,16 +72,39 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, cons link_box_ = new QCheckBox(tr("Link Speed and Duration")); link_box_->setChecked(true); speed_layout->addWidget(link_box_, row, 0, 1, 2); + + row++; + + reverse_box_ = new QCheckBox(tr("Reverse")); + speed_layout->addWidget(reverse_box_, row, 0, 1, 2); + + row++; + + maintain_audio_pitch_box_ = new QCheckBox(tr("Maintain Audio Pitch")); + speed_layout->addWidget(maintain_audio_pitch_box_, row, 0, 1, 2); + + row++; + + ripple_box_ = new QCheckBox(tr("Ripple Trailing Clips")); + speed_layout->addWidget(ripple_box_, row, 0, 1, 2); } - reverse_box_ = new QCheckBox(tr("Reverse")); - layout->addWidget(reverse_box_); + { + auto loop_box = new QGroupBox(tr("Loop")); + layout->addWidget(loop_box); - maintain_audio_pitch_box_ = new QCheckBox(tr("Maintain Audio Pitch")); - layout->addWidget(maintain_audio_pitch_box_); + auto loop_layout = new QGridLayout(loop_box); - ripple_box_ = new QCheckBox(tr("Ripple Trailing Clips")); - layout->addWidget(ripple_box_); + int row = 0; + + loop_layout->addWidget(new QLabel(tr("Loop:")), row, 0); + + loop_combo_ = new QComboBox(); + loop_combo_->addItem(tr("None"), Decoder::kLoopModeOff); + loop_combo_->addItem(tr("Loop"), Decoder::kLoopModeLoop); + loop_combo_->addItem(tr("Clamp"), Decoder::kLoopModeClamp); + loop_layout->addWidget(loop_combo_, row, 1); + } QDialogButtonBox *btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); btns->setCenterButtons(true); @@ -94,6 +117,7 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, cons start_duration_ = clips.first()->length(); start_reverse_ = clips.first()->reverse(); start_maintain_audio_pitch_ = clips.first()->maintain_audio_pitch(); + start_loop_ = clips.first()->loop_mode(); for (int i=1; i &clips, cons if (start_maintain_audio_pitch_ != -1 && clip_maintain_pitch != start_maintain_audio_pitch_) { start_maintain_audio_pitch_ = -1; } + + if (start_loop_ != -1 && c->loop_mode() != start_loop_) { + start_loop_ = -1; + } } if (qIsNaN(start_speed_)) { @@ -141,6 +169,12 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, cons } else { maintain_audio_pitch_box_->setChecked(start_maintain_audio_pitch_); } + + if (start_loop_ == -1) { + loop_combo_->setCurrentIndex(-1); + } else { + loop_combo_->setCurrentIndex(start_loop_); + } } void SpeedDurationDialog::accept() @@ -211,6 +245,12 @@ void SpeedDurationDialog::accept() } } + if (loop_combo_->currentIndex() != -1) { + foreach (ClipBlock *c, clips_) { + command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(c, ClipBlock::kLoopModeInput)), loop_combo_->currentData())); + } + } + Core::instance()->undo_stack()->push(command); super::accept(); diff --git a/app/dialog/speedduration/speeddurationdialog.h b/app/dialog/speedduration/speeddurationdialog.h index 82d95d12e..3e97d718d 100644 --- a/app/dialog/speedduration/speeddurationdialog.h +++ b/app/dialog/speedduration/speeddurationdialog.h @@ -22,6 +22,7 @@ #define SPEEDDURATIONDIALOG_H #include +#include #include #include "node/block/clip/clip.h" @@ -62,6 +63,8 @@ private: QCheckBox *ripple_box_; + QComboBox *loop_combo_; + int start_reverse_; int start_maintain_audio_pitch_; @@ -70,6 +73,8 @@ private: rational start_duration_; + int start_loop_; + rational timebase_; private slots: diff --git a/app/node/audio/pan/pan.cpp b/app/node/audio/pan/pan.cpp index 220ba9585..77e194506 100644 --- a/app/node/audio/pan/pan.cpp +++ b/app/node/audio/pan/pan.cpp @@ -69,8 +69,6 @@ void PanNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeV // Create a sample job SampleBuffer samples = value[kSamplesInput].toSamples(); if (samples.is_allocated()) { - bool pushed_job = false; - // This node is only compatible with stereo audio if (samples.audio_params().channel_count() == 2) { // If the input is static, we can just do it now which will be faster @@ -83,15 +81,14 @@ void PanNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeV samples.transform_volume_for_channel(1, 1.0f + pan_volume); } } + + table->Push(NodeValue(NodeValue::kSamples, samples, this)); } else { // Requires job - - pushed_job = true; table->Push(NodeValue::kSamples, SampleJob(kSamplesInput, value), this); } - } - - if (!pushed_job) { + } else { + // Pass right through table->Push(value[kSamplesInput]); } } diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 241484965..b94bd0c0e 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -34,6 +34,7 @@ const QString ClipBlock::kMediaInInput = QStringLiteral("media_in_in"); const QString ClipBlock::kSpeedInput = QStringLiteral("speed_in"); const QString ClipBlock::kReverseInput = QStringLiteral("reverse_in"); const QString ClipBlock::kMaintainAudioPitchInput = QStringLiteral("maintain_audio_pitch_in"); +const QString ClipBlock::kLoopModeInput = QStringLiteral("loop_in"); ClipBlock::ClipBlock() : in_transition_(nullptr), @@ -56,6 +57,8 @@ ClipBlock::ClipBlock() : //SetValueHintForInput(kBufferIn, ValueHint(NodeValue::kBuffer)); SetEffectInput(kBufferIn); + + AddInput(kLoopModeInput, NodeValue::kCombo, 0, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); } QString ClipBlock::Name() const @@ -89,7 +92,7 @@ void ClipBlock::set_length_and_media_out(const rational &length) if (reverse()) { // Calculate media_in adjustment - rational proposed_media_in = SequenceToMediaTime(this->length() - length, true); + rational proposed_media_in = SequenceToMediaTime(this->length() - length, kSTMIgnoreReverse | kSTMIgnoreLoop); set_media_in(proposed_media_in); } @@ -104,11 +107,9 @@ void ClipBlock::set_length_and_media_in(const rational &length) if (!reverse()) { // Calculate media_in adjustment - rational proposed_media_in = SequenceToMediaTime(this->length() - length, false, true); + waveform_.TrimIn(SequenceToMediaTime(this->length() - length, kSTMIgnoreSpeed | kSTMIgnoreLoop) - media_in()); - waveform_.TrimIn(proposed_media_in - media_in()); - - set_media_in(proposed_media_in); + set_media_in(SequenceToMediaTime(this->length() - length, kSTMIgnoreLoop)); } else { // Trim waveform out point waveform_.TrimIn(this->length() - length); @@ -127,7 +128,7 @@ void ClipBlock::set_media_in(const rational &media_in) SetStandardValue(kMediaInInput, QVariant::fromValue(media_in)); } -rational ClipBlock::SequenceToMediaTime(const rational &sequence_time, bool ignore_reverse, bool ignore_speed) const +rational ClipBlock::SequenceToMediaTime(const rational &sequence_time, uint64_t flags) const { // These constants are not considered "values" per se, so we don't modify them if (sequence_time == RATIONAL_MIN || sequence_time == RATIONAL_MAX) { @@ -136,11 +137,11 @@ rational ClipBlock::SequenceToMediaTime(const rational &sequence_time, bool igno rational media_time = sequence_time; - if (reverse() && !ignore_reverse) { + if (reverse() && !(flags & kSTMIgnoreReverse)) { media_time = length() - media_time; } - if (!ignore_speed) { + if (!(flags & kSTMIgnoreSpeed)) { double speed_value = speed(); if (qIsNull(speed_value)) { // Effectively holds the frame at the in point @@ -153,6 +154,23 @@ rational ClipBlock::SequenceToMediaTime(const rational &sequence_time, bool igno media_time += media_in(); + /*if (!(flags & kSTMIgnoreLoop) + && this->loop_mode() != kLoopModeOff + && connected_viewer_ + && !connected_viewer_->GetLength().isNull() + && (media_time < 0 || media_time >= connected_viewer_->GetLength())) { + if (loop_mode() == kLoopModeLoop) { + while (media_time < 0) { + media_time += connected_viewer_->GetLength(); + } + while (media_time >= connected_viewer_->GetLength()) { + media_time -= connected_viewer_->GetLength(); + } + } else if (loop_mode() == kLoopModeClamp) { + media_time = std::clamp(media_time, rational(0), connected_viewer_->GetLength()-connected_viewer_->GetVideoParams().frame_rate_as_time_base()); + } + }*/ + return media_time; } @@ -204,17 +222,17 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int if (new_connected_viewer != connected_viewer_) { if (connected_viewer_) { - disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); - disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); - disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); + disconnect(connected_viewer_->GetMarkers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); + disconnect(connected_viewer_->GetMarkers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); + disconnect(connected_viewer_->GetMarkers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); } connected_viewer_ = new_connected_viewer; if (connected_viewer_) { - connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); - connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); - connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); + connect(connected_viewer_->GetMarkers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); + connect(connected_viewer_->GetMarkers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); + connect(connected_viewer_->GetMarkers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); } } @@ -282,6 +300,13 @@ void ClipBlock::Retranslate() SetInputName(kSpeedInput, tr("Speed")); SetInputName(kReverseInput, tr("Reverse")); SetInputName(kMaintainAudioPitchInput, tr("Maintain Audio Pitch")); + SetInputName(kLoopModeInput, tr("Loop")); + SetComboBoxStrings(kLoopModeInput, {tr("None"), tr("Loop"), tr("Clamp")}); +} + +TimeRange ClipBlock::media_range() const +{ + return InputTimeAdjustment(kBufferIn, -1, TimeRange(0, length())); } } diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index fce20dbbd..c6571d2b1 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -22,6 +22,7 @@ #define CLIPBLOCK_H #include "audio/audiovisualwaveform.h" +#include "codec/decoder.h" #include "node/block/block.h" namespace olive { @@ -119,17 +120,41 @@ public: return connected_viewer_; } + TimeRange media_range() const; + + /** + * @brief Get currently set loop mode + */ + Decoder::LoopMode loop_mode() const + { + return static_cast(GetStandardValue(kLoopModeInput).toInt()); + } + + void set_loop_mode(Decoder::LoopMode l) + { + SetStandardValue(kLoopModeInput, int(l)); + } + static const QString kBufferIn; static const QString kMediaInInput; static const QString kSpeedInput; static const QString kReverseInput; static const QString kMaintainAudioPitchInput; + static const QString kLoopModeInput; protected: virtual void LinkChangeEvent() override; private: - rational SequenceToMediaTime(const rational& sequence_time, bool ignore_reverse = false, bool ignore_speed = false) const; + enum SequenceToMediaTimeFlag + { + kSTMNone = 0x0, + kSTMIgnoreReverse = 0x1, + kSTMIgnoreSpeed = 0x2, + kSTMIgnoreLoop = 0x4 + }; + + rational SequenceToMediaTime(const rational& sequence_time, uint64_t flags = kSTMNone) const; rational MediaToSequenceTime(const rational& media_time) const; diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp index 3fd55b653..9a0622f75 100644 --- a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp @@ -53,13 +53,6 @@ ShaderCode CrossDissolveTransition::GetShaderCode(const ShaderRequest &request) return ShaderCode(FileFunctions::ReadFileAsString(":/shaders/crossdissolve.frag"), QString()); } -void CrossDissolveTransition::ShaderJobEvent(const NodeValueRow &value, ShaderJob &job) const -{ - Q_UNUSED(value) - - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); -} - void CrossDissolveTransition::SampleJobEvent(const SampleBuffer &from_samples, const SampleBuffer &to_samples, SampleBuffer &out_samples, double time_in) const { for (int i=0; i -#include "common/range.h" -#include "core.h" -#include "node/traverser.h" - namespace olive { +const QString TransformDistortNode::kParentInput = QStringLiteral("parent_in"); const QString TransformDistortNode::kTextureInput = QStringLiteral("tex_in"); const QString TransformDistortNode::kAutoscaleInput = QStringLiteral("autoscale_in"); const QString TransformDistortNode::kInterpolationInput = QStringLiteral("interpolation_in"); @@ -36,6 +33,8 @@ const QString TransformDistortNode::kInterpolationInput = QStringLiteral("interp TransformDistortNode::TransformDistortNode() { + AddInput(kParentInput, NodeValue::kMatrix); + AddInput(kAutoscaleInput, NodeValue::kCombo, 0); AddInput(kInterpolationInput, NodeValue::kCombo, 2); @@ -73,6 +72,7 @@ void TransformDistortNode::Retranslate() { super::Retranslate(); + SetInputName(kParentInput, tr("Parent")); SetInputName(kAutoscaleInput, tr("Auto-Scale")); SetInputName(kTextureInput, tr("Texture")); SetInputName(kInterpolationInput, tr("Interpolation")); @@ -84,12 +84,12 @@ void TransformDistortNode::Retranslate() void TransformDistortNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { // Generate matrix - QMatrix4x4 generated_matrix = GenerateMatrix(value, false, false, false); + QMatrix4x4 generated_matrix = GenerateMatrix(value, false, false, false, value[kParentInput].toMatrix()); // Pop texture NodeValue texture_meta = value[kTextureInput]; - bool pushed_job = false; + QVariant job_to_push; // If we have a texture, generate a matrix and make it happen if (TexturePtr texture = texture_meta.toTexture()) { @@ -103,19 +103,17 @@ void TransformDistortNode::Value(const NodeValueRow &value, const NodeGlobals &g job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, real_matrix, this)); job.SetInterpolation(QStringLiteral("ove_maintex"), static_cast(value[kInterpolationInput].toInt())); - // FIXME: This should be optimized, we can use matrix math to determine if this operation will - // end up with gaps in the screen that will require an alpha channel. - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); - - table->Push(NodeValue::kTexture, QVariant::fromValue(job), this); - - pushed_job = true; + job_to_push = QVariant::fromValue(job); } } - if (!pushed_job) { + table->Push(NodeValue::kMatrix, QVariant::fromValue(generated_matrix), this); + + if (job_to_push.isNull()) { // Re-push whatever value we received table->Push(texture_meta); + } else { + table->Push(NodeValue::kTexture, job_to_push, this); } } @@ -133,7 +131,7 @@ void TransformDistortNode::GizmoDragStart(const NodeValueRow &row, double x, dou if (gizmo == anchor_gizmo_) { - gizmo_inverted_transform_ = GenerateMatrix(row, true, true, false).toTransform().inverted(); + gizmo_inverted_transform_ = GenerateMatrix(row, true, true, false, row[kParentInput].toMatrix()).toTransform().inverted(); } else if (IsAScaleGizmo(gizmo)) { @@ -175,7 +173,7 @@ void TransformDistortNode::GizmoDragStart(const NodeValueRow &row, double x, dou } // Store current matrix - gizmo_inverted_transform_ = GenerateMatrix(row, true, true, true).toTransform().inverted(); + gizmo_inverted_transform_ = GenerateMatrix(row, true, true, true, row[kParentInput].toMatrix()).toTransform().inverted(); } else if (gizmo == rotation_gizmo_) { @@ -360,7 +358,7 @@ void TransformDistortNode::UpdateGizmoPositions(const NodeValueRow &row, const N // Fold values into a matrix for the rectangle QMatrix4x4 rectangle_matrix; rectangle_matrix.scale(sequence_half_res); - rectangle_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, false, false, false), + rectangle_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, false, false, false, row[kParentInput].toMatrix()), sequence_res, tex_sz, tex_offset, @@ -380,7 +378,7 @@ void TransformDistortNode::UpdateGizmoPositions(const NodeValueRow &row, const N // Draw anchor point QMatrix4x4 anchor_matrix; anchor_matrix.scale(sequence_half_res); - anchor_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, true, false, false), + anchor_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, true, false, false, row[kParentInput].toMatrix()), sequence_res, tex_sz, tex_offset, @@ -406,7 +404,8 @@ void TransformDistortNode::UpdateGizmoPositions(const NodeValueRow &row, const N QTransform TransformDistortNode::GizmoTransformation(const NodeValueRow &row, const NodeGlobals &globals) const { if (TexturePtr texture = row[kTextureInput].toTexture()) { - auto m = GenerateMatrix(row, false, false, false); + //auto m = GenerateMatrix(row, false, false, false, row[kParentInput].toMatrix()); + auto m = GenerateMatrix(row, false, false, false, QMatrix4x4()); return GenerateAutoScaledMatrix(m, row, globals, texture->params()).toTransform(); } return super::GizmoTransformation(row, globals); diff --git a/app/node/distort/transform/transformdistortnode.h b/app/node/distort/transform/transformdistortnode.h index b6ae714b9..be17de561 100644 --- a/app/node/distort/transform/transformdistortnode.h +++ b/app/node/distort/transform/transformdistortnode.h @@ -84,6 +84,7 @@ public: virtual void UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals &globals) override; virtual QTransform GizmoTransformation(const NodeValueRow &row, const NodeGlobals &globals) const override; + static const QString kParentInput; static const QString kTextureInput; static const QString kAutoscaleInput; static const QString kInterpolationInput; diff --git a/app/node/effect/opacity/opacityeffect.cpp b/app/node/effect/opacity/opacityeffect.cpp index 9ebabbc20..6c60b36f1 100644 --- a/app/node/effect/opacity/opacityeffect.cpp +++ b/app/node/effect/opacity/opacityeffect.cpp @@ -52,7 +52,6 @@ void OpacityEffect::Value(const NodeValueRow &value, const NodeGlobals &globals, // If there's no texture, no need to run an operation if (job.Get(kTextureInput).toTexture()) { if (!qFuzzyCompare(job.Get(kValueInput).toDouble(), 1.0)) { - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); table->Push(NodeValue::kTexture, QVariant::fromValue(job), this); } else { // 1.0 float is a no-op, so just push the texture diff --git a/app/node/factory.cpp b/app/node/factory.cpp index dd82a0805..41d6f55a5 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -62,6 +62,7 @@ #include "project/folder/folder.h" #include "project/footage/footage.h" #include "project/sequence/sequence.h" +#include "time/timeformat/timeformat.h" #include "time/timeoffset/timeoffsetnode.h" #include "time/timeremap/timeremap.h" @@ -291,6 +292,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new MaskDistortNode(); case kDropShadowFilter: return new DropShadowFilter(); + case kTimeFormat: + return new TimeFormatNode(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index d7ca955ff..b6f6037f0 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -75,6 +75,7 @@ public: kChromaKey, kMaskDistort, kDropShadowFilter, + kTimeFormat, // Count value kInternalNodeCount diff --git a/app/node/filter/blur/blur.cpp b/app/node/filter/blur/blur.cpp index 25a1984c2..0ac4eb6d5 100644 --- a/app/node/filter/blur/blur.cpp +++ b/app/node/filter/blur/blur.cpp @@ -159,11 +159,6 @@ void BlurFilterNode::Value(const NodeValueRow &value, const NodeGlobals &globals } if (can_push_job) { - // If we're not repeating pixels, expect an alpha channel to appear - if (!job.Get(kRepeatEdgePixelsInput).toBool()) { - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); - } - table->Push(NodeValue::kTexture, QVariant::fromValue(job), this); } else { // If we're not performing the blur job, just push the texture diff --git a/app/node/generator/matrix/matrix.cpp b/app/node/generator/matrix/matrix.cpp index 81e43c4ea..81dbdb8f6 100644 --- a/app/node/generator/matrix/matrix.cpp +++ b/app/node/generator/matrix/matrix.cpp @@ -90,11 +90,11 @@ void MatrixGenerator::Retranslate() void MatrixGenerator::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { // Push matrix output - QMatrix4x4 mat = GenerateMatrix(value, false, false, false); + QMatrix4x4 mat = GenerateMatrix(value, false, false, false, QMatrix4x4()); table->Push(NodeValue::kMatrix, mat, this); } -QMatrix4x4 MatrixGenerator::GenerateMatrix(const NodeValueRow &value, bool ignore_anchor, bool ignore_position, bool ignore_scale) const +QMatrix4x4 MatrixGenerator::GenerateMatrix(const NodeValueRow &value, bool ignore_anchor, bool ignore_position, bool ignore_scale, const QMatrix4x4 &mat) const { QVector2D anchor; QVector2D position; @@ -116,17 +116,17 @@ QMatrix4x4 MatrixGenerator::GenerateMatrix(const NodeValueRow &value, bool ignor value[kRotationInput].toDouble(), scale, value[kUniformScaleInput].toBool(), - anchor); + anchor, + mat); } QMatrix4x4 MatrixGenerator::GenerateMatrix(const QVector2D& pos, const float& rot, const QVector2D& scale, bool uniform_scale, - const QVector2D& anchor) + const QVector2D& anchor, + QMatrix4x4 mat) { - QMatrix4x4 mat; - // Position mat.translate(pos); diff --git a/app/node/generator/matrix/matrix.h b/app/node/generator/matrix/matrix.h index 5d93cb51d..60ea1a035 100644 --- a/app/node/generator/matrix/matrix.h +++ b/app/node/generator/matrix/matrix.h @@ -53,12 +53,13 @@ public: static const QString kAnchorInput; protected: - QMatrix4x4 GenerateMatrix(const NodeValueRow &value, bool ignore_anchor, bool ignore_position, bool ignore_scale) const; + QMatrix4x4 GenerateMatrix(const NodeValueRow &value, bool ignore_anchor, bool ignore_position, bool ignore_scale, const QMatrix4x4 &mat) const; static QMatrix4x4 GenerateMatrix(const QVector2D &pos, const float &rot, const QVector2D &scale, bool uniform_scale, - const QVector2D &anchor); + const QVector2D &anchor, + QMatrix4x4 mat); virtual void InputValueChangedEvent(const QString& input, int element) override; diff --git a/app/node/generator/polygon/polygon.cpp b/app/node/generator/polygon/polygon.cpp index de458c6e8..ec65f8b7f 100644 --- a/app/node/generator/polygon/polygon.cpp +++ b/app/node/generator/polygon/polygon.cpp @@ -23,8 +23,6 @@ #include #include -#include "common/cpuoptimize.h" - namespace olive { const QString PolygonGenerator::kPointsInput = QStringLiteral("points_in"); @@ -89,20 +87,25 @@ void PolygonGenerator::Retranslate() SetInputName(kColorInput, tr("Color")); } -GenerateJob PolygonGenerator::GetGenerateJob(const NodeValueRow &value) const +ShaderJob PolygonGenerator::GetGenerateJob(const NodeValueRow &value) const { GenerateJob job; job.Insert(value); - job.SetRequestedFormat(VideoParams::kFormatFloat32); - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); + job.SetRequestedFormat(VideoParams::kFormatUnsigned8); - return job; + // Conversion to RGB + ShaderJob rgb; + rgb.SetShaderID(QStringLiteral("rgb")); + rgb.Insert(QStringLiteral("texture_in"), NodeValue(NodeValue::kTexture, job, this)); + rgb.Insert(QStringLiteral("color_in"), value[kColorInput]); + + return rgb; } void PolygonGenerator::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { - GenerateJob job = GetGenerateJob(value); + ShaderJob job = GetGenerateJob(value); PushMergableJob(value, QVariant::fromValue(job), table); } @@ -113,7 +116,7 @@ void PolygonGenerator::GenerateFrame(FramePtr frame, const GenerateJob &job) con // QImages only support integer pixels and we use float pixels, so what we do here is draw onto // a single-channel QImage (alpha only) and then transplant that alpha channel to our float buffer // with correct float RGB. - QImage img(frame->width(), frame->height(), QImage::Format_Grayscale8); + QImage img((uchar *) frame->data(), frame->width(), frame->height(), frame->linesize_bytes(), QImage::Format_RGBA8888_Premultiplied); img.fill(Qt::transparent); QVector points = job.Get(kPointsInput).value< QVector >(); @@ -128,34 +131,6 @@ void PolygonGenerator::GenerateFrame(FramePtr frame, const GenerateJob &job) con p.setPen(Qt::NoPen); p.drawPath(path); - - // Transplant alpha channel to frame - Color rgba = job.Get(kColorInput).toColor(); -#if defined(Q_PROCESSOR_X86) || defined(Q_PROCESSOR_ARM) - __m128 sse_color = _mm_loadu_ps(rgba.data()); -#endif - - float *frame_dst = reinterpret_cast(frame->data()); - for (int y=0; yheight(); y++) { - uchar *src_y = img.bits() + img.bytesPerLine() * y; - float *dst_y = frame_dst + y*frame->linesize_pixels()*VideoParams::kRGBAChannelCount; - - for (int x=0; xwidth(); x++) { - float alpha = float(src_y[x]) / 255.0f; - float *dst = dst_y + x*VideoParams::kRGBAChannelCount; - -#if defined(Q_PROCESSOR_X86) || defined(Q_PROCESSOR_ARM) - __m128 sse_alpha = _mm_load1_ps(&alpha); - __m128 sse_res = _mm_mul_ps(sse_color, sse_alpha); - - _mm_store_ps(dst, sse_res); -#else - for (int i=0; i @@ -241,6 +216,15 @@ void PolygonGenerator::UpdateGizmoPositions(const NodeValueRow &row, const NodeG poly_gizmo_->SetPath(GeneratePath(points).translated(half_res)); } +ShaderCode PolygonGenerator::GetShaderCode(const ShaderRequest &request) const +{ + if (request.id == QStringLiteral("rgb")) { + return ShaderCode(FileFunctions::ReadFileAsString(":/shaders/rgb.frag")); + } else { + return super::GetShaderCode(request); + } +} + void PolygonGenerator::GizmoDragMove(double x, double y, const Qt::KeyboardModifiers &modifiers) { DraggableGizmo *gizmo = static_cast(sender()); diff --git a/app/node/generator/polygon/polygon.h b/app/node/generator/polygon/polygon.h index 7a6bbc340..dd7e5623b 100644 --- a/app/node/generator/polygon/polygon.h +++ b/app/node/generator/polygon/polygon.h @@ -54,11 +54,13 @@ public: virtual void UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals &globals) override; + virtual ShaderCode GetShaderCode(const ShaderRequest &request) const override; + static const QString kPointsInput; static const QString kColorInput; protected: - GenerateJob GetGenerateJob(const NodeValueRow &value) const; + ShaderJob GetGenerateJob(const NodeValueRow &value) const; protected slots: virtual void GizmoDragMove(double x, double y, const Qt::KeyboardModifiers &modifiers) override; diff --git a/app/node/generator/shape/shapenode.cpp b/app/node/generator/shape/shapenode.cpp index 59b6e071c..d4808d768 100644 --- a/app/node/generator/shape/shapenode.cpp +++ b/app/node/generator/shape/shapenode.cpp @@ -25,10 +25,14 @@ namespace olive { #define super ShapeNodeBase QString ShapeNode::kTypeInput = QStringLiteral("type_in"); +QString ShapeNode::kRadiusInput = QStringLiteral("radius_in"); ShapeNode::ShapeNode() { PrependInput(kTypeInput, NodeValue::kCombo); + + AddInput(kRadiusInput, NodeValue::kFloat, 20.0); + SetInputProperty(kRadiusInput, QStringLiteral("min"), 0.0); } QString ShapeNode::Name() const @@ -56,9 +60,10 @@ void ShapeNode::Retranslate() super::Retranslate(); SetInputName(kTypeInput, tr("Type")); + SetInputName(kRadiusInput, tr("Radius")); // Coordinate with Type enum - SetComboBoxStrings(kTypeInput, {tr("Rectangle"), tr("Ellipse")}); + SetComboBoxStrings(kTypeInput, {tr("Rectangle"), tr("Ellipse"), tr("Rounded Rectangle")}); } ShaderCode ShapeNode::GetShaderCode(const ShaderRequest &request) const @@ -76,10 +81,23 @@ void ShapeNode::Value(const NodeValueRow &value, const NodeGlobals &globals, Nod job.Insert(value); job.Insert(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, globals.resolution(), this)); - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); job.SetShaderID(QStringLiteral("shape")); PushMergableJob(value, QVariant::fromValue(job), table); } +void ShapeNode::InputValueChangedEvent(const QString &input, int element) +{ + if (input == kTypeInput) { + InputFlags i = GetInputFlags(kRadiusInput); + if (GetStandardValue(kTypeInput).toInt() == kRoundedRectangle) { + i &= InputFlag(~kInputFlagHidden); + } else { + i |= kInputFlagHidden; + } + SetInputFlags(kRadiusInput, i); + } + super::InputValueChangedEvent(input, element); +} + } diff --git a/app/node/generator/shape/shapenode.h b/app/node/generator/shape/shapenode.h index 3dfdccbaf..285fc5bb5 100644 --- a/app/node/generator/shape/shapenode.h +++ b/app/node/generator/shape/shapenode.h @@ -33,7 +33,8 @@ public: enum Type { kRectangle, - kEllipse + kEllipse, + kRoundedRectangle }; NODE_DEFAULT_FUNCTIONS(ShapeNode) @@ -49,6 +50,10 @@ public: virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; static QString kTypeInput; + static QString kRadiusInput; + +protected: + virtual void InputValueChangedEvent(const QString &input, int element) override; }; diff --git a/app/node/generator/text/textv1.cpp b/app/node/generator/text/textv1.cpp index 22f4d30c7..93d36737f 100644 --- a/app/node/generator/text/textv1.cpp +++ b/app/node/generator/text/textv1.cpp @@ -94,7 +94,6 @@ void TextGeneratorV1::Value(const NodeValueRow &value, const NodeGlobals &global { GenerateJob job; job.Insert(value); - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); if (!job.Get(kTextInput).toString().isEmpty()) { table->Push(NodeValue::kTexture, QVariant::fromValue(job), this); diff --git a/app/node/generator/text/textv2.cpp b/app/node/generator/text/textv2.cpp index 0df750fc6..754c0baaf 100644 --- a/app/node/generator/text/textv2.cpp +++ b/app/node/generator/text/textv2.cpp @@ -97,7 +97,6 @@ void TextGeneratorV2::Value(const NodeValueRow &value, const NodeGlobals &global { GenerateJob job; job.Insert(value); - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); job.SetRequestedFormat(VideoParams::kFormatFloat32); if (!job.Get(kTextInput).toString().isEmpty()) { diff --git a/app/node/generator/text/textv3.cpp b/app/node/generator/text/textv3.cpp index da6f9bfe8..a723e67aa 100644 --- a/app/node/generator/text/textv3.cpp +++ b/app/node/generator/text/textv3.cpp @@ -24,9 +24,10 @@ #include #include -#include "common/functiontimer.h" #include "common/html.h" +#include "core.h" #include "node/project/project.h" +#include "widget/nodeparamview/nodeparamviewundo.h" namespace olive { @@ -39,17 +40,30 @@ enum TextVerticalAlign { }; const QString TextGeneratorV3::kTextInput = QStringLiteral("text_in"); +const QString TextGeneratorV3::kVerticalAlignmentInput = QStringLiteral("valign_in"); +const QString TextGeneratorV3::kUseArgsInput = QStringLiteral("use_args_in"); +const QString TextGeneratorV3::kArgsInput = QStringLiteral("args_in"); TextGeneratorV3::TextGeneratorV3() : - ShapeNodeBase(false) + ShapeNodeBase(false), + dont_emit_valign_(false) { AddInput(kTextInput, NodeValue::kText, QStringLiteral("

%1

").arg(tr("Sample Text"))); SetInputProperty(kTextInput, QStringLiteral("vieweronly"), true); SetStandardValue(kSizeInput, QVector2D(400, 300)); + AddInput(kVerticalAlignmentInput, NodeValue::kCombo, InputFlags(kInputFlagHidden | kInputFlagStatic)); + + AddInput(kUseArgsInput, NodeValue::kBoolean, true, InputFlags(kInputFlagHidden | kInputFlagStatic)); + + AddInput(kArgsInput, NodeValue::kText, InputFlags(kInputFlagArray)); + SetInputProperty(kArgsInput, QStringLiteral("arraystart"), 1); + text_gizmo_ = new TextGizmo(this); text_gizmo_->SetInput(NodeInput(this, kTextInput)); + connect(text_gizmo_, &TextGizmo::Activated, this, &TextGeneratorV3::GizmoActivated); + connect(text_gizmo_, &TextGizmo::Deactivated, this, &TextGeneratorV3::GizmoDeactivated); } QString TextGeneratorV3::Name() const @@ -77,15 +91,32 @@ void TextGeneratorV3::Retranslate() super::Retranslate(); SetInputName(kTextInput, tr("Text")); + SetInputName(kVerticalAlignmentInput, tr("Vertical Alignment")); + SetComboBoxStrings(kVerticalAlignmentInput, {tr("Top"), tr("Middle"), tr("Bottom")}); + SetInputName(kArgsInput, tr("Arguments")); } void TextGeneratorV3::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { GenerateJob job; job.Insert(value); - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); job.SetRequestedFormat(VideoParams::kFormatUnsigned8); + if (value[kUseArgsInput].toBool()) { + auto args = value[kArgsInput].toArray(); + if (!args.empty()) { + QStringList list; + list.reserve(args.size()); + for (int i=0; icolor_manager()->GetDefaultInputColorSpace()); @@ -124,6 +155,18 @@ void TextGeneratorV3::GenerateFrame(FramePtr frame, const GenerateJob& job) cons p.translate(frame->video_params().width()/2, frame->video_params().height()/2); p.setClipRect(0, 0, size.x(), size.y()); + switch (static_cast(job.Get(kVerticalAlignmentInput).toInt())) { + case kVAlignTop: + // Do nothing + break; + case kVAlignMiddle: + p.translate(0, size.y()/2-text_doc.size().height()/2); + break; + case kVAlignBottom: + p.translate(0, size.y()-text_doc.size().height()); + break; + } + // Ensure default text color is white QAbstractTextDocumentLayout::PaintContext ctx; ctx.palette.setColor(QPalette::Text, Qt::white); @@ -140,4 +183,97 @@ void TextGeneratorV3::UpdateGizmoPositions(const NodeValueRow &row, const NodeGl text_gizmo_->SetHtml(row[kTextInput].toString()); } +Qt::Alignment TextGeneratorV3::GetQtAlignmentFromOurs(VerticalAlignment v) +{ + switch (v) { + case kVAlignTop: + return Qt::AlignTop; + case kVAlignMiddle: + return Qt::AlignVCenter; + case kVAlignBottom: + return Qt::AlignBottom; + } + return Qt::Alignment(); +} + +TextGeneratorV3::VerticalAlignment TextGeneratorV3::GetOurAlignmentFromQts(Qt::Alignment v) +{ + switch (v) { + case Qt::AlignTop: + return kVAlignTop; + case Qt::AlignVCenter: + return kVAlignMiddle; + case Qt::AlignBottom: + return kVAlignBottom; + } + + return kVAlignTop; +} + +QString TextGeneratorV3::FormatString(const QString &input, const QStringList &args) +{ + QString output; + output.reserve(input.size()); + + for (int i=0; i= 0 && index < args.size()) { + output.append(args.at(index)); + } + } else { + output.append(this_char); + } + } else { + output.append(this_char); + } + } + + return output; +} + +void TextGeneratorV3::InputValueChangedEvent(const QString &input, int element) +{ + if (input == kVerticalAlignmentInput && !dont_emit_valign_) { + text_gizmo_->SetVerticalAlignment(GetQtAlignmentFromOurs(GetVerticalAlignment())); + } + + super::InputValueChangedEvent(input, element); +} + +void TextGeneratorV3::GizmoActivated() +{ + SetStandardValue(kUseArgsInput, false); + connect(text_gizmo_, &TextGizmo::VerticalAlignmentChanged, this, &TextGeneratorV3::SetVerticalAlignmentUndoable); + dont_emit_valign_ = true; +} + +void TextGeneratorV3::GizmoDeactivated() +{ + SetStandardValue(kUseArgsInput, true); + disconnect(text_gizmo_, &TextGizmo::VerticalAlignmentChanged, this, &TextGeneratorV3::SetVerticalAlignmentUndoable); + dont_emit_valign_ = true; +} + +void TextGeneratorV3::SetVerticalAlignmentUndoable(Qt::Alignment a) +{ + Core::instance()->undo_stack()->push(new NodeParamSetStandardValueCommand(NodeInput(this, kVerticalAlignmentInput), GetOurAlignmentFromQts(a))); +} + } diff --git a/app/node/generator/text/textv3.h b/app/node/generator/text/textv3.h index 69f08f617..b7d6a5674 100644 --- a/app/node/generator/text/textv3.h +++ b/app/node/generator/text/textv3.h @@ -47,11 +47,41 @@ public: virtual void UpdateGizmoPositions(const NodeValueRow &row, const NodeGlobals &globals) override; + enum VerticalAlignment + { + kVAlignTop, + kVAlignMiddle, + kVAlignBottom + }; + + VerticalAlignment GetVerticalAlignment() const + { + return static_cast(GetStandardValue(kVerticalAlignmentInput).toInt()); + } + + static Qt::Alignment GetQtAlignmentFromOurs(VerticalAlignment v); + static VerticalAlignment GetOurAlignmentFromQts(Qt::Alignment v); + static const QString kTextInput; + static const QString kVerticalAlignmentInput; + static const QString kUseArgsInput; + static const QString kArgsInput; + + static QString FormatString(const QString &input, const QStringList &args); + +protected: + virtual void InputValueChangedEvent(const QString &input, int element) override; private: TextGizmo *text_gizmo_; + bool dont_emit_valign_; + +private slots: + void GizmoActivated(); + void GizmoDeactivated(); + void SetVerticalAlignmentUndoable(Qt::Alignment a); + }; } diff --git a/app/node/gizmo/gizmo.cpp b/app/node/gizmo/gizmo.cpp index f1e677f2a..7591115e9 100644 --- a/app/node/gizmo/gizmo.cpp +++ b/app/node/gizmo/gizmo.cpp @@ -28,4 +28,9 @@ NodeGizmo::NodeGizmo(QObject *parent) : setParent(parent); } +NodeGizmo::~NodeGizmo() +{ + setParent(nullptr); +} + } diff --git a/app/node/gizmo/gizmo.h b/app/node/gizmo/gizmo.h index 741a9dfdb..bcc878e4f 100644 --- a/app/node/gizmo/gizmo.h +++ b/app/node/gizmo/gizmo.h @@ -33,6 +33,7 @@ class NodeGizmo : public QObject Q_OBJECT public: explicit NodeGizmo(QObject *parent = nullptr); + virtual ~NodeGizmo() override; virtual void Draw(QPainter *p) const {} diff --git a/app/node/gizmo/text.cpp b/app/node/gizmo/text.cpp index 6e2b97c5b..6d6816948 100644 --- a/app/node/gizmo/text.cpp +++ b/app/node/gizmo/text.cpp @@ -26,7 +26,8 @@ namespace olive { TextGizmo::TextGizmo(QObject *parent) - : NodeGizmo{parent} + : NodeGizmo{parent}, + valign_(Qt::AlignTop) { } diff --git a/app/node/gizmo/text.h b/app/node/gizmo/text.h index 0c74d7cdf..5c3218671 100644 --- a/app/node/gizmo/text.h +++ b/app/node/gizmo/text.h @@ -42,6 +42,22 @@ public: void UpdateInputHtml(const QString &s, const rational &time); + Qt::Alignment GetVerticalAlignment() const + { + return valign_; + } + + void SetVerticalAlignment(Qt::Alignment va) + { + valign_ = va; + emit VerticalAlignmentChanged(valign_); + } + +signals: + void Activated(); + void Deactivated(); + void VerticalAlignmentChanged(Qt::Alignment va); + private: QRectF rect_; @@ -49,6 +65,8 @@ private: NodeKeyframeTrackReference input_; + Qt::Alignment valign_; + }; } diff --git a/app/node/keyframe.cpp b/app/node/keyframe.cpp index eb6d5dea1..8a30ec560 100644 --- a/app/node/keyframe.cpp +++ b/app/node/keyframe.cpp @@ -99,7 +99,7 @@ const NodeKeyframe::Type &NodeKeyframe::type() const void NodeKeyframe::set_type(const NodeKeyframe::Type &type) { if (type_ != type) { - type_ = type; + set_type_no_bezier_adj(type); if (type_ == kBezier) { // Set some sane defaults if this keyframe existed in the track and was just changed @@ -120,11 +120,15 @@ void NodeKeyframe::set_type(const NodeKeyframe::Type &type) } } } - - emit TypeChanged(type_); } } +void NodeKeyframe::set_type_no_bezier_adj(const Type &type) +{ + type_ = type; + emit TypeChanged(type_); +} + const QPointF &NodeKeyframe::bezier_control_in() const { return bezier_control_in_; diff --git a/app/node/keyframe.h b/app/node/keyframe.h index ffe5a7728..e81de91f6 100644 --- a/app/node/keyframe.h +++ b/app/node/keyframe.h @@ -98,6 +98,7 @@ public: */ const Type& type() const; void set_type(const Type& type); + void set_type_no_bezier_adj(const Type& type); /** * @brief For bezier interpolation, the control point leading into this keyframe diff --git a/app/node/keying/chromakey/chromakey.cpp b/app/node/keying/chromakey/chromakey.cpp index 4d7701476..70babc998 100644 --- a/app/node/keying/chromakey/chromakey.cpp +++ b/app/node/keying/chromakey/chromakey.cpp @@ -132,7 +132,6 @@ void ChromaKeyNode::Value(const NodeValueRow &value, const NodeGlobals &globals, ColorTransformJob job; job.Insert(value); - job.SetAlphaChannelRequired(ColorTransformJob::kAlphaForceOn); job.SetColorProcessor(processor()); job.SetInputTexture(value[kTextureInput].toTexture()); job.SetNeedsCustomShader(this); diff --git a/app/node/keying/colordifferencekey/colordifferencekey.cpp b/app/node/keying/colordifferencekey/colordifferencekey.cpp index e4629816b..7ee988d66 100644 --- a/app/node/keying/colordifferencekey/colordifferencekey.cpp +++ b/app/node/keying/colordifferencekey/colordifferencekey.cpp @@ -95,7 +95,6 @@ void ColorDifferenceKeyNode::Value(const NodeValueRow &value, const NodeGlobals { ShaderJob job; job.Insert(value); - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); // If there's no texture, no need to run an operation if (job.Get(kTextureInput).toTexture()) { diff --git a/app/node/math/math/mathbase.cpp b/app/node/math/math/mathbase.cpp index 70ba8433a..a248bb41c 100644 --- a/app/node/math/math/mathbase.cpp +++ b/app/node/math/math/mathbase.cpp @@ -372,9 +372,6 @@ void MathNodeBase::ValueInternal(Operation operation, Pairing pairing, const QSt // Replace with adjusted matrix job.Insert(val_a.type() == NodeValue::kTexture ? param_b_in : param_a_in, NodeValue(NodeValue::kMatrix, adjusted_matrix, this)); - - // It's likely an alpha channel will result from this operation - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOn); } } diff --git a/app/node/math/merge/merge.cpp b/app/node/math/merge/merge.cpp index 227ea6577..acf271869 100644 --- a/app/node/math/merge/merge.cpp +++ b/app/node/math/merge/merge.cpp @@ -90,12 +90,6 @@ void MergeNode::Value(const NodeValueRow &value, const NodeGlobals &globals, Nod // We only have a base texture, no need to alpha over table->Push(job.Get(kBaseIn)); } else { - // We have both textures, push the job - if (base_tex->channel_count() < VideoParams::kRGBAChannelCount) { - // Base has no alpha, therefore this merge operation will not add an alpha channel - job.SetAlphaChannelRequired(GenerateJob::kAlphaForceOff); - } - table->Push(NodeValue::kTexture, QVariant::fromValue(job), this); } } diff --git a/app/node/output/track/tracklist.cpp b/app/node/output/track/tracklist.cpp index bd8b7e813..6759d54b5 100644 --- a/app/node/output/track/tracklist.cpp +++ b/app/node/output/track/tracklist.cpp @@ -38,7 +38,7 @@ TrackList::TrackList(Sequence *parent, const Track::Type &type, const QString &t Track *TrackList::GetTrackAt(int index) const { - if (index < track_cache_.size()) { + if (index >= 0 && index < track_cache_.size()) { return track_cache_.at(index); } else { return nullptr; @@ -81,6 +81,9 @@ void TrackList::TrackConnected(Node *node, int element) UpdateTrackIndexesFrom(cache_index); connect(track, &Track::TrackLengthChanged, this, &TrackList::UpdateTotalLength); + connect(track, &Track::TrackHeightChangedInPixels, this, [this](int height){ + emit TrackHeightChanged(static_cast(sender()), height); + }); track->set_type(type_); track->set_sequence(parent()); diff --git a/app/node/output/track/tracklist.h b/app/node/output/track/tracklist.h index 6096d0858..bfa76a30e 100644 --- a/app/node/output/track/tracklist.h +++ b/app/node/output/track/tracklist.h @@ -101,6 +101,8 @@ signals: void TrackRemoved(Track* track); + void TrackHeightChanged(Track *track, int height); + private: void UpdateTrackIndexesFrom(int index); diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 50caee988..11f6142e2 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -58,7 +58,8 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream SetFlags(kDontShowInParamView); - timeline_points_ = new TimelinePoints(this); + workarea_ = new TimelineWorkArea(this); + markers_ = new TimelineMarkerList(this); } QString ViewerOutput::Name() const @@ -331,7 +332,7 @@ rational ViewerOutput::VerifyLengthInternal(Track::Type type) const case Track::kVideo: if (IsInputConnected(kTextureInput)) { NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kTextureInput), TimeRange(0, 0)); - rational r = t.Get(NodeValue::kRational, QStringLiteral("length")).value(); + rational r = t.Get(NodeValue::kRational, QStringLiteral("length")).toRational(); if (!r.isNaN()) { return r; } @@ -340,7 +341,7 @@ rational ViewerOutput::VerifyLengthInternal(Track::Type type) const case Track::kAudio: if (IsInputConnected(kSamplesInput)) { NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kSamplesInput), TimeRange(0, 0)); - rational r = t.Get(NodeValue::kRational, QStringLiteral("length")).value();; + rational r = t.Get(NodeValue::kRational, QStringLiteral("length")).toRational(); if (!r.isNaN()) { return r; } @@ -375,6 +376,20 @@ Node::ValueHint ViewerOutput::GetConnectedSampleValueHint() return GetValueHintForInput(kSamplesInput); } +void ViewerOutput::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + if (HasInputWithID(kTextureInput)) { + NodeValue repush = value[kTextureInput]; + repush.set_tag(Track::Reference(Track::kVideo, 0).ToString()); + table->Push(repush); + } + if (HasInputWithID(kSamplesInput)) { + NodeValue repush = value[kSamplesInput]; + repush.set_tag(Track::Reference(Track::kAudio, 0).ToString()); + table->Push(value[kSamplesInput]); + } +} + void ViewerOutput::InputValueChangedEvent(const QString &input, int element) { if (element == 0) { diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index c68225df8..81e96f1d2 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -21,6 +21,7 @@ #ifndef VIEWER_H #define VIEWER_H +#include "codec/encoder.h" #include "common/rational.h" #include "node/node.h" #include "node/output/track/track.h" @@ -29,7 +30,8 @@ #include "render/framehashcache.h" #include "render/subtitleparams.h" #include "render/videoparams.h" -#include "timeline/timelinepoints.h" +#include "timeline/timelinemarker.h" +#include "timeline/timelineworkarea.h" namespace olive { @@ -125,7 +127,7 @@ public: return InputArraySize(kSubtitleParamsInput); } - int GetTotalStreamCount() const + virtual int GetTotalStreamCount() const { return GetVideoStreamCount() + GetAudioStreamCount() + GetSubtitleStreamCount(); } @@ -142,10 +144,8 @@ public: const rational &GetVideoLength() const { return video_length_; } const rational &GetAudioLength() const { return audio_length_; } - TimelinePoints* GetTimelinePoints() - { - return timeline_points_; - } + TimelineWorkArea *GetWorkArea() const { return workarea_; } + TimelineMarkerList *GetMarkers() const { return markers_; } QVector GetEnabledStreamsAsReferences() const; @@ -163,6 +163,11 @@ public: virtual ValueHint GetConnectedSampleValueHint(); + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; + + const EncodingParams &GetLastUsedEncodingParams() const { return last_used_encoding_params_; } + void SetLastUsedEncodingParams(const EncodingParams &p) { last_used_encoding_params_ = p; } + static const QString kVideoParamsInput; static const QString kAudioParamsInput; static const QString kSubtitleParamsInput; @@ -212,7 +217,10 @@ private: AudioParams cached_audio_params_; - TimelinePoints *timeline_points_; + TimelineWorkArea *workarea_; + TimelineMarkerList *markers_; + + EncodingParams last_used_encoding_params_; }; diff --git a/app/node/param.h b/app/node/param.h index 4c57e9489..41bfc6736 100644 --- a/app/node/param.h +++ b/app/node/param.h @@ -37,6 +37,7 @@ enum InputFlag { kInputFlagArray = 0x1, kInputFlagNotKeyframable = 0x2, kInputFlagNotConnectable = 0x4, + kInputFlagStatic = kInputFlagNotKeyframable | kInputFlagNotConnectable, kInputFlagHidden = 0x8 }; diff --git a/app/node/project/folder/folder.cpp b/app/node/project/folder/folder.cpp index 3f8ffe700..4bcd7cabc 100644 --- a/app/node/project/folder/folder.cpp +++ b/app/node/project/folder/folder.cpp @@ -73,6 +73,21 @@ bool Folder::ChildExistsWithName(const QString &s) const return ChildExistsWithNameInternal(this, s); } +bool Folder::HasChildRecursive(Node *child) const +{ + for (Node *i : item_children_) { + if (i == child) { + return true; + } else if (Folder *f = dynamic_cast(i)) { + if (f->HasChildRecursive(child)) { + return true; + } + } + } + + return false; +} + int Folder::index_of_child_in_array(Node *item) const { int index_of_item = item_children_.indexOf(item); diff --git a/app/node/project/folder/folder.h b/app/node/project/folder/folder.h index 7274960f1..22d25f110 100644 --- a/app/node/project/folder/folder.h +++ b/app/node/project/folder/folder.h @@ -65,6 +65,8 @@ public: bool ChildExistsWithName(const QString& s) const; + bool HasChildRecursive(Node *child) const; + int item_child_count() const { return item_children_.size(); diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 4a5e5f0a5..459e1711f 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -37,7 +37,6 @@ namespace olive { const QString Footage::kFilenameInput = QStringLiteral("file_in"); -const QString Footage::kLoopModeInput = QStringLiteral("loop_in"); #define super ViewerOutput @@ -45,12 +44,11 @@ Footage::Footage(const QString &filename) : ViewerOutput(false, false), timestamp_(0), valid_(false), - cancelled_(nullptr) + cancelled_(nullptr), + total_stream_count_(0) { SetCacheTextures(true); - PrependInput(kLoopModeInput, NodeValue::kCombo, 0, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); - PrependInput(kFilenameInput, NodeValue::kFile, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); Clear(); @@ -70,8 +68,6 @@ void Footage::Retranslate() super::Retranslate(); SetInputName(kFilenameInput, tr("Filename")); - SetInputName(kLoopModeInput, tr("Loop Mode")); - SetComboBoxStrings(kLoopModeInput, {tr("None"), tr("Loop"), tr("Clamp")}); } void Footage::InputValueChangedEvent(const QString &input, int element) @@ -130,6 +126,9 @@ void Footage::Clear() // Clear decoder link decoder_.clear(); + // Clear total stream count + total_stream_count_ = 0; + // Reset ready state valid_ = false; } @@ -139,11 +138,6 @@ void Footage::SetValid() valid_ = true; } -Footage::LoopMode Footage::loop_mode() const -{ - return static_cast(GetStandardValue(kLoopModeInput).toInt()); -} - QString Footage::filename() const { return GetStandardValue(kFilenameInput).toString(); @@ -263,17 +257,15 @@ void Footage::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeV // Pop filename from table QString file = value[kFilenameInput].toString(); - LoopMode loop_mode = static_cast(value[kLoopModeInput].toInt()); - // If the file exists and the reference is valid, push a footage job to the renderer - if (QFileInfo(file).exists()) { + if (QFileInfo::exists(file)) { // Push length - table->Push(NodeValue::kRational, QVariant::fromValue(GetLength()), this, false, QStringLiteral("length")); + table->Push(NodeValue::kRational, QVariant::fromValue(GetLength()), this, QStringLiteral("length")); // Push each stream as a footage job for (int i=0; iPush(type, QVariant::fromValue(job), this, false, ref.ToString()); + table->Push(type, QVariant::fromValue(job), this, ref.ToString()); } } } @@ -339,7 +331,7 @@ bool TimeIsOutOfBounds(const rational& time, const rational& length) return time < 0 || time >= length; } -rational Footage::AdjustTimeByLoopMode(rational time, Footage::LoopMode loop_mode, const rational &length, VideoParams::Type type, const rational& timebase) +rational Footage::AdjustTimeByLoopMode(rational time, Decoder::LoopMode loop_mode, const rational &length, VideoParams::Type type, const rational& timebase) { if (type == VideoParams::kVideoTypeStill) { // No looping for still images @@ -348,15 +340,15 @@ rational Footage::AdjustTimeByLoopMode(rational time, Footage::LoopMode loop_mod if (TimeIsOutOfBounds(time, length)) { switch (loop_mode) { - case kLoopModeOff: + case Decoder::kLoopModeOff: // Return no time to indicate no frame should be shown here time = rational::NaN; break; - case kLoopModeClamp: + case Decoder::kLoopModeClamp: // Clamp footage time to length time = clamp(time, rational(0), length - timebase); break; - case kLoopModeLoop: + case Decoder::kLoopModeLoop: // Loop footage time around job length do { if (time >= length) { @@ -509,6 +501,8 @@ void Footage::Reprobe() SetStream(Track::kSubtitle, QVariant::fromValue(footage_info.GetSubtitleStreams().at(i)), i); } + total_stream_count_ = footage_info.GetStreamCount(); + SetValid(); } @@ -524,10 +518,13 @@ VideoParams Footage::MergeVideoStream(const VideoParams &base, const VideoParams merged.set_interlacing(over.interlacing()); merged.set_colorspace(over.colorspace()); merged.set_premultiplied_alpha(over.premultiplied_alpha()); - if (merged.video_type() == VideoParams::kVideoTypeImageSequence && over.video_type() == VideoParams::kVideoTypeImageSequence) { + merged.set_video_type(over.video_type()); + merged.set_color_range(over.color_range()); + if (merged.video_type() == VideoParams::kVideoTypeImageSequence) { merged.set_start_time(over.start_time()); merged.set_duration(over.duration()); merged.set_frame_rate(over.frame_rate()); + merged.set_time_base(over.time_base()); } return merged; diff --git a/app/node/project/footage/footage.h b/app/node/project/footage/footage.h index 4db9cdf59..7eb38f3c5 100644 --- a/app/node/project/footage/footage.h +++ b/app/node/project/footage/footage.h @@ -24,12 +24,13 @@ #include #include +#include "codec/decoder.h" #include "common/rational.h" #include "footagedescription.h" #include "node/output/viewer/viewer.h" #include "render/audioparams.h" +#include "render/cancelatom.h" #include "render/videoparams.h" -#include "timeline/timelinepoints.h" namespace olive { @@ -44,12 +45,6 @@ class Footage : public ViewerOutput { Q_OBJECT public: - enum LoopMode { - kLoopModeOff, - kLoopModeLoop, - kLoopModeClamp - }; - /** * @brief Footage Constructor */ @@ -101,11 +96,6 @@ public: */ void SetValid(); - /** - * @brief Get currently set loop mode - */ - LoopMode loop_mode() const; - /** * @brief Return the current filename of this Footage object */ @@ -142,7 +132,7 @@ public: */ void set_timestamp(const qint64 &t); - void SetCancelPointer(const QAtomicInt* c) + void SetCancelPointer(CancelAtom *c) { cancelled_ = c; } @@ -183,15 +173,16 @@ public: virtual Node *GetConnectedSampleOutput() override; - static rational AdjustTimeByLoopMode(rational time, LoopMode loop_mode, const rational& length, VideoParams::Type type, const rational &timebase); + static rational AdjustTimeByLoopMode(rational time, Decoder::LoopMode loop_mode, const rational& length, VideoParams::Type type, const rational &timebase); virtual void LoadFinishedEvent() override; virtual qint64 creation_time() const override; virtual qint64 mod_time() const override; + virtual int GetTotalStreamCount() const override { return total_stream_count_; } + static const QString kFilenameInput; - static const QString kLoopModeInput; protected: virtual void InputValueChangedEvent(const QString &input, int element) override; @@ -233,7 +224,9 @@ private: bool valid_; - const QAtomicInt* cancelled_; + CancelAtom *cancelled_; + + int total_stream_count_; private slots: void CheckFootage(); diff --git a/app/node/project/footage/footagedescription.cpp b/app/node/project/footage/footagedescription.cpp index 610db5c39..742372586 100644 --- a/app/node/project/footage/footagedescription.cpp +++ b/app/node/project/footage/footagedescription.cpp @@ -43,9 +43,11 @@ bool FootageDescription::Load(const QString &filename) // Default to first version of metadata (which wasn't versioned at all) unsigned version = 1; - XMLAttributeLoop((&reader), attr) { - if (attr.name() == QStringLiteral("version")) { - version = attr.value().toUInt(); + { + XMLAttributeLoop((&reader), attr) { + if (attr.name() == QStringLiteral("version")) { + version = attr.value().toUInt(); + } } } @@ -58,6 +60,14 @@ bool FootageDescription::Load(const QString &filename) if (reader.name() == QStringLiteral("decoder")) { decoder_ = reader.readElementText(); } else if (reader.name() == QStringLiteral("streams")) { + { + XMLAttributeLoop((&reader), attr) { + if (attr.name() == QStringLiteral("count")) { + total_stream_count_ = attr.value().toInt(); + } + } + } + while (XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("video")) { VideoParams vp; @@ -116,6 +126,8 @@ bool FootageDescription::Save(const QString &filename) const writer.writeStartElement(QStringLiteral("streams")); + writer.writeAttribute(QStringLiteral("count"), QString::number(total_stream_count_)); + foreach (const VideoParams& vp, video_streams_) { writer.writeStartElement(QStringLiteral("video")); vp.Save(&writer); diff --git a/app/node/project/footage/footagedescription.h b/app/node/project/footage/footagedescription.h index 930a6e8be..42e6a07be 100644 --- a/app/node/project/footage/footagedescription.h +++ b/app/node/project/footage/footagedescription.h @@ -32,7 +32,8 @@ class FootageDescription { public: FootageDescription(const QString& decoder = QString()) : - decoder_(decoder) + decoder_(decoder), + total_stream_count_(0) { } @@ -118,6 +119,9 @@ public: return StreamIsVideo(index) || StreamIsAudio(index) || StreamIsSubtitle(index); } + int GetStreamCount() const { return total_stream_count_; } + void SetStreamCount(int s) { total_stream_count_ = s; } + bool Load(const QString& filename); bool Save(const QString& filename) const; @@ -138,7 +142,7 @@ public: } private: - static constexpr unsigned kFootageMetaVersion = 2; + static constexpr unsigned kFootageMetaVersion = 5; QString decoder_; @@ -148,6 +152,8 @@ private: QVector subtitle_streams_; + int total_stream_count_; + }; } diff --git a/app/node/project/project.cpp b/app/node/project/project.cpp index 4cf192643..b84c8ee22 100644 --- a/app/node/project/project.cpp +++ b/app/node/project/project.cpp @@ -23,6 +23,7 @@ #include #include +#include "common/qtutils.h" #include "common/xmlutils.h" #include "core.h" #include "dialog/progress/progress.h" @@ -173,16 +174,7 @@ void Project::RegenerateUuid() Project *Project::GetProjectFromObject(const QObject *o) { - QObject *t = o->parent(); - - while (t) { - if (Project *p = dynamic_cast(t)) { - return p; - } - t = t->parent(); - } - - return nullptr; + return QtUtils::GetParentOfType(o); } void Project::ColorManagerValueChanged(const NodeInput &input, const TimeRange &range) diff --git a/app/node/project/projectviewmodel.cpp b/app/node/project/projectviewmodel.cpp index 3aefbb13d..c7e558249 100644 --- a/app/node/project/projectviewmodel.cpp +++ b/app/node/project/projectviewmodel.cpp @@ -170,6 +170,11 @@ QVariant ProjectViewModel::data(const QModelIndex &index, int role) const } } break; + case Qt::EditRole: + if (column_type == kName) { + return internal_item->GetLabel(); + } + break; case Qt::DecorationRole: // If this is the first column, return the Item's icon if (column_type == kName) { diff --git a/app/node/project/sequence/sequence.h b/app/node/project/sequence/sequence.h index 25220a4f5..4f8b62d04 100644 --- a/app/node/project/sequence/sequence.h +++ b/app/node/project/sequence/sequence.h @@ -23,7 +23,6 @@ #include "node/output/track/tracklist.h" #include "node/output/viewer/viewer.h" -#include "timeline/timelinepoints.h" namespace olive { diff --git a/app/node/project/serializer/serializer.cpp b/app/node/project/serializer/serializer.cpp index 362d48dc5..60ab3521b 100644 --- a/app/node/project/serializer/serializer.cpp +++ b/app/node/project/serializer/serializer.cpp @@ -159,6 +159,11 @@ ProjectSerializer::Result ProjectSerializer::Save(const SaveData &data, const QS Result inner_result = Save(&writer, data, type); + if (writer.hasError()) { + Result r(kXmlError); + return r; + } + project_file.close(); if (inner_result != kSuccess) { @@ -262,7 +267,7 @@ ProjectSerializer::Result ProjectSerializer::LoadWithSerializerVersion(uint vers Result r(kSuccess); if (reader->hasError()) { r = Result(kXmlError); - r.SetDetails(reader->errorString()); + r.SetDetails(QCoreApplication::translate("Serializer", "%1 on line %2").arg(reader->errorString(), QString::number(reader->lineNumber()))); } r.SetLoadData(ld); return r; diff --git a/app/node/project/serializer/serializer210528.cpp b/app/node/project/serializer/serializer210528.cpp index f0ad798ad..ea30b7578 100644 --- a/app/node/project/serializer/serializer210528.cpp +++ b/app/node/project/serializer/serializer210528.cpp @@ -496,7 +496,7 @@ void ProjectSerializer210528::LoadNodeCustom(QXmlStreamReader *reader, Node *nod while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("points")) { - LoadTimelinePoints(reader, viewer->GetTimelinePoints()); + LoadTimelinePoints(reader, viewer); } else if (reader->name() == QStringLiteral("timestamp") && footage) { footage->set_timestamp(reader->readElementText().toLongLong()); } else { @@ -553,13 +553,13 @@ void ProjectSerializer210528::LoadNodeCustom(QXmlStreamReader *reader, Node *nod } } -void ProjectSerializer210528::LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const +void ProjectSerializer210528::LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *points) const { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("markers")) { - LoadMarkerList(reader, points->markers()); + LoadMarkerList(reader, points->GetMarkers()); } else if (reader->name() == QStringLiteral("workarea")) { - LoadWorkArea(reader, points->workarea()); + LoadWorkArea(reader, points->GetWorkArea()); } else { reader->skipCurrentElement(); } diff --git a/app/node/project/serializer/serializer210528.h b/app/node/project/serializer/serializer210528.h index 2bf195064..537b89cf4 100644 --- a/app/node/project/serializer/serializer210528.h +++ b/app/node/project/serializer/serializer210528.h @@ -78,7 +78,7 @@ private: void LoadNodeCustom(QXmlStreamReader *reader, Node *node, XMLNodeData &xml_node_data) const; - void LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const; + void LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *points) const; void LoadWorkArea(QXmlStreamReader *reader, TimelineWorkArea *workarea) const; diff --git a/app/node/project/serializer/serializer210907.cpp b/app/node/project/serializer/serializer210907.cpp index edcb86ae3..dc5e127e7 100644 --- a/app/node/project/serializer/serializer210907.cpp +++ b/app/node/project/serializer/serializer210907.cpp @@ -488,7 +488,7 @@ void ProjectSerializer210907::LoadNodeCustom(QXmlStreamReader *reader, Node *nod while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("points")) { - LoadTimelinePoints(reader, viewer->GetTimelinePoints()); + LoadTimelinePoints(reader, viewer); } else if (reader->name() == QStringLiteral("timestamp") && footage) { footage->set_timestamp(reader->readElementText().toLongLong()); } else { @@ -545,13 +545,13 @@ void ProjectSerializer210907::LoadNodeCustom(QXmlStreamReader *reader, Node *nod } } -void ProjectSerializer210907::LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const +void ProjectSerializer210907::LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *points) const { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("markers")) { - LoadMarkerList(reader, points->markers()); + LoadMarkerList(reader, points->GetMarkers()); } else if (reader->name() == QStringLiteral("workarea")) { - LoadWorkArea(reader, points->workarea()); + LoadWorkArea(reader, points->GetWorkArea()); } else { reader->skipCurrentElement(); } diff --git a/app/node/project/serializer/serializer210907.h b/app/node/project/serializer/serializer210907.h index 6a56d43b5..c3e8033de 100644 --- a/app/node/project/serializer/serializer210907.h +++ b/app/node/project/serializer/serializer210907.h @@ -77,7 +77,7 @@ private: void LoadNodeCustom(QXmlStreamReader *reader, Node *node, XMLNodeData &xml_node_data) const; - void LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const; + void LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *points) const; void LoadWorkArea(QXmlStreamReader *reader, TimelineWorkArea *workarea) const; diff --git a/app/node/project/serializer/serializer211228.cpp b/app/node/project/serializer/serializer211228.cpp index 41254b105..94a6ce1ed 100644 --- a/app/node/project/serializer/serializer211228.cpp +++ b/app/node/project/serializer/serializer211228.cpp @@ -538,7 +538,7 @@ void ProjectSerializer211228::LoadNodeCustom(QXmlStreamReader *reader, Node *nod while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("points")) { - LoadTimelinePoints(reader, viewer->GetTimelinePoints()); + LoadTimelinePoints(reader, viewer); } else if (reader->name() == QStringLiteral("timestamp") && footage) { footage->set_timestamp(reader->readElementText().toLongLong()); } else { @@ -595,13 +595,13 @@ void ProjectSerializer211228::LoadNodeCustom(QXmlStreamReader *reader, Node *nod } } -void ProjectSerializer211228::LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const +void ProjectSerializer211228::LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *points) const { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("markers")) { - LoadMarkerList(reader, points->markers()); + LoadMarkerList(reader, points->GetMarkers()); } else if (reader->name() == QStringLiteral("workarea")) { - LoadWorkArea(reader, points->workarea()); + LoadWorkArea(reader, points->GetWorkArea()); } else { reader->skipCurrentElement(); } diff --git a/app/node/project/serializer/serializer211228.h b/app/node/project/serializer/serializer211228.h index 49733a5d7..bc424bc4c 100644 --- a/app/node/project/serializer/serializer211228.h +++ b/app/node/project/serializer/serializer211228.h @@ -78,7 +78,7 @@ private: void LoadNodeCustom(QXmlStreamReader *reader, Node *node, XMLNodeData &xml_node_data) const; - void LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const; + void LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *points) const; void LoadWorkArea(QXmlStreamReader *reader, TimelineWorkArea *workarea) const; diff --git a/app/node/project/serializer/serializer220403.cpp b/app/node/project/serializer/serializer220403.cpp index 2075b6ce8..fc90aab6f 100644 --- a/app/node/project/serializer/serializer220403.cpp +++ b/app/node/project/serializer/serializer220403.cpp @@ -25,6 +25,20 @@ namespace olive { +// These wrappers may appear to do nothing, but they seem to address a bug where sometimes +// QXmlStreamWriter would insert nonsense null characters into a project. This is probably some +// real edge case like compiler optimization or some bullshit like that. I don't even know if it +// affects all platforms, but it definitely affected me on Linux. +void WriteStartElement(QXmlStreamWriter *writer, const QString &s) +{ + writer->writeStartElement(s); +} + +void WriteEndElement(QXmlStreamWriter *writer) +{ + writer->writeEndElement(); +} + ProjectSerializer220403::LoadData ProjectSerializer220403::Load(Project *project, QXmlStreamReader *reader, void *reserved) const { QMap > properties; @@ -319,23 +333,23 @@ void ProjectSerializer220403::Save(QXmlStreamWriter *writer, const SaveData &dat { if (!data.GetOnlySerializeMarkers().empty()) { - writer->writeStartElement(QStringLiteral("markers")); + WriteStartElement(writer, QStringLiteral("markers")); for (auto it=data.GetOnlySerializeMarkers().cbegin(); it!=data.GetOnlySerializeMarkers().cend(); it++) { TimelineMarker *marker = *it; - writer->writeStartElement(QStringLiteral("marker")); + WriteStartElement(writer, QStringLiteral("marker")); SaveMarker(writer, marker); - writer->writeEndElement(); // marker + WriteEndElement(writer); // marker } - writer->writeEndElement(); // markers + WriteEndElement(writer); // markers } else if (!data.GetOnlySerializeKeyframes().empty()) { - writer->writeStartElement(QStringLiteral("keyframes")); + WriteStartElement(writer, QStringLiteral("keyframes")); // Organize keyframes into node+input QHash > > > > organized; @@ -346,57 +360,57 @@ void ProjectSerializer220403::Save(QXmlStreamWriter *writer, const SaveData &dat } for (auto it=organized.cbegin(); it!=organized.cend(); it++) { - writer->writeStartElement(QStringLiteral("node")); + WriteStartElement(writer, QStringLiteral("node")); writer->writeAttribute(QStringLiteral("id"), it.key()); for (auto jt=it.value().cbegin(); jt!=it.value().cend(); jt++) { - writer->writeStartElement(QStringLiteral("input")); + WriteStartElement(writer, QStringLiteral("input")); writer->writeAttribute(QStringLiteral("id"), jt.key()); for (auto kt=jt.value().cbegin(); kt!=jt.value().cend(); kt++) { - writer->writeStartElement(QStringLiteral("element")); + WriteStartElement(writer, QStringLiteral("element")); writer->writeAttribute(QStringLiteral("id"), QString::number(kt.key())); for (auto lt=kt.value().cbegin(); lt!=kt.value().cend(); lt++) { const QVector &keys = lt.value(); - writer->writeStartElement(QStringLiteral("track")); + WriteStartElement(writer, QStringLiteral("track")); writer->writeAttribute(QStringLiteral("id"), QString::number(lt.key())); for (NodeKeyframe *key : keys) { - writer->writeStartElement(QStringLiteral("key")); + WriteStartElement(writer, QStringLiteral("key")); SaveKeyframe(writer, key, key->parent()->GetInputDataType(key->input())); - writer->writeEndElement(); // key + WriteEndElement(writer); // key } - writer->writeEndElement(); // track + WriteEndElement(writer); // track } - writer->writeEndElement(); // element + WriteEndElement(writer); // element } - writer->writeEndElement(); // input + WriteEndElement(writer); // input } - writer->writeEndElement(); // node; + WriteEndElement(writer); // node; } - writer->writeEndElement(); // keyframes + WriteEndElement(writer); // keyframes } else if (Project *project = data.GetProject()) { writer->writeTextElement(QStringLiteral("uuid"), project->GetUuid().toString()); - writer->writeStartElement(QStringLiteral("nodes")); + WriteStartElement(writer, QStringLiteral("nodes")); const QVector &using_node_list = (data.GetOnlySerializeNodes().isEmpty()) ? project->nodes() : data.GetOnlySerializeNodes(); foreach (Node* node, using_node_list) { - writer->writeStartElement(QStringLiteral("node")); + WriteStartElement(writer, QStringLiteral("node")); if (node == project->root()) { writer->writeAttribute(QStringLiteral("root"), QStringLiteral("1")); @@ -410,39 +424,39 @@ void ProjectSerializer220403::Save(QXmlStreamWriter *writer, const SaveData &dat SaveNode(node, writer); - writer->writeEndElement(); // node + WriteEndElement(writer); // node } - writer->writeEndElement(); // nodes + WriteEndElement(writer); // nodes - writer->writeStartElement(QStringLiteral("positions")); + WriteStartElement(writer, QStringLiteral("positions")); foreach (Node* context, using_node_list) { const Node::PositionMap &map = context->GetContextPositions(); if (!map.isEmpty()) { - writer->writeStartElement(QStringLiteral("context")); + WriteStartElement(writer, QStringLiteral("context")); writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(context))); for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { if (data.GetOnlySerializeNodes().isEmpty() || data.GetOnlySerializeNodes().contains(jt.key())) { - writer->writeStartElement(QStringLiteral("node")); + WriteStartElement(writer, QStringLiteral("node")); SavePosition(writer, jt.key(), jt.value()); - writer->writeEndElement(); // node + WriteEndElement(writer); // node } } - writer->writeEndElement(); // context + WriteEndElement(writer); // context } } - writer->writeEndElement(); // positions + WriteEndElement(writer); // positions - writer->writeStartElement(QStringLiteral("properties")); + WriteStartElement(writer, QStringLiteral("properties")); for (auto it=data.GetProperties().cbegin(); it!=data.GetProperties().cend(); it++) { - writer->writeStartElement(QStringLiteral("node")); + WriteStartElement(writer, QStringLiteral("node")); writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(it.key()))); @@ -450,10 +464,10 @@ void ProjectSerializer220403::Save(QXmlStreamWriter *writer, const SaveData &dat writer->writeTextElement(jt.key(), jt.value()); } - writer->writeEndElement(); // node + WriteEndElement(writer); // node } - writer->writeEndElement(); // properties + WriteEndElement(writer); // properties // Save main window project layout project->GetLayoutInfo().toXml(writer); @@ -560,50 +574,50 @@ void ProjectSerializer220403::SaveNode(Node *node, QXmlStreamWriter *writer) con writer->writeTextElement(QStringLiteral("color"), QString::number(node->GetOverrideColor())); foreach (const QString& input, node->inputs()) { - writer->writeStartElement(QStringLiteral("input")); + WriteStartElement(writer, QStringLiteral("input")); SaveInput(node, writer, input); - writer->writeEndElement(); // input + WriteEndElement(writer); // input } - writer->writeStartElement(QStringLiteral("links")); + WriteStartElement(writer, QStringLiteral("links")); foreach (Node* link, node->links()) { writer->writeTextElement(QStringLiteral("link"), QString::number(reinterpret_cast(link))); } - writer->writeEndElement(); // links + WriteEndElement(writer); // links - writer->writeStartElement(QStringLiteral("connections")); + WriteStartElement(writer, QStringLiteral("connections")); for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - writer->writeStartElement(QStringLiteral("connection")); + WriteStartElement(writer, QStringLiteral("connection")); writer->writeAttribute(QStringLiteral("input"), it->first.input()); writer->writeAttribute(QStringLiteral("element"), QString::number(it->first.element())); writer->writeTextElement(QStringLiteral("output"), QString::number(reinterpret_cast(it->second))); - writer->writeEndElement(); // connection + WriteEndElement(writer); // connection } - writer->writeEndElement(); // connections + WriteEndElement(writer); // connections - writer->writeStartElement(QStringLiteral("hints")); + WriteStartElement(writer, QStringLiteral("hints")); for (auto it=node->GetValueHints().cbegin(); it!=node->GetValueHints().cend(); it++) { - writer->writeStartElement(QStringLiteral("hint")); + WriteStartElement(writer, QStringLiteral("hint")); writer->writeAttribute(QStringLiteral("input"), it.key().input); writer->writeAttribute(QStringLiteral("element"), QString::number(it.key().element)); SaveValueHint(&it.value(), writer); - writer->writeEndElement(); // hint + WriteEndElement(writer); // hint } - writer->writeEndElement(); + WriteEndElement(writer); // hints - writer->writeStartElement(QStringLiteral("custom")); + WriteStartElement(writer, QStringLiteral("custom")); SaveNodeCustom(writer, node); - writer->writeEndElement(); // custom + WriteEndElement(writer); // custom } void ProjectSerializer220403::LoadInput(Node *node, QXmlStreamReader *reader, XMLNodeData &xml_node_data) const @@ -673,27 +687,27 @@ void ProjectSerializer220403::SaveInput(Node *node, QXmlStreamWriter *writer, co { writer->writeAttribute(QStringLiteral("id"), id); - writer->writeStartElement(QStringLiteral("primary")); + WriteStartElement(writer, QStringLiteral("primary")); SaveImmediate(writer, node, id, -1); - writer->writeEndElement(); // primary + WriteEndElement(writer); // primary - writer->writeStartElement(QStringLiteral("subelements")); + WriteStartElement(writer, QStringLiteral("subelements")); int arr_sz = node->InputArraySize(id); writer->writeAttribute(QStringLiteral("count"), QString::number(arr_sz)); for (int i=0; iwriteStartElement(QStringLiteral("element")); + WriteStartElement(writer, QStringLiteral("element")); SaveImmediate(writer, node, id, i); - writer->writeEndElement(); // element + WriteEndElement(writer); // element } - writer->writeEndElement(); // subelements + WriteEndElement(writer); // subelements } void ProjectSerializer220403::LoadImmediate(QXmlStreamReader *reader, Node *node, const QString& input, int element, XMLNodeData &xml_node_data) const @@ -803,10 +817,10 @@ void ProjectSerializer220403::SaveImmediate(QXmlStreamWriter *writer, Node *node NodeValue::Type data_type = node->GetInputDataType(input); // Write standard value - writer->writeStartElement(QStringLiteral("standard")); + WriteStartElement(writer, QStringLiteral("standard")); foreach (const QVariant& v, node->GetSplitStandardValue(input, element)) { - writer->writeStartElement(QStringLiteral("track")); + WriteStartElement(writer, QStringLiteral("track")); if (data_type == NodeValue::kVideoParams) { v.value().Save(writer); @@ -816,29 +830,29 @@ void ProjectSerializer220403::SaveImmediate(QXmlStreamWriter *writer, Node *node writer->writeCharacters(NodeValue::ValueToString(data_type, v, true)); } - writer->writeEndElement(); // track + WriteEndElement(writer); // track } - writer->writeEndElement(); // standard + WriteEndElement(writer); // standard // Write keyframes - writer->writeStartElement(QStringLiteral("keyframes")); + WriteStartElement(writer, QStringLiteral("keyframes")); for (const NodeKeyframeTrack& track : node->GetKeyframeTracks(input, element)) { - writer->writeStartElement(QStringLiteral("track")); + WriteStartElement(writer, QStringLiteral("track")); for (NodeKeyframe* key : track) { - writer->writeStartElement(QStringLiteral("key")); + WriteStartElement(writer, QStringLiteral("key")); SaveKeyframe(writer, key, data_type); - writer->writeEndElement(); // key + WriteEndElement(writer); // key } - writer->writeEndElement(); // track + WriteEndElement(writer); // track } - writer->writeEndElement(); // keyframes + WriteEndElement(writer); // keyframes if (data_type == NodeValue::kColor) { // Save color management information @@ -865,7 +879,7 @@ void ProjectSerializer220403::LoadKeyframe(QXmlStreamReader *reader, NodeKeyfram } else if (attr.name() == QStringLiteral("time")) { key->set_time(rational::fromString(attr.value().toString())); } else if (attr.name() == QStringLiteral("type")) { - key->set_type(static_cast(attr.value().toInt())); + key->set_type_no_bezier_adj(static_cast(attr.value().toInt())); } else if (attr.name() == QStringLiteral("inhandlex")) { key_in_handle.setX(attr.value().toDouble()); } else if (attr.name() == QStringLiteral("inhandley")) { @@ -989,7 +1003,7 @@ void ProjectSerializer220403::LoadNodeCustom(QXmlStreamReader *reader, Node *nod while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("points")) { - LoadTimelinePoints(reader, viewer->GetTimelinePoints()); + LoadTimelinePoints(reader, viewer); } else if (reader->name() == QStringLiteral("timestamp") && footage) { footage->set_timestamp(reader->readElementText().toLongLong()); } else { @@ -1083,9 +1097,9 @@ void ProjectSerializer220403::SaveNodeCustom(QXmlStreamWriter *writer, Node *nod { if (ViewerOutput *viewer = dynamic_cast(node)) { // Write TimelinePoints - writer->writeStartElement(QStringLiteral("points")); - SaveTimelinePoints(writer, viewer->GetTimelinePoints()); - writer->writeEndElement(); // points + WriteStartElement(writer, QStringLiteral("points")); + SaveTimelinePoints(writer, viewer); + WriteEndElement(writer); // points if (Footage *footage = dynamic_cast(node)) { writer->writeTextElement(QStringLiteral("timestamp"), QString::number(footage->timestamp())); @@ -1093,10 +1107,10 @@ void ProjectSerializer220403::SaveNodeCustom(QXmlStreamWriter *writer, Node *nod } else if (Track *track = dynamic_cast(node)) { writer->writeTextElement(QStringLiteral("height"), QString::number(track->GetTrackHeight())); } else if (NodeGroup *group = dynamic_cast(node)) { - writer->writeStartElement(QStringLiteral("inputpassthroughs")); + WriteStartElement(writer, QStringLiteral("inputpassthroughs")); foreach (const NodeGroup::InputPassthrough &ip, group->GetInputPassthroughs()) { - writer->writeStartElement(QStringLiteral("inputpassthrough")); + WriteStartElement(writer, QStringLiteral("inputpassthrough")); // Reference to inner input writer->writeTextElement(QStringLiteral("node"), QString::number(reinterpret_cast(ip.second.node()))); @@ -1117,47 +1131,47 @@ void ProjectSerializer220403::SaveNodeCustom(QXmlStreamWriter *writer, Node *nod writer->writeTextElement(QStringLiteral("default"), NodeValue::ValueToString(data_type, group_input.GetDefaultValue(), false)); - writer->writeStartElement(QStringLiteral("properties")); + WriteStartElement(writer, QStringLiteral("properties")); auto p = group_input.GetProperties(); for (auto it=p.cbegin(); it!=p.cend(); it++) { - writer->writeStartElement(QStringLiteral("property")); + WriteStartElement(writer, QStringLiteral("property")); writer->writeTextElement(QStringLiteral("key"), it.key()); writer->writeTextElement(QStringLiteral("value"), it.value().toString()); - writer->writeEndElement(); // property + WriteEndElement(writer); // property } - writer->writeEndElement(); // properties + WriteEndElement(writer); // properties - writer->writeEndElement(); // input + WriteEndElement(writer); // input } - writer->writeEndElement(); // inputpassthroughs + WriteEndElement(writer); // inputpassthroughs writer->writeTextElement(QStringLiteral("outputpassthrough"), QString::number(reinterpret_cast(group->GetOutputPassthrough()))); } } -void ProjectSerializer220403::LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const +void ProjectSerializer220403::LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *viewer) const { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("markers")) { - LoadMarkerList(reader, points->markers()); + LoadMarkerList(reader, viewer->GetMarkers()); } else if (reader->name() == QStringLiteral("workarea")) { - LoadWorkArea(reader, points->workarea()); + LoadWorkArea(reader, viewer->GetWorkArea()); } else { reader->skipCurrentElement(); } } } -void ProjectSerializer220403::SaveTimelinePoints(QXmlStreamWriter *writer, TimelinePoints *points) const +void ProjectSerializer220403::SaveTimelinePoints(QXmlStreamWriter *writer, ViewerOutput *viewer) const { - writer->writeStartElement(QStringLiteral("workarea")); - SaveWorkArea(writer, points->workarea()); - writer->writeEndElement(); // workarea + WriteStartElement(writer, QStringLiteral("workarea")); + SaveWorkArea(writer, viewer->GetWorkArea()); + WriteEndElement(writer); // workarea - writer->writeStartElement(QStringLiteral("markers")); - SaveMarkerList(writer, points->markers()); - writer->writeEndElement(); // markers + WriteStartElement(writer, QStringLiteral("markers")); + SaveMarkerList(writer, viewer->GetMarkers()); + WriteEndElement(writer); // markers } void ProjectSerializer220403::LoadMarker(QXmlStreamReader *reader, TimelineMarker *marker) const @@ -1238,11 +1252,11 @@ void ProjectSerializer220403::SaveMarkerList(QXmlStreamWriter *writer, TimelineM for (auto it=markers->cbegin(); it!=markers->cend(); it++) { TimelineMarker* marker = *it; - writer->writeStartElement(QStringLiteral("marker")); + WriteStartElement(writer, QStringLiteral("marker")); SaveMarker(writer, marker); - writer->writeEndElement(); // marker + WriteEndElement(writer); // marker } } @@ -1273,13 +1287,13 @@ void ProjectSerializer220403::LoadValueHint(Node::ValueHint *hint, QXmlStreamRea void ProjectSerializer220403::SaveValueHint(const Node::ValueHint *hint, QXmlStreamWriter *writer) const { - writer->writeStartElement(QStringLiteral("types")); + WriteStartElement(writer, QStringLiteral("types")); for (auto it=hint->types().cbegin(); it!=hint->types().cend(); it++) { writer->writeTextElement(QStringLiteral("type"), QString::number(*it)); } - writer->writeEndElement(); // types + WriteEndElement(writer); // types writer->writeTextElement(QStringLiteral("index"), QString::number(hint->index())); diff --git a/app/node/project/serializer/serializer220403.h b/app/node/project/serializer/serializer220403.h index 4612b2fce..fbeb4bfc2 100644 --- a/app/node/project/serializer/serializer220403.h +++ b/app/node/project/serializer/serializer220403.h @@ -100,9 +100,9 @@ private: void SaveNodeCustom(QXmlStreamWriter *writer, Node *node) const; - void LoadTimelinePoints(QXmlStreamReader *reader, TimelinePoints *points) const; + void LoadTimelinePoints(QXmlStreamReader *reader, ViewerOutput *viewer) const; - void SaveTimelinePoints(QXmlStreamWriter *writer, TimelinePoints *points) const; + void SaveTimelinePoints(QXmlStreamWriter *writer, ViewerOutput *viewer) const; void LoadMarker(QXmlStreamReader *reader, TimelineMarker *marker) const; diff --git a/app/node/time/CMakeLists.txt b/app/node/time/CMakeLists.txt index e23a85367..9f3aa1dc5 100644 --- a/app/node/time/CMakeLists.txt +++ b/app/node/time/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(timeformat) add_subdirectory(timeoffset) add_subdirectory(timeremap) diff --git a/app/threading/CMakeLists.txt b/app/node/time/timeformat/CMakeLists.txt similarity index 80% rename from app/threading/CMakeLists.txt rename to app/node/time/timeformat/CMakeLists.txt index ada17c49c..552649a6d 100644 --- a/app/threading/CMakeLists.txt +++ b/app/node/time/timeformat/CMakeLists.txt @@ -16,11 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - threading/threadticket.cpp - threading/threadticket.h - threading/threadticketwatcher.cpp - threading/threadticketwatcher.h - threading/threadpool.cpp - threading/threadpool.h + node/time/timeformat/timeformat.cpp + node/time/timeformat/timeformat.h PARENT_SCOPE ) diff --git a/app/node/time/timeformat/timeformat.cpp b/app/node/time/timeformat/timeformat.cpp new file mode 100644 index 000000000..50a870a86 --- /dev/null +++ b/app/node/time/timeformat/timeformat.cpp @@ -0,0 +1,79 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 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 "timeformat.h" + +#include + +namespace olive { + +#define super Node + +const QString TimeFormatNode::kTimeInput = QStringLiteral("time_in"); +const QString TimeFormatNode::kFormatInput = QStringLiteral("format_in"); +const QString TimeFormatNode::kLocalTimeInput = QStringLiteral("localtime_in"); + +TimeFormatNode::TimeFormatNode() +{ + AddInput(kTimeInput, NodeValue::kFloat); + AddInput(kFormatInput, NodeValue::kText, QStringLiteral("hh:mm:ss")); + AddInput(kLocalTimeInput, NodeValue::kBoolean); +} + +QString TimeFormatNode::Name() const +{ + return tr("Time Format"); +} + +QString TimeFormatNode::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.timeformat"); +} + +QVector TimeFormatNode::Category() const +{ + return {kCategoryGenerator}; +} + +QString TimeFormatNode::Description() const +{ + return tr("Format time (in Unix epoch seconds) into a string."); +} + +void TimeFormatNode::Retranslate() +{ + super::Retranslate(); + + SetInputName(kTimeInput, tr("Time")); + SetInputName(kFormatInput, tr("Format")); + SetInputName(kLocalTimeInput, tr("Interpret time as local time")); +} + +void TimeFormatNode::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const +{ + qint64 ms_since_epoch = value[kTimeInput].toDouble()*1000; + bool time_is_local = value[kLocalTimeInput].toBool(); + QDateTime dt = QDateTime::fromMSecsSinceEpoch(ms_since_epoch, time_is_local ? Qt::LocalTime : Qt::UTC); + QString format = value[kFormatInput].toString(); + QString output = dt.toString(format); + table->Push(NodeValue(NodeValue::kText, output, this)); +} + +} diff --git a/app/threading/threadticketwatcher.h b/app/node/time/timeformat/timeformat.h similarity index 54% rename from app/threading/threadticketwatcher.h rename to app/node/time/timeformat/timeformat.h index 3b442e03e..ddc7ac8c1 100644 --- a/app/threading/threadticketwatcher.h +++ b/app/node/time/timeformat/timeformat.h @@ -18,47 +18,36 @@ ***/ -#ifndef RENDERTICKETWATCHER_H -#define RENDERTICKETWATCHER_H +#ifndef TIMEFORMAT_H +#define TIMEFORMAT_H -#include "threadticket.h" +#include "node/node.h" namespace olive { -class RenderTicketWatcher : public QObject +class TimeFormatNode : public Node { Q_OBJECT public: - RenderTicketWatcher(QObject* parent = nullptr); + TimeFormatNode(); - RenderTicketPtr GetTicket() const - { - return ticket_; - } + NODE_DEFAULT_FUNCTIONS(TimeFormatNode) - void SetTicket(RenderTicketPtr ticket); + virtual QString Name() const override; + virtual QString id() const override; + virtual QVector Category() const override; + virtual QString Description() const override; - bool IsRunning(); + virtual void Retranslate() override; - void WaitForFinished(); + virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override; - QVariant Get(); - - bool HasResult(); - - void Cancel(); - -signals: - void Finished(RenderTicketWatcher* watcher); - -private: - RenderTicketPtr ticket_; - -private slots: - void TicketFinished(); + static const QString kTimeInput; + static const QString kFormatInput; + static const QString kLocalTimeInput; }; } -#endif // RENDERTICKETWATCHER_H +#endif // TIMEFORMAT_H diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index a75ab921e..ea21ffa83 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -21,6 +21,7 @@ #include "traverser.h" #include "node.h" +#include "node/block/clip/clip.h" #include "render/job/footagejob.h" #include "render/rendermanager.h" @@ -30,6 +31,12 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa { NodeValueDatabase database; + // HACK: Pick up loop mode from clips + Decoder::LoopMode old_loop_mode = loop_mode_; + if (const ClipBlock *clip = dynamic_cast(node)) { + loop_mode_ = clip->loop_mode(); + } + // We need to insert tables into the database for each input foreach (const QString& input, node->inputs()) { if (IsCancelled()) { @@ -39,6 +46,8 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa database.Insert(input, ProcessInput(node, input, range)); } + loop_mode_ = old_loop_mode; + return database; } @@ -159,34 +168,6 @@ NodeGlobals NodeTraverser::GenerateGlobals(const VideoParams ¶ms, const Time int NodeTraverser::GetChannelCountFromJob(const GenerateJob &job) { - int max_channel_count = 0; - - // Find maximum channel count - for (auto it=job.GetValues().cbegin(); it!=job.GetValues().cend(); it++) { - if (it.value().type() == NodeValue::kTexture) { - if (TexturePtr tex = it.value().toTexture()) { - max_channel_count = qMax(max_channel_count, tex->channel_count()); - } - } - } - if (max_channel_count == 0) { - max_channel_count = VideoParams::kRGBChannelCount; - } - - switch (job.GetAlphaChannelRequired()) { - case GenerateJob::kAlphaForceOn: - return VideoParams::kRGBAChannelCount; - case GenerateJob::kAlphaForceOff: - if (max_channel_count >= 1 && max_channel_count < VideoParams::kRGBChannelCount) { - return max_channel_count; - } else { - return VideoParams::kRGBChannelCount; - } - case GenerateJob::kAlphaAuto: - return max_channel_count; - } - - // Default fallback, should never get here return VideoParams::kRGBAChannelCount; } @@ -260,8 +241,8 @@ NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& inpu NodeTraverser::NodeTraverser() : cancel_(nullptr), - heard_cancel_(false), - transform_(nullptr) + transform_(nullptr), + loop_mode_(Decoder::kLoopModeOff) { } @@ -350,7 +331,9 @@ NodeValueTable NodeTraverser::GenerateBlockTable(const Track *track, const TimeR NodeValueTable table; if (active_block) { + block_stack_.push_back(active_block); table = GenerateTable(active_block, Track::TransformRangeForBlock(active_block, range), track); + block_stack_.pop_back(); } return table; @@ -435,7 +418,7 @@ void NodeTraverser::ResolveJobs(NodeValue &val, const TimeRange &range) if (job.type() == Track::kVideo) { - rational footage_time = Footage::AdjustTimeByLoopMode(range.in(), job.loop_mode(), job.length(), job.video_params().video_type(), job.video_params().frame_rate_as_time_base()); + rational footage_time = Footage::AdjustTimeByLoopMode(range.in(), loop_mode_, job.length(), job.video_params().video_type(), job.video_params().frame_rate_as_time_base()); TexturePtr tex; diff --git a/app/node/traverser.h b/app/node/traverser.h index e129096ab..84f1b1606 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -26,6 +26,7 @@ #include "codec/decoder.h" #include "common/cancelableobject.h" #include "node/output/track/track.h" +#include "render/cancelatom.h" #include "render/job/footagejob.h" #include "render/job/colortransformjob.h" #include "value.h" @@ -130,27 +131,26 @@ protected: bool IsCancelled() { - bool c = cancel_ && *cancel_; - if (c) { - heard_cancel_ = true; - } - return c; + return cancel_ && cancel_->IsCancelled(); } - bool HeardCancel() const { return heard_cancel_; } - - const QAtomicInt *GetCancelPointer() const + bool HeardCancel() const { - return cancel_; + return cancel_ && cancel_->HeardCancel(); } - void SetCancelPointer(const QAtomicInt *cancel) - { - cancel_ = cancel; - } + CancelAtom *GetCancelPointer() const { return cancel_; } + void SetCancelPointer(CancelAtom *cancel) { cancel_ = cancel; } void ResolveJobs(NodeValue &value, const TimeRange &range); + Block *GetCurrentBlock() const + { + return block_stack_.empty() ? nullptr : block_stack_.back(); + } + + Decoder::LoopMode loop_mode() const { return loop_mode_; } + private: void PreProcessRow(const TimeRange &range, NodeValueRow &row); @@ -160,13 +160,16 @@ private: AudioParams audio_params_; - const QAtomicInt *cancel_; - bool heard_cancel_; + CancelAtom *cancel_; const Node *transform_start_; const Node *transform_now_; QTransform *transform_; + std::list block_stack_; + + Decoder::LoopMode loop_mode_; + }; } diff --git a/app/node/value.h b/app/node/value.h index 6cf1e537d..2a0b14620 100644 --- a/app/node/value.h +++ b/app/node/value.h @@ -208,6 +208,12 @@ public: set_value(data); } + template + NodeValue(Type type, const T& data, const Node* from, const QString& tag) : + NodeValue(type, data, from, false, tag) + { + } + Type type() const { return type_; @@ -236,6 +242,11 @@ public: return tag_; } + void set_tag(const QString& tag) + { + tag_ = tag; + } + const Node* source() const { return from_; @@ -326,6 +337,7 @@ public: QVector3D toVec3() const { return value(); } QVector4D toVec4() const { return value(); } Bezier toBezier() const { return value(); } + QVector toArray() const { return value >(); } private: Type type_; @@ -373,6 +385,12 @@ public: Push(NodeValue(type, data, from, array, tag)); } + template + void Push(NodeValue::Type type, const T& data, const Node *from, const QString& tag) + { + Push(NodeValue(type, data, from, false, tag)); + } + void Prepend(const NodeValue& value) { values_.prepend(value); @@ -384,6 +402,12 @@ public: Prepend(NodeValue(type, data, from, array, tag)); } + template + void Prepend(NodeValue::Type type, const T& data, const Node *from, const QString& tag) + { + Prepend(NodeValue(type, data, from, false, tag)); + } + const NodeValue& at(int index) const { return values_.at(index); diff --git a/app/panel/audiomonitor/audiomonitor.cpp b/app/panel/audiomonitor/audiomonitor.cpp index 84297447a..3bf6a734b 100644 --- a/app/panel/audiomonitor/audiomonitor.cpp +++ b/app/panel/audiomonitor/audiomonitor.cpp @@ -20,12 +20,16 @@ #include "audiomonitor.h" +#include "panel/panelmanager.h" + namespace olive { +#define super PanelWidget + AudioMonitorPanel::AudioMonitorPanel(QWidget *parent) : - PanelWidget(QStringLiteral("AudioMonitor"), parent) + super(QStringLiteral("AudioMonitor"), parent) { - audio_monitor_ = new AudioMonitor(this); + audio_monitor_ = new AudioMonitor(); setWidget(audio_monitor_); diff --git a/app/panel/footageviewer/footageviewer.cpp b/app/panel/footageviewer/footageviewer.cpp index ebb86a007..d005f11cf 100644 --- a/app/panel/footageviewer/footageviewer.cpp +++ b/app/panel/footageviewer/footageviewer.cpp @@ -20,12 +20,12 @@ #include "footageviewer.h" -#include "widget/viewer/footageviewer.h" - namespace olive { +#define super ViewerPanelBase + FootageViewerPanel::FootageViewerPanel(QWidget *parent) : - ViewerPanelBase(QStringLiteral("FootageViewerPanel"), parent) + super(QStringLiteral("FootageViewerPanel"), parent) { // Set ViewerWidget as the central widget FootageViewerWidget* fvw = new FootageViewerWidget(); @@ -38,6 +38,11 @@ FootageViewerPanel::FootageViewerPanel(QWidget *parent) : SetShowAndRaiseOnConnect(); } +void FootageViewerPanel::OverrideWorkArea(const TimeRange &r) +{ + GetFootageViewerWidget()->OverrideWorkArea(r); +} + QVector FootageViewerPanel::GetSelectedFootage() const { QVector list; @@ -51,7 +56,7 @@ QVector FootageViewerPanel::GetSelectedFootage() const void FootageViewerPanel::Retranslate() { - ViewerPanelBase::Retranslate(); + super::Retranslate(); SetTitle(tr("Footage Viewer")); } diff --git a/app/panel/footageviewer/footageviewer.h b/app/panel/footageviewer/footageviewer.h index 2b07cc233..9f546d036 100644 --- a/app/panel/footageviewer/footageviewer.h +++ b/app/panel/footageviewer/footageviewer.h @@ -25,6 +25,7 @@ #include "panel/viewer/viewerbase.h" #include "panel/project/footagemanagementpanel.h" +#include "widget/viewer/footageviewer.h" namespace olive { @@ -36,6 +37,13 @@ class FootageViewerPanel : public ViewerPanelBase, public FootageManagementPanel public: FootageViewerPanel(QWidget* parent); + void OverrideWorkArea(const TimeRange &r); + + FootageViewerWidget *GetFootageViewerWidget() const + { + return static_cast(GetTimeBasedWidget()); + } + virtual QVector GetSelectedFootage() const override; protected: diff --git a/app/panel/node/node.h b/app/panel/node/node.h index a572989c4..3fc07b49d 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -104,6 +104,11 @@ public: node_widget_->view()->ZoomOut(); } + virtual void RenameSelected() override + { + node_widget_->view()->LabelSelectedNodes(); + } + public slots: void Select(const QVector &p) { diff --git a/app/panel/panelmanager.cpp b/app/panel/panelmanager.cpp index a85285a4f..8bf6dfe44 100644 --- a/app/panel/panelmanager.cpp +++ b/app/panel/panelmanager.cpp @@ -28,7 +28,8 @@ PanelManager* PanelManager::instance_ = nullptr; PanelManager::PanelManager(QObject *parent) : QObject(parent), - locked_(false) + locked_(false), + suppress_changed_signal_(false) { } @@ -66,8 +67,10 @@ PanelWidget *PanelManager::CurrentlyFocused(bool enable_hover) const PanelWidget *PanelManager::CurrentlyHovered() const { + QPoint global_mouse = QCursor::pos(); + foreach (PanelWidget* panel, focus_history_) { - if (panel->underMouse()) { + if (panel->rect().contains(panel->mapFromGlobal(global_mouse))) { return panel; } } @@ -163,7 +166,9 @@ void PanelManager::FocusChanged(QWidget *old, QWidget *now) focus_history_.move(panel_index, 0); } - emit FocusedPanelChanged(panel_cast_test); + if (!suppress_changed_signal_) { + emit FocusedPanelChanged(panel_cast_test); + } } break; diff --git a/app/panel/panelmanager.h b/app/panel/panelmanager.h index 23feeb02f..45639b7e8 100644 --- a/app/panel/panelmanager.h +++ b/app/panel/panelmanager.h @@ -122,6 +122,11 @@ public: */ void UnregisterPanel(PanelWidget *panel); + void SetSuppressChangedSignal(bool e) + { + suppress_changed_signal_ = e; + } + public slots: /** * @brief Connect this to a QApplication's SIGNAL(focusChanged()) @@ -157,6 +162,8 @@ private: */ static PanelManager* instance_; + bool suppress_changed_signal_; + }; template diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index d6c353330..d97eb7bd3 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -146,6 +146,11 @@ void ProjectPanel::DeleteSelected() explorer_->DeleteSelected(); } +void ProjectPanel::RenameSelected() +{ + explorer_->RenameSelectedItem(); +} + void ProjectPanel::Edit(Node* item) { explorer_->Edit(item); @@ -169,7 +174,10 @@ void ProjectPanel::ItemDoubleClickSlot(Node *item) Core::instance()->DialogImportShow(); } else if (dynamic_cast(item)) { // Open this footage in a FootageViewer - PanelManager::instance()->MostRecentlyFocused()->ConnectViewerNode(static_cast(item)); + auto panel = PanelManager::instance()->MostRecentlyFocused(); + panel->ConnectViewerNode(static_cast(item)); + panel->raise(); + panel->setFocus(); } else if (dynamic_cast(item)) { // Open this sequence in the Timeline Core::instance()->main_window()->OpenSequence(static_cast(item)); diff --git a/app/panel/project/project.h b/app/panel/project/project.h index c4720fe22..d4918b7b3 100644 --- a/app/panel/project/project.h +++ b/app/panel/project/project.h @@ -52,9 +52,9 @@ public: ProjectViewModel* model() const; - bool SelectItem(Node *n) + bool SelectItem(Node *n, bool deselect_all_first = true) { - return explorer_->SelectItem(n); + return explorer_->SelectItem(n, deselect_all_first); } virtual void SelectAll() override; @@ -62,6 +62,8 @@ public: virtual void DeleteSelected() override; + virtual void RenameSelected() override; + public slots: void Edit(Node *item); diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index f08f7b998..24e14320f 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -36,6 +36,7 @@ TimelinePanel::TimelinePanel(QWidget *parent) : connect(tw, &TimelineWidget::BlockSelectionChanged, this, &TimelinePanel::BlockSelectionChanged); connect(tw, &TimelineWidget::RequestCaptureStart, this, &TimelinePanel::RequestCaptureStart); connect(tw, &TimelineWidget::RevealViewerInProject, this, &TimelinePanel::RevealViewerInProject); + connect(tw, &TimelineWidget::RevealViewerInFootageViewer, this, &TimelinePanel::RevealViewerInFootageViewer); } void TimelinePanel::SplitAtPlayhead() @@ -153,6 +154,11 @@ void TimelinePanel::MoveOutToPlayhead() timeline_widget()->MoveOutToPlayhead(); } +void TimelinePanel::RenameSelected() +{ + timeline_widget()->RenameSelectedBlocks(); +} + void TimelinePanel::InsertFootageAtPlayhead(const QVector &footage) { timeline_widget()->InsertFootageAtPlayhead(footage); diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 90de35332..611fd275c 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -86,6 +86,13 @@ public: virtual void MoveOutToPlayhead() override; + virtual void RenameSelected() override; + + void AddDefaultTransitionsToSelected() + { + timeline_widget()->AddDefaultTransitionsToSelected(); + } + void ShowSpeedDurationDialogForSelectedClips() { timeline_widget()->ShowSpeedDurationDialogForSelectedClips(); @@ -109,6 +116,7 @@ signals: void RequestCaptureStart(const TimeRange &time, const Track::Reference &track); void RevealViewerInProject(ViewerOutput *r); + void RevealViewerInFootageViewer(ViewerOutput *r, const TimeRange &range); }; diff --git a/app/panel/viewer/viewerbase.cpp b/app/panel/viewer/viewerbase.cpp index 7076ae510..6c90506b6 100644 --- a/app/panel/viewer/viewerbase.cpp +++ b/app/panel/viewer/viewerbase.cpp @@ -27,6 +27,7 @@ namespace olive { ViewerPanelBase::ViewerPanelBase(const QString& object_name, QWidget *parent) : TimeBasedPanel(object_name, parent) { + connect(PanelManager::instance(), &PanelManager::FocusedPanelChanged, this, &ViewerPanelBase::FocusedPanelChanged); } void ViewerPanelBase::PlayPause() @@ -101,4 +102,12 @@ void ViewerPanelBase::SetViewerWidget(ViewerWidget *vw) SetTimeBasedWidget(vw); } +void ViewerPanelBase::FocusedPanelChanged(PanelWidget *panel) +{ + auto vw = static_cast(GetTimeBasedWidget()); + if (vw->IsPlaying() && panel != this) { + vw->Pause(); + } +} + } diff --git a/app/panel/viewer/viewerbase.h b/app/panel/viewer/viewerbase.h index e5fe7cdff..1ca7c06ff 100644 --- a/app/panel/viewer/viewerbase.h +++ b/app/panel/viewer/viewerbase.h @@ -93,6 +93,9 @@ signals: protected: void SetViewerWidget(ViewerWidget *vw); +private slots: + void FocusedPanelChanged(PanelWidget *panel); + }; } diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 8a37e1776..dcde3f3ed 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -24,6 +24,7 @@ set(OLIVE_SOURCES render/audioparams.h render/audioplaybackcache.cpp render/audioplaybackcache.h + render/cancelatom.h render/color.cpp render/color.h render/colorprocessor.cpp @@ -46,8 +47,6 @@ set(OLIVE_SOURCES render/renderer.cpp render/renderer.h render/rendercache.h - render/rendererthreadwrapper.cpp - render/rendererthreadwrapper.h render/renderjobtracker.cpp render/renderjobtracker.h render/rendermanager.cpp @@ -55,6 +54,8 @@ set(OLIVE_SOURCES render/rendermodes.h render/renderprocessor.cpp render/renderprocessor.h + render/renderticket.cpp + render/renderticket.h render/shadercode.h render/subtitleparams.cpp render/subtitleparams.h diff --git a/app/render/cancelatom.h b/app/render/cancelatom.h new file mode 100644 index 000000000..d4db43c1c --- /dev/null +++ b/app/render/cancelatom.h @@ -0,0 +1,48 @@ +#ifndef CANCELATOM_H +#define CANCELATOM_H + +#include + +namespace olive { + +class CancelAtom +{ +public: + CancelAtom() : + cancelled_(false), + heard_(false) + {} + + bool IsCancelled() + { + QMutexLocker locker(&mutex_); + if (cancelled_) { + heard_ = true; + } + return cancelled_; + } + + void Cancel() + { + QMutexLocker locker(&mutex_); + cancelled_ = true; + } + + bool HeardCancel() + { + QMutexLocker locker(&mutex_); + return heard_; + } + +private: + QMutex mutex_; + + bool cancelled_; + + bool heard_; + +}; + +} + +#endif // CANCELATOM_H diff --git a/app/render/job/footagejob.h b/app/render/job/footagejob.h index 6952d76b4..319be8a38 100644 --- a/app/render/job/footagejob.h +++ b/app/render/job/footagejob.h @@ -29,17 +29,15 @@ class FootageJob { public: FootageJob() : - type_(Track::kNone), - loop_mode_(Footage::kLoopModeOff) + type_(Track::kNone) { } - FootageJob(const QString& decoder, const QString& filename, Track::Type type, const rational& length, Footage::LoopMode loop_mode) : + FootageJob(const QString& decoder, const QString& filename, Track::Type type, const rational& length) : decoder_(decoder), filename_(filename), type_(type), - length_(length), - loop_mode_(loop_mode) + length_(length) { } @@ -98,16 +96,6 @@ public: length_ = length; } - Footage::LoopMode loop_mode() const - { - return loop_mode_; - } - - void set_loop_mode(Footage::LoopMode loop_mode) - { - loop_mode_ = loop_mode; - } - private: QString decoder_; @@ -123,8 +111,6 @@ private: rational length_; - Footage::LoopMode loop_mode_; - }; } diff --git a/app/render/job/generatejob.h b/app/render/job/generatejob.h index 38354aa55..0109e1195 100644 --- a/app/render/job/generatejob.h +++ b/app/render/job/generatejob.h @@ -28,22 +28,11 @@ namespace olive { class GenerateJob : public AcceleratedJob { public: - enum AlphaChannelSetting { - kAlphaAuto, - kAlphaForceOn, - kAlphaForceOff - }; - GenerateJob() { - alpha_channel_required_ = kAlphaAuto; requested_format_ = VideoParams::kFormatInvalid; } - AlphaChannelSetting GetAlphaChannelRequired() const { return alpha_channel_required_; } - - void SetAlphaChannelRequired(AlphaChannelSetting e) { alpha_channel_required_ = e; } - VideoParams::Format GetRequestedFormat() const { return requested_format_; } void SetRequestedFormat(VideoParams::Format f) { requested_format_ = f; } @@ -52,8 +41,6 @@ public: void SetColorspace(const QString &s) { colorspace_ = s; } private: - AlphaChannelSetting alpha_channel_required_; - VideoParams::Format requested_format_; QString colorspace_; diff --git a/app/render/ocioconf/config.ocio b/app/render/ocioconf/config.ocio index dcf75c465..5ae436b2b 100755 --- a/app/render/ocioconf/config.ocio +++ b/app/render/ocioconf/config.ocio @@ -16,7 +16,7 @@ luma: [0.2126, 0.7152, 0.0722] description: A filmlike dynamic range encoding set for Blender roles: - default: sRGB OETF + default: Rec.709 OETF reference: Linear scene_linear: Linear data: Non-Colour Data @@ -281,6 +281,20 @@ colorspaces: - ! {matrix: [0.606530, 0.220408, 0.123479, 0, 0.267989, 0.832731, -0.100720, 0, -0.029442, -0.086611, 1.204861, 0, 0, 0, 0, 1]} - ! {src: CIE-XYZ D65, dst: reference} + - ! + name: Rec.709 OETF + family: Camera Footage + equalitygroup: "" + bitdepth: 32f + description: | + Rec.709 OETF + isdata: false + allocation: uniform + allocationvars: [0, 1] + to_reference: ! + children: + - ! {src: rec709_to_linear.spi1d, interpolation: linear} + - ! name: Non-Colour Data family: diff --git a/app/render/ocioconf/luts/rec709_to_linear.spi1d b/app/render/ocioconf/luts/rec709_to_linear.spi1d new file mode 100644 index 000000000..28bd19666 --- /dev/null +++ b/app/render/ocioconf/luts/rec709_to_linear.spi1d @@ -0,0 +1,4102 @@ +Version 1 +From 0.000000 1.000000 +Length 4096 +Components 1 +{ + 0.0 + 5.42667221453e-05 + 0.000108533444291 + 0.000162800162798 + 0.000217066888581 + 0.000271333614364 + 0.000325600325596 + 0.000379867036827 + 0.000434133777162 + 0.000488400517497 + 0.000542667228729 + 0.00059693393996 + 0.000651200651191 + 0.000705467362422 + 0.000759734073654 + 0.000814000843093 + 0.000868267554324 + 0.000922534265555 + 0.000976801034994 + 0.00103106768802 + 0.00108533445746 + 0.00113960111048 + 0.00119386787992 + 0.00124813453294 + 0.00130240130238 + 0.00135666807182 + 0.00141093472484 + 0.00146520149428 + 0.00151946814731 + 0.00157373491675 + 0.00162800168619 + 0.00168226833921 + 0.00173653510865 + 0.00179080176167 + 0.00184506853111 + 0.00189933518413 + 0.00195360206999 + 0.0020078686066 + 0.00206213537604 + 0.00211640214548 + 0.00217066891491 + 0.00222493545152 + 0.00227920222096 + 0.0023334689904 + 0.00238773575984 + 0.00244200252928 + 0.00249626906589 + 0.00255053583533 + 0.00260480260476 + 0.0026590693742 + 0.00271333614364 + 0.00276760268025 + 0.00282186944969 + 0.00287613621913 + 0.00293040298857 + 0.00298466975801 + 0.00303893629462 + 0.00309320306405 + 0.00314746983349 + 0.00320173660293 + 0.00325600337237 + 0.00331026990898 + 0.00336453667842 + 0.00341880344786 + 0.0034730702173 + 0.0035273367539 + 0.00358160352334 + 0.00363587029278 + 0.00369013706222 + 0.00374440383166 + 0.00379867036827 + 0.00385293713771 + 0.00390720413998 + 0.00396147044376 + 0.00401573721319 + 0.00407000398263 + 0.00412427075207 + 0.00417853752151 + 0.00423280429095 + 0.00428707106039 + 0.00434133782983 + 0.00439560459927 + 0.00444987090304 + 0.00450413767248 + 0.00455840444192 + 0.00461267121136 + 0.0046669379808 + 0.00472120475024 + 0.00477547151968 + 0.00482973828912 + 0.00488400505856 + 0.004938271828 + 0.00499253813177 + 0.00504680490121 + 0.00510107167065 + 0.00515533844009 + 0.00520960520953 + 0.00526387197897 + 0.00531813874841 + 0.00537240551785 + 0.00542667228729 + 0.00548093859106 + 0.0055352053605 + 0.00558947212994 + 0.00564373889938 + 0.00569800566882 + 0.00575227243826 + 0.0058065392077 + 0.00586080597714 + 0.00591507274657 + 0.00596933951601 + 0.00602360581979 + 0.00607787258923 + 0.00613213935867 + 0.00618640612811 + 0.00624067289755 + 0.00629493966699 + 0.00634920643643 + 0.00640347320586 + 0.0064577399753 + 0.00651200674474 + 0.00656627304852 + 0.00662053981796 + 0.0066748065874 + 0.00672907335684 + 0.00678334012628 + 0.00683760689571 + 0.00689187366515 + 0.00694614043459 + 0.00700040720403 + 0.00705467350781 + 0.00710894027725 + 0.00716320704669 + 0.00721747381613 + 0.00727174058557 + 0.007326007355 + 0.00738027412444 + 0.00743454089388 + 0.00748880766332 + 0.00754307443276 + 0.00759734073654 + 0.00765160750598 + 0.00770587427542 + 0.00776014104486 + 0.00781440827996 + 0.00786867458373 + 0.00792294088751 + 0.00797720812261 + 0.00803147442639 + 0.00808574166149 + 0.00814000796527 + 0.00819427520037 + 0.00824854150414 + 0.00830280873924 + 0.00835707504302 + 0.0084113413468 + 0.0084656085819 + 0.00851987488568 + 0.00857414212078 + 0.00862840842456 + 0.00868267565966 + 0.00873694196343 + 0.00879120919853 + 0.00884547550231 + 0.00889974180609 + 0.00895400904119 + 0.00900827534497 + 0.00906254258007 + 0.00911680888385 + 0.00917107611895 + 0.00922534242272 + 0.00927960965782 + 0.0093338759616 + 0.00938814226538 + 0.00944240950048 + 0.00949667580426 + 0.00955094303936 + 0.00960520934314 + 0.00965947657824 + 0.00971374288201 + 0.00976801011711 + 0.00982227642089 + 0.00987654365599 + 0.00993080995977 + 0.00998507626355 + 0.0100393434986 + 0.0100936098024 + 0.0101478770375 + 0.0102021433413 + 0.0102564105764 + 0.0103106768802 + 0.0103649441153 + 0.0104192104191 + 0.0104734767228 + 0.0105277439579 + 0.0105820102617 + 0.0106362774968 + 0.0106905438006 + 0.0107448110357 + 0.0107990773395 + 0.0108533445746 + 0.0109076108783 + 0.0109618771821 + 0.0110161444172 + 0.011070410721 + 0.0111246779561 + 0.0111789442599 + 0.011233211495 + 0.0112874777988 + 0.0113417450339 + 0.0113960113376 + 0.0114502785727 + 0.0115045448765 + 0.0115588111803 + 0.0116130784154 + 0.0116673447192 + 0.0117216119543 + 0.011775878258 + 0.0118301454931 + 0.0118844117969 + 0.011938679032 + 0.0119929453358 + 0.0120472116396 + 0.0121014788747 + 0.0121557451785 + 0.0122100124136 + 0.0122642787173 + 0.0123185459524 + 0.0123728122562 + 0.0124270794913 + 0.0124813457951 + 0.0125356120989 + 0.012589879334 + 0.0126441456378 + 0.0126984128729 + 0.0127526791766 + 0.0128069464117 + 0.0128612127155 + 0.0129154799506 + 0.0129697462544 + 0.0130240134895 + 0.0130782797933 + 0.013132546097 + 0.0131868133321 + 0.0132410796359 + 0.013295346871 + 0.0133496131748 + 0.0134038804099 + 0.0134581467137 + 0.0135124139488 + 0.0135666802526 + 0.0136209465563 + 0.0136752137914 + 0.0137294800952 + 0.0137837473303 + 0.0138380136341 + 0.0138922808692 + 0.013946547173 + 0.0140008144081 + 0.0140550807118 + 0.0141093470156 + 0.0141636142507 + 0.0142178805545 + 0.0142721477896 + 0.0143264140934 + 0.0143806813285 + 0.0144349476323 + 0.0144892148674 + 0.0145434811711 + 0.0145977474749 + 0.01465201471 + 0.0147062810138 + 0.0147605482489 + 0.0148148145527 + 0.0148690817878 + 0.0149233480915 + 0.0149776153266 + 0.0150318816304 + 0.0150861488655 + 0.0151404151693 + 0.0151946814731 + 0.0152489487082 + 0.015303215012 + 0.0153574822471 + 0.0154117485508 + 0.0154660157859 + 0.0155202820897 + 0.0155745493248 + 0.0156288165599 + 0.0156830828637 + 0.0157373491675 + 0.0157916154712 + 0.015845881775 + 0.0159001499414 + 0.0159544162452 + 0.016008682549 + 0.0160629488528 + 0.0161172170192 + 0.016171483323 + 0.0162257496268 + 0.0162800159305 + 0.0163342822343 + 0.0163885504007 + 0.0164428167045 + 0.0164970830083 + 0.0165513493121 + 0.0166056174785 + 0.0166598837823 + 0.016714150086 + 0.0167684163898 + 0.0168226826936 + 0.01687695086 + 0.0169312171638 + 0.0169854834676 + 0.0170397497714 + 0.0170940179378 + 0.0171482842416 + 0.0172025505453 + 0.0172568168491 + 0.0173110831529 + 0.0173653513193 + 0.0174196176231 + 0.0174738839269 + 0.0175281502306 + 0.0175824183971 + 0.0176366847008 + 0.0176909510046 + 0.0177452173084 + 0.0177994836122 + 0.0178537517786 + 0.0179080180824 + 0.0179622843862 + 0.0179615281522 + 0.0180157013237 + 0.0180699639022 + 0.0181243177503 + 0.0181787591428 + 0.0182332918048 + 0.0182879138738 + 0.0183426272124 + 0.0183974280953 + 0.0184523202479 + 0.0185073018074 + 0.0185623746365 + 0.01861753501 + 0.0186727885157 + 0.0187281295657 + 0.0187835618854 + 0.018839083612 + 0.0188946966082 + 0.0189503990114 + 0.0190061908215 + 0.0190620739013 + 0.019118046388 + 0.0191741101444 + 0.0192302651703 + 0.0192865077406 + 0.0193428434432 + 0.0193992685527 + 0.0194557830691 + 0.0195123888552 + 0.0195690840483 + 0.0196258705109 + 0.0196827482432 + 0.0197397153825 + 0.0197967737913 + 0.0198539234698 + 0.0199111625552 + 0.0199684929103 + 0.0200259126723 + 0.0200834255666 + 0.0201410278678 + 0.020198719576 + 0.0202565044165 + 0.0203143786639 + 0.0203723441809 + 0.020430399105 + 0.0204885471612 + 0.0205467846245 + 0.0206051133573 + 0.0206635333598 + 0.0207220446318 + 0.0207806453109 + 0.0208393391222 + 0.0208981223404 + 0.020956998691 + 0.0210159644485 + 0.0210750214756 + 0.0211341697723 + 0.0211934093386 + 0.0212527401745 + 0.0213121622801 + 0.0213716756552 + 0.0214312803 + 0.0214909762144 + 0.0215507633984 + 0.021610641852 + 0.0216706115752 + 0.0217306725681 + 0.0217908266932 + 0.0218510702252 + 0.0219114068896 + 0.0219718329608 + 0.0220323521644 + 0.0220929626375 + 0.0221536643803 + 0.0222144592553 + 0.0222753435373 + 0.0223363209516 + 0.0223973896354 + 0.0224585495889 + 0.0225198026747 + 0.02258114703 + 0.022642582655 + 0.0227041095495 + 0.0227657295763 + 0.0228274390101 + 0.0228892434388 + 0.0229511372745 + 0.0230131242424 + 0.0230752043426 + 0.0231373757124 + 0.0231996383518 + 0.0232619922608 + 0.0233244393021 + 0.0233869794756 + 0.0234496109188 + 0.0235123336315 + 0.0235751494765 + 0.0236380565912 + 0.023701056838 + 0.0237641483545 + 0.0238273330033 + 0.0238906107843 + 0.0239539798349 + 0.0240174401551 + 0.0240809954703 + 0.0241446401924 + 0.0242083799094 + 0.024272210896 + 0.0243361331522 + 0.0244001504034 + 0.0244642589241 + 0.0245284587145 + 0.0245927534997 + 0.0246571395546 + 0.0247216168791 + 0.0247861891985 + 0.0248508527875 + 0.0249156095088 + 0.0249804593623 + 0.0250454004854 + 0.0251104366034 + 0.0251755639911 + 0.025240784511 + 0.0253060963005 + 0.0253715030849 + 0.0254370030016 + 0.0255025941879 + 0.0255682785064 + 0.0256340559572 + 0.0256999265403 + 0.0257658902556 + 0.0258319471031 + 0.025898097083 + 0.0259643401951 + 0.0260306764394 + 0.0260971039534 + 0.0261636264622 + 0.0262302421033 + 0.0262969508767 + 0.0263637527823 + 0.0264306459576 + 0.0264976341277 + 0.0265647154301 + 0.0266318917274 + 0.0266991592944 + 0.0267665199935 + 0.0268339756876 + 0.0269015226513 + 0.0269691646099 + 0.0270368997008 + 0.0271047279239 + 0.0271726492792 + 0.0272406656295 + 0.0273087732494 + 0.0273769758642 + 0.0274452716112 + 0.0275136623532 + 0.0275821462274 + 0.0276507232338 + 0.0277193933725 + 0.0277881566435 + 0.0278570149094 + 0.0279259663075 + 0.0279950127006 + 0.0280641522259 + 0.0281333848834 + 0.0282027125359 + 0.0282721333206 + 0.0283416472375 + 0.0284112561494 + 0.0284809581935 + 0.0285507552326 + 0.0286206454039 + 0.0286906305701 + 0.0287607088685 + 0.0288308802992 + 0.0289011467248 + 0.0289715081453 + 0.0290419626981 + 0.0291125103831 + 0.0291831549257 + 0.0292538907379 + 0.0293247234076 + 0.0293956492096 + 0.0294666681439 + 0.029537782073 + 0.0296089909971 + 0.0296802930534 + 0.0297516901046 + 0.0298231821507 + 0.0298947673291 + 0.0299664475024 + 0.0300382226706 + 0.0301100928336 + 0.030182056129 + 0.0302541144192 + 0.0303262658417 + 0.0303985141218 + 0.0304708555341 + 0.0305432919413 + 0.0306158214808 + 0.0306884478778 + 0.030761167407 + 0.0308339819312 + 0.0309068914503 + 0.0309798959643 + 0.0310529954731 + 0.0311261899769 + 0.031199477613 + 0.0312728621066 + 0.0313463397324 + 0.0314199142158 + 0.0314935818315 + 0.0315673425794 + 0.0316412001848 + 0.0317151546478 + 0.0317892022431 + 0.0318633429706 + 0.0319375805557 + 0.0320119149983 + 0.0320863425732 + 0.0321608632803 + 0.032235480845 + 0.0323101952672 + 0.0323850028217 + 0.0324599072337 + 0.032534904778 + 0.0326099991798 + 0.0326851867139 + 0.0327604711056 + 0.0328358523548 + 0.0329113267362 + 0.0329868979752 + 0.0330625623465 + 0.0331383235753 + 0.0332141779363 + 0.0332901291549 + 0.0333661772311 + 0.0334423184395 + 0.0335185565054 + 0.0335948914289 + 0.0336713194847 + 0.033747844398 + 0.0338244661689 + 0.033901181072 + 0.0339779928327 + 0.0340548977256 + 0.0341318994761 + 0.0342089980841 + 0.0342861935496 + 0.0343634821475 + 0.0344408676028 + 0.0345183499157 + 0.0345959253609 + 0.0346735976636 + 0.0347513668239 + 0.0348292291164 + 0.0349071882665 + 0.0349852442741 + 0.0350633971393 + 0.035141646862 + 0.035219989717 + 0.0352984294295 + 0.0353769659996 + 0.0354555957019 + 0.0355343222618 + 0.0356131494045 + 0.0356920659542 + 0.0357710830867 + 0.0358501970768 + 0.0359294041991 + 0.036008708179 + 0.0360881090164 + 0.0361676067114 + 0.0362471975386 + 0.0363268889487 + 0.036406673491 + 0.0364865548909 + 0.0365665331483 + 0.0366466082633 + 0.0367267765105 + 0.0368070453405 + 0.0368874073029 + 0.0369678661227 + 0.0370484255254 + 0.0371290780604 + 0.0372098274529 + 0.0372906699777 + 0.0373716130853 + 0.0374526530504 + 0.0375337861478 + 0.0376150198281 + 0.0376963466406 + 0.0377777740359 + 0.0378592945635 + 0.0379409119487 + 0.0380226299167 + 0.0381044410169 + 0.0381863489747 + 0.03826835379 + 0.0383504554629 + 0.0384326539934 + 0.0385149493814 + 0.0385973416269 + 0.03867983073 + 0.0387624166906 + 0.0388450995088 + 0.0389278791845 + 0.039010759443 + 0.0390937328339 + 0.0391768030822 + 0.0392599701881 + 0.0393432341516 + 0.0394265986979 + 0.0395100563765 + 0.0395936109126 + 0.0396772660315 + 0.0397610142827 + 0.0398448631167 + 0.039928805083 + 0.0400128476322 + 0.0400969870389 + 0.0401812233031 + 0.0402655564249 + 0.0403499864042 + 0.0404345132411 + 0.0405191406608 + 0.0406038612127 + 0.0406886823475 + 0.0407736003399 + 0.0408586151898 + 0.0409437268972 + 0.0410289354622 + 0.0411142408848 + 0.0411996468902 + 0.0412851497531 + 0.0413707457483 + 0.0414564460516 + 0.0415422394872 + 0.0416281297803 + 0.0417141206563 + 0.0418002083898 + 0.0418863929808 + 0.0419726744294 + 0.0420590527356 + 0.0421455316246 + 0.0422321073711 + 0.0423187799752 + 0.0424055494368 + 0.0424924194813 + 0.0425793863833 + 0.0426664501429 + 0.04275361076 + 0.0428408719599 + 0.0429282300174 + 0.0430156849325 + 0.0431032404304 + 0.0431908890605 + 0.0432786382735 + 0.0433664880693 + 0.0434544309974 + 0.0435424745083 + 0.043630618602 + 0.043718855828 + 0.0438071936369 + 0.0438956283033 + 0.0439841635525 + 0.0440727956593 + 0.0441615246236 + 0.0442503541708 + 0.0443392805755 + 0.0444283038378 + 0.0445174276829 + 0.0446066483855 + 0.0446959659457 + 0.0447853840888 + 0.0448748990893 + 0.0449645146728 + 0.0450542271137 + 0.0451440364122 + 0.0452339462936 + 0.0453239530325 + 0.0454140603542 + 0.0455042645335 + 0.0455945655704 + 0.04568496719 + 0.0457754656672 + 0.0458660647273 + 0.0459567606449 + 0.0460475571454 + 0.0461384505033 + 0.0462294444442 + 0.0463205352426 + 0.0464117266238 + 0.0465030148625 + 0.0465943999588 + 0.046685885638 + 0.0467774719 + 0.0468691550195 + 0.0469609349966 + 0.0470528155565 + 0.0471447966993 + 0.0472368746996 + 0.0473290532827 + 0.0474213287234 + 0.047513704747 + 0.047606177628 + 0.047698751092 + 0.0477914214134 + 0.0478841923177 + 0.0479770600796 + 0.0480700284243 + 0.0481630973518 + 0.0482562631369 + 0.0483495295048 + 0.0484428927302 + 0.0485363565385 + 0.0486299209297 + 0.0487235821784 + 0.0488173440099 + 0.0489112026989 + 0.0490051619709 + 0.0490992218256 + 0.0491933785379 + 0.049287635833 + 0.0493819899857 + 0.0494764484465 + 0.0495710000396 + 0.0496656559408 + 0.0497604086995 + 0.0498552620411 + 0.0499502122402 + 0.0500452667475 + 0.050140414387 + 0.0502356663346 + 0.0503310151398 + 0.0504264645278 + 0.0505220144987 + 0.0506176613271 + 0.0507134087384 + 0.0508092567325 + 0.0509052015841 + 0.0510012507439 + 0.0510973967612 + 0.051193639636 + 0.051289986819 + 0.0513864308596 + 0.0514829754829 + 0.0515796169639 + 0.0516763627529 + 0.0517732053995 + 0.051870148629 + 0.0519671924412 + 0.052064333111 + 0.052161578089 + 0.0522589199245 + 0.0523563623428 + 0.0524539016187 + 0.0525515452027 + 0.0526492856443 + 0.0527471266687 + 0.0528450682759 + 0.052943110466 + 0.0530412532389 + 0.0531394928694 + 0.053237836808 + 0.0533362776041 + 0.0534348189831 + 0.0535334609449 + 0.0536321997643 + 0.0537310428917 + 0.0538299828768 + 0.0539290271699 + 0.0540281683207 + 0.0541274100542 + 0.0542267523706 + 0.0543261952698 + 0.0544257387519 + 0.0545253828168 + 0.0546251237392 + 0.0547249689698 + 0.0548249110579 + 0.0549249574542 + 0.055025100708 + 0.0551253445446 + 0.0552256889641 + 0.0553261376917 + 0.0554266832769 + 0.0555273294449 + 0.0556280761957 + 0.0557289235294 + 0.0558298714459 + 0.0559309162199 + 0.0560320653021 + 0.0561333149672 + 0.056234665215 + 0.0563361160457 + 0.0564376674592 + 0.0565393194556 + 0.0566410720348 + 0.0567429251969 + 0.0568448752165 + 0.0569469295442 + 0.0570490844548 + 0.0571513399482 + 0.0572536960244 + 0.0573561526835 + 0.0574587136507 + 0.0575613714755 + 0.0576641298831 + 0.0577669888735 + 0.057869952172 + 0.0579730123281 + 0.0580761767924 + 0.0581794381142 + 0.0582828037441 + 0.0583862699568 + 0.0584898330271 + 0.0585935004056 + 0.0586972720921 + 0.0588011406362 + 0.0589051097631 + 0.0590091794729 + 0.0591133534908 + 0.0592176280916 + 0.0593219995499 + 0.0594264753163 + 0.0595310553908 + 0.0596357323229 + 0.0597405098379 + 0.0598453916609 + 0.0599503703415 + 0.0600554533303 + 0.0601606369019 + 0.0602659247816 + 0.0603713095188 + 0.0604767985642 + 0.0605823844671 + 0.0606880746782 + 0.0607938691974 + 0.0608997605741 + 0.061005756259 + 0.0611118488014 + 0.0612180456519 + 0.0613243468106 + 0.0614307448268 + 0.0615372471511 + 0.0616438500583 + 0.0617505535483 + 0.0618573613465 + 0.0619642660022 + 0.062071274966 + 0.062178388238 + 0.0622855983675 + 0.0623929128051 + 0.0625003278255 + 0.0626078471541 + 0.0627154633403 + 0.0628231838346 + 0.0629310011864 + 0.0630389302969 + 0.063146956265 + 0.0632550790906 + 0.0633633062243 + 0.0634716376662 + 0.0635800734162 + 0.0636886060238 + 0.0637972429395 + 0.0639059767127 + 0.0640148222446 + 0.0641237571836 + 0.0642328038812 + 0.0643419474363 + 0.0644511952996 + 0.064560547471 + 0.0646699965 + 0.0647795498371 + 0.0648892074823 + 0.0649989619851 + 0.065108820796 + 0.065218783915 + 0.0653288438916 + 0.0654390081763 + 0.0655492767692 + 0.0656596496701 + 0.0657701194286 + 0.0658806934953 + 0.06599137187 + 0.0661021471024 + 0.0662130266428 + 0.0663240104914 + 0.0664350986481 + 0.0665462836623 + 0.0666575729847 + 0.0667689666152 + 0.0668804571033 + 0.0669920518994 + 0.0671037510037 + 0.0672155544162 + 0.0673274546862 + 0.0674394592643 + 0.0675515681505 + 0.0676637813449 + 0.0677760913968 + 0.0678885057569 + 0.068001024425 + 0.0681136474013 + 0.0682263672352 + 0.0683391988277 + 0.0684521272779 + 0.0685651525855 + 0.0686782896519 + 0.0687915235758 + 0.0689048618078 + 0.069018304348 + 0.0691318437457 + 0.0692454949021 + 0.0693592429161 + 0.0694730952382 + 0.0695870444179 + 0.0697011053562 + 0.0698152631521 + 0.0699295252562 + 0.0700438916683 + 0.0701583623886 + 0.0702729299664 + 0.0703876018524 + 0.0705023780465 + 0.0706172585487 + 0.0707322433591 + 0.070847325027 + 0.0709625184536 + 0.0710778087378 + 0.07119320333 + 0.0713087022305 + 0.0714242979884 + 0.0715399980545 + 0.0716558098793 + 0.0717717185616 + 0.0718877315521 + 0.0720038414001 + 0.0721200630069 + 0.0722363814712 + 0.0723528042436 + 0.0724693387747 + 0.0725859627128 + 0.0727026984096 + 0.0728195384145 + 0.0729364752769 + 0.0730535238981 + 0.0731706693769 + 0.0732879191637 + 0.0734052732587 + 0.0735227242112 + 0.0736402869225 + 0.0737579464912 + 0.0738757178187 + 0.0739935860038 + 0.074111558497 + 0.0742296352983 + 0.0743478164077 + 0.0744661018252 + 0.0745844841003 + 0.0747029781342 + 0.0748215690255 + 0.074940264225 + 0.0750590637326 + 0.0751779675484 + 0.0752969756722 + 0.0754160881042 + 0.0755353048444 + 0.0756546184421 + 0.0757740437984 + 0.0758935660124 + 0.076013199985 + 0.0761329308152 + 0.0762527659535 + 0.0763727054 + 0.0764927491546 + 0.0766128972173 + 0.0767331495881 + 0.0768535062671 + 0.0769739598036 + 0.0770945250988 + 0.0772151947021 + 0.077335961163 + 0.0774568393826 + 0.0775778144598 + 0.0776988938451 + 0.0778200775385 + 0.0779413729906 + 0.0780627653003 + 0.0781842619181 + 0.078305862844 + 0.078427568078 + 0.0785493776202 + 0.0786712840199 + 0.0787933021784 + 0.0789154246449 + 0.0790376514196 + 0.0791599825025 + 0.0792824104428 + 0.0794049501419 + 0.0795275941491 + 0.0796503350139 + 0.0797731876373 + 0.0798961371183 + 0.0800191983581 + 0.0801423564553 + 0.0802656263113 + 0.0803889930248 + 0.0805124714971 + 0.0806360468268 + 0.0807597339153 + 0.0808835178614 + 0.0810074135661 + 0.0811314061284 + 0.0812555029988 + 0.081379711628 + 0.0815040171146 + 0.08162843436 + 0.081752948463 + 0.0818775743246 + 0.0820022970438 + 0.0821271315217 + 0.0822520628572 + 0.0823771059513 + 0.082502245903 + 0.0826274976134 + 0.082752853632 + 0.0828783065081 + 0.0830038711429 + 0.0831295400858 + 0.0832553058863 + 0.0833811834455 + 0.0835071653128 + 0.0836332514882 + 0.0837594419718 + 0.0838857367635 + 0.0840121358633 + 0.0841386392713 + 0.0842652469873 + 0.0843919590116 + 0.0845187753439 + 0.0846457034349 + 0.0847727283835 + 0.0848998576403 + 0.0850270986557 + 0.0851544365287 + 0.0852818861604 + 0.0854094401002 + 0.0855370908976 + 0.0856648534536 + 0.0857927203178 + 0.0859206914902 + 0.0860487669706 + 0.0861769467592 + 0.0863052383065 + 0.0864336267114 + 0.0865621194243 + 0.086690723896 + 0.0868194252253 + 0.0869482383132 + 0.0870771557093 + 0.0872061774135 + 0.0873353034258 + 0.0874645337462 + 0.0875938683748 + 0.0877233073115 + 0.087852858007 + 0.0879825055599 + 0.0881122648716 + 0.0882421284914 + 0.0883720964193 + 0.0885021686554 + 0.0886323451996 + 0.0887626260519 + 0.0888930186629 + 0.0890235081315 + 0.0891541093588 + 0.0892848074436 + 0.0894156172872 + 0.0895465314388 + 0.0896775573492 + 0.0898086801171 + 0.0899399071932 + 0.0900712460279 + 0.0902026891708 + 0.0903342366219 + 0.090465888381 + 0.0905976444483 + 0.0907295048237 + 0.0908614769578 + 0.0909935534 + 0.0911257341504 + 0.0912580192089 + 0.0913904085755 + 0.0915229022503 + 0.0916555076838 + 0.0917882174253 + 0.0919210240245 + 0.0920539423823 + 0.0921869724989 + 0.092320099473 + 0.0924533382058 + 0.0925866812468 + 0.0927201285958 + 0.092853680253 + 0.0929873362184 + 0.0931211039424 + 0.093254968524 + 0.0933889448643 + 0.0935230329633 + 0.0936572179198 + 0.0937915071845 + 0.0939259082079 + 0.0940604135394 + 0.0941950231791 + 0.0943297445774 + 0.0944645628333 + 0.0945994928479 + 0.0947345271707 + 0.0948696658015 + 0.0950049161911 + 0.0951402708888 + 0.0952757298946 + 0.0954112932086 + 0.0955469608307 + 0.0956827402115 + 0.0958186239004 + 0.0959546118975 + 0.0960907042027 + 0.0962269082665 + 0.096363209188 + 0.0964996218681 + 0.096636146307 + 0.0967727676034 + 0.0969095006585 + 0.0970463380218 + 0.0971832796931 + 0.0973203331232 + 0.0974574908614 + 0.0975947529078 + 0.0977321192622 + 0.0978695973754 + 0.0980071797967 + 0.0981448665261 + 0.0982826575637 + 0.09842056036 + 0.0985585674644 + 0.0986966788769 + 0.0988349020481 + 0.0989732220769 + 0.099111661315 + 0.0992501974106 + 0.0993888452649 + 0.0995275974274 + 0.099666453898 + 0.0998054146767 + 0.0999444872141 + 0.10008366406 + 0.100222952664 + 0.100362338126 + 0.100501835346 + 0.100641444325 + 0.100781150162 + 0.100920967758 + 0.101060889661 + 0.101200923324 + 0.101341061294 + 0.101481303573 + 0.101621650159 + 0.101762108505 + 0.101902671158 + 0.102043345571 + 0.102184124291 + 0.102325007319 + 0.102465994656 + 0.102607093751 + 0.102748297155 + 0.102889612317 + 0.103031024337 + 0.103172548115 + 0.103314183652 + 0.103455923498 + 0.103597767651 + 0.103739716113 + 0.103881776333 + 0.104023940861 + 0.104166217148 + 0.104308597744 + 0.104451082647 + 0.104593679309 + 0.104736380279 + 0.104879185557 + 0.105022102594 + 0.10516512394 + 0.105308249593 + 0.105451487005 + 0.105594828725 + 0.105738282204 + 0.105881839991 + 0.106025502086 + 0.106169275939 + 0.106313154101 + 0.106457144022 + 0.106601238251 + 0.106745436788 + 0.106889739633 + 0.107034154236 + 0.107178680599 + 0.107323311269 + 0.107468046248 + 0.107612892985 + 0.107757844031 + 0.107902899384 + 0.108048066497 + 0.108193337917 + 0.108338721097 + 0.108484208584 + 0.108629800379 + 0.108775503933 + 0.108921319246 + 0.109067231417 + 0.109213255346 + 0.109359391034 + 0.10950563103 + 0.109651975334 + 0.109798431396 + 0.109944999218 + 0.110091663897 + 0.110238447785 + 0.110385328531 + 0.110532321036 + 0.110679425299 + 0.110826633871 + 0.11097394675 + 0.111121371388 + 0.111268900335 + 0.11141654104 + 0.111564286053 + 0.111712142825 + 0.111860103905 + 0.112008169293 + 0.11215634644 + 0.112304635346 + 0.11245302856 + 0.112601526082 + 0.112750135362 + 0.112898848951 + 0.113047674298 + 0.113196603954 + 0.113345645368 + 0.11349479109 + 0.113644048572 + 0.113793410361 + 0.113942883909 + 0.114092461765 + 0.114242143929 + 0.114391945302 + 0.114541843534 + 0.114691853523 + 0.114841975272 + 0.114992201328 + 0.115142539144 + 0.115292981267 + 0.115443527699 + 0.115594185889 + 0.115744955838 + 0.115895830095 + 0.116046816111 + 0.116197906435 + 0.116349108517 + 0.116500414908 + 0.116651825607 + 0.116803355515 + 0.116954982281 + 0.117106728256 + 0.117258571088 + 0.11741053313 + 0.11756259203 + 0.117714770138 + 0.117867052555 + 0.11801943928 + 0.118171937764 + 0.118324548006 + 0.118477262557 + 0.118630081415 + 0.118783012033 + 0.118936054409 + 0.119089201093 + 0.119242459536 + 0.119395822287 + 0.119549296796 + 0.119702883065 + 0.119856566191 + 0.120010368526 + 0.120164275169 + 0.120318293571 + 0.120472416282 + 0.120626650751 + 0.120780989528 + 0.120935440063 + 0.121090002358 + 0.121244668961 + 0.121399439871 + 0.121554322541 + 0.121709316969 + 0.121864423156 + 0.122019633651 + 0.122174948454 + 0.122330375016 + 0.122485913336 + 0.122641555965 + 0.122797310352 + 0.122953176498 + 0.123109146953 + 0.123265221715 + 0.123421415687 + 0.123577713966 + 0.123734116554 + 0.123890630901 + 0.124047257006 + 0.12420398742 + 0.124360829592 + 0.124517783523 + 0.124674841762 + 0.124832011759 + 0.124989286065 + 0.12514667213 + 0.125304162502 + 0.125461772084 + 0.125619485974 + 0.125777304173 + 0.12593524158 + 0.126093283296 + 0.126251429319 + 0.126409679651 + 0.126568049192 + 0.126726523042 + 0.1268851161 + 0.127043798566 + 0.127202615142 + 0.127361521125 + 0.127520546317 + 0.127679675817 + 0.127838909626 + 0.127998262644 + 0.12815771997 + 0.128317281604 + 0.128476962447 + 0.128636747599 + 0.128796651959 + 0.128956645727 + 0.129116758704 + 0.129276990891 + 0.129437312484 + 0.129597753286 + 0.129758313298 + 0.129918977618 + 0.130079746246 + 0.130240619183 + 0.130401611328 + 0.130562707782 + 0.130723908544 + 0.130885228515 + 0.131046652794 + 0.131208196282 + 0.131369829178 + 0.131531581283 + 0.131693452597 + 0.131855428219 + 0.132017508149 + 0.132179692388 + 0.132341995835 + 0.132504418492 + 0.132666930556 + 0.13282956183 + 0.132992297411 + 0.133155152202 + 0.1333181113 + 0.133481174707 + 0.133644357324 + 0.133807644248 + 0.133971050382 + 0.134134545922 + 0.134298175573 + 0.134461894631 + 0.134625732899 + 0.134789675474 + 0.134953737259 + 0.135117903352 + 0.135282173753 + 0.135446563363 + 0.135611057281 + 0.135775670409 + 0.135940372944 + 0.136105209589 + 0.136270135641 + 0.136435180902 + 0.136600330472 + 0.136765599251 + 0.136930972338 + 0.137096464634 + 0.137262046337 + 0.137427762151 + 0.137593567371 + 0.137759491801 + 0.137925520539 + 0.138091668487 + 0.138257920742 + 0.138424292207 + 0.13859076798 + 0.138757348061 + 0.138924047351 + 0.139090850949 + 0.139257758856 + 0.139424785972 + 0.139591917396 + 0.139759168029 + 0.13992652297 + 0.14009398222 + 0.140261560678 + 0.140429243445 + 0.140597045422 + 0.140764936805 + 0.140932962298 + 0.1411010921 + 0.14126932621 + 0.141437664628 + 0.141606122255 + 0.141774699092 + 0.141943365335 + 0.142112165689 + 0.14228105545 + 0.142450064421 + 0.142619177699 + 0.142788410187 + 0.142957746983 + 0.143127202988 + 0.143296763301 + 0.143466427922 + 0.143636211753 + 0.143806114793 + 0.14397610724 + 0.144146218896 + 0.144316449761 + 0.144486784935 + 0.144657224417 + 0.144827783108 + 0.144998446107 + 0.145169213414 + 0.145340099931 + 0.145511105657 + 0.145682215691 + 0.145853430033 + 0.146024763584 + 0.146196201444 + 0.146367743611 + 0.146539404988 + 0.146711185575 + 0.146883055568 + 0.147055059671 + 0.147227153182 + 0.147399365902 + 0.147571697831 + 0.147744134068 + 0.147916674614 + 0.148089334369 + 0.148262098432 + 0.148434981704 + 0.148607969284 + 0.148781076074 + 0.148954287171 + 0.149127602577 + 0.149301037192 + 0.149474576116 + 0.149648234248 + 0.149821996689 + 0.149995878339 + 0.150169864297 + 0.150343969464 + 0.15051817894 + 0.150692492723 + 0.150866925716 + 0.151041463017 + 0.151216119528 + 0.151390880346 + 0.151565760374 + 0.15174074471 + 0.151915848255 + 0.152091056108 + 0.15226636827 + 0.152441799641 + 0.152617350221 + 0.152793005109 + 0.152968764305 + 0.153144642711 + 0.153320625424 + 0.153496727347 + 0.153672933578 + 0.153849244118 + 0.154025688767 + 0.154202222824 + 0.15437887609 + 0.154555648565 + 0.154732525349 + 0.15490950644 + 0.155086606741 + 0.15526381135 + 0.155441135168 + 0.155618563294 + 0.15579611063 + 0.155973762274 + 0.156151533127 + 0.156329408288 + 0.156507402658 + 0.156685501337 + 0.156863719225 + 0.157042041421 + 0.157220467925 + 0.157399013638 + 0.157577678561 + 0.157756447792 + 0.157935321331 + 0.158114314079 + 0.158293426037 + 0.158472642303 + 0.158651962876 + 0.158831402659 + 0.159010961652 + 0.159190624952 + 0.159370392561 + 0.159550279379 + 0.159730270505 + 0.15991038084 + 0.160090595484 + 0.160270929337 + 0.160451382399 + 0.160631924868 + 0.160812601447 + 0.160993382335 + 0.16117426753 + 0.161355271935 + 0.161536380649 + 0.161717608571 + 0.161898940802 + 0.162080392241 + 0.162261947989 + 0.162443622947 + 0.162625402212 + 0.162807300687 + 0.162989318371 + 0.163171425462 + 0.163353666663 + 0.163536012173 + 0.16371846199 + 0.163901031017 + 0.164083704352 + 0.164266496897 + 0.16444940865 + 0.164632409811 + 0.164815545082 + 0.164998784661 + 0.165182128549 + 0.165365591645 + 0.165549173951 + 0.165732860565 + 0.165916651487 + 0.166100561619 + 0.16628459096 + 0.166468724608 + 0.166652977467 + 0.166837334633 + 0.167021811008 + 0.167206391692 + 0.167391076684 + 0.167575895786 + 0.167760804296 + 0.167945846915 + 0.168130993843 + 0.168316245079 + 0.168501615524 + 0.168687090278 + 0.16887268424 + 0.169058397412 + 0.169244214892 + 0.169430136681 + 0.169616177678 + 0.169802337885 + 0.1699886024 + 0.170174986124 + 0.170361474156 + 0.170548081398 + 0.170734792948 + 0.170921623707 + 0.171108573675 + 0.17129561305 + 0.171482786536 + 0.17167006433 + 0.171857461333 + 0.172044962645 + 0.172232568264 + 0.172420307994 + 0.172608137131 + 0.172796100378 + 0.172984167933 + 0.173172339797 + 0.17336063087 + 0.173549026251 + 0.173737555742 + 0.173926174641 + 0.174114912748 + 0.174303770065 + 0.17449273169 + 0.174681812525 + 0.174871012568 + 0.17506031692 + 0.17524972558 + 0.175439253449 + 0.175628900528 + 0.175818651915 + 0.176008522511 + 0.176198497415 + 0.176388591528 + 0.176578804851 + 0.176769122481 + 0.17695954442 + 0.17715010047 + 0.177340745926 + 0.177531525493 + 0.177722409368 + 0.177913397551 + 0.178104504943 + 0.178295731544 + 0.178487062454 + 0.178678512573 + 0.178870067 + 0.179061740637 + 0.179253518581 + 0.179445430636 + 0.179637432098 + 0.17982955277 + 0.18002179265 + 0.18021415174 + 0.180406615138 + 0.180599182844 + 0.18079186976 + 0.180984675884 + 0.181177586317 + 0.181370615959 + 0.181563764811 + 0.18175701797 + 0.181950375438 + 0.182143867016 + 0.182337462902 + 0.182531163096 + 0.1827249825 + 0.182918921113 + 0.183112964034 + 0.183307126164 + 0.183501392603 + 0.183695778251 + 0.183890283108 + 0.184084892273 + 0.184279620647 + 0.18447445333 + 0.184669405222 + 0.184864476323 + 0.185059651732 + 0.18525493145 + 0.185450345278 + 0.185645863414 + 0.185841485858 + 0.186037242413 + 0.186233103275 + 0.186429068446 + 0.186625152826 + 0.186821356416 + 0.187017664313 + 0.18721409142 + 0.187410622835 + 0.187607273459 + 0.187804043293 + 0.188000917435 + 0.188197910786 + 0.188395023346 + 0.188592240214 + 0.188789576292 + 0.188987016678 + 0.189184576273 + 0.189382255077 + 0.18958003819 + 0.189777940512 + 0.189975947142 + 0.190174072981 + 0.190372318029 + 0.190570667386 + 0.190769135952 + 0.190967723727 + 0.191166415811 + 0.191365227103 + 0.191564157605 + 0.191763192415 + 0.191962331533 + 0.192161604762 + 0.192360982299 + 0.192560464144 + 0.192760080099 + 0.192959800363 + 0.193159624934 + 0.193359568715 + 0.193559631705 + 0.193759813905 + 0.193960100412 + 0.194160491228 + 0.194361016154 + 0.194561645389 + 0.194762378931 + 0.194963246584 + 0.195164218545 + 0.195365294814 + 0.195566490293 + 0.19576780498 + 0.195969238877 + 0.196170777082 + 0.196372434497 + 0.196574196219 + 0.196776077151 + 0.196978077292 + 0.197180181742 + 0.1973824054 + 0.197584748268 + 0.197787195444 + 0.197989761829 + 0.198192447424 + 0.198395237327 + 0.198598146439 + 0.19880117476 + 0.199004307389 + 0.199207559228 + 0.199410930276 + 0.199614405632 + 0.199818000197 + 0.200021699071 + 0.200225532055 + 0.200429454446 + 0.200633510947 + 0.200837671757 + 0.201041951776 + 0.201246351004 + 0.20145085454 + 0.201655477285 + 0.201860204339 + 0.202065065503 + 0.202270016074 + 0.202475100756 + 0.202680289745 + 0.202885597944 + 0.203091025352 + 0.203296557069 + 0.203502207994 + 0.203707963228 + 0.203913852572 + 0.204119846225 + 0.204325944185 + 0.204532176256 + 0.204738512635 + 0.204944953322 + 0.20515152812 + 0.205358207226 + 0.20556499064 + 0.205771908164 + 0.205978929996 + 0.206186071038 + 0.206393316388 + 0.206600680947 + 0.206808164716 + 0.207015752792 + 0.207223474979 + 0.207431286573 + 0.207639232278 + 0.20784728229 + 0.208055451512 + 0.208263739944 + 0.208472132683 + 0.208680644631 + 0.208889275789 + 0.209098011255 + 0.209306865931 + 0.209515839815 + 0.209724932909 + 0.209934130311 + 0.210143446922 + 0.210352867842 + 0.210562422872 + 0.21077208221 + 0.210981845856 + 0.211191743612 + 0.211401745677 + 0.211611866951 + 0.211822092533 + 0.212032452226 + 0.212242901325 + 0.212453484535 + 0.212664172053 + 0.212874993682 + 0.213085904717 + 0.213296949863 + 0.213508099318 + 0.213719367981 + 0.213930740952 + 0.214142248034 + 0.214353859425 + 0.214565590024 + 0.214777424932 + 0.214989379048 + 0.215201452374 + 0.21541364491 + 0.215625941753 + 0.215838357806 + 0.216050893068 + 0.216263532639 + 0.216476306319 + 0.216689184308 + 0.216902166605 + 0.217115283012 + 0.217328503728 + 0.217541843653 + 0.217755287886 + 0.217968851328 + 0.218182533979 + 0.21839633584 + 0.21861025691 + 0.218824282289 + 0.219038426876 + 0.219252675772 + 0.219467058778 + 0.219681546092 + 0.219896152616 + 0.220110863447 + 0.220325708389 + 0.22054065764 + 0.220755726099 + 0.220970898867 + 0.221186205745 + 0.221401616931 + 0.221617132425 + 0.22183278203 + 0.222048535943 + 0.222264409065 + 0.222480401397 + 0.222696498036 + 0.222912728786 + 0.223129048944 + 0.223345503211 + 0.223562076688 + 0.223778754473 + 0.223995551467 + 0.22421246767 + 0.224429488182 + 0.224646627903 + 0.224863886833 + 0.225081264973 + 0.22529874742 + 0.225516363978 + 0.225734069943 + 0.225951910019 + 0.226169869304 + 0.226387932897 + 0.226606115699 + 0.22682441771 + 0.22704282403 + 0.227261349559 + 0.227479994297 + 0.227698758245 + 0.227917641401 + 0.228136628866 + 0.22835573554 + 0.228574961424 + 0.228794306517 + 0.229013755918 + 0.229233324528 + 0.229453012347 + 0.229672819376 + 0.229892730713 + 0.230112761259 + 0.230332911015 + 0.230553179979 + 0.230773568153 + 0.230994060636 + 0.231214672327 + 0.231435403228 + 0.231656238437 + 0.231877207756 + 0.232098281384 + 0.23231947422 + 0.232540786266 + 0.232762202621 + 0.232983738184 + 0.233205392957 + 0.233427166939 + 0.23364906013 + 0.23387105763 + 0.234093174338 + 0.234315410256 + 0.234537765384 + 0.23476023972 + 0.234982818365 + 0.235205516219 + 0.235428333282 + 0.235651269555 + 0.235874310136 + 0.236097469926 + 0.236320748925 + 0.236544147134 + 0.236767664552 + 0.236991286278 + 0.237215027213 + 0.237438887358 + 0.237662866712 + 0.237886965275 + 0.238111168146 + 0.238335490227 + 0.238559931517 + 0.238784492016 + 0.239009171724 + 0.239233955741 + 0.239458858967 + 0.239683881402 + 0.239909023046 + 0.2401342839 + 0.240359649062 + 0.240585133433 + 0.240810737014 + 0.241036459804 + 0.241262301803 + 0.24148824811 + 0.241714313626 + 0.241940498352 + 0.242166802287 + 0.242393225431 + 0.242619752884 + 0.242846399546 + 0.243073165417 + 0.243300050497 + 0.243527054787 + 0.243754178286 + 0.243981406093 + 0.244208753109 + 0.244436219335 + 0.24466380477 + 0.244891494513 + 0.245119318366 + 0.245347246528 + 0.245575293899 + 0.245803460479 + 0.246031746268 + 0.246260136366 + 0.246488645673 + 0.24671728909 + 0.246946036816 + 0.247174888849 + 0.247403874993 + 0.247632965446 + 0.247862190008 + 0.248091518879 + 0.248320966959 + 0.248550534248 + 0.248780205846 + 0.249010011554 + 0.24923992157 + 0.249469950795 + 0.24970009923 + 0.249930366874 + 0.250160753727 + 0.250391244888 + 0.250621855259 + 0.250852584839 + 0.251083433628 + 0.251314401627 + 0.251545488834 + 0.251776665449 + 0.252007991076 + 0.252239435911 + 0.252470970154 + 0.252702653408 + 0.252934426069 + 0.25316631794 + 0.25339832902 + 0.253630459309 + 0.253862708807 + 0.254095077515 + 0.254327565432 + 0.254560172558 + 0.254792898893 + 0.255025714636 + 0.25525867939 + 0.255491733551 + 0.255724936724 + 0.255958229303 + 0.256191670895 + 0.256425201893 + 0.2566588521 + 0.256892621517 + 0.257126510143 + 0.257360517979 + 0.257594645023 + 0.257828861475 + 0.258063226938 + 0.258297711611 + 0.25853228569 + 0.258767008781 + 0.25900182128 + 0.259236752987 + 0.259471833706 + 0.259707003832 + 0.259942293167 + 0.260177701712 + 0.260413229465 + 0.260648876429 + 0.260884642601 + 0.261120527983 + 0.261356532574 + 0.261592626572 + 0.261828869581 + 0.262065201998 + 0.262301683426 + 0.262538254261 + 0.262774974108 + 0.263011783361 + 0.263248711824 + 0.263485759497 + 0.263722926378 + 0.263960242271 + 0.264197617769 + 0.264435142279 + 0.264672785997 + 0.264910548925 + 0.265148431063 + 0.265386402607 + 0.265624523163 + 0.265862762928 + 0.2661010921 + 0.266339570284 + 0.266578137875 + 0.266816824675 + 0.267055660486 + 0.267294585705 + 0.267533630133 + 0.26777279377 + 0.268012076616 + 0.268251478672 + 0.268490999937 + 0.268730640411 + 0.268970400095 + 0.269210278988 + 0.269450247288 + 0.269690364599 + 0.26993060112 + 0.270170927048 + 0.270411401987 + 0.270651966333 + 0.270892649889 + 0.271133482456 + 0.27137440443 + 0.271615445614 + 0.271856635809 + 0.272097915411 + 0.272339314222 + 0.272580832243 + 0.272822469473 + 0.273064225912 + 0.273306101561 + 0.273548096418 + 0.273790180683 + 0.27403241396 + 0.274274766445 + 0.27451723814 + 0.274759799242 + 0.275002509356 + 0.275245308876 + 0.275488257408 + 0.275731295347 + 0.275974482298 + 0.276217758656 + 0.276461154222 + 0.276704698801 + 0.276948332787 + 0.277192085981 + 0.277435958385 + 0.277679949999 + 0.277924060822 + 0.278168290854 + 0.278412640095 + 0.278657108545 + 0.278901696205 + 0.279146403074 + 0.279391229153 + 0.27963617444 + 0.279881209135 + 0.280126392841 + 0.280371695757 + 0.280617088079 + 0.280862629414 + 0.281108289957 + 0.281354039907 + 0.281599938869 + 0.281845927238 + 0.282092034817 + 0.282338291407 + 0.282584637403 + 0.28283110261 + 0.283077716827 + 0.283324420452 + 0.283571243286 + 0.283818185329 + 0.284065276384 + 0.284312456846 + 0.284559756517 + 0.284807175398 + 0.285054713488 + 0.285302370787 + 0.285550147295 + 0.285798043013 + 0.286046028137 + 0.286294162273 + 0.286542415619 + 0.286790788174 + 0.287039279938 + 0.287287861109 + 0.287536591291 + 0.287785440683 + 0.288034409285 + 0.288283467293 + 0.288532674313 + 0.288781970739 + 0.289031416178 + 0.289280951023 + 0.28953063488 + 0.289780408144 + 0.29003033042 + 0.290280342102 + 0.290530502796 + 0.290780752897 + 0.29103115201 + 0.29128164053 + 0.291532248259 + 0.291782975197 + 0.292033851147 + 0.292284816504 + 0.29253590107 + 0.292787104845 + 0.293038457632 + 0.293289899826 + 0.293541461229 + 0.293793141842 + 0.294044941664 + 0.294296860695 + 0.294548898935 + 0.294801056385 + 0.295053333044 + 0.295305728912 + 0.29555824399 + 0.295810878277 + 0.296063631773 + 0.296316504478 + 0.296569496393 + 0.296822607517 + 0.297075837851 + 0.297329187393 + 0.297582656145 + 0.297836244106 + 0.298089951277 + 0.298343747854 + 0.298597693443 + 0.298851758242 + 0.299105942249 + 0.299360245466 + 0.29961463809 + 0.299869179726 + 0.30012384057 + 0.300378620625 + 0.300633490086 + 0.300888508558 + 0.30114364624 + 0.301398903131 + 0.30165424943 + 0.30190974474 + 0.302165359259 + 0.302421063185 + 0.302676916122 + 0.302932888269 + 0.303188949823 + 0.303445160389 + 0.303701490164 + 0.303957909346 + 0.304214477539 + 0.304471164942 + 0.304727941751 + 0.304984867573 + 0.305241882801 + 0.305499047041 + 0.30575633049 + 0.306013703346 + 0.306271225214 + 0.306528836489 + 0.306786596775 + 0.307044476271 + 0.307302445173 + 0.307560563087 + 0.307818770409 + 0.308077126741 + 0.308335572481 + 0.308594167233 + 0.308852881193 + 0.309111684561 + 0.30937063694 + 0.309629678726 + 0.309888869524 + 0.310148179531 + 0.310407578945 + 0.310667127371 + 0.310926765203 + 0.311186552048 + 0.311446458101 + 0.311706453562 + 0.311966598034 + 0.312226861715 + 0.312487214804 + 0.312747716904 + 0.313008308411 + 0.313269048929 + 0.313529908657 + 0.313790857792 + 0.314051955938 + 0.314313173294 + 0.314574480057 + 0.314835935831 + 0.315097510815 + 0.315359205008 + 0.315620988607 + 0.315882921219 + 0.31614497304 + 0.316407114267 + 0.316669404507 + 0.316931813955 + 0.317194342613 + 0.31745699048 + 0.317719727755 + 0.31798261404 + 0.318245619535 + 0.31850874424 + 0.318771988153 + 0.319035351276 + 0.319298803806 + 0.319562405348 + 0.319826126099 + 0.320089966059 + 0.320353925228 + 0.320618003607 + 0.320882201195 + 0.321146517992 + 0.321410953999 + 0.321675509214 + 0.32194018364 + 0.322204977274 + 0.322469890118 + 0.322734922171 + 0.323000103235 + 0.323265373707 + 0.323530763388 + 0.323796272278 + 0.324061900377 + 0.324327647686 + 0.324593544006 + 0.324859529734 + 0.32512563467 + 0.325391858816 + 0.325658231974 + 0.325924694538 + 0.326191276312 + 0.326458007097 + 0.32672482729 + 0.326991796494 + 0.327258855104 + 0.327526062727 + 0.327793359756 + 0.328060805798 + 0.328328341246 + 0.328596025705 + 0.328863799572 + 0.32913172245 + 0.329399764538 + 0.329667896032 + 0.329936176538 + 0.330204576254 + 0.330473095179 + 0.33074170351 + 0.331010460854 + 0.331279337406 + 0.331548333168 + 0.331817448139 + 0.33208668232 + 0.332356035709 + 0.332625508308 + 0.332895100117 + 0.333164811134 + 0.333434641361 + 0.333704590797 + 0.333974659443 + 0.334244847298 + 0.334515184164 + 0.334785610437 + 0.33505615592 + 0.335326820612 + 0.335597634315 + 0.335868537426 + 0.336139589548 + 0.336410731077 + 0.336682021618 + 0.336953401566 + 0.337224930525 + 0.337496548891 + 0.337768316269 + 0.338040202856 + 0.33831217885 + 0.338584303856 + 0.338856548071 + 0.339128911495 + 0.339401394129 + 0.339673966169 + 0.339946687222 + 0.340219527483 + 0.340492486954 + 0.340765595436 + 0.341038793325 + 0.341312110424 + 0.341585546732 + 0.341859102249 + 0.342132776976 + 0.342406600714 + 0.342680513859 + 0.342954546213 + 0.343228727579 + 0.343502998352 + 0.343777418137 + 0.34405195713 + 0.344326585531 + 0.344601362944 + 0.344876229763 + 0.345151245594 + 0.345426380634 + 0.345701634884 + 0.345977008343 + 0.346252501011 + 0.346528112888 + 0.346803843975 + 0.347079694271 + 0.347355663776 + 0.347631752491 + 0.347907960415 + 0.34818431735 + 0.348460763693 + 0.348737329245 + 0.349014043808 + 0.349290847778 + 0.34956780076 + 0.349844843149 + 0.35012203455 + 0.35039934516 + 0.350676745176 + 0.350954294205 + 0.351231962442 + 0.351509749889 + 0.351787656546 + 0.352065682411 + 0.352343827486 + 0.35262209177 + 0.352900475264 + 0.353178977966 + 0.353457599878 + 0.353736370802 + 0.354015231133 + 0.354294210672 + 0.354573339224 + 0.354852586985 + 0.355131924152 + 0.355411410332 + 0.355690985918 + 0.355970710516 + 0.356250554323 + 0.35653051734 + 0.356810599566 + 0.357090801001 + 0.357371121645 + 0.357651561499 + 0.357932120562 + 0.358212798834 + 0.358493626118 + 0.358774542809 + 0.359055608511 + 0.35933676362 + 0.359618067741 + 0.359899461269 + 0.360181003809 + 0.360462665558 + 0.360744416714 + 0.361026316881 + 0.361308336258 + 0.361590474844 + 0.361872732639 + 0.362155109644 + 0.36243763566 + 0.362720251083 + 0.363002985716 + 0.363285839558 + 0.363568842411 + 0.363851934671 + 0.364135175943 + 0.364418536425 + 0.364701986313 + 0.364985585213 + 0.365269303322 + 0.36555314064 + 0.365837097168 + 0.366121172905 + 0.366405367851 + 0.366689682007 + 0.366974145174 + 0.367258697748 + 0.367543369532 + 0.367828190327 + 0.368113100529 + 0.368398159742 + 0.368683338165 + 0.368968605995 + 0.369254022837 + 0.369539558887 + 0.369825214148 + 0.370110988617 + 0.370396882296 + 0.370682924986 + 0.370969057083 + 0.37125530839 + 0.371541708708 + 0.371828198433 + 0.37211483717 + 0.372401565313 + 0.372688442469 + 0.372975438833 + 0.373262554407 + 0.37354978919 + 0.373837143183 + 0.374124616385 + 0.374412208796 + 0.374699950218 + 0.374987781048 + 0.375275731087 + 0.375563830137 + 0.375852018595 + 0.376140356064 + 0.376428812742 + 0.37671738863 + 0.377006083727 + 0.377294898033 + 0.377583831549 + 0.377872884274 + 0.378162056208 + 0.378451377153 + 0.378740787506 + 0.379030317068 + 0.379319995642 + 0.379609793425 + 0.379899680614 + 0.380189716816 + 0.380479872227 + 0.380770146847 + 0.381060540676 + 0.381351083517 + 0.381641715765 + 0.381932467222 + 0.382223367691 + 0.382514357567 + 0.382805496454 + 0.383096724749 + 0.383388102055 + 0.38367959857 + 0.383971214294 + 0.384262949228 + 0.384554803371 + 0.384846776724 + 0.385138899088 + 0.385431110859 + 0.385723471642 + 0.386015921831 + 0.386308521032 + 0.386601239443 + 0.386894077063 + 0.387187033892 + 0.38748010993 + 0.387773305178 + 0.388066619635 + 0.388360053301 + 0.388653635979 + 0.388947308064 + 0.38924112916 + 0.389535069466 + 0.389829099178 + 0.390123277903 + 0.390417575836 + 0.390711992979 + 0.391006559134 + 0.391301214695 + 0.391595989466 + 0.391890913248 + 0.392185926437 + 0.392481088638 + 0.392776370049 + 0.393071770668 + 0.393367290497 + 0.393662929535 + 0.393958687782 + 0.394254565239 + 0.394550561905 + 0.394846707582 + 0.395142972469 + 0.395439326763 + 0.395735830069 + 0.396032452583 + 0.396329194307 + 0.396626055241 + 0.396923035383 + 0.397220134735 + 0.397517383099 + 0.397814720869 + 0.398112207651 + 0.39840978384 + 0.398707509041 + 0.399005353451 + 0.39930331707 + 0.399601399899 + 0.399899601936 + 0.400197952986 + 0.400496393442 + 0.40079498291 + 0.401093661785 + 0.401392489672 + 0.401691436768 + 0.401990503073 + 0.402289688587 + 0.402588993311 + 0.402888417244 + 0.403187990189 + 0.40348765254 + 0.403787463903 + 0.404087394476 + 0.404387414455 + 0.404687583447 + 0.404987871647 + 0.405288308859 + 0.405588835478 + 0.405889481306 + 0.406190276146 + 0.406491160393 + 0.406792193651 + 0.407093346119 + 0.407394617796 + 0.407696008682 + 0.407997518778 + 0.408299148083 + 0.408600926399 + 0.408902794123 + 0.409204810858 + 0.409506946802 + 0.409809201956 + 0.410111576319 + 0.410414069891 + 0.410716682673 + 0.411019414663 + 0.411322295666 + 0.411625266075 + 0.411928385496 + 0.412231624126 + 0.412534981966 + 0.412838459015 + 0.413142055273 + 0.413445770741 + 0.413749605417 + 0.414053589106 + 0.414357662201 + 0.414661884308 + 0.414966225624 + 0.41527068615 + 0.415575265884 + 0.415879964828 + 0.416184812784 + 0.416489750147 + 0.416794836521 + 0.417100042105 + 0.417405337095 + 0.417710781097 + 0.418016344309 + 0.418322056532 + 0.418627858162 + 0.418933779001 + 0.419239848852 + 0.419546037912 + 0.419852346182 + 0.420158773661 + 0.420465320349 + 0.420771986246 + 0.421078771353 + 0.421385705471 + 0.421692728996 + 0.421999901533 + 0.422307193279 + 0.422614604235 + 0.422922134399 + 0.423229783773 + 0.423537582159 + 0.423845469952 + 0.424153506756 + 0.424461632967 + 0.42476990819 + 0.425078302622 + 0.425386816263 + 0.425695478916 + 0.426004230976 + 0.426313132048 + 0.426622122526 + 0.426931262016 + 0.427240520716 + 0.427549898624 + 0.427859395742 + 0.428169041872 + 0.428478777409 + 0.428788661957 + 0.429098665714 + 0.429408758879 + 0.429719001055 + 0.430029392242 + 0.430339872837 + 0.430650472641 + 0.430961221457 + 0.431272059679 + 0.431583046913 + 0.431894153357 + 0.432205379009 + 0.432516753674 + 0.432828217745 + 0.433139801025 + 0.433451533318 + 0.433763384819 + 0.43407535553 + 0.43438744545 + 0.434699654579 + 0.435011982918 + 0.435324460268 + 0.435637056828 + 0.435949742794 + 0.436262577772 + 0.43657553196 + 0.436888635159 + 0.437201827765 + 0.43751513958 + 0.437828600407 + 0.438142180443 + 0.438455879688 + 0.438769698143 + 0.439083635807 + 0.43939769268 + 0.439711898565 + 0.440026193857 + 0.440340638161 + 0.440655201674 + 0.440969884396 + 0.441284686327 + 0.44159963727 + 0.44191467762 + 0.442229866982 + 0.442545175552 + 0.442860603333 + 0.443176150322 + 0.443491816521 + 0.443807601929 + 0.444123536348 + 0.444439560175 + 0.444755733013 + 0.445072025061 + 0.445388436317 + 0.445704996586 + 0.446021646261 + 0.446338444948 + 0.446655333042 + 0.446972370148 + 0.447289526463 + 0.447606831789 + 0.447924226522 + 0.448241740465 + 0.448559403419 + 0.448877185583 + 0.449195086956 + 0.449513107538 + 0.44983124733 + 0.450149536133 + 0.450467914343 + 0.450786441565 + 0.451105087996 + 0.451423853636 + 0.451742738485 + 0.452061742544 + 0.452380895615 + 0.452700167894 + 0.453019529581 + 0.453339040279 + 0.453658699989 + 0.453978449106 + 0.454298317432 + 0.45461833477 + 0.454938471317 + 0.455258727074 + 0.455579102039 + 0.455899596214 + 0.456220209599 + 0.456540971994 + 0.4568618536 + 0.457182824612 + 0.457503944635 + 0.457825213671 + 0.458146572113 + 0.458468079567 + 0.458789676428 + 0.4591114223 + 0.459433287382 + 0.459755271673 + 0.460077404976 + 0.460399627686 + 0.460721999407 + 0.461044490337 + 0.461367100477 + 0.461689829826 + 0.462012678385 + 0.462335675955 + 0.462658762932 + 0.46298199892 + 0.463305354118 + 0.463628828526 + 0.463952451944 + 0.46427616477 + 0.464600026608 + 0.464924007654 + 0.46524810791 + 0.465572327375 + 0.46589666605 + 0.466221153736 + 0.466545730829 + 0.466870456934 + 0.467195302248 + 0.467520266771 + 0.467845380306 + 0.468170583248 + 0.468495935202 + 0.468821406364 + 0.469146996737 + 0.469472706318 + 0.469798535109 + 0.470124512911 + 0.470450609922 + 0.470776826143 + 0.471103161573 + 0.471429616213 + 0.471756190062 + 0.472082912922 + 0.472409754992 + 0.47273671627 + 0.473063796759 + 0.473390996456 + 0.473718315363 + 0.474045783281 + 0.474373370409 + 0.474701076746 + 0.475028902292 + 0.475356847048 + 0.475684940815 + 0.476013153791 + 0.476341456175 + 0.476669937372 + 0.476998507977 + 0.47732719779 + 0.477656036615 + 0.47798499465 + 0.478314042091 + 0.478643268347 + 0.478972584009 + 0.479302018881 + 0.479631602764 + 0.479961305857 + 0.480291128159 + 0.48062106967 + 0.480951160192 + 0.481281340122 + 0.481611669064 + 0.481942117214 + 0.482272684574 + 0.482603371143 + 0.482934206724 + 0.483265131712 + 0.483596205711 + 0.48392739892 + 0.48425874114 + 0.484590172768 + 0.484921753407 + 0.485253423452 + 0.48558524251 + 0.485917210579 + 0.486249268055 + 0.48658144474 + 0.486913770437 + 0.487246215343 + 0.487578779459 + 0.487911462784 + 0.48824429512 + 0.488577246666 + 0.488910287619 + 0.489243477583 + 0.489576816559 + 0.489910244942 + 0.490243822336 + 0.49057751894 + 0.490911304951 + 0.491245269775 + 0.491579324007 + 0.49191352725 + 0.492247819901 + 0.492582261562 + 0.492916822433 + 0.493251532316 + 0.493586331606 + 0.493921279907 + 0.494256347418 + 0.494591534138 + 0.494926840067 + 0.495262295008 + 0.495597839355 + 0.495933532715 + 0.496269345284 + 0.496605306864 + 0.496941357851 + 0.49727755785 + 0.497613877058 + 0.497950315475 + 0.498286873102 + 0.498623549938 + 0.498960375786 + 0.499297320843 + 0.499634385109 + 0.499971568584 + 0.500308871269 + 0.500646352768 + 0.500983893871 + 0.501321613789 + 0.501659393311 + 0.501997351646 + 0.502335429192 + 0.502673625946 + 0.50301194191 + 0.503350377083 + 0.503688931465 + 0.504027605057 + 0.504366457462 + 0.504705369473 + 0.505044460297 + 0.505383610725 + 0.505722939968 + 0.50606238842 + 0.506401956081 + 0.506741642952 + 0.507081508636 + 0.507421433926 + 0.507761478424 + 0.508101701736 + 0.508442044258 + 0.508782446384 + 0.509123027325 + 0.509463727474 + 0.509804546833 + 0.510145485401 + 0.510486602783 + 0.51082777977 + 0.511169075966 + 0.511510550976 + 0.511852145195 + 0.512193799019 + 0.512535631657 + 0.512877583504 + 0.51321965456 + 0.51356190443 + 0.513904213905 + 0.51424664259 + 0.514589250088 + 0.514931917191 + 0.515274763107 + 0.515617728233 + 0.515960812569 + 0.516304016113 + 0.516647338867 + 0.51699078083 + 0.517334401608 + 0.517678081989 + 0.518021941185 + 0.518365859985 + 0.5187099576 + 0.519054174423 + 0.519398510456 + 0.519742965698 + 0.52008754015 + 0.520432293415 + 0.520777106285 + 0.521122097969 + 0.521467149258 + 0.52181237936 + 0.522157728672 + 0.522503197193 + 0.522848784924 + 0.523194491863 + 0.523540318012 + 0.523886322975 + 0.524232387543 + 0.524578630924 + 0.524924993515 + 0.52527141571 + 0.52561801672 + 0.525964736938 + 0.526311635971 + 0.526658594608 + 0.527005672455 + 0.527352929115 + 0.52770024538 + 0.528047740459 + 0.528395354748 + 0.528743088245 + 0.529090940952 + 0.529438912868 + 0.529787003994 + 0.530135273933 + 0.530483603477 + 0.530832111835 + 0.531180739403 + 0.531529426575 + 0.531878292561 + 0.532227277756 + 0.53257638216 + 0.532925665379 + 0.533275008202 + 0.533624529839 + 0.53397411108 + 0.534323871136 + 0.534673750401 + 0.535023748875 + 0.535373866558 + 0.535724103451 + 0.536074459553 + 0.536424994469 + 0.536775588989 + 0.537126362324 + 0.537477254868 + 0.537828266621 + 0.538179397583 + 0.538530647755 + 0.538882017136 + 0.539233505726 + 0.53958517313 + 0.539936900139 + 0.540288805962 + 0.540640830994 + 0.540992975235 + 0.541345238686 + 0.541697621346 + 0.542050123215 + 0.542402744293 + 0.542755544186 + 0.543108403683 + 0.543461441994 + 0.543814599514 + 0.544167876244 + 0.544521272182 + 0.544874787331 + 0.545228421688 + 0.545582234859 + 0.545936107635 + 0.546290159225 + 0.546644330025 + 0.546998620033 + 0.547353029251 + 0.547707557678 + 0.548062205315 + 0.54841697216 + 0.54877191782 + 0.549126982689 + 0.549482107162 + 0.54983741045 + 0.550192832947 + 0.550548374653 + 0.550904035568 + 0.551259875298 + 0.551615774632 + 0.551971852779 + 0.552327990532 + 0.552684307098 + 0.553040742874 + 0.553397297859 + 0.553753972054 + 0.554110825062 + 0.554467737675 + 0.554824769497 + 0.555181980133 + 0.555539309978 + 0.555896759033 + 0.556254327297 + 0.556612014771 + 0.556969821453 + 0.55732780695 + 0.557685852051 + 0.558044075966 + 0.558402359486 + 0.558760821819 + 0.559119403362 + 0.559478104115 + 0.559836983681 + 0.560195922852 + 0.560554981232 + 0.560914218426 + 0.561273574829 + 0.561633050442 + 0.561992645264 + 0.562352359295 + 0.562712192535 + 0.563072144985 + 0.563432276249 + 0.563792467117 + 0.5641528368 + 0.564513325691 + 0.564873933792 + 0.565234661102 + 0.565595507622 + 0.565956473351 + 0.566317617893 + 0.566678822041 + 0.567040205002 + 0.567401707172 + 0.567763328552 + 0.568125069141 + 0.56848692894 + 0.568848967552 + 0.569211065769 + 0.5695733428 + 0.569935679436 + 0.570298194885 + 0.570660829544 + 0.571023583412 + 0.571386516094 + 0.571749508381 + 0.572112619877 + 0.572475910187 + 0.572839319706 + 0.573202848434 + 0.573566496372 + 0.573930263519 + 0.574294149876 + 0.574658155441 + 0.575022339821 + 0.57538664341 + 0.575751006603 + 0.576115548611 + 0.576480209827 + 0.576845049858 + 0.577209949493 + 0.577574968338 + 0.577940165997 + 0.57830542326 + 0.578670859337 + 0.579036414623 + 0.579402089119 + 0.579767882824 + 0.580133855343 + 0.580499887466 + 0.580866098404 + 0.581232428551 + 0.581598818302 + 0.581965386868 + 0.582332134247 + 0.582698941231 + 0.583065867424 + 0.583432972431 + 0.583800137043 + 0.584167480469 + 0.584534943104 + 0.584902524948 + 0.585270226002 + 0.585638046265 + 0.586006045341 + 0.586374104023 + 0.586742341518 + 0.587110698223 + 0.587479174137 + 0.58784776926 + 0.588216483593 + 0.58858537674 + 0.588954329491 + 0.589323461056 + 0.58969271183 + 0.590062022209 + 0.590431571007 + 0.590801179409 + 0.591170907021 + 0.591540753841 + 0.591910779476 + 0.59228092432 + 0.592651188374 + 0.593021571636 + 0.593392074108 + 0.593762695789 + 0.59413343668 + 0.594504356384 + 0.594875395298 + 0.595246493816 + 0.595617771149 + 0.59598916769 + 0.596360743046 + 0.596732378006 + 0.597104132175 + 0.597476065159 + 0.597848117352 + 0.598220288754 + 0.598592579365 + 0.598964989185 + 0.599337518215 + 0.599710166454 + 0.600082993507 + 0.60045593977 + 0.600829005241 + 0.601202189922 + 0.601575493813 + 0.601948916912 + 0.602322459221 + 0.602696180344 + 0.603070020676 + 0.603443920612 + 0.603817999363 + 0.604192256927 + 0.604566574097 + 0.604941010475 + 0.605315625668 + 0.605690300465 + 0.606065154076 + 0.606440126896 + 0.606815218925 + 0.607190430164 + 0.607565820217 + 0.607941269875 + 0.608316898346 + 0.608692646027 + 0.609068512917 + 0.609444499016 + 0.609820604324 + 0.610196828842 + 0.610573232174 + 0.61094969511 + 0.611326336861 + 0.61170309782 + 0.612079977989 + 0.612456977367 + 0.612834095955 + 0.613211393356 + 0.613588809967 + 0.613966286182 + 0.614343941212 + 0.61472171545 + 0.615099668503 + 0.61547768116 + 0.615855813026 + 0.616234123707 + 0.616612553596 + 0.616991102695 + 0.617369771004 + 0.617748558521 + 0.618127465248 + 0.618506550789 + 0.618885695934 + 0.619265019894 + 0.619644463062 + 0.62002402544 + 0.620403707027 + 0.620783567429 + 0.621163487434 + 0.621543586254 + 0.621923804283 + 0.622304081917 + 0.622684597969 + 0.623065173626 + 0.623445868492 + 0.623826742172 + 0.624207675457 + 0.624588787556 + 0.624970018864 + 0.625351369381 + 0.625732839108 + 0.626114487648 + 0.626496195793 + 0.626878082752 + 0.627260088921 + 0.627642214298 + 0.628024458885 + 0.628406822681 + 0.628789365292 + 0.629171967506 + 0.629554748535 + 0.629937648773 + 0.630320668221 + 0.630703806877 + 0.631087064743 + 0.631470501423 + 0.631854057312 + 0.632237672806 + 0.632621467113 + 0.63300538063 + 0.633389413357 + 0.633773624897 + 0.634157896042 + 0.634542346001 + 0.634926915169 + 0.635311603546 + 0.635696411133 + 0.636081337929 + 0.636466443539 + 0.636851608753 + 0.637236952782 + 0.637622416019 + 0.638007998466 + 0.638393700123 + 0.638779520988 + 0.639165520668 + 0.639551579952 + 0.63993781805 + 0.640324175358 + 0.640710651875 + 0.641097247601 + 0.641484022141 + 0.641870856285 + 0.642257869244 + 0.642645001411 + 0.643032252789 + 0.643419623375 + 0.643807113171 + 0.644194722176 + 0.644582509995 + 0.644970417023 + 0.64535844326 + 0.645746588707 + 0.646134853363 + 0.646523237228 + 0.646911799908 + 0.647300481796 + 0.647689223289 + 0.648078143597 + 0.648467183113 + 0.648856401443 + 0.649245679379 + 0.649635136127 + 0.650024712086 + 0.650414347649 + 0.65080422163 + 0.651194155216 + 0.651584208012 + 0.651974439621 + 0.652364730835 + 0.652755200863 + 0.6531457901 + 0.653536498547 + 0.653927385807 + 0.654318332672 + 0.654709458351 + 0.655100703239 + 0.655492007732 + 0.655883550644 + 0.65627515316 + 0.656666874886 + 0.657058775425 + 0.657450735569 + 0.657842874527 + 0.658235132694 + 0.658627569675 + 0.659020066261 + 0.659412682056 + 0.659805476665 + 0.660198390484 + 0.660591423512 + 0.660984575748 + 0.661377847195 + 0.661771297455 + 0.66216480732 + 0.662558495998 + 0.662952303886 + 0.663346230984 + 0.66374027729 + 0.664134502411 + 0.664528787136 + 0.664923250675 + 0.665317833424 + 0.665712535381 + 0.666107356548 + 0.666502356529 + 0.666897416115 + 0.667292654514 + 0.667688012123 + 0.668083488941 + 0.668479084969 + 0.668874800205 + 0.669270694256 + 0.669666707516 + 0.67006278038 + 0.670459032059 + 0.670855402946 + 0.671251952648 + 0.671648561954 + 0.672045350075 + 0.672442257404 + 0.672839283943 + 0.673236429691 + 0.673633694649 + 0.674031078815 + 0.674428641796 + 0.674826323986 + 0.675224125385 + 0.675622045994 + 0.676020085812 + 0.676418304443 + 0.67681658268 + 0.67721503973 + 0.67761361599 + 0.678012311459 + 0.678411126137 + 0.678810119629 + 0.679209172726 + 0.679608404636 + 0.680007755756 + 0.680407226086 + 0.680806815624 + 0.681206524372 + 0.681606411934 + 0.682006418705 + 0.682406544685 + 0.682806789875 + 0.683207154274 + 0.683607637882 + 0.684008300304 + 0.684409081936 + 0.684809923172 + 0.685210943222 + 0.685612142086 + 0.686013400555 + 0.686414837837 + 0.686816334724 + 0.687218010426 + 0.687619805336 + 0.688021719456 + 0.688423812389 + 0.688825964928 + 0.68922829628 + 0.689630746841 + 0.690033316612 + 0.690436005592 + 0.690838873386 + 0.691241800785 + 0.691644906998 + 0.69204813242 + 0.692451477051 + 0.692854940891 + 0.693258583546 + 0.693662285805 + 0.694066166878 + 0.69447016716 + 0.694874286652 + 0.695278525352 + 0.695682942867 + 0.696087419987 + 0.69649207592 + 0.696896851063 + 0.697301745415 + 0.697706758976 + 0.698111951351 + 0.698517203331 + 0.698922634125 + 0.699328184128 + 0.69973385334 + 0.700139641762 + 0.700545608997 + 0.700951635838 + 0.701357841492 + 0.701764166355 + 0.702170610428 + 0.702577233315 + 0.702983915806 + 0.703390777111 + 0.703797757626 + 0.704204857349 + 0.704612076283 + 0.705019414425 + 0.705426931381 + 0.705834507942 + 0.706242263317 + 0.706650137901 + 0.707058191299 + 0.707466304302 + 0.707874596119 + 0.70828294754 + 0.708691477776 + 0.70910012722 + 0.709508955479 + 0.709917843342 + 0.710326910019 + 0.710736036301 + 0.711145341396 + 0.711554765701 + 0.71196436882 + 0.712374031544 + 0.712783873081 + 0.713193833828 + 0.713603913784 + 0.714014112949 + 0.714424431324 + 0.714834928513 + 0.715245485306 + 0.715656220913 + 0.716067075729 + 0.71647810936 + 0.716889202595 + 0.717300474644 + 0.717711806297 + 0.718123316765 + 0.718534946442 + 0.718946754932 + 0.719358623028 + 0.719770669937 + 0.720182836056 + 0.720595121384 + 0.721007525921 + 0.721420049667 + 0.721832752228 + 0.722245514393 + 0.722658455372 + 0.72307151556 + 0.723484754562 + 0.723898053169 + 0.72431153059 + 0.724725067616 + 0.725138783455 + 0.725552618504 + 0.725966632366 + 0.726380705833 + 0.726794958115 + 0.727209329605 + 0.727623820305 + 0.728038430214 + 0.728453159332 + 0.728868067265 + 0.729283094406 + 0.729698240757 + 0.730113506317 + 0.730528891087 + 0.73094445467 + 0.731360077858 + 0.73177587986 + 0.732191801071 + 0.732607841492 + 0.733024060726 + 0.733440339565 + 0.733856797218 + 0.734273374081 + 0.734690070152 + 0.735106885433 + 0.735523879528 + 0.735940933228 + 0.736358165741 + 0.736775517464 + 0.737192988396 + 0.737610638142 + 0.738028347492 + 0.738446235657 + 0.738864243031 + 0.739282369614 + 0.739700615406 + 0.740119040012 + 0.740537524223 + 0.740956187248 + 0.741374969482 + 0.741793870926 + 0.742212951183 + 0.742632091045 + 0.743051409721 + 0.743470847607 + 0.743890404701 + 0.744310081005 + 0.744729936123 + 0.74514991045 + 0.745569944382 + 0.745990216732 + 0.746410548687 + 0.746830999851 + 0.747251629829 + 0.747672379017 + 0.748093247414 + 0.74851423502 + 0.748935341835 + 0.749356627464 + 0.749777972698 + 0.750199496746 + 0.750621140003 + 0.75104290247 + 0.75146484375 + 0.75188690424 + 0.752309024334 + 0.752731323242 + 0.753153800964 + 0.753576338291 + 0.753999054432 + 0.754421830177 + 0.754844784737 + 0.755267858505 + 0.755691111088 + 0.756114423275 + 0.756537914276 + 0.756961524487 + 0.757385253906 + 0.757809102535 + 0.758233129978 + 0.758657217026 + 0.759081482887 + 0.759505867958 + 0.759930372238 + 0.760355055332 + 0.760779798031 + 0.761204719543 + 0.761629760265 + 0.762054920197 + 0.762480258942 + 0.762905657291 + 0.763331234455 + 0.763756930828 + 0.76418274641 + 0.764608681202 + 0.765034794807 + 0.765460968018 + 0.765887320042 + 0.766313791275 + 0.766740381718 + 0.767167150974 + 0.76759403944 + 0.768020987511 + 0.768448114395 + 0.768875420094 + 0.769302785397 + 0.769730329514 + 0.770157933235 + 0.770585715771 + 0.771013617516 + 0.771441698074 + 0.771869838238 + 0.772298157215 + 0.772726595402 + 0.773155152798 + 0.773583829403 + 0.774012684822 + 0.774441659451 + 0.774870753288 + 0.775299966335 + 0.775729298592 + 0.776158750057 + 0.776588380337 + 0.777018129826 + 0.777447998524 + 0.777877986431 + 0.778308153152 + 0.778738379478 + 0.779168784618 + 0.779599308968 + 0.780029952526 + 0.780460774899 + 0.780891656876 + 0.781322717667 + 0.781753897667 + 0.782185196877 + 0.7826166749 + 0.783048212528 + 0.78347992897 + 0.783911764622 + 0.784343719482 + 0.784775853157 + 0.785208046436 + 0.78564041853 + 0.786072909832 + 0.786505520344 + 0.786938250065 + 0.7873711586 + 0.787804186344 + 0.788237333298 + 0.788670599461 + 0.789103984833 + 0.789537549019 + 0.78997117281 + 0.790404975414 + 0.790838897228 + 0.791272997856 + 0.791707158089 + 0.792141497135 + 0.792575955391 + 0.793010532856 + 0.79344522953 + 0.793880105019 + 0.794315099716 + 0.794750154018 + 0.795185446739 + 0.795620799065 + 0.796056270599 + 0.796491920948 + 0.796927690506 + 0.797363579273 + 0.79779958725 + 0.79823577404 + 0.79867208004 + 0.799108445644 + 0.799545049667 + 0.799981713295 + 0.800418496132 + 0.800855457783 + 0.801292538643 + 0.801729738712 + 0.802167057991 + 0.802604556084 + 0.803042173386 + 0.803479850292 + 0.803917765617 + 0.804355740547 + 0.804793834686 + 0.805232107639 + 0.805670499802 + 0.806109011173 + 0.806547641754 + 0.806986451149 + 0.807425379753 + 0.807864427567 + 0.808303594589 + 0.808742880821 + 0.809182345867 + 0.809621870518 + 0.810061573982 + 0.810501396656 + 0.810941398144 + 0.811381459236 + 0.811821699142 + 0.812262058258 + 0.812702536583 + 0.813143134117 + 0.813583910465 + 0.814024806023 + 0.814465820789 + 0.814906954765 + 0.815348207951 + 0.81578963995 + 0.816231191158 + 0.816672861576 + 0.817114651203 + 0.81755656004 + 0.81799864769 + 0.818440854549 + 0.818883180618 + 0.819325625896 + 0.819768190384 + 0.820210933685 + 0.820653796196 + 0.821096777916 + 0.821539878845 + 0.821983158588 + 0.822426497936 + 0.822870016098 + 0.823313653469 + 0.823757410049 + 0.824201345444 + 0.824645400047 + 0.825089514256 + 0.825533866882 + 0.825978279114 + 0.826422810555 + 0.826867520809 + 0.827312350273 + 0.827757298946 + 0.828202426434 + 0.828647613525 + 0.829092979431 + 0.829538464546 + 0.829984068871 + 0.830429792404 + 0.830875694752 + 0.831321716309 + 0.831767857075 + 0.83221411705 + 0.83266055584 + 0.833107054234 + 0.833553731441 + 0.834000527859 + 0.834447443485 + 0.834894537926 + 0.835341751575 + 0.83578902483 + 0.836236536503 + 0.83668410778 + 0.837131798267 + 0.837579667568 + 0.838027656078 + 0.838475763798 + 0.838924050331 + 0.839372396469 + 0.839820921421 + 0.840269565582 + 0.840718328953 + 0.841167271137 + 0.841616272926 + 0.842065453529 + 0.842514753342 + 0.842964231968 + 0.843413770199 + 0.843863487244 + 0.844313323498 + 0.844763278961 + 0.845213353634 + 0.845663607121 + 0.846113920212 + 0.846564412117 + 0.847015082836 + 0.84746581316 + 0.847916722298 + 0.84836769104 + 0.848818838596 + 0.849270164967 + 0.849721550941 + 0.85017311573 + 0.850624799728 + 0.851076602936 + 0.851528525352 + 0.851980626583 + 0.852432787418 + 0.852885127068 + 0.853337645531 + 0.853790223598 + 0.85424298048 + 0.854695796967 + 0.855148792267 + 0.855601966381 + 0.8560552001 + 0.856508612633 + 0.856962144375 + 0.857415795326 + 0.857869565487 + 0.858323514462 + 0.858777523041 + 0.859231710434 + 0.859686076641 + 0.860140502453 + 0.860595107079 + 0.861049771309 + 0.861504614353 + 0.861959636211 + 0.862414717674 + 0.862869977951 + 0.863325357437 + 0.863780856133 + 0.864236474037 + 0.864692270756 + 0.865148186684 + 0.865604221821 + 0.866060376167 + 0.866516649723 + 0.866973102093 + 0.867429673672 + 0.86788636446 + 0.868343174458 + 0.868800163269 + 0.86925727129 + 0.86971449852 + 0.870171844959 + 0.870629310608 + 0.87108695507 + 0.871544718742 + 0.872002601624 + 0.872460603714 + 0.872918725014 + 0.873377025127 + 0.87383544445 + 0.874293982983 + 0.874752700329 + 0.87521147728 + 0.875670433044 + 0.876129508018 + 0.876588702202 + 0.877048075199 + 0.877507567406 + 0.877967119217 + 0.878426909447 + 0.878886759281 + 0.87934678793 + 0.879806876183 + 0.88026714325 + 0.88072758913 + 0.881188094616 + 0.881648778915 + 0.882109582424 + 0.882570505142 + 0.88303154707 + 0.883492767811 + 0.883954107761 + 0.884415566921 + 0.88487714529 + 0.885338842869 + 0.885800719261 + 0.886262714863 + 0.886724829674 + 0.887187063694 + 0.887649476528 + 0.888112008572 + 0.888574659824 + 0.889037430286 + 0.889500379562 + 0.889963388443 + 0.890426576138 + 0.890889883041 + 0.891353368759 + 0.891816914082 + 0.892280638218 + 0.892744481564 + 0.893208444118 + 0.893672585487 + 0.894136846066 + 0.894601225853 + 0.89506572485 + 0.895530343056 + 0.895995140076 + 0.896460056305 + 0.896925091743 + 0.897390246391 + 0.897855520248 + 0.898320972919 + 0.8987865448 + 0.899252235889 + 0.899718105793 + 0.900184035301 + 0.900650143623 + 0.901116371155 + 0.9015827775 + 0.90204924345 + 0.902515888214 + 0.902982652187 + 0.90344953537 + 0.903916597366 + 0.904383718967 + 0.904851019382 + 0.905318498611 + 0.905786037445 + 0.906253755093 + 0.906721532345 + 0.907189488411 + 0.907657623291 + 0.908125817776 + 0.908594191074 + 0.909062683582 + 0.9095312953 + 0.910000085831 + 0.910468935966 + 0.910937964916 + 0.911407113075 + 0.911876440048 + 0.912345826626 + 0.912815392017 + 0.913285076618 + 0.913754880428 + 0.914224863052 + 0.914694905281 + 0.915165126324 + 0.91563552618 + 0.916105985641 + 0.916576623917 + 0.917047321796 + 0.917518258095 + 0.917989253998 + 0.91846036911 + 0.918931663036 + 0.919403076172 + 0.919874608517 + 0.920346319675 + 0.920818150043 + 0.921290099621 + 0.921762168407 + 0.922234356403 + 0.922706723213 + 0.923179209232 + 0.923651814461 + 0.924124538898 + 0.92459744215 + 0.925070464611 + 0.925543606281 + 0.926016867161 + 0.92649024725 + 0.926963806152 + 0.927437484264 + 0.927911281586 + 0.928385257721 + 0.928859293461 + 0.929333508015 + 0.929807841778 + 0.930282354355 + 0.930756926537 + 0.931231677532 + 0.931706547737 + 0.932181596756 + 0.932656705379 + 0.933131992817 + 0.933607399464 + 0.93408292532 + 0.93455862999 + 0.935034394264 + 0.935510337353 + 0.935986399651 + 0.936462640762 + 0.936938941479 + 0.937415421009 + 0.937892019749 + 0.938368797302 + 0.93884563446 + 0.939322650433 + 0.939799785614 + 0.940277099609 + 0.940754473209 + 0.941232025623 + 0.941709697247 + 0.942187488079 + 0.942665457726 + 0.943143486977 + 0.943621695042 + 0.944100022316 + 0.944578528404 + 0.945057153702 + 0.945535838604 + 0.946014761925 + 0.94649374485 + 0.94697290659 + 0.947452127934 + 0.947931528091 + 0.948411107063 + 0.94889074564 + 0.94937056303 + 0.94985049963 + 0.950330555439 + 0.950810790062 + 0.951291143894 + 0.951771616936 + 0.952252209187 + 0.952732920647 + 0.953213810921 + 0.953694820404 + 0.954175949097 + 0.954657256603 + 0.955138623714 + 0.95562016964 + 0.956101834774 + 0.956583678722 + 0.957065582275 + 0.957547664642 + 0.958029866219 + 0.958512246609 + 0.958994686604 + 0.959477305412 + 0.95996004343 + 0.960442960262 + 0.960925936699 + 0.961409091949 + 0.961892366409 + 0.962375760078 + 0.962859332561 + 0.963342964649 + 0.963826775551 + 0.964310765266 + 0.964794814587 + 0.965279042721 + 0.965763390064 + 0.966247856617 + 0.966732442379 + 0.967217206955 + 0.96770209074 + 0.968187093735 + 0.968672275543 + 0.969157516956 + 0.969642937183 + 0.97012847662 + 0.97061419487 + 0.971099972725 + 0.971585929394 + 0.972072005272 + 0.972558259964 + 0.973044574261 + 0.973531067371 + 0.974017679691 + 0.974504470825 + 0.974991321564 + 0.975478351116 + 0.975965499878 + 0.976452767849 + 0.976940214634 + 0.977427780628 + 0.977915465832 + 0.978403270245 + 0.978891253471 + 0.979379296303 + 0.979867517948 + 0.980355918407 + 0.980844378471 + 0.981333017349 + 0.981821775436 + 0.982310652733 + 0.982799708843 + 0.983288884163 + 0.983778178692 + 0.98426759243 + 0.984757125378 + 0.985246837139 + 0.98573666811 + 0.98622661829 + 0.986716747284 + 0.987206935883 + 0.987697303295 + 0.988187849522 + 0.988678455353 + 0.989169239998 + 0.989660143852 + 0.990151166916 + 0.990642309189 + 0.991133630276 + 0.991625070572 + 0.992116630077 + 0.992608368397 + 0.993100166321 + 0.993592143059 + 0.994084239006 + 0.994576513767 + 0.995068848133 + 0.995561361313 + 0.996054053307 + 0.996546804905 + 0.997039735317 + 0.997532784939 + 0.99802595377 + 0.99851924181 + 0.999012708664 + 0.999506294727 + 1.0 +} diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index ade819167..7ef116a21 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -78,18 +78,15 @@ private: #define PRINT_GL_ERRORS ErrorPrinter __e(__FUNCTION__, functions_) #define GL_PREAMBLE \ - QMutexLocker __l(&global_opengl_mutex); + //QMutexLocker __l(&global_opengl_mutex); -QMutex global_opengl_mutex; +//QMutex global_opengl_mutex; OpenGLRenderer::OpenGLRenderer(QObject* parent) : Renderer(parent), - cache_timer_(this), context_(nullptr), framebuffer_(0) { - cache_timer_.setInterval(kTextureCacheMaxSize); - connect(&cache_timer_, &QTimer::timeout, this, &OpenGLRenderer::GarbageCollectTextureCache); } OpenGLRenderer::~OpenGLRenderer() @@ -110,7 +107,7 @@ void OpenGLRenderer::Init(QOpenGLContext *existing_ctx) bool OpenGLRenderer::Init() { - QMutexLocker locker(&global_opengl_mutex); + GL_PREAMBLE; if (context_) { qCritical() << "Can't initialize already initialized OpenGLRenderer"; @@ -156,8 +153,6 @@ void OpenGLRenderer::PostInit() // Set up framebuffer used for various things functions_->glGenFramebuffers(1, &framebuffer_); - - cache_timer_.start(); } void OpenGLRenderer::DestroyInternal() @@ -169,19 +164,12 @@ void OpenGLRenderer::DestroyInternal() functions_->glDeleteFramebuffers(1, &framebuffer_); framebuffer_ = 0; - for (auto it=texture_cache_.cbegin(); it!=texture_cache_.cend(); it++) { - functions_->glDeleteTextures(1, &it->texture); - } - texture_cache_.clear(); - // Delete context if it belongs to us if (context_->parent() == this) { delete context_; } context_ = nullptr; } - - cache_timer_.stop(); } void OpenGLRenderer::ClearDestination(Texture *texture, double r, double g, double b, double a) @@ -189,7 +177,7 @@ void OpenGLRenderer::ClearDestination(Texture *texture, double r, double g, doub GL_PREAMBLE; if (texture) { - AttachTextureAsDestination(texture); + AttachTextureAsDestination(texture->id()); } ClearDestinationInternal(r, g, b, a); @@ -199,54 +187,45 @@ void OpenGLRenderer::ClearDestination(Texture *texture, double r, double g, doub } } -QVariant OpenGLRenderer::CreateNativeTexture2D(int width, int height, VideoParams::Format format, int channel_count, const void *data, int linesize) +QVariant OpenGLRenderer::CreateNativeTexture(int width, int height, int depth, VideoParams::Format format, int channel_count, const void *data, int linesize) { GL_PREAMBLE; - return CreateNativeTexture2DInternal(width, height, format, channel_count, data, linesize); -} + bool is_3d = depth > 1; -QVariant OpenGLRenderer::CreateNativeTexture3D(int width, int height, int depth, VideoParams::Format format, int channel_count, const void *data, int linesize) -{ - GL_PREAMBLE; + // Generate new texture + GLuint texture; + functions_->glGenTextures(1, &texture); + texture_params_.insert(texture, {width, height, depth, format, channel_count}); - GLuint texture = GetCachedTexture(width, height, depth, format, channel_count); + functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize); - // If no texture in cache, generate new texture - bool new_tex = (texture == 0); - if (new_tex) { - functions_->glGenTextures(1, &texture); - texture_params_.insert(texture, {width, height, depth, format, channel_count}); + GLenum target_current = is_3d ? GL_TEXTURE_BINDING_3D : GL_TEXTURE_BINDING_2D; + GLenum target = is_3d ? GL_TEXTURE_3D : GL_TEXTURE_2D; + + GLint current_tex; + functions_->glGetIntegerv(target_current, ¤t_tex); + + functions_->glBindTexture(target, texture); + + if (is_3d) { + context_->extraFunctions()->glTexImage3D(target, 0, GetInternalFormat(format, channel_count), + width, height, depth, 0, GetPixelFormat(channel_count), + GetPixelType(format), data); + } else { + functions_->glTexImage2D(target, 0, GetInternalFormat(format, channel_count), + width, height, 0, GetPixelFormat(channel_count), + GetPixelType(format), data); } - if (new_tex || data) { - functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize); + functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - GLint current_tex; - functions_->glGetIntegerv(GL_TEXTURE_BINDING_3D, ¤t_tex); - - functions_->glBindTexture(GL_TEXTURE_3D, texture); - - if (new_tex) { - context_->extraFunctions()->glTexImage3D(GL_TEXTURE_3D, 0, GetInternalFormat(format, channel_count), - width, height, depth, 0, GetPixelFormat(channel_count), - GetPixelType(format), data); - } else { - context_->extraFunctions()->glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, - width, height, depth, - GetPixelFormat(channel_count), GetPixelType(format), - data); - } - - functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - - functions_->glBindTexture(GL_TEXTURE_3D, current_tex); - } + functions_->glBindTexture(target, current_tex); return texture; } -void OpenGLRenderer::AttachTextureAsDestination(Texture* texture) +void OpenGLRenderer::AttachTextureAsDestination(const QVariant &texture) { PRINT_GL_ERRORS; @@ -254,7 +233,7 @@ void OpenGLRenderer::AttachTextureAsDestination(Texture* texture) functions_->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - texture->id().value(), + texture.value(), 0); } @@ -268,10 +247,7 @@ void OpenGLRenderer::DestroyNativeTexture(QVariant texture) GLuint t = texture.value(); if (t > 0) { - TextureCacheKey key = texture_params_.value(t); - TextureCacheEntry entry = {key, t, QDateTime::currentMSecsSinceEpoch()}; - - texture_cache_.append(entry); + functions_->glDeleteTextures(1, &t); } } @@ -315,15 +291,16 @@ void OpenGLRenderer::DestroyNativeShader(QVariant shader) functions_->glDeleteProgram(program); } -void OpenGLRenderer::UploadToTexture(Texture *texture, const void *data, int linesize) +void OpenGLRenderer::UploadToTexture(const QVariant &handle, const VideoParams &p, const void *data, int linesize) { GL_PREAMBLE; - GLuint t = texture->id().value(); - const VideoParams& p = texture->params(); + GLuint t = handle.value(); - GLenum tex_type = texture->type() == Texture::k2D ? GL_TEXTURE_2D : GL_TEXTURE_3D; - GLenum tex_binding = texture->type() == Texture::k2D ? GL_TEXTURE_BINDING_2D : GL_TEXTURE_BINDING_3D; + bool is_3d = p.is_3d(); + + GLenum tex_type = !is_3d ? GL_TEXTURE_2D : GL_TEXTURE_3D; + GLenum tex_binding = !is_3d ? GL_TEXTURE_BINDING_2D : GL_TEXTURE_BINDING_3D; // Store currently bound texture so it can be restored later GLint current_tex; @@ -336,7 +313,7 @@ void OpenGLRenderer::UploadToTexture(Texture *texture, const void *data, int lin { PRINT_GL_ERRORS; - if (texture->type() == Texture::k2D) { + if (!is_3d) { functions_->glTexSubImage2D(tex_type, 0, 0, 0, p.effective_width(), p.effective_height(), GetPixelFormat(p.channel_count()), GetPixelType(p.format()), @@ -354,16 +331,14 @@ void OpenGLRenderer::UploadToTexture(Texture *texture, const void *data, int lin functions_->glBindTexture(tex_type, current_tex); } -void OpenGLRenderer::DownloadFromTexture(Texture* texture, void *data, int linesize) +void OpenGLRenderer::DownloadFromTexture(const QVariant &id, const VideoParams &p, void *data, int linesize) { GL_PREAMBLE; - const VideoParams& p = texture->params(); - GLint current_tex; functions_->glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤t_tex); - AttachTextureAsDestination(texture); + AttachTextureAsDestination(id); functions_->glPixelStorei(GL_PACK_ROW_LENGTH, linesize); @@ -389,12 +364,12 @@ void OpenGLRenderer::Flush() { GL_PREAMBLE; - functions_->glFinish(); + functions_->glFlush(); } Color OpenGLRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt) { - AttachTextureAsDestination(texture); + AttachTextureAsDestination(texture->id()); QByteArray data(VideoParams::GetBytesPerPixel(texture->format(), texture->channel_count()), Qt::Uninitialized); @@ -530,7 +505,7 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video functions_->glActiveTexture(GL_TEXTURE0 + i); - GLenum target = (texture && texture->type() == Texture::k3D) ? GL_TEXTURE_3D : GL_TEXTURE_2D; + GLenum target = (texture && texture->params().is_3d()) ? GL_TEXTURE_3D : GL_TEXTURE_2D; functions_->glBindTexture(target, tex_id); if (tex_id) { @@ -612,11 +587,11 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video TexturePtr output_tex, input_tex; if (real_iteration_count > 1) { // Create one texture to bounce off - output_tex = CreateTextureFromNativeHandle(CreateNativeTexture2DInternal(destination_params), destination_params); + output_tex = CreateTexture(destination_params); if (real_iteration_count > 2) { // Create a second texture bounce off - input_tex = CreateTextureFromNativeHandle(CreateNativeTexture2DInternal(destination_params), destination_params); + input_tex = CreateTexture(destination_params); } } @@ -632,7 +607,7 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video // This is the last iteration, draw to the destination if (destination) { // If we have a destination texture, draw to it - AttachTextureAsDestination(destination); + AttachTextureAsDestination(destination->id()); } else if (iteration > 0) { // Otherwise, if we were iterating before, detach texture now DetachTextureAsDestination(); @@ -644,7 +619,7 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video } } else { // Always draw to output_tex, which gets swapped with input_tex every iteration - AttachTextureAsDestination(output_tex.get()); + AttachTextureAsDestination(output_tex->id()); } if (iteration > 0) { @@ -676,7 +651,7 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video // Release any textures we bound before for (int i=textures_to_bind.size()-1; i>=0; i--) { TexturePtr texture = textures_to_bind.at(i).texture; - GLenum target = (texture && texture->type() == Texture::k3D) ? GL_TEXTURE_3D : GL_TEXTURE_2D; + GLenum target = (texture && texture->params().is_3d()) ? GL_TEXTURE_3D : GL_TEXTURE_2D; functions_->glActiveTexture(GL_TEXTURE0 + i); functions_->glBindTexture(target, 0); } @@ -816,71 +791,6 @@ void OpenGLRenderer::ClearDestinationInternal(double r, double g, double b, doub functions_->glClear(GL_COLOR_BUFFER_BIT); } -QVariant OpenGLRenderer::CreateNativeTexture2DInternal(int width, int height, VideoParams::Format format, int channel_count, const void *data, int linesize) -{ - GLuint texture = GetCachedTexture(width, height, 1, format, channel_count); - - // If no texture in cache, generate new texture - bool new_tex = (texture == 0); - if (new_tex) { - functions_->glGenTextures(1, &texture); - texture_params_.insert(texture, {width, height, 1, format, channel_count}); - } - - if (new_tex || data) { - functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize); - - GLint current_tex; - functions_->glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤t_tex); - - functions_->glBindTexture(GL_TEXTURE_2D, texture); - - { - PRINT_GL_ERRORS; - if (new_tex) { - functions_->glTexImage2D(GL_TEXTURE_2D, 0, GetInternalFormat(format, channel_count), - width, height, 0, GetPixelFormat(channel_count), - GetPixelType(format), data); - } else { - functions_->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, - width, height, - GetPixelFormat(channel_count), GetPixelType(format), - data); - } - } - - functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - - functions_->glBindTexture(GL_TEXTURE_2D, current_tex); - } - - return texture; -} - -QVariant OpenGLRenderer::CreateNativeTexture2DInternal(const VideoParams ¶ms, const void *data, int linesize) -{ - return CreateNativeTexture2DInternal(params.effective_width(), params.effective_height(), params.format(), params.channel_count(), data, linesize); -} - -GLuint OpenGLRenderer::GetCachedTexture(int width, int height, int depth, VideoParams::Format format, int channel_count) -{ - TextureCacheKey input_key = {width, height, depth, format, channel_count}; - - for (int i=0; iage < max_age) { - GL_PREAMBLE; - GLuint t = it->texture; - texture_params_.remove(t); - functions_->glDeleteTextures(1, &t); - it = texture_cache_.erase(it); - } else { - it++; - } - } -} - } diff --git a/app/render/opengl/openglrenderer.h b/app/render/opengl/openglrenderer.h index b1ec02474..fe7f0098a 100644 --- a/app/render/opengl/openglrenderer.h +++ b/app/render/opengl/openglrenderer.h @@ -47,37 +47,35 @@ public: virtual void PostDestroy() override; -public slots: virtual void PostInit() override; - virtual void DestroyInternal() override; - virtual void ClearDestination(olive::Texture *texture = nullptr, double r = 0.0, double g = 0.0, double b = 0.0, double a = 0.0) override; - virtual QVariant CreateNativeTexture2D(int width, int height, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) override; - virtual QVariant CreateNativeTexture3D(int width, int height, int depth, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) override; - - virtual void DestroyNativeTexture(QVariant texture) override; - virtual QVariant CreateNativeShader(olive::ShaderCode code) override; virtual void DestroyNativeShader(QVariant shader) override; - virtual void UploadToTexture(olive::Texture* texture, const void* data, int linesize) override; + virtual void UploadToTexture(const QVariant &handle, const VideoParams ¶ms, const void* data, int linesize) override; - virtual void DownloadFromTexture(olive::Texture* texture, void* data, int linesize) override; + virtual void DownloadFromTexture(const QVariant &handle, const VideoParams ¶ms, void* data, int linesize) override; virtual void Flush() override; virtual Color GetPixelFromTexture(olive::Texture *texture, const QPointF &pt) override; -protected slots: +protected: virtual void Blit(QVariant shader, olive::ShaderJob job, olive::Texture* destination, olive::VideoParams destination_params, bool clear_destination) override; + virtual QVariant CreateNativeTexture(int width, int height, int depth, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) override; + + virtual void DestroyNativeTexture(QVariant texture) override; + + virtual void DestroyInternal() override; + private: static GLint GetInternalFormat(VideoParams::Format format, int channel_layout); @@ -85,7 +83,7 @@ private: static GLenum GetPixelFormat(int channel_count); - void AttachTextureAsDestination(olive::Texture* texture); + void AttachTextureAsDestination(const QVariant &texture); void DetachTextureAsDestination(); @@ -93,15 +91,8 @@ private: void ClearDestinationInternal(double r = 0.0, double g = 0.0, double b = 0.0, double a = 0.0); - QVariant CreateNativeTexture2DInternal(int width, int height, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0); - QVariant CreateNativeTexture2DInternal(const VideoParams ¶ms, const void* data = nullptr, int linesize = 0); - - GLuint GetCachedTexture(int width, int height, int depth, VideoParams::Format format, int channel_count); - GLuint CompileShader(GLenum type, const QString &code); - QTimer cache_timer_; - QOpenGLContext* context_; QOpenGLFunctions* functions_; @@ -124,21 +115,10 @@ private: } }; - struct TextureCacheEntry { - TextureCacheKey key; - GLuint texture; - qint64 age; - }; - - QVector texture_cache_; - QMap texture_params_; static const int kTextureCacheMaxSize; -private slots: - void GarbageCollectTextureCache(); - }; } diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 9ea8439cb..15cce575a 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -61,7 +61,7 @@ PreviewAutoCacher::~PreviewAutoCacher() SetViewerNode(nullptr); } -RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, RenderTicketPriority priority) +RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, bool dry) { // If we have a single frame render queued (but not yet sent to the RenderManager), cancel it now CancelQueuedSingleFrameRender(); @@ -70,7 +70,7 @@ RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, RenderTicke auto sfr = std::make_shared(); sfr->Start(); sfr->setProperty("time", QVariant::fromValue(t)); - sfr->setProperty("priority", int(priority)); + sfr->setProperty("dry", dry); // Queue it and try to render single_frame_render_ = sfr; @@ -79,9 +79,9 @@ RenderTicketPtr PreviewAutoCacher::GetSingleFrame(const rational &t, RenderTicke return sfr; } -RenderTicketPtr PreviewAutoCacher::GetRangeOfAudio(TimeRange range, RenderTicketPriority priority) +RenderTicketPtr PreviewAutoCacher::GetRangeOfAudio(TimeRange range) { - return RenderAudio(range, false, priority); + return RenderAudio(range, false); } void PreviewAutoCacher::VideoInvalidated(const TimeRange &range) @@ -193,6 +193,17 @@ void PreviewAutoCacher::VideoRendered() { RenderTicketWatcher* watcher = static_cast(sender()); + // Process passthroughs no matter what, if the viewer was switched, the passthrough map would be + // cleared anyway + QVector tickets = video_immediate_passthroughs_.take(watcher); + foreach (RenderTicketPtr t, tickets) { + if (watcher->HasResult()) { + t->Finish(watcher->Get()); + } else { + t->Finish(); + } + } + // If the task list doesn't contain this watcher, presumably it was cleared as a result of a // viewer switch, so we'll completely ignore this watcher auto it = video_tasks_.find(watcher); @@ -214,17 +225,6 @@ void PreviewAutoCacher::VideoRendered() TryRender(); } - // Process passthroughs no matter what, if the viewer was switched, the passthrough map would be - // cleared anyway - QVector tickets = video_immediate_passthroughs_.take(watcher); - foreach (RenderTicketPtr t, tickets) { - if (watcher->HasResult()) { - t->Finish(watcher->Get()); - } else { - t->Finish(); - } - } - delete watcher; } @@ -565,17 +565,19 @@ void PreviewAutoCacher::TryRender() } if (single_frame_render_) { - // Check if already caching this - RenderTicketWatcher *watcher = RenderFrame(single_frame_render_->property("time").value(), - RenderTicketPriority(single_frame_render_->property("priority").toInt()), - nullptr); - video_immediate_passthroughs_[watcher].append(single_frame_render_); - + // Make an explicit copy of the render ticket here - it seems that on some systems it can be set + // to NULL before we're done with it... + RenderTicketPtr t = single_frame_render_; single_frame_render_ = nullptr; + + RenderTicketWatcher *watcher = RenderFrame(t->property("time").value(), + nullptr, + t->property("dry").toBool()); + video_immediate_passthroughs_[watcher].append(t); } - // Ensure we are running tasks if we have any - const int max_tasks = RenderManager::GetNumberOfIdealConcurrentJobs(); + // Completely arbitrary number. I don't know what's optimal for this yet. + const int max_tasks = 4; // Handle video tasks rational t; @@ -585,7 +587,7 @@ void PreviewAutoCacher::TryRender() // We want this hash, if we're not already rendering, start render now if (!render_task) { // Don't render any hash more than once - RenderFrame(t, RenderTicketPriority::kNormal, viewer_node_->video_frame_cache()); + RenderFrame(t, viewer_node_->video_frame_cache(), false); } emit SignalCacheProxyTaskProgress(double(queued_frame_iterator_.frame_index()) / double(queued_frame_iterator_.size())); @@ -605,13 +607,13 @@ void PreviewAutoCacher::TryRender() r.set_out(qMin(r.out(), r.in() + AudioVisualWaveform::kMinimumSampleRate.flipped())); // Start job - RenderAudio(r, true, RenderTicketPriority::kNormal); + RenderAudio(r, true); audio_iterator_.remove(r); } } -RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, RenderTicketPriority priority, FrameHashCache *cache) +RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, FrameHashCache *cache, bool dry) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("job", QVariant::fromValue(last_update_time_)); @@ -625,19 +627,19 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, RenderMode::kOffline, cache, - priority, - RenderManager::kTexture)); + dry ? RenderManager::kNull : RenderManager::kTexture)); + return watcher; } -RenderTicketPtr PreviewAutoCacher::RenderAudio(Node *node, const TimeRange &r, bool generate_waveforms, RenderTicketPriority priority) +RenderTicketPtr PreviewAutoCacher::RenderAudio(Node *node, const TimeRange &r, bool generate_waveforms) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("job", QVariant::fromValue(last_update_time_)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::AudioRendered); audio_tasks_.insert(watcher, r); - RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(node, r, copied_viewer_node_->GetAudioParams(), RenderMode::kOffline, generate_waveforms, priority); + RenderTicketPtr ticket = RenderManager::instance()->RenderAudio(node, r, copied_viewer_node_->GetAudioParams(), RenderMode::kOffline, generate_waveforms); watcher->SetTicket(ticket); return ticket; } diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 4c20a72d6..0ce429673 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -33,8 +33,6 @@ #include "render/audioparams.h" #include "render/renderjobtracker.h" #include "render/rendermanager.h" -#include "threading/threadpool.h" -#include "threading/threadticketwatcher.h" namespace olive { @@ -51,9 +49,9 @@ public: virtual ~PreviewAutoCacher() override; - RenderTicketPtr GetSingleFrame(const rational& t, RenderTicketPriority prioritize); + RenderTicketPtr GetSingleFrame(const rational& t, bool dry = false); - RenderTicketPtr GetRangeOfAudio(TimeRange range, RenderTicketPriority prioritize); + RenderTicketPtr GetRangeOfAudio(TimeRange range); /** * @brief Set the viewer node to auto-cache @@ -100,16 +98,16 @@ signals: private: void TryRender(); - RenderTicketWatcher *RenderFrame(Node *node, const rational &time, RenderTicketPriority priority, FrameHashCache *cache); - RenderTicketWatcher *RenderFrame(const rational &time, RenderTicketPriority priority, FrameHashCache *cache) + RenderTicketWatcher *RenderFrame(Node *node, const rational &time, FrameHashCache *cache, bool dry); + RenderTicketWatcher *RenderFrame(const rational &time, FrameHashCache *cache, bool dry) { - return RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(), time, priority, cache); + return RenderFrame(copied_viewer_node_->GetConnectedTextureOutput(), time, cache, dry); } - RenderTicketPtr RenderAudio(Node *node, const TimeRange &range, bool generate_waveforms, RenderTicketPriority priority); - RenderTicketPtr RenderAudio(const TimeRange &range, bool generate_waveforms, RenderTicketPriority priority) + RenderTicketPtr RenderAudio(Node *node, const TimeRange &range, bool generate_waveforms); + RenderTicketPtr RenderAudio(const TimeRange &range, bool generate_waveforms) { - return RenderAudio(copied_viewer_node_->GetConnectedSampleOutput(), range, generate_waveforms, priority); + return RenderAudio(copied_viewer_node_->GetConnectedSampleOutput(), range, generate_waveforms); } /** diff --git a/app/render/rendercache.h b/app/render/rendercache.h index 114244fb8..77a56780d 100644 --- a/app/render/rendercache.h +++ b/app/render/rendercache.h @@ -39,15 +39,10 @@ private: }; -struct DecoderPair { - DecoderPair() - { - decoder = nullptr; - last_modified = 0; - } - - DecoderPtr decoder; - qint64 last_modified; +struct DecoderPair +{ + DecoderPtr decoder = nullptr; + qint64 last_modified = 0; }; using DecoderCache = RenderCache; diff --git a/app/render/renderer.cpp b/app/render/renderer.cpp index 80683c285..e6754e5c7 100644 --- a/app/render/renderer.cpp +++ b/app/render/renderer.cpp @@ -20,36 +20,83 @@ #include "renderer.h" +#include +#include +#include #include -#include "common/ocioutils.h" - namespace olive { Renderer::Renderer(QObject *parent) : QObject(parent) { - -} - -TexturePtr Renderer::CreateTexture(const VideoParams ¶ms, Texture::Type type, const void *data, int linesize) -{ - QVariant v; - - if (type == Texture::k3D) { - v = CreateNativeTexture3D(params.effective_width(), params.effective_height(), - params.effective_depth(), params.format(), params.channel_count(), data, linesize); - } else { - v = CreateNativeTexture2D(params.effective_width(), params.effective_height(), params.format(), - params.channel_count(), data, linesize); - } - - return CreateTextureFromNativeHandle(v, params, type); } TexturePtr Renderer::CreateTexture(const VideoParams ¶ms, const void *data, int linesize) { - return CreateTexture(params, Texture::k2D, data, linesize); + QVariant v; + + if (USE_TEXTURE_CACHE) { + QMutexLocker locker(&texture_cache_lock_); + for (auto it=texture_cache_.begin(); it!=texture_cache_.end(); it++) { + if (it->width == params.effective_width() + && it->height == params.effective_height() + && it->depth == params.effective_depth() + && it->format == params.format() + && it->channel_count == params.channel_count()) { + v = it->handle; + texture_cache_.erase(it); + break; + } + } + } + + if (v.isNull()) { + v = CreateNativeTexture(params.effective_width(), params.effective_height(), params.effective_depth(), + params.format(), params.channel_count(), data, linesize); + } else if (data) { + UploadToTexture(v, params, data, linesize); + } else { + this->Flush(); + } + + return CreateTextureFromNativeHandle(v, params); +} + +void Renderer::DestroyTexture(Texture *texture) +{ + if (USE_TEXTURE_CACHE) { + // HACK: Dirty, dirty hack. OpenGL uses "contexts" to store all of its data, and each context + // can only be used by the thread that created it. However there are also "shared contexts" + // where assets from one context can be used in another. We use shared contexts so that + // textures rendered in the background can be displayed on the screen, travelling from + // a background thread to the main UI thread. However, when that texture is destroyed, it + // comes back here to be placed in the texture cache. But that leads to a race condition + // because it will call the background thread's renderer in the main thread. Since all + // assets are shared, we could technically just get the texture to call "destroy" in the + // viewer's renderer instance, but that would mean all textures would end up stranded + // there unusable by the background renderer, negating the very advantage of the texture + // cache in the first place. Therefore, we simply allow the thread calling to happen, and + // use mutexes to prevent race conditions. + // + // Presumably Vulkan would not have this issue because it allows for application-wide + // instances and multithreading. + texture_cache_lock_.lock(); + texture_cache_.push_back({texture->params().effective_width(), + texture->params().effective_height(), + texture->params().effective_depth(), + texture->params().format(), + texture->params().channel_count(), + texture->id(), + QDateTime::currentMSecsSinceEpoch()}); + texture_cache_lock_.unlock(); + + if (QThread::currentThread() == this->thread()) { + ClearOldTextures(); + } + } else { + DestroyNativeTexture(texture->id()); + } } TexturePtr Renderer::InterlaceTexture(TexturePtr top, TexturePtr bottom, const VideoParams ¶ms) @@ -72,8 +119,22 @@ TexturePtr Renderer::InterlaceTexture(TexturePtr top, TexturePtr bottom, const V return output; } +QVariant Renderer::GetDefaultShader() +{ + if (default_shader_.isNull()) { + default_shader_ = CreateNativeShader(ShaderCode(QString(), QString())); + } + + return default_shader_; +} + void Renderer::Destroy() { + if (!default_shader_.isNull()) { + DestroyNativeShader(default_shader_); + default_shader_.clear(); + } + color_cache_.clear(); if (!interlace_texture_.isNull()) { @@ -81,16 +142,21 @@ void Renderer::Destroy() interlace_texture_.clear(); } + for (auto it=texture_cache_.begin(); it!=texture_cache_.end(); it++) { + DestroyNativeTexture(it->handle); + } + texture_cache_.clear(); + DestroyInternal(); } -TexturePtr Renderer::CreateTextureFromNativeHandle(const QVariant &v, const VideoParams ¶ms, Texture::Type type) +TexturePtr Renderer::CreateTextureFromNativeHandle(const QVariant &v, const VideoParams ¶ms) { if (v.isNull()) { return nullptr; } - return std::make_shared(this, v, params, type); + return std::make_shared(this, v, params); } bool Renderer::GetColorContext(const ColorTransformJob &color_job, Renderer::ColorContext *ctx) @@ -161,8 +227,7 @@ bool Renderer::GetColorContext(const ColorTransformJob &color_job, Renderer::Col } // Allocate 3D LUT - color_ctx.lut3d_textures[i].texture = CreateTexture(VideoParams(edge_len, edge_len, edge_len, VideoParams::kFormatFloat32, VideoParams::kRGBChannelCount), - Texture::k3D, values); + color_ctx.lut3d_textures[i].texture = CreateTexture(VideoParams(edge_len, edge_len, edge_len, VideoParams::kFormatFloat32, VideoParams::kRGBChannelCount), values); color_ctx.lut3d_textures[i].name = sampler_name; color_ctx.lut3d_textures[i].interpolation = (interpolation == OCIO::INTERP_NEAREST) ? Texture::kNearest : Texture::kLinear; } @@ -192,9 +257,7 @@ bool Renderer::GetColorContext(const ColorTransformJob &color_job, Renderer::Col } // Allocate 1D LUT - color_ctx.lut1d_textures[i].texture = CreateTexture(VideoParams(width, height, VideoParams::kFormatFloat32, (channel == OCIO::GpuShaderDesc::TEXTURE_RED_CHANNEL) ? 1 : VideoParams::kRGBChannelCount), - Texture::k2D, - values); + color_ctx.lut1d_textures[i].texture = CreateTexture(VideoParams(width, height, VideoParams::kFormatFloat32, (channel == OCIO::GpuShaderDesc::TEXTURE_RED_CHANNEL) ? 1 : VideoParams::kRGBChannelCount), values); color_ctx.lut1d_textures[i].name = sampler_name; color_ctx.lut1d_textures[i].interpolation = (interpolation == OCIO::INTERP_NEAREST) ? Texture::kNearest : Texture::kLinear; } @@ -205,6 +268,20 @@ bool Renderer::GetColorContext(const ColorTransformJob &color_job, Renderer::Col } } +void Renderer::ClearOldTextures() +{ + QMutexLocker locker(&texture_cache_lock_); + + for (auto it=texture_cache_.begin(); it!=texture_cache_.end(); ) { + if (it->accessed < QDateTime::currentMSecsSinceEpoch() - MAX_TEXTURE_LIFE) { + DestroyNativeTexture(it->handle); + it = texture_cache_.erase(it); + } else { + it++; + } + } +} + void Renderer::BlitColorManaged(const ColorTransformJob &color_job, Texture *destination, const VideoParams ¶ms) { ColorContext color_ctx; @@ -218,7 +295,6 @@ void Renderer::BlitColorManaged(const ColorTransformJob &color_job, Texture *des job.Insert(QStringLiteral("ove_cropmatrix"), NodeValue(NodeValue::kMatrix, color_job.GetCropMatrix().inverted())); job.Insert(QStringLiteral("ove_maintex_alpha"), NodeValue(NodeValue::kInt, int(color_job.GetInputAlphaAssociation()))); job.Insert(color_job.GetValues()); - job.SetAlphaChannelRequired(color_job.GetAlphaChannelRequired()); foreach (const ColorContext::LUT& l, color_ctx.lut3d_textures) { job.Insert(l.name, NodeValue(NodeValue::kTexture, QVariant::fromValue(l.texture))); diff --git a/app/render/renderer.h b/app/render/renderer.h index 749d777cf..76651c05c 100644 --- a/app/render/renderer.h +++ b/app/render/renderer.h @@ -45,9 +45,10 @@ public: virtual bool Init() = 0; - TexturePtr CreateTexture(const VideoParams& params, Texture::Type type, const void* data = nullptr, int linesize = 0); TexturePtr CreateTexture(const VideoParams& params, const void *data = nullptr, int linesize = 0); + void DestroyTexture(Texture *texture); + void BlitToTexture(QVariant shader, olive::ShaderJob job, olive::Texture* destination, @@ -76,43 +77,40 @@ public: TexturePtr InterlaceTexture(TexturePtr top, TexturePtr bottom, const VideoParams ¶ms); + QVariant GetDefaultShader(); + void Destroy(); virtual void PostDestroy() = 0; -public slots: virtual void PostInit() = 0; - virtual void DestroyInternal() = 0; - virtual void ClearDestination(olive::Texture *texture = nullptr, double r = 0.0, double g = 0.0, double b = 0.0, double a = 0.0) = 0; - virtual QVariant CreateNativeTexture2D(int width, int height, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) = 0; - virtual QVariant CreateNativeTexture3D(int width, int height, int depth, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) = 0; - - virtual void DestroyNativeTexture(QVariant texture) = 0; - virtual QVariant CreateNativeShader(olive::ShaderCode code) = 0; virtual void DestroyNativeShader(QVariant shader) = 0; - virtual void UploadToTexture(olive::Texture* texture, const void* data, int linesize) = 0; + virtual void UploadToTexture(const QVariant &handle, const VideoParams ¶ms, const void* data, int linesize) = 0; - virtual void DownloadFromTexture(olive::Texture* texture, void* data, int linesize) = 0; + virtual void DownloadFromTexture(const QVariant &handle, const VideoParams ¶ms, void* data, int linesize) = 0; virtual void Flush() = 0; virtual Color GetPixelFromTexture(olive::Texture *texture, const QPointF &pt) = 0; -protected slots: +protected: virtual void Blit(QVariant shader, olive::ShaderJob job, olive::Texture* destination, olive::VideoParams destination_params, bool clear_destination) = 0; -protected: - TexturePtr CreateTextureFromNativeHandle(const QVariant &v, const VideoParams ¶ms, Texture::Type type = Texture::k2D); + virtual QVariant CreateNativeTexture(int width, int height, int depth, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) = 0; + + virtual void DestroyNativeTexture(QVariant texture) = 0; + + virtual void DestroyInternal() = 0; private: struct ColorContext { @@ -128,14 +126,37 @@ private: }; + TexturePtr CreateTextureFromNativeHandle(const QVariant &v, const VideoParams ¶ms); + bool GetColorContext(const ColorTransformJob &color_job, ColorContext* ctx); + void ClearOldTextures(); + QHash color_cache_; + struct CachedTexture + { + int width; + int height; + int depth; + VideoParams::Format format; + int channel_count; + QVariant handle; + qint64 accessed; + }; + + static const int MAX_TEXTURE_LIFE = 5000; + static const bool USE_TEXTURE_CACHE = true; + std::list texture_cache_; + QMutex color_cache_mutex_; + QVariant default_shader_; + QVariant interlace_texture_; + QMutex texture_cache_lock_; + }; } diff --git a/app/render/rendererthreadwrapper.cpp b/app/render/rendererthreadwrapper.cpp deleted file mode 100644 index 50abd8b77..000000000 --- a/app/render/rendererthreadwrapper.cpp +++ /dev/null @@ -1,181 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 "rendererthreadwrapper.h" - -namespace olive { - -RendererThreadWrapper::RendererThreadWrapper(Renderer *inner, QObject *parent) : - Renderer(parent), - inner_(inner), - thread_(nullptr) -{ -} - -bool RendererThreadWrapper::Init() -{ - // Init context in main thread - if (!inner_->Init()) { - return false; - } - - // Create thread - thread_ = new QThread(this); - thread_->start(QThread::IdlePriority); - - // Move context to thread - inner_->moveToThread(thread_); - - // Queue post-init in new thread - QMetaObject::invokeMethod(inner_, "PostInit", Qt::BlockingQueuedConnection); - - return true; -} - -void RendererThreadWrapper::PostInit() -{ - // Do nothing -} - -void RendererThreadWrapper::DestroyInternal() -{ - if (thread_) { - QMetaObject::invokeMethod(inner_, "DestroyInternal", Qt::BlockingQueuedConnection); - - thread_->quit(); - thread_->wait(); - delete thread_; - thread_ = nullptr; - - // Destroy in main thread - inner_->PostDestroy(); - } -} - -void RendererThreadWrapper::ClearDestination(Texture *texture, double r, double g, double b, double a) -{ - QMetaObject::invokeMethod(inner_, "ClearDestination", Qt::BlockingQueuedConnection, - OLIVE_NS_ARG(Texture*, texture), - Q_ARG(double, r), - Q_ARG(double, g), - Q_ARG(double, b), - Q_ARG(double, a)); -} - -QVariant RendererThreadWrapper::CreateNativeTexture2D(int width, int height, VideoParams::Format format, int channel_count, const void *data, int linesize) -{ - QVariant v; - - QMetaObject::invokeMethod(inner_, "CreateNativeTexture2D", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, v), - Q_ARG(int, width), - Q_ARG(int, height), - OLIVE_NS_ARG(VideoParams::Format, format), - Q_ARG(int, channel_count), - Q_ARG(const void*, data), - Q_ARG(int, linesize)); - - return v; -} - -QVariant RendererThreadWrapper::CreateNativeTexture3D(int width, int height, int depth, VideoParams::Format format, int channel_count, const void *data, int linesize) -{ - QVariant v; - - QMetaObject::invokeMethod(inner_, "CreateNativeTexture3D", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, v), - Q_ARG(int, width), - Q_ARG(int, height), - Q_ARG(int, depth), - OLIVE_NS_ARG(VideoParams::Format, format), - Q_ARG(int, channel_count), - Q_ARG(const void*, data), - Q_ARG(int, linesize)); - - return v; -} - -void RendererThreadWrapper::DestroyNativeTexture(QVariant texture) -{ - QMetaObject::invokeMethod(inner_, "DestroyNativeTexture", Qt::BlockingQueuedConnection, - Q_ARG(QVariant, texture)); -} - -QVariant RendererThreadWrapper::CreateNativeShader(ShaderCode code) -{ - QVariant v; - - QMetaObject::invokeMethod(inner_, "CreateNativeShader", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, v), - OLIVE_NS_ARG(ShaderCode, code)); - - return v; -} - -void RendererThreadWrapper::DestroyNativeShader(QVariant shader) -{ - QMetaObject::invokeMethod(inner_, "DestroyNativeShader", Qt::BlockingQueuedConnection, - Q_ARG(QVariant, shader)); -} - -void RendererThreadWrapper::UploadToTexture(Texture *texture, const void *data, int linesize) -{ - QMetaObject::invokeMethod(inner_, "UploadToTexture", Qt::BlockingQueuedConnection, - OLIVE_NS_ARG(Texture*, texture), - Q_ARG(const void*, data), - Q_ARG(int, linesize)); -} - -void RendererThreadWrapper::DownloadFromTexture(Texture *texture, void *data, int linesize) -{ - QMetaObject::invokeMethod(inner_, "DownloadFromTexture", Qt::BlockingQueuedConnection, - OLIVE_NS_ARG(Texture*, texture), - Q_ARG(void*, data), - Q_ARG(int, linesize)); -} - -void RendererThreadWrapper::Flush() -{ - QMetaObject::invokeMethod(inner_, "Flush", Qt::BlockingQueuedConnection); -} - -Color RendererThreadWrapper::GetPixelFromTexture(Texture *texture, const QPointF &pt) -{ - Color c; - - QMetaObject::invokeMethod(inner_, "GetPixelFromTexture", Qt::BlockingQueuedConnection, - OLIVE_NS_RETURN_ARG(Color, c), - OLIVE_NS_ARG(Texture*, texture), - Q_ARG(QPointF, pt)); - - return c; -} - -void RendererThreadWrapper::Blit(QVariant shader, ShaderJob job, Texture *destination, VideoParams destination_params, bool clear_destination) -{ - QMetaObject::invokeMethod(inner_, "Blit", Qt::BlockingQueuedConnection, - Q_ARG(QVariant, shader), - OLIVE_NS_ARG(ShaderJob, job), - OLIVE_NS_ARG(Texture*, destination), - OLIVE_NS_ARG(VideoParams, destination_params), - Q_ARG(bool, clear_destination)); -} - -} diff --git a/app/render/rendererthreadwrapper.h b/app/render/rendererthreadwrapper.h deleted file mode 100644 index 873e43832..000000000 --- a/app/render/rendererthreadwrapper.h +++ /dev/null @@ -1,86 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 RENDERCONTEXTTHREADWRAPPER_H -#define RENDERCONTEXTTHREADWRAPPER_H - -#include - -#include "renderer.h" - -namespace olive { - -class RendererThreadWrapper : public Renderer -{ -public: - RendererThreadWrapper(Renderer* inner, QObject* parent = nullptr); - - virtual ~RendererThreadWrapper() override - { - Destroy(); - PostDestroy(); - delete inner_; - } - - virtual bool Init() override; - - virtual void PostDestroy() override {} - -public slots: - virtual void PostInit() override; - - virtual void DestroyInternal() override; - - virtual void ClearDestination(olive::Texture *texture = nullptr, double r = 0.0, double g = 0.0, double b = 0.0, double a = 0.0) override; - - virtual QVariant CreateNativeTexture2D(int width, int height, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) override; - virtual QVariant CreateNativeTexture3D(int width, int height, int depth, olive::VideoParams::Format format, int channel_count, const void* data = nullptr, int linesize = 0) override; - - virtual void DestroyNativeTexture(QVariant texture) override; - - virtual QVariant CreateNativeShader(olive::ShaderCode code) override; - - virtual void DestroyNativeShader(QVariant shader) override; - - virtual void UploadToTexture(olive::Texture* texture, const void* data, int linesize) override; - - virtual void DownloadFromTexture(olive::Texture* texture, void* data, int linesize) override; - - virtual void Flush() override; - - virtual Color GetPixelFromTexture(olive::Texture *texture, const QPointF &pt) override; - -protected slots: - virtual void Blit(QVariant shader, - olive::ShaderJob job, - olive::Texture* destination, - olive::VideoParams destination_params, - bool clear_destination) override; - -private: - Renderer* inner_; - - QThread* thread_; - -}; - -} - -#endif // RENDERCONTEXTTHREADWRAPPER_H diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index c34b050d8..bf7dc449b 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -27,7 +27,6 @@ #include "config/config.h" #include "core.h" #include "render/opengl/openglrenderer.h" -#include "render/rendererthreadwrapper.h" #include "renderprocessor.h" #include "task/conform/conform.h" #include "task/taskmanager.h" @@ -36,49 +35,61 @@ namespace olive { RenderManager* RenderManager::instance_ = nullptr; +const rational RenderManager::kDryRunInterval = rational(10); RenderManager::RenderManager(QObject *parent) : - ThreadPool(0, parent), - backend_(kOpenGL) + backend_(kOpenGL), + aggressive_gc_(0) { - Renderer* graphics_renderer = nullptr; - if (backend_ == kOpenGL) { - graphics_renderer = new OpenGLRenderer(); - } - - if (graphics_renderer) { - context_ = new RendererThreadWrapper(graphics_renderer, this); - context_->Init(); - context_->PostInit(); - + context_ = new OpenGLRenderer(); decoder_cache_ = new DecoderCache(); shader_cache_ = new ShaderCache(); - default_shader_ = context_->CreateNativeShader(ShaderCode(QString(), QString())); } else { qCritical() << "Tried to initialize unknown graphics backend"; context_ = nullptr; decoder_cache_ = nullptr; } + + if (context_) { + video_thread_ = new RenderThread(context_, decoder_cache_, shader_cache_, this); + dry_run_thread_ = new RenderThread(nullptr, decoder_cache_, shader_cache_, this); + audio_thread_ = new RenderThread(nullptr, decoder_cache_, shader_cache_, this); + + video_thread_->start(QThread::IdlePriority); + dry_run_thread_->start(QThread::IdlePriority); + audio_thread_->start(QThread::IdlePriority); + } + + decoder_clear_timer_ = new QTimer(this); + decoder_clear_timer_->setInterval(kDecoderMaximumInactivity); + connect(decoder_clear_timer_, &QTimer::timeout, this, &RenderManager::ClearOldDecoders); + decoder_clear_timer_->start(); } RenderManager::~RenderManager() { if (context_) { - context_->DestroyNativeShader(default_shader_); - delete shader_cache_; delete decoder_cache_; - context_->Destroy(); + video_thread_->quit(); + video_thread_->wait(); + + dry_run_thread_->quit(); + dry_run_thread_->wait(); + context_->PostDestroy(); delete context_; + + audio_thread_->quit(); + audio_thread_->wait(); } } RenderTicketPtr RenderManager::RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m, ColorManager* color_manager, const rational& time, RenderMode::Mode mode, - FrameHashCache* cache, RenderTicketPriority priority, ReturnType return_type) + FrameHashCache* cache, ReturnType return_type) { return RenderFrame(node, color_manager, @@ -89,9 +100,9 @@ RenderTicketPtr RenderManager::RenderFrame(Node *node, const VideoParams &vparam QSize(0, 0), QMatrix4x4(), VideoParams::kFormatInvalid, + 0, nullptr, cache, - priority, return_type); } @@ -100,8 +111,9 @@ RenderTicketPtr RenderManager::RenderFrame(Node *node, ColorManager* color_manag const VideoParams &video_params, const AudioParams &audio_params, const QSize& force_size, const QMatrix4x4& force_matrix, VideoParams::Format force_format, + int force_channel_count, ColorProcessorPtr force_color_output, - FrameHashCache* cache, RenderTicketPriority priority, ReturnType return_type) + FrameHashCache* cache, ReturnType return_type) { // Create ticket RenderTicketPtr ticket = std::make_shared(); @@ -111,6 +123,7 @@ RenderTicketPtr RenderManager::RenderFrame(Node *node, ColorManager* color_manag ticket->setProperty("size", force_size); ticket->setProperty("matrix", force_matrix); ticket->setProperty("format", force_format); + ticket->setProperty("channelcount", force_channel_count); ticket->setProperty("mode", mode); ticket->setProperty("type", kTypeVideo); ticket->setProperty("colormanager", Node::PtrToValue(color_manager)); @@ -125,12 +138,16 @@ RenderTicketPtr RenderManager::RenderFrame(Node *node, ColorManager* color_manag ticket->setProperty("cacheuuid", QVariant::fromValue(cache->GetUuid())); } - AddTicket(ticket, priority); + if (return_type == ReturnType::kNull) { + dry_run_thread_->AddTicket(ticket); + } else { + video_thread_->AddTicket(ticket); + } return ticket; } -RenderTicketPtr RenderManager::RenderAudio(Node *node, const TimeRange &r, const AudioParams ¶ms, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority) +RenderTicketPtr RenderManager::RenderAudio(Node *node, const TimeRange &r, const AudioParams ¶ms, RenderMode::Mode mode, bool generate_waveforms) { // Create ticket RenderTicketPtr ticket = std::make_shared(); @@ -142,22 +159,133 @@ RenderTicketPtr RenderManager::RenderAudio(Node *node, const TimeRange &r, const ticket->setProperty("enablewaveforms", generate_waveforms); ticket->setProperty("aparam", QVariant::fromValue(params)); - AddTicket(ticket, priority); + audio_thread_->AddTicket(ticket); return ticket; } -void RenderManager::RunTicket(RenderTicketPtr ticket) const +bool RenderManager::RemoveTicket(RenderTicketPtr ticket) { - // Setup the ticket for ::Process - ticket->Start(); + if (video_thread_->RemoveTicket(ticket)) { + return true; + } else if (audio_thread_->RemoveTicket(ticket)) { + return true; + } else if (dry_run_thread_->RemoveTicket(ticket)) { + return true; + } else { + return false; + } +} - if (ticket->IsCancelled()) { - ticket->Finish(); - return; +void RenderManager::SetAggressiveGarbageCollection(bool enabled) +{ + aggressive_gc_ += enabled ? 1 : -1; + + if (aggressive_gc_ > 0) { + decoder_clear_timer_->setInterval(kDecoderMaximumInactivityAggressive); + } else { + decoder_clear_timer_->setInterval(kDecoderMaximumInactivity); + } +} + +void RenderManager::ClearOldDecoders() +{ + QMutexLocker locker(decoder_cache_->mutex()); + + qint64 min_age = QDateTime::currentMSecsSinceEpoch() - kDecoderMaximumInactivity; + + for (auto it=decoder_cache_->begin(); it!=decoder_cache_->end(); ) { + DecoderPair decoder = it.value(); + + if (decoder.decoder->GetLastAccessedTime() < min_age) { + decoder.decoder->Close(); + it = decoder_cache_->erase(it); + } else { + it++; + } + } +} + +RenderThread::RenderThread(Renderer *renderer, DecoderCache *decoder_cache, ShaderCache *shader_cache, QObject *parent) : + QThread(parent), + cancelled_(false), + context_(renderer), + decoder_cache_(decoder_cache), + shader_cache_(shader_cache) +{ + if (context_) { + context_->Init(); + context_->moveToThread(this); + } +} + +void RenderThread::AddTicket(RenderTicketPtr ticket) +{ + QMutexLocker locker(&mutex_); + queue_.push_back(ticket); + wait_.wakeOne(); +} + +bool RenderThread::RemoveTicket(RenderTicketPtr ticket) +{ + QMutexLocker locker(&mutex_); + + auto it = std::find(queue_.begin(), queue_.end(), ticket); + if (it == queue_.end()) { + return false; } - RenderProcessor::Process(ticket, context_, decoder_cache_, shader_cache_, default_shader_); + queue_.erase(it); + return true; +} + +void RenderThread::quit() +{ + QMutexLocker locker(&mutex_); + cancelled_ = true; + wait_.wakeOne(); +} + +void RenderThread::run() +{ + if (context_) { + context_->PostInit(); + } + + QMutexLocker locker(&mutex_); + + while (!cancelled_) { + if (queue_.empty()) { + wait_.wait(&mutex_); + } + + if (cancelled_) { + break; + } + + if (!queue_.empty()) { + RenderTicketPtr ticket = queue_.front(); + queue_.pop_front(); + + locker.unlock(); + + // Setup the ticket for ::Process + ticket->Start(); + + if (ticket->IsCancelled()) { + ticket->Finish(); + } else { + RenderProcessor::Process(ticket, context_, decoder_cache_, shader_cache_); + } + + locker.relock(); + } + } + + if (context_) { + context_->Destroy(); + context_->moveToThread(this->thread()); + } } } diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index 8753f7c90..6eed0a5bc 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -30,12 +30,44 @@ #include "node/output/viewer/viewer.h" #include "node/traverser.h" #include "render/renderer.h" +#include "render/renderticket.h" #include "rendercache.h" -#include "threading/threadpool.h" namespace olive { -class RenderManager : public ThreadPool +class RenderThread : public QThread +{ + Q_OBJECT +public: + RenderThread(Renderer *renderer, DecoderCache *decoder_cache, ShaderCache *shader_cache, QObject *parent = nullptr); + + void AddTicket(RenderTicketPtr ticket); + + bool RemoveTicket(RenderTicketPtr ticket); + + void quit(); + +protected: + virtual void run() override; + +private: + QMutex mutex_; + + QWaitCondition wait_; + + std::list queue_; + + bool cancelled_; + + Renderer *context_; + + DecoderCache *decoder_cache_; + + ShaderCache *shader_cache_; + +}; + +class RenderManager : public QObject { Q_OBJECT public: @@ -65,9 +97,12 @@ public: enum ReturnType { kTexture, - kFrame + kFrame, + kNull }; + static const rational kDryRunInterval; + /** * @brief Asynchronously generate a frame at a given time * @@ -78,14 +113,15 @@ public: */ RenderTicketPtr RenderFrame(Node *node, const VideoParams &vparam, const AudioParams ¶m, ColorManager* color_manager, const rational& time, RenderMode::Mode mode, - FrameHashCache* cache = nullptr, RenderTicketPriority priority = RenderTicketPriority::kNormal, ReturnType return_type = kFrame); + FrameHashCache* cache = nullptr, ReturnType return_type = kFrame); RenderTicketPtr RenderFrame(Node *node, ColorManager* color_manager, const rational& time, RenderMode::Mode mode, const VideoParams& video_params, const AudioParams& audio_params, const QSize& force_size, const QMatrix4x4& force_matrix, VideoParams::Format force_format, + int force_channel_count, ColorProcessorPtr force_color_output, - FrameHashCache* cache = nullptr, RenderTicketPriority priority = RenderTicketPriority::kNormal, ReturnType return_type = kFrame); + FrameHashCache* cache = nullptr, ReturnType return_type = kFrame); /** * @brief Asynchronously generate a chunk of audio @@ -94,9 +130,9 @@ public: * * This function is thread-safe. */ - RenderTicketPtr RenderAudio(Node *viewer, const TimeRange& r, const AudioParams& params, RenderMode::Mode mode, bool generate_waveforms, RenderTicketPriority priority = RenderTicketPriority::kNormal); + RenderTicketPtr RenderAudio(Node *viewer, const TimeRange& r, const AudioParams& params, RenderMode::Mode mode, bool generate_waveforms); - virtual void RunTicket(RenderTicketPtr ticket) const override; + bool RemoveTicket(RenderTicketPtr ticket); enum TicketType { kTypeVideo, @@ -108,10 +144,8 @@ public: return backend_; } - static int GetNumberOfIdealConcurrentJobs() - { - return QThread::idealThreadCount(); - } +public slots: + void SetAggressiveGarbageCollection(bool enabled); signals: @@ -130,7 +164,19 @@ private: ShaderCache* shader_cache_; - QVariant default_shader_; + static constexpr auto kDecoderMaximumInactivityAggressive = 1000; + static constexpr auto kDecoderMaximumInactivity = 5000; + + int aggressive_gc_; + + QTimer *decoder_clear_timer_; + + RenderThread *video_thread_; + RenderThread *dry_run_thread_; + RenderThread *audio_thread_; + +private slots: + void ClearOldDecoders(); }; diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 37e11a66a..94cea35f8 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -35,12 +35,11 @@ namespace olive { #define super NodeTraverser -RenderProcessor::RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx, DecoderCache* decoder_cache, ShaderCache *shader_cache, QVariant default_shader) : +RenderProcessor::RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx, DecoderCache* decoder_cache, ShaderCache *shader_cache) : ticket_(ticket), render_ctx_(render_ctx), decoder_cache_(decoder_cache), - shader_cache_(shader_cache), - default_shader_(default_shader) + shader_cache_(shader_cache) { } @@ -76,7 +75,12 @@ FramePtr RenderProcessor::GenerateFrame(TexturePtr texture, const rational& time frame_params.set_format(frame_format); } - frame_params.set_channel_count(texture ? texture->channel_count() : VideoParams::kRGBChannelCount); + int force_channel_count = ticket_->property("channelcount").toInt(); + if (force_channel_count != 0) { + frame_params.set_channel_count(force_channel_count); + } else { + frame_params.set_channel_count(texture ? texture->channel_count() : VideoParams::kRGBAChannelCount); + } FramePtr frame = Frame::Create(); frame->set_timestamp(time); @@ -115,14 +119,16 @@ FramePtr RenderProcessor::GenerateFrame(TexturePtr texture, const rational& time job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture))); job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, matrix)); - render_ctx_->BlitToTexture(default_shader_, job, blit_tex.get()); + render_ctx_->BlitToTexture(render_ctx_->GetDefaultShader(), job, blit_tex.get()); } // Replace texture that we're going to download in the next step texture = blit_tex; } - render_ctx_->DownloadFromTexture(texture.get(), frame->data(), frame->linesize_pixels()); + render_ctx_->Flush(); + + render_ctx_->DownloadFromTexture(texture->id(), texture->params(), frame->data(), frame->linesize_pixels()); } return frame; @@ -133,7 +139,7 @@ void RenderProcessor::Run() // Depending on the render ticket type, start a job RenderManager::TicketType type = ticket_->property("type").value(); - SetCancelPointer(&ticket_->IsCancelled()); + SetCancelPointer(ticket_->GetCancelAtom()); SetCacheVideoParams(ticket_->property("vparam").value()); SetCacheAudioParams(ticket_->property("aparam").value()); @@ -150,53 +156,57 @@ void RenderProcessor::Run() TexturePtr texture = GenerateTexture(time, frame_length); - if (GetCacheVideoParams().interlacing() != VideoParams::kInterlaceNone) { - // Get next between frame and interlace it - TexturePtr top = texture; - TexturePtr bottom = GenerateTexture(time + frame_length, frame_length); - - if (GetCacheVideoParams().interlacing() == VideoParams::kInterlacedBottomFirst) { - std::swap(top, bottom); - } - - texture = render_ctx_->InterlaceTexture(top, bottom, GetCacheVideoParams()); - } - - if (HeardCancel()) { - // Finish cancelled ticket with nothing since we can't guarantee the frame we generated - // is actually "complete + if (!render_ctx_) { ticket_->Finish(); } else { - RenderManager::ReturnType return_type = RenderManager::ReturnType(ticket_->property("return").toInt()); + if (GetCacheVideoParams().interlacing() != VideoParams::kInterlaceNone) { + // Get next between frame and interlace it + TexturePtr top = texture; + TexturePtr bottom = GenerateTexture(time + frame_length, frame_length); - FramePtr frame; - QString cache = ticket_->property("cache").toString(); - - if (return_type == RenderManager::kFrame || !cache.isEmpty()) { - // Convert to CPU frame - frame = GenerateFrame(texture, time); - - // Save to cache if requested - if (!cache.isEmpty()) { - rational timebase = ticket_->property("cachetimebase").value(); - QUuid uuid = ticket_->property("cacheuuid").value(); - bool cache_result = FrameHashCache::SaveCacheFrame(cache, uuid, time, timebase, frame); - ticket_->setProperty("cached", cache_result); + if (GetCacheVideoParams().interlacing() == VideoParams::kInterlacedBottomFirst) { + std::swap(top, bottom); } + + texture = render_ctx_->InterlaceTexture(top, bottom, GetCacheVideoParams()); } - if (return_type == RenderManager::kTexture) { - // Return GPU texture - if (!texture) { - texture = render_ctx_->CreateTexture(GetCacheVideoParams()); - render_ctx_->ClearDestination(texture.get()); + if (HeardCancel()) { + // Finish cancelled ticket with nothing since we can't guarantee the frame we generated + // is actually "complete + ticket_->Finish(); + } else { + RenderManager::ReturnType return_type = RenderManager::ReturnType(ticket_->property("return").toInt()); + + FramePtr frame; + QString cache = ticket_->property("cache").toString(); + + if (return_type == RenderManager::kFrame || !cache.isEmpty()) { + // Convert to CPU frame + frame = GenerateFrame(texture, time); + + // Save to cache if requested + if (!cache.isEmpty()) { + rational timebase = ticket_->property("cachetimebase").value(); + QUuid uuid = ticket_->property("cacheuuid").value(); + bool cache_result = FrameHashCache::SaveCacheFrame(cache, uuid, time, timebase, frame); + ticket_->setProperty("cached", cache_result); + } } - render_ctx_->Flush(); + if (return_type == RenderManager::kTexture) { + // Return GPU texture + if (!texture) { + texture = render_ctx_->CreateTexture(GetCacheVideoParams()); + render_ctx_->ClearDestination(texture.get()); + } - ticket_->Finish(QVariant::fromValue(texture)); - } else { - ticket_->Finish(QVariant::fromValue(frame)); + render_ctx_->Flush(); + + ticket_->Finish(QVariant::fromValue(texture)); + } else { + ticket_->Finish(QVariant::fromValue(frame)); + } } } break; @@ -256,22 +266,27 @@ DecoderPtr RenderProcessor::ResolveDecoderFromInput(const QString& decoder_id, c // No decoder decoder.decoder = Decoder::CreateFromID(decoder_id); decoder.last_modified = file_last_modified; + decoder_cache_->insert(stream, decoder); + locker.unlock(); - if (decoder.decoder->Open(stream)) { - decoder_cache_->insert(stream, decoder); - } else { + if (!decoder.decoder->Open(stream)) { qWarning() << "Failed to open decoder for" << stream.filename() << "::" << stream.stream(); return nullptr; } + + if (!render_ctx_) { + // Assume dry run and increment access time + decoder.decoder->IncrementAccessTime(RenderManager::kDryRunInterval.toDouble() * 1000); + } } return decoder.decoder; } -void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, DecoderCache *decoder_cache, ShaderCache *shader_cache, QVariant default_shader) +void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, DecoderCache *decoder_cache, ShaderCache *shader_cache) { - RenderProcessor p(ticket, render_ctx, decoder_cache, shader_cache, default_shader); + RenderProcessor p(ticket, render_ctx, decoder_cache, shader_cache); p.Run(); } @@ -422,7 +437,7 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination, const FootageJ qWarning() << "HAVEN'T GOTTEN DEFAULT INPUT COLORSPACE"; } - Decoder::CodecStream default_codec_stream(stream.filename(), stream_data.stream_index()); + Decoder::CodecStream default_codec_stream(stream.filename(), stream_data.stream_index(), GetCurrentBlock()); QString decoder_id = stream.decoder(); @@ -435,21 +450,23 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination, const FootageJ break; case VideoParams::kVideoTypeImageSequence: { - // Since image sequences involve multiple files, we don't engage the decoder cache - decoder = Decoder::CreateFromID(decoder_id); + if (render_ctx_) { + // Since image sequences involve multiple files, we don't engage the decoder cache + decoder = Decoder::CreateFromID(decoder_id); - QString frame_filename; + QString frame_filename; - int64_t frame_number = stream_data.get_time_in_timebase_units(input_time); - frame_filename = Decoder::TransformImageSequenceFileName(stream.filename(), frame_number); + int64_t frame_number = stream_data.get_time_in_timebase_units(input_time); + frame_filename = Decoder::TransformImageSequenceFileName(stream.filename(), frame_number); - // Decoder will close automatically since it's a stream_ptr - decoder->Open(Decoder::CodecStream(frame_filename, stream_data.stream_index())); + // Decoder will close automatically since it's a stream_ptr + decoder->Open(Decoder::CodecStream(frame_filename, stream_data.stream_index(), GetCurrentBlock())); + } break; } } - if (decoder) { + if (decoder && render_ctx_) { Decoder::RetrieveVideoParams p; p.divider = stream.video_params().divider(); p.maximum_format = destination->format(); @@ -458,7 +475,15 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination, const FootageJ VideoParams tex_params = stream.video_params(); if (tex_params.is_valid()) { - TexturePtr unmanaged_texture = decoder->RetrieveVideo(render_ctx_, (stream_data.video_type() == VideoParams::kVideoTypeVideo) ? input_time : Decoder::kAnyTimecode, p, GetCancelPointer()); + TexturePtr unmanaged_texture; + + p.renderer = render_ctx_; + p.time = (stream_data.video_type() == VideoParams::kVideoTypeVideo) ? input_time : Decoder::kAnyTimecode; + p.cancelled = GetCancelPointer(); + p.force_range = stream_data.color_range(); + p.src_interlacing = stream_data.interlacing(); + + unmanaged_texture = decoder->RetrieveVideo(p); if (unmanaged_texture) { // We convert to our rendering pixel format, since that will always be float-based which @@ -490,7 +515,7 @@ void RenderProcessor::ProcessVideoFootage(TexturePtr destination, const FootageJ void RenderProcessor::ProcessAudioFootage(SampleBuffer &destination, const FootageJob &stream, const TimeRange &input_time) { - DecoderPtr decoder = ResolveDecoderFromInput(stream.decoder(), Decoder::CodecStream(stream.filename(), stream.audio_params().stream_index())); + DecoderPtr decoder = ResolveDecoderFromInput(stream.decoder(), Decoder::CodecStream(stream.filename(), stream.audio_params().stream_index(), nullptr)); if (decoder) { const AudioParams& audio_params = GetCacheAudioParams(); @@ -498,7 +523,7 @@ void RenderProcessor::ProcessAudioFootage(SampleBuffer &destination, const Foota Decoder::RetrieveAudioStatus status = decoder->RetrieveAudio(destination, input_time, audio_params, stream.cache_path(), - stream.loop_mode(), + loop_mode(), static_cast(ticket_->property("mode").toInt())); if (status == Decoder::kWaitingForConform) { @@ -509,7 +534,9 @@ void RenderProcessor::ProcessAudioFootage(SampleBuffer &destination, const Foota void RenderProcessor::ProcessShader(TexturePtr destination, const Node *node, const TimeRange &range, const ShaderJob &job) { - Q_UNUSED(range) + if (!render_ctx_) { + return; + } QString full_shader_id = QStringLiteral("%1:%2").arg(node->id(), job.GetShaderID()); @@ -563,11 +590,19 @@ void RenderProcessor::ProcessSamples(SampleBuffer &destination, const Node *node void RenderProcessor::ProcessColorTransform(TexturePtr destination, const Node *node, const ColorTransformJob &job) { + if (!render_ctx_) { + return; + } + render_ctx_->BlitColorManaged(job, destination.get()); } void RenderProcessor::ProcessFrameGeneration(TexturePtr destination, const Node *node, const GenerateJob &job) { + if (!render_ctx_) { + return; + } + FramePtr frame = Frame::Create(); frame->set_video_params(destination->params()); @@ -583,8 +618,21 @@ bool RenderProcessor::CanCacheFrames() return ticket_->property("type").value() == RenderManager::kTypeVideo; } +TexturePtr RenderProcessor::CreateTexture(const VideoParams &p) +{ + if (render_ctx_) { + return render_ctx_->CreateTexture(p); + } else { + return super::CreateTexture(p); + } +} + void RenderProcessor::ConvertToReferenceSpace(TexturePtr destination, TexturePtr source, const QString &input_cs) { + if (!render_ctx_) { + return; + } + ColorManager* color_manager = Node::ValueToPtr(ticket_->property("colormanager")); ColorProcessorPtr cp = ColorProcessor::Create(color_manager, input_cs, color_manager->GetReferenceColorSpace()); diff --git a/app/render/renderprocessor.h b/app/render/renderprocessor.h index 1fb6ea23f..9d7942c7a 100644 --- a/app/render/renderprocessor.h +++ b/app/render/renderprocessor.h @@ -25,14 +25,14 @@ #include "node/traverser.h" #include "render/renderer.h" #include "rendercache.h" -#include "threading/threadticket.h" +#include "renderticket.h" namespace olive { class RenderProcessor : public NodeTraverser { public: - static void Process(RenderTicketPtr ticket, Renderer* render_ctx, DecoderCache* decoder_cache, ShaderCache* shader_cache, QVariant default_shader); + static void Process(RenderTicketPtr ticket, Renderer* render_ctx, DecoderCache* decoder_cache, ShaderCache* shader_cache); struct RenderedWaveform { const ClipBlock* block; @@ -58,10 +58,7 @@ protected: virtual bool CanCacheFrames() override; - virtual TexturePtr CreateTexture(const VideoParams &p) override - { - return render_ctx_->CreateTexture(p); - } + virtual TexturePtr CreateTexture(const VideoParams &p) override; virtual SampleBuffer CreateSampleBuffer(const AudioParams ¶ms, int sample_count) override { @@ -71,7 +68,7 @@ protected: virtual void ConvertToReferenceSpace(TexturePtr destination, TexturePtr source, const QString &input_cs) override; private: - RenderProcessor(RenderTicketPtr ticket, Renderer* render_ctx, DecoderCache* decoder_cache, ShaderCache* shader_cache, QVariant default_shader); + RenderProcessor(RenderTicketPtr ticket, Renderer* render_ctx, DecoderCache* decoder_cache, ShaderCache* shader_cache); TexturePtr GenerateTexture(const rational& time, const rational& frame_length); @@ -89,8 +86,6 @@ private: ShaderCache* shader_cache_; - QVariant default_shader_; - }; } diff --git a/app/threading/threadticket.cpp b/app/render/renderticket.cpp similarity index 61% rename from app/threading/threadticket.cpp rename to app/render/renderticket.cpp index 8f9715424..beba8b808 100644 --- a/app/threading/threadticket.cpp +++ b/app/render/renderticket.cpp @@ -18,7 +18,7 @@ ***/ -#include "threadticket.h" +#include "renderticket.h" namespace olive { @@ -128,4 +128,82 @@ void RenderTicket::FinishInternal(bool has_result, QVariant result) } } +RenderTicketWatcher::RenderTicketWatcher(QObject *parent) : + QObject(parent), + ticket_(nullptr) +{ +} + +void RenderTicketWatcher::SetTicket(RenderTicketPtr ticket) +{ + if (ticket_) { + qCritical() << "Tried to set a ticket on a RenderTicketWatcher twice"; + return; + } + + if (!ticket) { + qCritical() << "Tried to set a null ticket on a RenderTicketWatcher"; + return; + } + + ticket_ = ticket; + + // Lock ticket so we can query if it's already finished by the time this code runs + QMutexLocker locker(ticket->lock()); + + connect(ticket_.get(), &RenderTicket::Finished, this, &RenderTicketWatcher::TicketFinished); + + if (!ticket_->IsRunning(false) && ticket_->GetFinishCount(false) > 0) { + // Ticket has already finished before, so we emit a signal + locker.unlock(); + TicketFinished(); + } +} + +bool RenderTicketWatcher::IsRunning() +{ + if (ticket_) { + return ticket_->IsRunning(); + } else { + return false; + } +} + +void RenderTicketWatcher::WaitForFinished() +{ + if (ticket_) { + ticket_->WaitForFinished(); + } +} + +QVariant RenderTicketWatcher::Get() +{ + if (ticket_) { + return ticket_->Get(); + } else { + return QVariant(); + } +} + +bool RenderTicketWatcher::HasResult() +{ + if (ticket_) { + return ticket_->HasResult(); + } else { + return false; + } +} + +void RenderTicketWatcher::Cancel() +{ + if (ticket_) { + ticket_->Cancel(); + } +} + +void RenderTicketWatcher::TicketFinished() +{ + emit Finished(this); +} + } diff --git a/app/threading/threadticket.h b/app/render/renderticket.h similarity index 87% rename from app/threading/threadticket.h rename to app/render/renderticket.h index fb42ab969..700053653 100644 --- a/app/threading/threadticket.h +++ b/app/render/renderticket.h @@ -132,6 +132,40 @@ private: using RenderTicketPtr = std::shared_ptr; +class RenderTicketWatcher : public QObject +{ + Q_OBJECT +public: + RenderTicketWatcher(QObject* parent = nullptr); + + RenderTicketPtr GetTicket() const + { + return ticket_; + } + + void SetTicket(RenderTicketPtr ticket); + + bool IsRunning(); + + void WaitForFinished(); + + QVariant Get(); + + bool HasResult(); + + void Cancel(); + +signals: + void Finished(RenderTicketWatcher* watcher); + +private: + RenderTicketPtr ticket_; + +private slots: + void TicketFinished(); + +}; + } Q_DECLARE_METATYPE(olive::RenderTicketPtr) diff --git a/app/render/texture.cpp b/app/render/texture.cpp index 33973ddaf..2c5c9fdea 100644 --- a/app/render/texture.cpp +++ b/app/render/texture.cpp @@ -29,21 +29,21 @@ const Texture::Interpolation Texture::kDefaultInterpolation = Texture::kMipmappe Texture::~Texture() { if (renderer_) { - renderer_->DestroyNativeTexture(id_); + renderer_->DestroyTexture(this); } } void Texture::Upload(void *data, int linesize) { if (renderer_) { - renderer_->UploadToTexture(this, data, linesize); + renderer_->UploadToTexture(this->id(), this->params(), data, linesize); } } void Texture::Download(void *data, int linesize) { if (renderer_) { - renderer_->DownloadFromTexture(this, data, linesize); + renderer_->DownloadFromTexture(this->id(), this->params(), data, linesize); } } diff --git a/app/render/texture.h b/app/render/texture.h index 612b3e2f2..6179878a8 100644 --- a/app/render/texture.h +++ b/app/render/texture.h @@ -32,11 +32,6 @@ class Renderer; class Texture { public: - enum Type { - k2D, - k3D - }; - enum Interpolation { kNearest, kLinear, @@ -50,19 +45,17 @@ public: */ Texture(const VideoParams& param) : renderer_(nullptr), - params_(param), - type_(k2D) + params_(param) { } /** * @brief Construct a real texture linked to a renderer backend */ - Texture(Renderer* renderer, const QVariant& native, const VideoParams& param, Type type) : + Texture(Renderer* renderer, const QVariant& native, const VideoParams& param) : renderer_(renderer), params_(param), - id_(native), - type_(type) + id_(native) { } @@ -117,11 +110,6 @@ public: return params_.pixel_aspect_ratio(); } - Type type() const - { - return type_; - } - Renderer* renderer() const { return renderer_; @@ -134,8 +122,6 @@ private: QVariant id_; - Type type_; - }; using TexturePtr = std::shared_ptr; diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index c40ff3684..14474d49c 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -126,8 +126,7 @@ VideoParams::VideoParams(int width, int height, const rational &time_base, Forma int VideoParams::generate_auto_divider(qint64 width, qint64 height) { - // Arbitrary pixel count (from 640x360) - const int target_res = 230400; + const int target_res = 1920*1080; qint64 megapixels = width * height; @@ -155,8 +154,8 @@ int VideoParams::generate_auto_divider(qint64 width, qint64 height) } } - // "Safe" fallback - return 2; + // Fallback + return 1; } } @@ -200,6 +199,15 @@ int VideoParams::GetBytesPerPixel(VideoParams::Format format, int channels) return GetBytesPerChannel(format) * channels; } +QString VideoParams::GetNameForDivider(int div) +{ + if (div == 1) { + return QCoreApplication::translate("VideoParams", "Full"); + } else { + return QCoreApplication::translate("VideoParams", "1/%1").arg(div); + } +} + bool VideoParams::FormatIsFloat(VideoParams::Format format) { switch (format) { @@ -261,12 +269,13 @@ void VideoParams::set_defaults_for_footage() premultiplied_alpha_ = false; x_ = 0; y_ = 0; + color_range_ = kColorRangeDefault; } void VideoParams::calculate_square_pixel_width() { if (pixel_aspect_ratio_.denominator() != 0) { - par_width_ = width_ * pixel_aspect_ratio_.numerator() / pixel_aspect_ratio_.denominator(); + par_width_ = qRound(width_ * pixel_aspect_ratio_.toDouble()); } else { par_width_ = width_; } @@ -365,6 +374,8 @@ void VideoParams::Load(QXmlStreamReader *reader) set_premultiplied_alpha(reader->readElementText().toInt()); } else if (reader->name() == QStringLiteral("colorspace")) { set_colorspace(reader->readElementText()); + } else if (reader->name() == QStringLiteral("colorrange")) { + set_color_range(static_cast(reader->readElementText().toInt())); } else { reader->skipCurrentElement(); } @@ -392,6 +403,7 @@ void VideoParams::Save(QXmlStreamWriter *writer) const writer->writeTextElement(QStringLiteral("duration"), QString::number(duration_)); writer->writeTextElement(QStringLiteral("premultipliedalpha"), QString::number(premultiplied_alpha_)); writer->writeTextElement(QStringLiteral("colorspace"), colorspace_); + writer->writeTextElement(QStringLiteral("colorrange"), QString::number(color_range_)); } } diff --git a/app/render/videoparams.h b/app/render/videoparams.h index 564985dc4..e65ad7a93 100644 --- a/app/render/videoparams.h +++ b/app/render/videoparams.h @@ -66,6 +66,14 @@ public: kVideoTypeStill, kVideoTypeImageSequence }; + enum ColorRange + { + kColorRangeLimited, // 16_235 + kColorRangeFull, // 0-255 + + kColorRangeDefault = kColorRangeLimited + }; + VideoParams(); VideoParams(int width, int height, Format format, int nb_channels, @@ -121,6 +129,8 @@ public: calculate_effective_size(); } + bool is_3d() const { return depth_ > 1; } + const rational& time_base() const { return time_base_; @@ -231,6 +241,8 @@ public: return GetBufferSize(width_, height_, format_, channel_count_); } + static QString GetNameForDivider(int div); + static bool FormatIsFloat(Format format); static QString GetFormatName(Format format); @@ -348,6 +360,9 @@ public: colorspace_ = c; } + const ColorRange &color_range() const { return color_range_; } + void set_color_range(const ColorRange &color_range) { color_range_ = color_range; } + int64_t get_time_in_timebase_units(const rational& time) const; void Load(QXmlStreamReader* reader); @@ -394,6 +409,7 @@ private: QString colorspace_; float x_; float y_; + ColorRange color_range_; }; diff --git a/app/shaders/rgb.frag b/app/shaders/rgb.frag new file mode 100644 index 000000000..84112d2a4 --- /dev/null +++ b/app/shaders/rgb.frag @@ -0,0 +1,15 @@ +// Input texture +uniform sampler2D texture_in; + +// Input texture coordinate +in vec2 ove_texcoord; +out vec4 frag_color; + +// Input color +uniform vec4 color_in; + +void main() { + vec4 color = texture(texture_in, ove_texcoord); + color.rgb = color_in.rgb * color.a; + frag_color = color; +} diff --git a/app/shaders/shape.frag b/app/shaders/shape.frag index f06b2ce7b..51b2c7270 100644 --- a/app/shaders/shape.frag +++ b/app/shaders/shape.frag @@ -3,14 +3,35 @@ in vec2 ove_texcoord; out vec4 frag_color; // Match with ShapeNode::Type -#define SHAPE_RECTANGLE 0 -#define SHAPE_ELLIPSE 1 +const int SHAPE_RECTANGLE = 0; +const int SHAPE_ELLIPSE = 1; +const int SHAPE_ROUNDEDRECT = 2; uniform vec2 pos_in; uniform vec2 size_in; uniform int type_in; uniform vec2 resolution_in; uniform vec4 color_in; +uniform float radius_in; + +vec4 draw_rect(vec2 real_position, vec2 real_size) +{ + if (ove_texcoord.x >= real_position.x && ove_texcoord.y >= real_position.y + && ove_texcoord.x < real_position.x+real_size.x && ove_texcoord.y < real_position.y+real_size.y) { + return color_in; + } else { + return vec4(0.0, 0.0, 0.0, 0.0); + } +} + +vec4 draw_ellipse(vec2 center, float radius, float aspect_ratio) { + vec2 offset = ove_texcoord*resolution_in - center; + offset.x /= aspect_ratio; + float d = length(offset)-radius; + float t = clamp(d, 0.0, 1.0); + + return color_in * (1.0-t); +} void main() { vec2 p = pos_in + resolution_in*0.5 - size_in*0.5; @@ -20,22 +41,42 @@ void main() { vec4 col = vec4(0.0); - if (type_in == SHAPE_RECTANGLE) { - if (ove_texcoord.x >= real_position.x && ove_texcoord.y >= real_position.y - && ove_texcoord.x < real_position.x+real_size.x && ove_texcoord.y < real_position.y+real_size.y) { - col = color_in; - } - } else if (type_in == SHAPE_ELLIPSE) { + switch (type_in) { + case SHAPE_RECTANGLE: + { + col = draw_rect(real_position, real_size); + break; + } + case SHAPE_ELLIPSE: + { vec2 center = p+size_in*0.5; float radius = size_in.y*0.5; float aspect_ratio = size_in.x/size_in.y; - - vec2 offset = ove_texcoord*resolution_in - center; - offset.x /= aspect_ratio; - float d = length(offset)-radius; - float t = clamp(d, 0.0, 1.0); - - col = color_in * (1.0-t); + col = draw_ellipse(center, radius, aspect_ratio); + break; + } + case SHAPE_ROUNDEDRECT: + { + // Limit radius so it is never larger than half the shortest size + float r = min(radius_in, min(size_in.y*0.5, size_in.x*0.5)); + vec2 real_rad = vec2(r / resolution_in.x, r / resolution_in.y); + if (ove_texcoord.x < real_position.x + real_rad.x && ove_texcoord.y < real_position.y + real_rad.y) { + // Top-left + col = draw_ellipse(p + r, r, 1.0); + } else if (ove_texcoord.x > real_position.x+real_size.x - real_rad.x && ove_texcoord.y < real_position.y + real_rad.y) { + // Top-right + col = draw_ellipse(vec2(p.x + size_in.x - r, p.y + r), r, 1.0); + } else if (ove_texcoord.x < real_position.x + real_rad.x && ove_texcoord.y > real_position.y + real_size.y - real_rad.y) { + // Bottom-left + col = draw_ellipse(vec2(p.x + r, p.y + size_in.y - r), r, 1.0); + } else if (ove_texcoord.x > real_position.x+real_size.x - real_rad.x && ove_texcoord.y > real_position.y + real_size.y - real_rad.y) { + // Bottom-right + col = draw_ellipse(vec2(p.x + size_in.x - r, p.y + size_in.y - r), r, 1.0); + } else { + col = draw_rect(real_position, real_size); + } + break; + } } frag_color = col; diff --git a/app/shaders/yuv2rgb.frag b/app/shaders/yuv2rgb.frag index 62c73e149..8c40b5918 100644 --- a/app/shaders/yuv2rgb.frag +++ b/app/shaders/yuv2rgb.frag @@ -3,32 +3,71 @@ uniform sampler2D u_channel; uniform sampler2D v_channel; uniform int bits_per_pixel; +uniform bool full_range; + +uniform int yuv_crv; +uniform int yuv_cgu; +uniform int yuv_cgv; +uniform int yuv_cbu; + +uniform int interlacing; +uniform int pixel_height; in vec2 ove_texcoord; out vec4 frag_color; -void main() { - vec4 rgba; +void main() +{ + vec2 real_coord = ove_texcoord; + if (interlacing != 0) { + float field_height = float(pixel_height / 2); + real_coord.y = floor(real_coord.y * field_height) + 0.25; + if (interlacing == 2) { + real_coord.y += 0.5; + } + real_coord.y /= field_height; + } + // Sample YUV planes vec3 yuv; + yuv.r = texture(y_channel, real_coord).r; + yuv.g = texture(u_channel, real_coord).r; + yuv.b = texture(v_channel, real_coord).r; - yuv.r = texture(y_channel, ove_texcoord).r; - yuv.g = texture(u_channel, ove_texcoord).r; - yuv.b = texture(v_channel, ove_texcoord).r; - + // Pixels will have come in aligned to 16-bit regardless of their actual bit depth, so they must + // be scaled as if they were actually 16-bit if (bits_per_pixel == 10) { yuv *= 64.0; } else if (bits_per_pixel == 12) { yuv *= 16.0; } - yuv.r = 1.1643 * (yuv.r - 0.0625); + // Convert YUV limited range from 16-235 to 0-255 + yuv.r -= 0.0625; // 16/256 + yuv.r *= 1.1643; // 255/219 + + // Convert 0.0-1.0 to -0.5-0.5 yuv.g = yuv.g - 0.5; yuv.b = yuv.b - 0.5; - rgba.r = yuv.r + 1.5958 * yuv.b; - rgba.g = yuv.r - 0.39173 * yuv.g - 0.81290 * yuv.b; - rgba.b = yuv.r + 2.017 * yuv.g; + // Use coefficients to weigh YUV into RGB + float crv = float(yuv_crv) / 65536.0; + float cgu = float(yuv_cgu) / 65536.0; + float cgv = float(yuv_cgv) / 65536.0; + float cbu = float(yuv_cbu) / 65536.0; + + vec4 rgba; + rgba.r = yuv.r + crv * yuv.b; + rgba.g = yuv.r - cgu * yuv.g - cgv * yuv.b; + rgba.b = yuv.r + cbu * yuv.g; + + // If the expected value is full range, transform to full range here + if (full_range) { + rgba.rgb /= 1.1643; + rgba.rgb += 0.0625; + } + + // Currently this shader is only used for RGB textures, so just set alpha to 1 rgba.a = 1.0; frag_color = rgba; diff --git a/app/task/conform/conform.cpp b/app/task/conform/conform.cpp index a0cd5fe72..4c73ef770 100644 --- a/app/task/conform/conform.cpp +++ b/app/task/conform/conform.cpp @@ -42,7 +42,7 @@ bool ConformTask::Run() connect(decoder.get(), &Decoder::IndexProgress, this, &ConformTask::ProgressChanged); - bool ret = decoder->ConformAudio(output_filenames_, params_, &IsCancelled()); + bool ret = decoder->ConformAudio(output_filenames_, params_, GetCancelAtom()); decoder->Close(); diff --git a/app/task/export/CMakeLists.txt b/app/task/export/CMakeLists.txt index 5c1bb1f41..7a7fad1bc 100644 --- a/app/task/export/CMakeLists.txt +++ b/app/task/export/CMakeLists.txt @@ -18,7 +18,5 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} task/export/export.h task/export/export.cpp - task/export/exportparams.h - task/export/exportparams.cpp PARENT_SCOPE ) diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index e471d6862..017fc59fb 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -27,7 +27,7 @@ namespace olive { ExportTask::ExportTask(ViewerOutput *viewer_node, ColorManager* color_manager, - const ExportParams& params) : + const EncodingParams& params) : color_manager_(color_manager), params_(params) { @@ -58,7 +58,14 @@ bool ExportTask::Run() params_.SetFilename(FileFunctions::GetSafeTemporaryFilename(real_filename)); } - encoder_ = Encoder::CreateFromID(params_.encoder(), params_); + // If we're exporting to a sidecar subtitle file, disable the subtitles in the main encoder + bool subtitles_enabled = params_.subtitles_enabled(); + EncodingParams sidecar_params = params_; + if (subtitles_enabled && params_.subtitles_are_sidecar()) { + params_.DisableSubtitles(); + } + + encoder_ = std::shared_ptr(Encoder::CreateFromParams(params_)); if (!encoder_) { SetError(tr("Failed to create encoder")); @@ -67,10 +74,38 @@ bool ExportTask::Run() if (!encoder_->Open()) { SetError(tr("Failed to open file: %1").arg(encoder_->GetError())); - encoder_->deleteLater(); return false; } + if (subtitles_enabled && params_.subtitles_are_sidecar()) { + // Construct sidecar params + sidecar_params.DisableVideo(); + sidecar_params.DisableAudio(); + + QString sidecar_filename; + { + QFileInfo fi(real_filename); + sidecar_filename = fi.completeBaseName(); + sidecar_filename.append('.'); + sidecar_filename.append(ExportFormat::GetExtension(sidecar_params.subtitle_sidecar_fmt())); + sidecar_filename = fi.dir().filePath(sidecar_filename); + } + sidecar_params.SetFilename(sidecar_filename); + + subtitle_encoder_ = std::shared_ptr(Encoder::CreateFromFormat(sidecar_params.subtitle_sidecar_fmt(), sidecar_params)); + if (!subtitle_encoder_) { + SetError(tr("Failed to create subtitle encoder")); + return false; + } + + if (!subtitle_encoder_->Open()) { + SetError(tr("Failed to open subtitle sidecar file: %1").arg(sidecar_filename)); + return false; + } + } else { + subtitle_encoder_ = encoder_; + } + if (params_.has_custom_range()) { // Render custom range only range = params_.custom_range(); @@ -91,12 +126,12 @@ bool ExportTask::Run() || video_params().height() != params_.video_params().height()) { video_force_size = QSize(params_.video_params().width(), params_.video_params().height()); - if (params_.video_scaling_method() != ExportParams::kStretch) { - video_force_matrix = ExportParams::GenerateMatrix(params_.video_scaling_method(), - video_params().width(), - video_params().height(), - params_.video_params().width(), - params_.video_params().height()); + if (params_.video_scaling_method() != EncodingParams::kStretch) { + video_force_matrix = EncodingParams::GenerateMatrix(params_.video_scaling_method(), + video_params().width(), + video_params().height(), + params_.video_params().width(), + params_.video_params().height()); } } else { // Disables forcing size in the renderer @@ -121,24 +156,29 @@ bool ExportTask::Run() audio_range = {range}; } - if (params_.subtitles_enabled()) { + if (subtitles_enabled) { subtitle_range = range; } Render(color_manager_, video_range, audio_range, subtitle_range, RenderMode::kOnline, nullptr, video_force_size, video_force_matrix, encoder_->GetDesiredPixelFormat(), - color_processor_); + VideoParams::kRGBAChannelCount, color_processor_); bool success = true; encoder_->Close(); - if (!encoder_->GetError().isEmpty()) { SetError(encoder_->GetError()); success = false; } - delete encoder_; + if (subtitle_encoder_ != encoder_) { + subtitle_encoder_->Close(); + if (!subtitle_encoder_->GetError().isEmpty()) { + SetError(subtitle_encoder_->GetError()); + success = false; + } + } // If cancelled, delete the file we made, which is always a file we created since we write to a // temp file during the actual encoding process @@ -209,8 +249,8 @@ bool ExportTask::AudioDownloaded(const TimeRange &range, const SampleBuffer &sam bool ExportTask::EncodeSubtitle(const SubtitleBlock *sub) { - if (!encoder_->WriteSubtitle(sub)) { - SetError(encoder_->GetError()); + if (!subtitle_encoder_->WriteSubtitle(sub)) { + SetError(subtitle_encoder_->GetError()); return false; } else { return true; diff --git a/app/task/export/export.h b/app/task/export/export.h index bf696978e..7dcd8cf99 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -21,7 +21,7 @@ #ifndef EXPORTTASK_H #define EXPORTTASK_H -#include "exportparams.h" +#include "codec/encoder.h" #include "node/output/viewer/viewer.h" #include "render/colorprocessor.h" #include "task/render/render.h" @@ -33,7 +33,7 @@ class ExportTask : public RenderTask { Q_OBJECT public: - ExportTask(ViewerOutput *viewer_node, ColorManager *color_manager, const ExportParams ¶ms); + ExportTask(ViewerOutput *viewer_node, ColorManager *color_manager, const EncodingParams ¶ms); protected: virtual bool Run() override; @@ -58,9 +58,11 @@ private: ColorManager* color_manager_; - ExportParams params_; + EncodingParams params_; - Encoder* encoder_; + std::shared_ptr encoder_; + + std::shared_ptr subtitle_encoder_; ColorProcessorPtr color_processor_; diff --git a/app/task/export/exportparams.cpp b/app/task/export/exportparams.cpp deleted file mode 100644 index dd7154044..000000000 --- a/app/task/export/exportparams.cpp +++ /dev/null @@ -1,125 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 "exportparams.h" - -namespace olive { - -ExportParams::ExportParams() : - video_scaling_method_(kStretch), - has_custom_range_(false) -{ -} - -const Encoder::Type &ExportParams::encoder() const -{ - return encoder_id_; -} - -void ExportParams::set_encoder(const Encoder::Type &id) -{ - encoder_id_ = id; -} - -bool ExportParams::has_custom_range() const -{ - return has_custom_range_; -} - -const TimeRange &ExportParams::custom_range() const -{ - return custom_range_; -} - -void ExportParams::set_custom_range(const TimeRange &custom_range) -{ - has_custom_range_ = true; - custom_range_ = custom_range; -} - -const ExportParams::VideoScalingMethod &ExportParams::video_scaling_method() const -{ - return video_scaling_method_; -} - -void ExportParams::set_video_scaling_method(const ExportParams::VideoScalingMethod &video_scaling_method) -{ - video_scaling_method_ = video_scaling_method; -} - -const ColorTransform &ExportParams::color_transform() const -{ - return color_transform_; -} - -void ExportParams::set_color_transform(const ColorTransform &color_transform) -{ - color_transform_ = color_transform; -} - -QMatrix4x4 ExportParams::GenerateMatrix(ExportParams::VideoScalingMethod method, - int source_width, int source_height, - int dest_width, int dest_height) -{ - QMatrix4x4 preview_matrix; - - if (method == ExportParams::kStretch) { - return preview_matrix; - } - - float export_ar = static_cast(dest_width) / static_cast(dest_height); - float source_ar = static_cast(source_width) / static_cast(source_height); - - if (qFuzzyCompare(export_ar, source_ar)) { - return preview_matrix; - } - - if ((export_ar > source_ar) == (method == ExportParams::kFit)) { - preview_matrix.scale(source_ar / export_ar, 1.0F); - } else { - preview_matrix.scale(1.0F, export_ar / source_ar); - } - - return preview_matrix; -} - -void ExportParams::Save(QXmlStreamWriter *writer) const -{ - writer->writeStartElement(QStringLiteral("export")); - - writer->writeTextElement(QStringLiteral("encoder"), QString::number(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 -} - -} diff --git a/app/task/export/exportparams.h b/app/task/export/exportparams.h deleted file mode 100644 index b72597a6c..000000000 --- a/app/task/export/exportparams.h +++ /dev/null @@ -1,75 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 EXPORTPARAMS_H -#define EXPORTPARAMS_H - -#include - -#include "codec/encoder.h" -#include "node/output/viewer/viewer.h" -#include "render/colortransform.h" - -namespace olive { - -class ExportParams : public EncodingParams { -public: - enum VideoScalingMethod { - kFit, - kStretch, - kCrop - }; - - ExportParams(); - - const Encoder::Type& encoder() const; - void set_encoder(const Encoder::Type& id); - - bool has_custom_range() const; - const TimeRange& custom_range() const; - void set_custom_range(const TimeRange& custom_range); - - const VideoScalingMethod& video_scaling_method() const; - void set_video_scaling_method(const VideoScalingMethod& video_scaling_method); - - const ColorTransform& color_transform() const; - void set_color_transform(const ColorTransform& color_transform); - - static QMatrix4x4 GenerateMatrix(ExportParams::VideoScalingMethod method, - int source_width, int source_height, - int dest_width, int dest_height); - - virtual void Save(QXmlStreamWriter* writer) const override; - -private: - Encoder::Type encoder_id_; - - VideoScalingMethod video_scaling_method_; - - bool has_custom_range_; - TimeRange custom_range_; - - ColorTransform color_transform_; - -}; - -} - -#endif // EXPORTPARAMS_H diff --git a/app/task/precache/precachetask.cpp b/app/task/precache/precachetask.cpp index e83de623b..b0145474c 100644 --- a/app/task/precache/precachetask.cpp +++ b/app/task/precache/precachetask.cpp @@ -65,9 +65,9 @@ bool PreCacheTask::Run() // Get list of invalidated ranges TimeRange intersection; - if (footage_->GetTimelinePoints()->workarea()->enabled()) { + if (footage_->GetWorkArea()->enabled()) { // If we're caching only in-out, limit the range to that - intersection = footage_->GetTimelinePoints()->workarea()->range(); + intersection = footage_->GetWorkArea()->range(); } else { // Otherwise use full length intersection = TimeRange(0, footage_->GetVideoLength()); diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 2968f6d5a..1bb764022 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -42,8 +42,10 @@ bool RenderTask::Render(ColorManager* manager, RenderMode::Mode mode, FrameHashCache* cache, const QSize &force_size, const QMatrix4x4 &force_matrix, VideoParams::Format force_format, - ColorProcessorPtr force_color_output) + int force_channel_count, ColorProcessorPtr force_color_output) { + QMetaObject::invokeMethod(RenderManager::instance(), "SetAggressiveGarbageCollection", Q_ARG(bool, true)); + // Run watchers in another thread so they can accept signals even while this thread is blocked QThread watcher_thread; watcher_thread.start(); @@ -86,15 +88,14 @@ bool RenderTask::Render(ColorManager* manager, rational next_frame; for (int i=0; i(viewer_); - if (sequence) { + if (Sequence *sequence = dynamic_cast(viewer_)) { TrackList *list = sequence->track_list(Track::kSubtitle); QVector block_indexes(list->GetTrackCount(), 0); @@ -104,6 +105,10 @@ bool RenderTask::Render(ColorManager* manager, for (int i=0; iGetTrackAt(i); + if (this_track->IsMuted()) { + continue; + } + int &this_block_index = block_indexes[i]; if (this_block_index >= this_track->Blocks().size()) { continue; @@ -194,7 +199,7 @@ bool RenderTask::Render(ColorManager* manager, } if (iterator.GetNext(&next_frame)) { - StartTicket(&watcher_thread, manager, next_frame, mode, cache, force_size, force_matrix, force_format, force_color_output); + StartTicket(&watcher_thread, manager, next_frame, mode, cache, force_size, force_matrix, force_format, force_channel_count, force_color_output); } } @@ -236,6 +241,8 @@ bool RenderTask::Render(ColorManager* manager, watcher_thread.quit(); watcher_thread.wait(); + QMetaObject::invokeMethod(RenderManager::instance(), "SetAggressiveGarbageCollection", Q_ARG(bool, false)); + return result; } @@ -275,7 +282,8 @@ void RenderTask::IncrementRunningTickets() void RenderTask::StartTicket(QThread* watcher_thread, ColorManager* manager, const rational& time, RenderMode::Mode mode, FrameHashCache* cache, const QSize &force_size, const QMatrix4x4 &force_matrix, - VideoParams::Format force_format, ColorProcessorPtr force_color_output) + VideoParams::Format force_format, int force_channel_count, + ColorProcessorPtr force_color_output) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); watcher->setProperty("time", QVariant::fromValue(time)); @@ -284,8 +292,8 @@ void RenderTask::StartTicket(QThread* watcher_thread, ColorManager* manager, watcher->SetTicket(RenderManager::instance()->RenderFrame(viewer_->GetConnectedTextureOutput(), manager, time, mode, video_params_, audio_params_, force_size, force_matrix, - force_format, force_color_output, - cache)); + force_format, force_channel_count, + force_color_output, cache)); } void RenderTask::TicketDone(RenderTicketWatcher* watcher) diff --git a/app/task/render/render.h b/app/task/render/render.h index d08c6d018..a9313e738 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -27,8 +27,7 @@ #include "node/color/colormanager/colormanager.h" #include "node/output/viewer/viewer.h" #include "task/task.h" -#include "threading/threadticket.h" -#include "threading/threadticketwatcher.h" +#include "render/renderticket.h" namespace olive { @@ -47,7 +46,7 @@ protected: FrameHashCache *cache, const QSize& force_size = QSize(0, 0), const QMatrix4x4& force_matrix = QMatrix4x4(), VideoParams::Format force_format = VideoParams::kFormatInvalid, - ColorProcessorPtr force_color_output = nullptr); + int force_channel_count = 0, ColorProcessorPtr force_color_output = nullptr); virtual bool DownloadFrame(QThread* thread, FramePtr frame, const rational &time); @@ -117,7 +116,7 @@ private: void IncrementRunningTickets(); - void StartTicket(QThread *watcher_thread, ColorManager *manager, const rational &time, RenderMode::Mode mode, FrameHashCache *cache, const QSize &force_size, const QMatrix4x4 &force_matrix, VideoParams::Format force_format, ColorProcessorPtr force_color_output); + void StartTicket(QThread *watcher_thread, ColorManager *manager, const rational &time, RenderMode::Mode mode, FrameHashCache *cache, const QSize &force_size, const QMatrix4x4 &force_matrix, VideoParams::Format force_format, int force_channel_count, ColorProcessorPtr force_color_output); ViewerOutput* viewer_; diff --git a/app/threading/threadpool.cpp b/app/threading/threadpool.cpp deleted file mode 100644 index 8b33d72db..000000000 --- a/app/threading/threadpool.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 "threadpool.h" - -namespace olive { - -ThreadPool::ThreadPool(unsigned threads, QObject *parent) : - QObject(parent) -{ - if (threads == 0) { - threads = std::thread::hardware_concurrency(); - } - - available_count_ = threads; - for (unsigned i = 0; i < threads; i += 1) { - worker_threads_.emplace_back(std::bind(&ThreadPool::thread_exec, this, &tasks_, &task_mutex_, &cond_)); - } - - // Make single reserved thread for high priority tasks (usually audio) so they don't get stuck - // behind a lot of slow tasks - high_thread_ = std::thread(std::thread(std::bind(&ThreadPool::thread_exec, this, &high_tasks_, &high_mutex_, &high_cond_))); -} - -void ThreadPool::AddTicket(RenderTicketPtr ticket, RenderTicketPriority priority) -{ - if (priority == RenderTicketPriority::kHigh) { - std::lock_guard lock(high_mutex_); - high_tasks_.emplace_back(std::move(ticket)); - high_cond_.notify_one(); - } else { - std::lock_guard lock(task_mutex_); - tasks_.emplace_back(std::move(ticket)); - cond_.notify_one(); - } -} - -bool ThreadPool::RemoveTicket(RenderTicketPtr ticket) -{ - { - std::lock_guard lock(task_mutex_); - const auto it = std::find(tasks_.begin(), tasks_.end(), ticket); - if (it != tasks_.end()) { - tasks_.erase(it); - return true; - } - } - - { - std::lock_guard lock(high_mutex_); - const auto it = std::find(high_tasks_.begin(), high_tasks_.end(), ticket); - if (it != high_tasks_.end()) { - high_tasks_.erase(it); - return true; - } - } - - return false; -} - -void ThreadPool::thread_exec(std::deque *queue, std::mutex *mutex, std::condition_variable *cond) -{ - while (true) { - TaskType task; - - { - std::unique_lock lock(*mutex); - cond->wait(lock, [this, queue]{ return this->end_threadp_ || !queue->empty(); }); - - if (this->end_threadp_ && queue->empty()) { - break; - } - - task = std::move(queue->front()); - queue->pop_front(); - } - - RunTicket(task); - } -} - -ThreadPool::~ThreadPool() -{ - end_threadp_ = true; - cond_.notify_all(); - high_cond_.notify_all(); - - for (auto &e : worker_threads_) { - e.join(); - } - high_thread_.join(); -} - -} diff --git a/app/threading/threadpool.h b/app/threading/threadpool.h deleted file mode 100644 index 3c1143f55..000000000 --- a/app/threading/threadpool.h +++ /dev/null @@ -1,71 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 THREADPOOL_H -#define THREADPOOL_H - -#include "threading/threadticket.h" - -#include -#include -#include -#include -#include - -namespace olive { - -enum class RenderTicketPriority { kHigh = 0, kNormal }; - -class ThreadPool : public QObject -{ - Q_OBJECT -public: - using TaskType = RenderTicketPtr; - ThreadPool(unsigned threads, QObject *parent); - - DISABLE_COPY_MOVE(ThreadPool) - - virtual void RunTicket(RenderTicketPtr ticket) const = 0; - void AddTicket(RenderTicketPtr ticket, RenderTicketPriority priority = RenderTicketPriority::kNormal); - bool RemoveTicket(RenderTicketPtr ticket); - - virtual ~ThreadPool() override; - -private: - void thread_exec(std::deque *queue, std::mutex *mutex, std::condition_variable *cond); - - std::vector worker_threads_; - std::deque tasks_; - std::mutex task_mutex_; - std::condition_variable cond_; - - std::thread high_thread_; - std::deque high_tasks_; - std::mutex high_mutex_; - std::condition_variable high_cond_; - - std::atomic_bool end_threadp_{false}; - std::atomic_int available_count_; - -}; - -} // namespace olive - -#endif // THREADPOOL_H diff --git a/app/threading/threadticketwatcher.cpp b/app/threading/threadticketwatcher.cpp deleted file mode 100644 index a6ca5b2eb..000000000 --- a/app/threading/threadticketwatcher.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 "threadticketwatcher.h" - -namespace olive { - -RenderTicketWatcher::RenderTicketWatcher(QObject *parent) : - QObject(parent), - ticket_(nullptr) -{ -} - -void RenderTicketWatcher::SetTicket(RenderTicketPtr ticket) -{ - if (ticket_) { - qCritical() << "Tried to set a ticket on a RenderTicketWatcher twice"; - return; - } - - if (!ticket) { - qCritical() << "Tried to set a null ticket on a RenderTicketWatcher"; - return; - } - - ticket_ = ticket; - - // Lock ticket so we can query if it's already finished by the time this code runs - QMutexLocker locker(ticket->lock()); - - connect(ticket_.get(), &RenderTicket::Finished, this, &RenderTicketWatcher::TicketFinished); - - if (!ticket_->IsRunning(false) && ticket_->GetFinishCount(false) > 0) { - // Ticket has already finished before, so we emit a signal - locker.unlock(); - TicketFinished(); - } -} - -bool RenderTicketWatcher::IsRunning() -{ - if (ticket_) { - return ticket_->IsRunning(); - } else { - return false; - } -} - -void RenderTicketWatcher::WaitForFinished() -{ - if (ticket_) { - ticket_->WaitForFinished(); - } -} - -QVariant RenderTicketWatcher::Get() -{ - if (ticket_) { - return ticket_->Get(); - } else { - return QVariant(); - } -} - -bool RenderTicketWatcher::HasResult() -{ - if (ticket_) { - return ticket_->HasResult(); - } else { - return false; - } -} - -void RenderTicketWatcher::Cancel() -{ - if (ticket_) { - ticket_->Cancel(); - } -} - -void RenderTicketWatcher::TicketFinished() -{ - emit Finished(this); -} - -} diff --git a/app/timeline/CMakeLists.txt b/app/timeline/CMakeLists.txt index 3fb988e83..f43d93de3 100644 --- a/app/timeline/CMakeLists.txt +++ b/app/timeline/CMakeLists.txt @@ -21,8 +21,6 @@ set(OLIVE_SOURCES timeline/timelinecoordinate.cpp timeline/timelinemarker.h timeline/timelinemarker.cpp - timeline/timelinepoints.h - timeline/timelinepoints.cpp timeline/timelineworkarea.h timeline/timelineworkarea.cpp PARENT_SCOPE diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp index d8b87b7fd..25de1ab9a 100644 --- a/app/timeline/timelinemarker.cpp +++ b/app/timeline/timelinemarker.cpp @@ -20,6 +20,8 @@ #include "timelinemarker.h" +#include + #include "common/qtutils.h" #include "common/xmlutils.h" #include "config/config.h" @@ -76,7 +78,7 @@ int TimelineMarker::GetMarkerHeight(const QFontMetrics &fm) return fm.height(); } -QRect TimelineMarker::Draw(QPainter *p, const QPoint &pt, double scale, bool selected) +QRect TimelineMarker::Draw(QPainter *p, const QPoint &pt, int max_right, double scale, bool selected) { QFontMetrics fm = p->fontMetrics(); @@ -96,6 +98,9 @@ QRect TimelineMarker::Draw(QPainter *p, const QPoint &pt, double scale, bool sel int top = pt.y() - marker_height; + QTextOption op(Qt::AlignLeft | Qt::AlignVCenter); + op.setWrapMode(QTextOption::NoWrap); + if (time_.out() != time_.in()) { QRect marker_rect(pt.x(), top, time_.length().toDouble() * scale, marker_height); @@ -103,7 +108,7 @@ QRect TimelineMarker::Draw(QPainter *p, const QPoint &pt, double scale, bool sel if (!name_.isEmpty()) { p->setPen(ColorCoding::GetUISelectorColor(ColorCoding::GetColor(color_))); - p->drawText(marker_rect.adjusted(marker_width/4, 0, 0, 0), name_, Qt::AlignLeft | Qt::AlignVCenter); + p->drawText(marker_rect.adjusted(marker_width/4, 0, 0, 0), name_, op); } return marker_rect; @@ -125,6 +130,16 @@ QRect TimelineMarker::Draw(QPainter *p, const QPoint &pt, double scale, bool sel p->setRenderHint(QPainter::Antialiasing); p->drawPolygon(points, 6); + if (!name_.isEmpty() && max_right != -1) { + QRect text_rect(right, top, max_right - right, marker_height); + + int padding = QtUtils::QFontMetricsWidth(p->fontMetrics(), QStringLiteral(" ")); + text_rect.adjust(padding, 0, - padding - half_width, 0); + + p->setPen(qApp->palette().text().color()); + p->drawText(text_rect, name_, op); + } + return QRect(left, top, marker_width, marker_height); } } diff --git a/app/timeline/timelinemarker.h b/app/timeline/timelinemarker.h index 7712111d2..47773d237 100644 --- a/app/timeline/timelinemarker.h +++ b/app/timeline/timelinemarker.h @@ -51,7 +51,7 @@ public: void set_color(int c); static int GetMarkerHeight(const QFontMetrics &fm); - QRect Draw(QPainter *p, const QPoint &pt, double scale, bool selected); + QRect Draw(QPainter *p, const QPoint &pt, int max_right, double scale, bool selected); signals: void TimeChanged(const TimeRange& time); diff --git a/app/timeline/timelinepoints.cpp b/app/timeline/timelinepoints.cpp deleted file mode 100644 index b7c2034bb..000000000 --- a/app/timeline/timelinepoints.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 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 "timelinepoints.h" - -#include "common/xmlutils.h" - -namespace olive { - -TimelinePoints::TimelinePoints(QObject *parent) : - QObject(parent) -{ - markers_ = new TimelineMarkerList(this); - workarea_ = new TimelineWorkArea(this); -} - -TimelineMarkerList *TimelinePoints::markers() -{ - return markers_; -} - -const TimelineMarkerList *TimelinePoints::markers() const -{ - return markers_; -} - -const TimelineWorkArea *TimelinePoints::workarea() const -{ - return workarea_; -} - -TimelineWorkArea *TimelinePoints::workarea() -{ - return workarea_; -} - -} diff --git a/app/ui/icons/icons.cpp b/app/ui/icons/icons.cpp index 85288cb6b..72068d9ad 100644 --- a/app/ui/icons/icons.cpp +++ b/app/ui/icons/icons.cpp @@ -75,6 +75,9 @@ QIcon icon::TextAlignLeft; QIcon icon::TextAlignRight; QIcon icon::TextAlignCenter; QIcon icon::TextAlignJustify; +QIcon icon::TextAlignTop; +QIcon icon::TextAlignBottom; +QIcon icon::TextAlignMiddle; QIcon icon::Snapping; QIcon icon::ZoomIn; QIcon icon::ZoomOut; @@ -93,6 +96,7 @@ QIcon icon::LockOpened; QIcon icon::LockClosed; QIcon icon::Pencil; QIcon icon::Subtitles; +QIcon icon::ColorPicker; void icon::LoadAll(const QString& theme) { @@ -145,6 +149,9 @@ void icon::LoadAll(const QString& theme) TextAlignRight = Create(theme, "align-right"); TextAlignCenter = Create(theme, "align-center"); TextAlignJustify = Create(theme, "align-justify-all"); + TextAlignTop = Create(theme, "align-v-top"); + TextAlignBottom = Create(theme, "align-v-bottom"); + TextAlignMiddle = Create(theme, "align-v-middle"); Snapping = Create(theme, "magnet"); ZoomIn = Create(theme, "zoomin"); @@ -158,6 +165,7 @@ void icon::LoadAll(const QString& theme) Plus = Create(theme, "plus"); Minus = Create(theme, "minus"); AddEffect = Create(theme, "add-effect"); + ColorPicker = Create(theme, "color-picker"); EyeOpened = Create(theme, "eye-opened"); EyeClosed = Create(theme, "eye-closed"); diff --git a/app/ui/icons/icons.h b/app/ui/icons/icons.h index 83a9f18dc..283f2be5f 100644 --- a/app/ui/icons/icons.h +++ b/app/ui/icons/icons.h @@ -85,6 +85,9 @@ extern QIcon TextAlignLeft; extern QIcon TextAlignRight; extern QIcon TextAlignCenter; extern QIcon TextAlignJustify; +extern QIcon TextAlignTop; +extern QIcon TextAlignBottom; +extern QIcon TextAlignMiddle; // Miscellaneous Icons extern QIcon Snapping; @@ -105,6 +108,7 @@ extern QIcon LockOpened; extern QIcon LockClosed; extern QIcon Pencil; extern QIcon Subtitles; +extern QIcon ColorPicker; /** * @brief Create an icon object loaded from file diff --git a/app/ui/style/olive-dark/png/align-v-bottom.128.disabled.png b/app/ui/style/olive-dark/png/align-v-bottom.128.disabled.png new file mode 100644 index 000000000..6f251fe6b Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-bottom.128.png b/app/ui/style/olive-dark/png/align-v-bottom.128.png new file mode 100644 index 000000000..3a9f225c8 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.128.png differ diff --git a/app/ui/style/olive-dark/png/align-v-bottom.16.disabled.png b/app/ui/style/olive-dark/png/align-v-bottom.16.disabled.png new file mode 100644 index 000000000..639415108 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-bottom.16.png b/app/ui/style/olive-dark/png/align-v-bottom.16.png new file mode 100644 index 000000000..c0b7c30a4 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.16.png differ diff --git a/app/ui/style/olive-dark/png/align-v-bottom.32.disabled.png b/app/ui/style/olive-dark/png/align-v-bottom.32.disabled.png new file mode 100644 index 000000000..55cd41b93 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-bottom.32.png b/app/ui/style/olive-dark/png/align-v-bottom.32.png new file mode 100644 index 000000000..3ebbf630d Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.32.png differ diff --git a/app/ui/style/olive-dark/png/align-v-bottom.64.disabled.png b/app/ui/style/olive-dark/png/align-v-bottom.64.disabled.png new file mode 100644 index 000000000..7f1cb106b Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-bottom.64.png b/app/ui/style/olive-dark/png/align-v-bottom.64.png new file mode 100644 index 000000000..8c390bae7 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-bottom.64.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.128.disabled.png b/app/ui/style/olive-dark/png/align-v-middle.128.disabled.png new file mode 100644 index 000000000..61b9179e1 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.128.png b/app/ui/style/olive-dark/png/align-v-middle.128.png new file mode 100644 index 000000000..e1ebb4945 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.128.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.16.disabled.png b/app/ui/style/olive-dark/png/align-v-middle.16.disabled.png new file mode 100644 index 000000000..313baeec0 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.16.png b/app/ui/style/olive-dark/png/align-v-middle.16.png new file mode 100644 index 000000000..413c3a6f8 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.16.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.32.disabled.png b/app/ui/style/olive-dark/png/align-v-middle.32.disabled.png new file mode 100644 index 000000000..c0c615fb3 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.32.png b/app/ui/style/olive-dark/png/align-v-middle.32.png new file mode 100644 index 000000000..8f690a186 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.32.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.64.disabled.png b/app/ui/style/olive-dark/png/align-v-middle.64.disabled.png new file mode 100644 index 000000000..9d7338716 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-middle.64.png b/app/ui/style/olive-dark/png/align-v-middle.64.png new file mode 100644 index 000000000..f15977c01 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-middle.64.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.128.disabled.png b/app/ui/style/olive-dark/png/align-v-top.128.disabled.png new file mode 100644 index 000000000..79130f1d9 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.128.png b/app/ui/style/olive-dark/png/align-v-top.128.png new file mode 100644 index 000000000..7c74a3f54 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.128.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.16.disabled.png b/app/ui/style/olive-dark/png/align-v-top.16.disabled.png new file mode 100644 index 000000000..44bd511bc Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.16.png b/app/ui/style/olive-dark/png/align-v-top.16.png new file mode 100644 index 000000000..c24e4cf43 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.16.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.32.disabled.png b/app/ui/style/olive-dark/png/align-v-top.32.disabled.png new file mode 100644 index 000000000..ccc12a0a3 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.32.png b/app/ui/style/olive-dark/png/align-v-top.32.png new file mode 100644 index 000000000..2ac8791bb Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.32.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.64.disabled.png b/app/ui/style/olive-dark/png/align-v-top.64.disabled.png new file mode 100644 index 000000000..763a62f10 Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/align-v-top.64.png b/app/ui/style/olive-dark/png/align-v-top.64.png new file mode 100644 index 000000000..89ef2e27e Binary files /dev/null and b/app/ui/style/olive-dark/png/align-v-top.64.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.128.disabled.png b/app/ui/style/olive-dark/png/color-picker.128.disabled.png new file mode 100644 index 000000000..cc0217515 Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.128.png b/app/ui/style/olive-dark/png/color-picker.128.png new file mode 100644 index 000000000..d6cb29d32 Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.128.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.16.disabled.png b/app/ui/style/olive-dark/png/color-picker.16.disabled.png new file mode 100644 index 000000000..0931639d8 Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.16.png b/app/ui/style/olive-dark/png/color-picker.16.png new file mode 100644 index 000000000..899bd566f Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.16.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.32.disabled.png b/app/ui/style/olive-dark/png/color-picker.32.disabled.png new file mode 100644 index 000000000..d64175801 Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.32.png b/app/ui/style/olive-dark/png/color-picker.32.png new file mode 100644 index 000000000..99dce99b7 Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.32.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.64.disabled.png b/app/ui/style/olive-dark/png/color-picker.64.disabled.png new file mode 100644 index 000000000..a59e16d59 Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/color-picker.64.png b/app/ui/style/olive-dark/png/color-picker.64.png new file mode 100644 index 000000000..928212473 Binary files /dev/null and b/app/ui/style/olive-dark/png/color-picker.64.png differ diff --git a/app/ui/style/olive-dark/svg/align-v-bottom.svg b/app/ui/style/olive-dark/svg/align-v-bottom.svg new file mode 100644 index 000000000..0ab39353e --- /dev/null +++ b/app/ui/style/olive-dark/svg/align-v-bottom.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-dark/svg/align-v-middle.svg b/app/ui/style/olive-dark/svg/align-v-middle.svg new file mode 100644 index 000000000..d5651ba57 --- /dev/null +++ b/app/ui/style/olive-dark/svg/align-v-middle.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-dark/svg/align-v-top.svg b/app/ui/style/olive-dark/svg/align-v-top.svg new file mode 100644 index 000000000..a83606198 --- /dev/null +++ b/app/ui/style/olive-dark/svg/align-v-top.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-dark/svg/color-picker.svg b/app/ui/style/olive-dark/svg/color-picker.svg new file mode 100644 index 000000000..8a3fb2c4e --- /dev/null +++ b/app/ui/style/olive-dark/svg/color-picker.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/png/align-v-bottom.128.disabled.png b/app/ui/style/olive-light/png/align-v-bottom.128.disabled.png new file mode 100644 index 000000000..db474fe20 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-bottom.128.png b/app/ui/style/olive-light/png/align-v-bottom.128.png new file mode 100644 index 000000000..daff9e2f4 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.128.png differ diff --git a/app/ui/style/olive-light/png/align-v-bottom.16.disabled.png b/app/ui/style/olive-light/png/align-v-bottom.16.disabled.png new file mode 100644 index 000000000..f782b2eaa Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-bottom.16.png b/app/ui/style/olive-light/png/align-v-bottom.16.png new file mode 100644 index 000000000..c7197e05f Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.16.png differ diff --git a/app/ui/style/olive-light/png/align-v-bottom.32.disabled.png b/app/ui/style/olive-light/png/align-v-bottom.32.disabled.png new file mode 100644 index 000000000..a3e7878ed Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-bottom.32.png b/app/ui/style/olive-light/png/align-v-bottom.32.png new file mode 100644 index 000000000..06fdaa829 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.32.png differ diff --git a/app/ui/style/olive-light/png/align-v-bottom.64.disabled.png b/app/ui/style/olive-light/png/align-v-bottom.64.disabled.png new file mode 100644 index 000000000..3a76099bd Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-bottom.64.png b/app/ui/style/olive-light/png/align-v-bottom.64.png new file mode 100644 index 000000000..603e6ab46 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-bottom.64.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.128.disabled.png b/app/ui/style/olive-light/png/align-v-middle.128.disabled.png new file mode 100644 index 000000000..5dde090d8 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.128.png b/app/ui/style/olive-light/png/align-v-middle.128.png new file mode 100644 index 000000000..60be15fe1 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.128.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.16.disabled.png b/app/ui/style/olive-light/png/align-v-middle.16.disabled.png new file mode 100644 index 000000000..ad42a4915 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.16.png b/app/ui/style/olive-light/png/align-v-middle.16.png new file mode 100644 index 000000000..68b05bd19 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.16.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.32.disabled.png b/app/ui/style/olive-light/png/align-v-middle.32.disabled.png new file mode 100644 index 000000000..546894d5c Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.32.png b/app/ui/style/olive-light/png/align-v-middle.32.png new file mode 100644 index 000000000..4bf0bb197 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.32.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.64.disabled.png b/app/ui/style/olive-light/png/align-v-middle.64.disabled.png new file mode 100644 index 000000000..18e4e8e21 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-middle.64.png b/app/ui/style/olive-light/png/align-v-middle.64.png new file mode 100644 index 000000000..12a8c8d81 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-middle.64.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.128.disabled.png b/app/ui/style/olive-light/png/align-v-top.128.disabled.png new file mode 100644 index 000000000..954694ce7 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.128.png b/app/ui/style/olive-light/png/align-v-top.128.png new file mode 100644 index 000000000..c4b242f76 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.128.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.16.disabled.png b/app/ui/style/olive-light/png/align-v-top.16.disabled.png new file mode 100644 index 000000000..a820ffcce Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.16.png b/app/ui/style/olive-light/png/align-v-top.16.png new file mode 100644 index 000000000..95122ccd7 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.16.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.32.disabled.png b/app/ui/style/olive-light/png/align-v-top.32.disabled.png new file mode 100644 index 000000000..fdaca9056 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.32.png b/app/ui/style/olive-light/png/align-v-top.32.png new file mode 100644 index 000000000..00e9ff230 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.32.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.64.disabled.png b/app/ui/style/olive-light/png/align-v-top.64.disabled.png new file mode 100644 index 000000000..449f94c5b Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/align-v-top.64.png b/app/ui/style/olive-light/png/align-v-top.64.png new file mode 100644 index 000000000..d82bbe0f5 Binary files /dev/null and b/app/ui/style/olive-light/png/align-v-top.64.png differ diff --git a/app/ui/style/olive-light/png/color-picker.128.disabled.png b/app/ui/style/olive-light/png/color-picker.128.disabled.png new file mode 100644 index 000000000..820b32c66 Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/color-picker.128.png b/app/ui/style/olive-light/png/color-picker.128.png new file mode 100644 index 000000000..05b5acab2 Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.128.png differ diff --git a/app/ui/style/olive-light/png/color-picker.16.disabled.png b/app/ui/style/olive-light/png/color-picker.16.disabled.png new file mode 100644 index 000000000..7fa58b54f Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/color-picker.16.png b/app/ui/style/olive-light/png/color-picker.16.png new file mode 100644 index 000000000..4c4154054 Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.16.png differ diff --git a/app/ui/style/olive-light/png/color-picker.32.disabled.png b/app/ui/style/olive-light/png/color-picker.32.disabled.png new file mode 100644 index 000000000..73d9e1a3a Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/color-picker.32.png b/app/ui/style/olive-light/png/color-picker.32.png new file mode 100644 index 000000000..427b035e2 Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.32.png differ diff --git a/app/ui/style/olive-light/png/color-picker.64.disabled.png b/app/ui/style/olive-light/png/color-picker.64.disabled.png new file mode 100644 index 000000000..e684a5d20 Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/color-picker.64.png b/app/ui/style/olive-light/png/color-picker.64.png new file mode 100644 index 000000000..e57fc9311 Binary files /dev/null and b/app/ui/style/olive-light/png/color-picker.64.png differ diff --git a/app/ui/style/olive-light/svg/align-v-bottom.svg b/app/ui/style/olive-light/svg/align-v-bottom.svg new file mode 100644 index 000000000..2518d29c5 --- /dev/null +++ b/app/ui/style/olive-light/svg/align-v-bottom.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/svg/align-v-middle.svg b/app/ui/style/olive-light/svg/align-v-middle.svg new file mode 100644 index 000000000..6b5af2129 --- /dev/null +++ b/app/ui/style/olive-light/svg/align-v-middle.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/svg/align-v-top.svg b/app/ui/style/olive-light/svg/align-v-top.svg new file mode 100644 index 000000000..41035f35e --- /dev/null +++ b/app/ui/style/olive-light/svg/align-v-top.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/svg/color-picker.svg b/app/ui/style/olive-light/svg/color-picker.svg new file mode 100644 index 000000000..af35d6f2b --- /dev/null +++ b/app/ui/style/olive-light/svg/color-picker.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index 8e7c945f5..588c39ac5 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -20,6 +20,7 @@ #include "audiomonitor.h" +#include #include #include @@ -35,8 +36,7 @@ const int kMaximumSmoothness = 8; QVector AudioMonitor::instances_; -AudioMonitor::AudioMonitor(QWidget *parent) : - QOpenGLWidget(parent), +AudioMonitor::AudioMonitor() : waveform_(nullptr), cached_channels_(0) { @@ -125,7 +125,10 @@ void AudioMonitor::SetUpdateLoop(bool e) void AudioMonitor::paintGL() { QPainter p(this); - p.fillRect(rect(), palette().window().color()); + QPalette palette = qApp->palette(); + QRect geometry(0, 0, width(), height()); + + p.fillRect(geometry, palette.window().color()); if (!params_.channel_count()) { return; @@ -137,12 +140,12 @@ void AudioMonitor::paintGL() int font_height = fm.height(); // Create rect where decibel markings will go on the side - QRect db_labels_rect = rect(); + QRect db_labels_rect = geometry; db_labels_rect.setWidth(QtUtils::QFontMetricsWidth(p.fontMetrics(), "-00")); db_labels_rect.adjust(0, font_height, 0, 0); // Determine rect where the main meter will go - QRect full_meter_rect = rect(); + QRect full_meter_rect = geometry; full_meter_rect.adjust(db_labels_rect.width(), font_height, 0, 0); // Width of each channel in the meter @@ -163,7 +166,7 @@ void AudioMonitor::paintGL() // Draw decibel markings QRect last_db_marking_rect; - cached_painter.setPen(palette().text().color()); + cached_painter.setPen(palette.text().color()); for (int i=0;i>=kDecibelMinimum;i-=kDecibelStep) { QString db_label; diff --git a/app/widget/audiomonitor/audiomonitor.h b/app/widget/audiomonitor/audiomonitor.h index d29b74491..8b66b941d 100644 --- a/app/widget/audiomonitor/audiomonitor.h +++ b/app/widget/audiomonitor/audiomonitor.h @@ -36,7 +36,7 @@ class AudioMonitor : public QOpenGLWidget { Q_OBJECT public: - AudioMonitor(QWidget* parent = nullptr); + AudioMonitor(); virtual ~AudioMonitor() override; diff --git a/app/widget/colorwheel/colorvalueswidget.cpp b/app/widget/colorwheel/colorvalueswidget.cpp index 88bf46012..85ac6fb62 100644 --- a/app/widget/colorwheel/colorvalueswidget.cpp +++ b/app/widget/colorwheel/colorvalueswidget.cpp @@ -26,6 +26,7 @@ #include "config/config.h" #include "core.h" +#include "ui/icons/icons.h" namespace olive { @@ -51,7 +52,9 @@ ColorValuesWidget::ColorValuesWidget(ColorManager *manager, QWidget *parent) : preview_->setFixedHeight(fontMetrics().height() * 3 / 2); preview_layout->addWidget(preview_); - color_picker_btn_ = new QPushButton(tr("Pick")); + color_picker_btn_ = new QPushButton(); + color_picker_btn_->setIcon(icon::ColorPicker); + color_picker_btn_->setFixedWidth(color_picker_btn_->sizeHint().height()); color_picker_btn_->setCheckable(true); connect(color_picker_btn_, &QPushButton::toggled, this, &ColorValuesWidget::ColorPickedBtnToggled); connect(Core::instance(), &Core::ColorPickerColorEmitted, this, &ColorValuesWidget::SetReferenceColor); diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index d58bde7cd..d85e6d168 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -103,6 +103,7 @@ CurveWidget::CurveWidget(QWidget *parent) : connect(view_, &CurveView::SelectionChanged, this, &CurveWidget::SelectionChanged); connect(view_, &CurveView::ScaleChanged, this, &CurveWidget::SetScale); connect(view_, &CurveView::Dragged, this, &CurveWidget::KeyframeViewDragged); + connect(view_, &CurveView::Released, this, &CurveWidget::KeyframeViewReleased); // TimeBasedWidget's scrollbar has extra functionality that we can take advantage of view_->setHorizontalScrollBar(scrollbar()); @@ -379,15 +380,14 @@ void CurveWidget::InputSelectionChanged(const NodeKeyframeTrackReference& ref) void CurveWidget::KeyframeViewDragged(int x, int y) { - QMetaObject::invokeMethod(this, "CatchUpScrollToPoint", Qt::QueuedConnection, - Q_ARG(int, x)); - QMetaObject::invokeMethod(this, "CatchUpYScrollToPoint", Qt::QueuedConnection, - Q_ARG(int, y)); + SetCatchUpScrollValue(x); + SetCatchUpScrollValue(view_->verticalScrollBar(), y, view_->height()); } -void CurveWidget::CatchUpYScrollToPoint(int point) +void CurveWidget::KeyframeViewReleased() { - PageScrollInternal(view_->verticalScrollBar(), view_->height(), point, false); + StopCatchUpScrollTimer(); + StopCatchUpScrollTimer(view_->verticalScrollBar()); } } diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index 5fae1b4df..eef0ab62f 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -78,6 +78,11 @@ protected: return &view_->GetKeyframeTracks(); } + virtual const TimeTargetObject *GetKeyframeTimeTarget() const override + { + return view_; + } + virtual const std::vector *GetSnapIgnoreKeyframes() const override { return &view_->GetSelectedKeyframes(); @@ -122,8 +127,7 @@ private slots: void InputSelectionChanged(const NodeKeyframeTrackReference& ref); void KeyframeViewDragged(int x, int y); - - void CatchUpYScrollToPoint(int point); + void KeyframeViewReleased(); }; diff --git a/app/widget/handmovableview/handmovableview.cpp b/app/widget/handmovableview/handmovableview.cpp index 25536ec7b..a44858ad8 100644 --- a/app/widget/handmovableview/handmovableview.cpp +++ b/app/widget/handmovableview/handmovableview.cpp @@ -31,8 +31,7 @@ namespace olive { HandMovableView::HandMovableView(QWidget* parent) : super(parent), - dragging_hand_(false), - scroll_zooms_by_default_(OLIVE_CONFIG("ScrollZooms").toBool()) + dragging_hand_(false) { connect(Core::instance(), &Core::ToolChanged, this, &HandMovableView::ApplicationToolChanged); } @@ -66,6 +65,8 @@ bool HandMovableView::HandPress(QMouseEvent *event) Qt::LeftButton, event->modifiers()); + transformed_pos_ = QPoint(0, 0); + super::mousePressEvent(&transformed); return true; @@ -78,12 +79,34 @@ bool HandMovableView::HandMove(QMouseEvent *event) { if (dragging_hand_) { // Transform mouse event to act like the left button is pressed + QPoint adjustment(0, 0); + QMouseEvent transformed(event->type(), - event->localPos(), + event->localPos() - transformed_pos_, Qt::LeftButton, Qt::LeftButton, event->modifiers()); + if (event->localPos().x() < 0) { + transformed_pos_.setX(transformed_pos_.x() + width()); + adjustment.setX(width()); + } else if (event->localPos().x() >= width()) { + transformed_pos_.setX(transformed_pos_.x() - width()); + adjustment.setX(-width()); + } + + if (event->pos().y() < 0) { + transformed_pos_.setY(transformed_pos_.y() + height()); + adjustment.setY(height()); + } else if (event->pos().y() >= height()) { + transformed_pos_.setY(transformed_pos_.y() - height()); + adjustment.setY(-height()); + } + + if (!adjustment.isNull()) { + QCursor::setPos(QCursor::pos() + adjustment); + } + super::mouseMoveEvent(&transformed); } return dragging_hand_; @@ -125,7 +148,7 @@ const HandMovableView::DragMode &HandMovableView::GetDefaultDragMode() const bool HandMovableView::WheelEventIsAZoomEvent(QWheelEvent *event) const { - return (static_cast(event->modifiers() & Qt::ControlModifier) == !scroll_zooms_by_default_); + return (static_cast(event->modifiers() & Qt::ControlModifier) == !OLIVE_CONFIG("ScrollZooms").toBool()); } void HandMovableView::wheelEvent(QWheelEvent *event) @@ -155,17 +178,4 @@ void HandMovableView::ZoomIntoCursorPosition(QWheelEvent *event, double multipli Q_UNUSED(cursor_pos) } -QAction *HandMovableView::AddSetScrollZoomsByDefaultActionToMenu(QMenu *m, bool autoconnect) -{ - QAction* ctrl_zoom = m->addAction(tr("Scroll Zooms By Default")); - ctrl_zoom->setCheckable(true); - ctrl_zoom->setChecked(GetScrollZoomsByDefault()); - - if (autoconnect) { - connect(ctrl_zoom, &QAction::triggered, this, &HandMovableView::SetScrollZoomsByDefault); - } - - return ctrl_zoom; -} - } diff --git a/app/widget/handmovableview/handmovableview.h b/app/widget/handmovableview/handmovableview.h index e9579e7dd..e8c01599b 100644 --- a/app/widget/handmovableview/handmovableview.h +++ b/app/widget/handmovableview/handmovableview.h @@ -34,19 +34,6 @@ class HandMovableView : public QGraphicsView public: HandMovableView(QWidget* parent = nullptr); - bool GetScrollZoomsByDefault() const - { - return scroll_zooms_by_default_; - } - - QAction* AddSetScrollZoomsByDefaultActionToMenu(QMenu* menu, bool autoconnect = true); - -public slots: - void SetScrollZoomsByDefault(bool e) - { - scroll_zooms_by_default_ = e; - } - protected: virtual void ToolChangedEvent(Tool::Item tool){Q_UNUSED(tool)} @@ -69,13 +56,7 @@ private: DragMode default_drag_mode_; - /** - * @brief Whether scrolling should perform a scroll or a zoom - * - * If TRUE, scrolling will ZOOM and Ctrl+Scroll with SCROLL. - * If FALSE (default), scrolling will SCROLL and Ctrl+Scroll will ZOOM. - */ - bool scroll_zooms_by_default_; + QPointF transformed_pos_; private slots: void ApplicationToolChanged(Tool::Item tool); diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index df0349475..9fead0fe1 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -261,7 +261,7 @@ void KeyframeView::mousePressEvent(QMouseEvent *event) if (FirstChanceMousePress(event)) { first_chance_mouse_event_ = true; } else if (NodeKeyframe *initial_key = selection_manager_.MousePress(event)) { - selection_manager_.DragStart(initial_key, event); + selection_manager_.DragStart(initial_key, event, this); KeyframeDragStart(event); } else { selection_manager_.RubberBandStart(event); @@ -290,8 +290,7 @@ void KeyframeView::mouseMoveEvent(QMouseEvent *event) if (event->buttons()) { // Signal cursor pos in case we should scroll to catch up to it - QPointF scene_pos = mapToScene(event->pos()); - emit Dragged(scene_pos.x(), scene_pos.y()); + emit Dragged(event->pos().x(), event->pos().y()); } } @@ -309,6 +308,7 @@ void KeyframeView::mouseReleaseEvent(QMouseEvent *event) selection_manager_.DragStop(command); KeyframeDragRelease(event, command); Core::instance()->undo_stack()->push(command); + emit Released(); } else if (selection_manager_.IsRubberBanding()) { selection_manager_.RubberBandStop(); Redraw(); @@ -569,10 +569,6 @@ void KeyframeView::ShowContextMenu() m.addSeparator(); - AddSetScrollZoomsByDefaultActionToMenu(&m); - - m.addSeparator(); - ContextMenuEvent(m); if (!GetSelectedKeyframes().empty()) { diff --git a/app/widget/keyframeview/keyframeview.h b/app/widget/keyframeview/keyframeview.h index 2558f8cc7..0ab17f254 100644 --- a/app/widget/keyframeview/keyframeview.h +++ b/app/widget/keyframeview/keyframeview.h @@ -88,6 +88,8 @@ signals: void SelectionChanged(); + void Released(); + protected: virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseMoveEvent(QMouseEvent *event) override; diff --git a/app/widget/manageddisplay/manageddisplay.cpp b/app/widget/manageddisplay/manageddisplay.cpp index 22589365e..62afcb876 100644 --- a/app/widget/manageddisplay/manageddisplay.cpp +++ b/app/widget/manageddisplay/manageddisplay.cpp @@ -23,18 +23,19 @@ #include #include +#include "panel/panelmanager.h" #include "render/opengl/openglrenderer.h" #include "render/rendermanager.h" namespace olive { +#define super QWidget + ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent) : QWidget(parent), color_manager_(nullptr), color_service_(nullptr) { - setContextMenuPolicy(Qt::CustomContextMenu); - QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); layout->setMargin(0); @@ -55,17 +56,22 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent) : &ManagedDisplayWidgetOpenGL::frameSwapped, this, &ManagedDisplayWidget::frameSwapped, Qt::DirectConnection); - connect(static_cast(inner_widget_), - &ManagedDisplayWidgetOpenGL::OnMouseMove, - this, &ManagedDisplayWidget::InnerWidgetMouseMove); + inner_widget_->installEventFilter(this); // Create OpenGL renderer attached_renderer_ = new OpenGLRenderer(this); + + // Create widget wrapper for OpenGL window +#ifdef USE_QOPENGLWINDOW + wrapper_ = QWidget::createWindowContainer(static_cast(inner_widget_)); +#else + wrapper_ = inner_widget_; +#endif + layout->addWidget(wrapper_); } else { inner_widget_ = nullptr; + wrapper_ = nullptr; } - - layout->addWidget(inner_widget_); } ManagedDisplayWidget::~ManagedDisplayWidget() @@ -253,6 +259,22 @@ void ManagedDisplayWidget::doneCurrent() } } +QPaintDevice *ManagedDisplayWidget::paint_device() const +{ + if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { + return static_cast(inner_widget_); + } else { + return nullptr; + } +} + +void ManagedDisplayWidget::SetInnerMouseTracking(bool e) +{ + if (wrapper_) { + wrapper_->setMouseTracking(e); + } +} + void ManagedDisplayWidget::update() { if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { @@ -260,6 +282,42 @@ void ManagedDisplayWidget::update() } } +bool ManagedDisplayWidget::eventFilter(QObject *o, QEvent *e) +{ + if (o != inner_widget_) { + return super::eventFilter(o, e); + } + + switch (e->type()) { + case QEvent::FocusIn: + // HACK: QWindow focus isn't accounted for in QApplication::focusChanged, so we handle it + // manually here. + PanelManager::instance()->FocusChanged(nullptr, this); + break; + case QEvent::ContextMenu: + { + QContextMenuEvent *ctx = static_cast(e); + emit customContextMenuRequested(ctx->pos()); + return true; + } + case QEvent::MouseButtonPress: + { + // HACK: QWindows don't seem to receive ContextMenu events on right click (only when pressing + // the menu button on the keyboard) so we handle it manually here + QMouseEvent *ev = static_cast(e); + if (ev->button() == Qt::RightButton) { + emit customContextMenuRequested(ev->pos()); + return true; + } + break; + } + default: + break; + } + + return super::eventFilter(o, e); +} + Menu* ManagedDisplayWidget::GetDisplayMenu(QMenu* parent, bool auto_connect) { QStringList displays = color_manager()->ListAvailableDisplays(); diff --git a/app/widget/manageddisplay/manageddisplay.h b/app/widget/manageddisplay/manageddisplay.h index 94648c22e..644ab0711 100644 --- a/app/widget/manageddisplay/manageddisplay.h +++ b/app/widget/manageddisplay/manageddisplay.h @@ -21,9 +21,15 @@ #ifndef MANAGEDDISPLAYOBJECT_H #define MANAGEDDISPLAYOBJECT_H +//#define USE_QOPENGLWINDOW + #include #include +#ifdef USE_QOPENGLWINDOW +#include +#else #include +#endif #include "node/color/colormanager/colormanager.h" #include "render/renderer.h" @@ -31,24 +37,23 @@ namespace olive { -class ManagedDisplayWidgetOpenGL : public QOpenGLWidget +class ManagedDisplayWidgetOpenGL +#ifdef USE_QOPENGLWINDOW + : public QOpenGLWindow +#else + : public QOpenGLWidget +#endif { Q_OBJECT public: - ManagedDisplayWidgetOpenGL(QWidget* parent = nullptr) : - QOpenGLWidget(parent) - { - } + ManagedDisplayWidgetOpenGL() = default; signals: + // Render signals void OnInit(); - void OnPaint(); - void OnDestroy(); - void OnMouseMove(QMouseEvent* e); - protected: virtual void initializeGL() override { @@ -63,13 +68,6 @@ protected: emit OnPaint(); } - virtual void mouseMoveEvent(QMouseEvent* e) override - { - emit OnMouseMove(e); - - QOpenGLWidget::mouseMoveEvent(e); - } - private slots: void DestroyListener() { @@ -135,6 +133,8 @@ public: */ void update(); + virtual bool eventFilter(QObject *o, QEvent *e) override; + public slots: /** * @brief Replaces the color transform with a new one @@ -159,8 +159,6 @@ signals: void frameSwapped(); - void InnerWidgetMouseMove(QMouseEvent* event); - protected: /** * @brief Provides access to the color processor (nullptr if none is set) @@ -188,11 +186,31 @@ protected: void doneCurrent(); - QWidget* inner_widget() const +#ifdef USE_QOPENGLWINDOW + QWindow* +#else + QWidget* +#endif + inner_widget() const { return inner_widget_; } + /** + * @brief Get inner widget as paint device for QPainter + * + * NOTE: This will be incompatible with QVulkanWindow so functions using it + * will need to be replaced soon. + */ + QPaintDevice *paint_device() const; + + void SetInnerMouseTracking(bool e); + + QRect GetInnerRect() const + { + return wrapper_ ? wrapper_->rect() : QRect(); + } + protected slots: /** * @brief Called whenever the internal rendering context has been created @@ -223,7 +241,12 @@ private: /** * @brief Main drawing surface abstraction */ +#ifdef USE_QOPENGLWINDOW + QWindow* inner_widget_; +#else QWidget* inner_widget_; +#endif + QWidget *wrapper_; /** * @brief Renderer abstraction diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 6dc566e31..6332046ba 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -43,6 +43,7 @@ MenuShared::MenuShared() edit_paste_item_ = Menu::CreateItem(this, "paste", this, &MenuShared::PasteTriggered, tr("Ctrl+V")); edit_paste_insert_item_ = Menu::CreateItem(this, "pasteinsert", this, &MenuShared::PasteInsertTriggered, tr("Ctrl+Shift+V")); edit_duplicate_item_ = Menu::CreateItem(this, "duplicate", this, &MenuShared::DuplicateTriggered, tr("Ctrl+D")); + edit_rename_item_ = Menu::CreateItem(this, "rename", this, &MenuShared::RenameSelectedTriggered, tr("F2")); edit_delete_item_ = Menu::CreateItem(this, "delete", this, &MenuShared::DeleteSelectedTriggered, tr("Del")); edit_ripple_delete_item_ = Menu::CreateItem(this, "rippledelete", this, &MenuShared::RippleDeleteTriggered, tr("Shift+Del")); edit_split_item_ = Menu::CreateItem(this, "split", this, &MenuShared::SplitAtPlayheadTriggered, tr("Ctrl+K")); @@ -131,6 +132,7 @@ void MenuShared::AddItemsForEditMenu(Menu *m, bool for_clips) m->addAction(edit_paste_item_); m->addAction(edit_paste_insert_item_); m->addAction(edit_duplicate_item_); + m->addAction(edit_rename_item_); m->addAction(edit_delete_item_); if (for_clips) { @@ -279,6 +281,11 @@ void MenuShared::DuplicateTriggered() PanelManager::instance()->CurrentlyFocused()->Duplicate(); } +void MenuShared::RenameSelectedTriggered() +{ + PanelManager::instance()->CurrentlyFocused()->RenameSelected(); +} + void MenuShared::EnableDisableTriggered() { PanelManager::instance()->CurrentlyFocused()->ToggleSelectedEnabled(); @@ -291,7 +298,7 @@ void MenuShared::NestTriggered() void MenuShared::DefaultTransitionTriggered() { - qDebug() << "FIXME: Stub"; + PanelManager::instance()->MostRecentlyFocused()->AddDefaultTransitionsToSelected(); } void MenuShared::TimecodeDisplayTriggered() @@ -333,6 +340,7 @@ void MenuShared::Retranslate() edit_paste_item_->setText(tr("&Paste")); edit_paste_insert_item_->setText(tr("Paste Insert")); edit_duplicate_item_->setText(tr("Duplicate")); + edit_rename_item_->setText(tr("Rename")); edit_delete_item_->setText(tr("Delete")); edit_ripple_delete_item_->setText(tr("Ripple Delete")); edit_split_item_->setText(tr("Split")); diff --git a/app/widget/menu/menushared.h b/app/widget/menu/menushared.h index 8935457ca..61a544e53 100644 --- a/app/widget/menu/menushared.h +++ b/app/widget/menu/menushared.h @@ -72,6 +72,7 @@ private: QAction* edit_paste_item_; QAction* edit_paste_insert_item_; QAction* edit_duplicate_item_; + QAction* edit_rename_item_; QAction* edit_delete_item_; QAction* edit_ripple_delete_item_; QAction* edit_split_item_; @@ -130,6 +131,8 @@ private slots: void DuplicateTriggered(); + void RenameSelectedTriggered(); + void EnableDisableTriggered(); void NestTriggered(); diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 99a4cd95f..3177da6a5 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -29,7 +29,6 @@ #include "common/functiontimer.h" #include "common/timecodefunctions.h" #include "node/output/viewer/viewer.h" -#include "node/project/serializer/serializer.h" #include "widget/nodeparamview/nodeparamviewundo.h" #include "widget/nodeview/nodeviewundo.h" #include "widget/timeruler/timeruler.h" @@ -85,6 +84,7 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : NodeParamViewItemTitleBar *title_bar = static_cast(c->titleBarWidget()); if (i == Track::kVideo || i == Track::kAudio) { + c->SetEffectType(static_cast(i)); title_bar->SetAddEffectButtonVisible(true); title_bar->SetText(tr("%1 Nodes").arg(Footage::GetStreamTypeName(static_cast(i)))); } else { @@ -131,6 +131,7 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : connect(keyframe_view_, &KeyframeView::TimeChanged, ruler(), &TimeRuler::SetTime); connect(keyframe_view_, &KeyframeView::TimeChanged, this, &NodeParamView::SetTime); connect(keyframe_view_, &KeyframeView::Dragged, this, &NodeParamView::KeyframeViewDragged); + connect(keyframe_view_, &KeyframeView::Released, this, &NodeParamView::KeyframeViewReleased); // Connect keyframe view scaling to this connect(keyframe_view_, &KeyframeView::ScaleChanged, this, &NodeParamView::SetScale); @@ -433,7 +434,7 @@ void NodeParamView::DeleteSelected() Node *n = item->GetNode(); Node *node_being_deleted = n; - Node *connected_to_effect_input = n; + Node *connected_to_effect_input = nullptr; while (true) { if (node_being_deleted->GetEffectInput().IsValid()) { @@ -609,26 +610,24 @@ bool NodeParamView::Paste() } } + return Paste(this, std::bind(&NodeParamView::GenerateExistingPasteMap, this, std::placeholders::_1)); +} + +bool NodeParamView::Paste(QWidget *parent, std::function(const ProjectSerializer::Result &)> get_existing_map_function) +{ ProjectSerializer::Result res = ProjectSerializer::Paste(QStringLiteral("nodes")); if (res.GetLoadedNodes().isEmpty()) { return false; } // Determine if any nodes of this type are already in the editor - QVector ignore_nodes; - QMap existing_nodes; - for (Node *n : res.GetLoadedNodes()) { - if (Node *existing = GetNodeWithIDAndIgnoreList(n->id(), ignore_nodes)) { - existing_nodes.insert(existing, n); - ignore_nodes.append(existing); - } - } + QHash existing_nodes = get_existing_map_function(res); QVector nodes_to_paste_as_new = res.GetLoadedNodes(); MultiUndoCommand *command = new MultiUndoCommand(); if (!existing_nodes.empty()) { - QMessageBox b(this); + QMessageBox b(parent); b.setWindowTitle(tr("Paste Nodes")); QStringList node_names; @@ -858,18 +857,29 @@ void NodeParamView::ToggleSelect(NodeParamViewItem *item) new_sel.append(item); SetSelectedNodes(new_sel, false); - if (item->GetNode()->HasGizmos() || !new_sel.contains(focused_node_)) { - if (item->GetNode()->HasGizmos()) { - focused_node_ = item; - } else { - focused_node_ = nullptr; - } + if (!new_sel.contains(focused_node_)) { + // This node gets sent to both the curve editor and viewer, so we focus it even if it has + // no gizmos + focused_node_ = item; emit FocusedNodeChanged(focused_node_ ? focused_node_->GetNode() : nullptr); } } } +QHash NodeParamView::GenerateExistingPasteMap(const ProjectSerializer::Result &r) +{ + QVector ignore_nodes; + QHash existing_nodes; + for (Node *n : r.GetLoadedNodes()) { + if (Node *existing = GetNodeWithIDAndIgnoreList(n->id(), ignore_nodes)) { + existing_nodes.insert(existing, n); + ignore_nodes.append(existing); + } + } + return existing_nodes; +} + void NodeParamView::UpdateGlobalScrollBar() { if (keyframe_view_) { @@ -928,7 +938,12 @@ void NodeParamView::KeyframeViewDragged(int x, int y) { Q_UNUSED(y) - QMetaObject::invokeMethod(this, "CatchUpScrollToPoint", Qt::QueuedConnection, Q_ARG(int, x)); + SetCatchUpScrollValue(x); +} + +void NodeParamView::KeyframeViewReleased() +{ + StopCatchUpScrollTimer(); } void NodeParamView::UpdateElementY() diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index ba918d373..1b78055a9 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -25,6 +25,7 @@ #include #include "node/node.h" +#include "node/project/serializer/serializer.h" #include "nodeparamviewcontext.h" #include "nodeparamviewdockarea.h" #include "nodeparamviewitem.h" @@ -75,6 +76,7 @@ public: virtual bool CopySelected(bool cut) override; virtual bool Paste() override; + static bool Paste(QWidget *parent, std::function(const ProjectSerializer::Result &)> get_existing_map_function); public slots: void SetContexts(const QVector &contexts); @@ -107,6 +109,11 @@ protected: return keyframe_view_ ? &keyframe_view_->GetSelectedKeyframes() : nullptr; } + virtual const TimeTargetObject *GetKeyframeTimeTarget() const override + { + return keyframe_view_; + } + private: void UpdateItemTime(const rational &time); @@ -129,6 +136,8 @@ private: void ToggleSelect(NodeParamViewItem *item); + QHash GenerateExistingPasteMap(const ProjectSerializer::Result &r); + KeyframeView* keyframe_view_; QVector context_items_; @@ -165,6 +174,7 @@ private slots: //void FocusChanged(QWidget *old, QWidget *now); void KeyframeViewDragged(int x, int y); + void KeyframeViewReleased(); void NodeAddedToContext(Node *n); diff --git a/app/widget/nodeparamview/nodeparamviewcontext.cpp b/app/widget/nodeparamview/nodeparamviewcontext.cpp index 6e83a617b..1d3a22253 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.cpp +++ b/app/widget/nodeparamview/nodeparamviewcontext.cpp @@ -31,7 +31,8 @@ namespace olive { #define super NodeParamViewItemBase NodeParamViewContext::NodeParamViewContext(QWidget *parent) : - super(parent) + super(parent), + type_(Track::kNone) { QWidget *body = new QWidget(); QHBoxLayout *body_layout = new QHBoxLayout(body); @@ -126,13 +127,30 @@ void NodeParamViewContext::SetTime(const rational &time) } } +void NodeParamViewContext::SetEffectType(Track::Type type) +{ + type_ = type; +} + void NodeParamViewContext::Retranslate() { } void NodeParamViewContext::AddEffectButtonClicked() { - Menu *m = NodeFactory::CreateMenu(this, false, Node::kCategoryUnknown, Node::kVideoEffect); + Node::Flag flag = Node::kNone; + + if (type_ == Track::kVideo) { + flag = Node::kVideoEffect; + } else { + flag = Node::kAudioEffect; + } + + if (flag == Node::kNone) { + return; + } + + Menu *m = NodeFactory::CreateMenu(this, false, Node::kCategoryUnknown, flag); connect(m, &Menu::triggered, this, &NodeParamViewContext::AddEffectMenuItemTriggered); m->exec(QCursor::pos()); delete m; diff --git a/app/widget/nodeparamview/nodeparamviewcontext.h b/app/widget/nodeparamview/nodeparamviewcontext.h index 256f772aa..11574e2a5 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.h +++ b/app/widget/nodeparamview/nodeparamviewcontext.h @@ -64,6 +64,8 @@ public: void SetTime(const rational &time); + void SetEffectType(Track::Type type); + signals: void AboutToDeleteItem(NodeParamViewItem *item); @@ -88,6 +90,8 @@ private: QVector items_; + Track::Type type_; + private slots: void AddEffectButtonClicked(); diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 603072e1a..7b0e82fd3 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -304,7 +304,7 @@ void NodeParamViewItemBody::Retranslate() if (ic.IsArray() && ic.element() >= 0) { // Make the label the array index - i.value().main_label->setText(tr("%1:").arg(ic.element())); + i.value().main_label->setText(tr("%1:").arg(ic.element() + ic.GetProperty(QStringLiteral("arraystart")).toInt())); } else { // Set to the input's name i.value().main_label->setText(tr("%1:").arg(ic.name())); diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 7e3f9e138..cf1610b02 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -779,10 +779,6 @@ void NodeView::ShowContextMenu(const QPoint &pos) if (itemAt(pos) && !selected.isEmpty()) { - // Label node action - QAction* label_action = m.addAction(tr("Label")); - connect(label_action, &QAction::triggered, this, &NodeView::LabelSelectedNodes); - // Grouping if (selected.size() == 1 && dynamic_cast(selected.first()->GetNode())) { QAction *ungroup_action = m.addAction(tr("Ungroup")); @@ -818,10 +814,6 @@ void NodeView::ShowContextMenu(const QPoint &pos) m.addSeparator(); - AddSetScrollZoomsByDefaultActionToMenu(&m); - - m.addSeparator(); - Menu* direction_menu = new Menu(tr("Direction"), &m); m.addMenu(direction_menu); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 15a5ddc28..bf2d0ee0d 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -112,6 +112,8 @@ public slots: void CenterOnNode(olive::Node *n); + void LabelSelectedNodes(); + signals: void NodesSelected(const QVector& nodes); @@ -275,8 +277,6 @@ private slots: void ShowNodeProperties(); - void LabelSelectedNodes(); - void ItemAboutToBeDeleted(NodeViewItem *item); void CloseOverlay(); diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 9a04ee961..7c6a78cee 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -373,7 +373,7 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti if (element_ == -1) { node_name = node_->GetInputName(input_); } else { - node_name = QString::number(element_); + node_name = QString::number(element_ + node_->GetInputProperty(input_, QStringLiteral("arraystart")).toInt()); } } @@ -581,7 +581,7 @@ QPointF NodeViewItem::GetInputPoint() const QPointF NodeViewItem::GetOutputPoint() const { QPointF p = output_connector_->scenePos(); - QRectF r = output_connector_->boundingRect(); + QRectF r = output_connector_->polygon().boundingRect(); switch (flow_dir_) { case NodeViewCommon::kLeftToRight: @@ -628,7 +628,7 @@ void NodeViewItem::UpdateNodePosition() void NodeViewItem::UpdateInputConnectorPosition() { - QRectF output_rect = input_connector_->boundingRect(); + QRectF output_rect = input_connector_->polygon().boundingRect(); NodeViewCommon::FlowDirection using_flow_dir = flow_dir_; diff --git a/app/widget/nodeview/nodeviewitemconnector.cpp b/app/widget/nodeview/nodeviewitemconnector.cpp index de723a36c..d8ad33fbc 100644 --- a/app/widget/nodeview/nodeviewitemconnector.cpp +++ b/app/widget/nodeview/nodeviewitemconnector.cpp @@ -81,4 +81,19 @@ void NodeViewItemConnector::SetFlowDirection(NodeViewCommon::FlowDirection dir) setPolygon(p); } +QPainterPath NodeViewItemConnector::shape() const +{ + // Yes, we skip QGraphicsPolygonItem because it adds the polygon. QGraphicsItem adds the + // boundingRect which we modify below + return QGraphicsItem::shape(); // clazy:exclude=skipped-base-method +} + +QRectF NodeViewItemConnector::boundingRect() const +{ + QRectF b = this->polygon().boundingRect(); + const int radius = QFontMetrics(QFont()).height()/2; + b.adjust(-radius, -radius, radius, radius); + return b; +} + } diff --git a/app/widget/nodeview/nodeviewitemconnector.h b/app/widget/nodeview/nodeviewitemconnector.h index b309f05e0..b207cb536 100644 --- a/app/widget/nodeview/nodeviewitemconnector.h +++ b/app/widget/nodeview/nodeviewitemconnector.h @@ -39,6 +39,9 @@ public: return output_; } + virtual QPainterPath shape() const override; + virtual QRectF boundingRect() const override; + private: bool output_; diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index 608c0ba5f..54df750c6 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -122,6 +122,8 @@ public: virtual void GoToNextCut(){} + virtual void RenameSelected(){} + virtual void DeleteSelected(){} virtual void RippleDelete(){} diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 2236d4b84..bdf853d99 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include "task/taskmanager.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" +#include "widget/nodeparamview/nodeparamviewundo.h" #include "window/mainwindow/mainwindow.h" #include "window/mainwindow/mainwindowundo.h" #include "widget/nodeview/nodeviewundo.h" @@ -90,10 +92,6 @@ ProjectExplorer::ProjectExplorer(QWidget *parent) : // Set default icon size SizeChangedSlot(kProjectIconSizeDefault); - // Set rename timer timeout - rename_timer_.setInterval(500); - connect(&rename_timer_, &QTimer::timeout, this, &ProjectExplorer::RenameTimerSlot); - connect(tree_view_, &ProjectExplorerTreeView::customContextMenuRequested, this, &ProjectExplorer::ShowContextMenu); connect(list_view_, &ProjectExplorerListView::customContextMenuRequested, this, &ProjectExplorer::ShowContextMenu); connect(icon_view_, &ProjectExplorerIconView::customContextMenuRequested, this, &ProjectExplorer::ShowContextMenu); @@ -135,8 +133,7 @@ void ProjectExplorer::Edit(Node *item) void ProjectExplorer::AddView(QAbstractItemView *view) { view->setModel(&sort_model_); - view->setEditTriggers(QAbstractItemView::NoEditTriggers); - connect(view, &QAbstractItemView::clicked, this, &ProjectExplorer::ItemClickedSlot); + view->setEditTriggers(QAbstractItemView::SelectedClicked); connect(view, &QAbstractItemView::doubleClicked, this, &ProjectExplorer::ItemDoubleClickedSlot); connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ProjectExplorer::ViewSelectionChanged); connect(view, SIGNAL(DoubleClickedEmptyArea()), this, SLOT(ViewEmptyAreaDoubleClickedSlot())); @@ -145,9 +142,6 @@ void ProjectExplorer::AddView(QAbstractItemView *view) void ProjectExplorer::BrowseToFolder(const QModelIndex &index) { - // Make sure any rename timers are stopped - rename_timer_.stop(); - // Set appropriate views to this index icon_view_->setRootIndex(index); list_view_->setRootIndex(index); @@ -262,53 +256,19 @@ QAbstractItemView *ProjectExplorer::CurrentView() const return static_cast(stacked_widget_->currentWidget()); } -void ProjectExplorer::ItemClickedSlot(const QModelIndex &index) -{ - if (index.isValid()) { - if (CurrentView()->selectionModel()->selectedRows().size() == 1) { - if (clicked_index_ == index) { - // The item has been clicked more than once, start a timer for renaming - rename_timer_.start(); - } else { - // Cache this index for the next click - clicked_index_ = index; - - // If the rename timer had started, stop it now - rename_timer_.stop(); - } - } else { - clicked_index_ = QModelIndex(); - rename_timer_.stop(); - } - } else { - // Stop the rename timer - rename_timer_.stop(); - } -} - void ProjectExplorer::ViewEmptyAreaDoubleClickedSlot() { - // Ensure no attempts to rename are made - clicked_index_ = QModelIndex(); - rename_timer_.stop(); - emit DoubleClickedItem(nullptr); } void ProjectExplorer::ItemDoubleClickedSlot(const QModelIndex &index) { - // Ensure no attempts to rename are made - clicked_index_ = QModelIndex(); - rename_timer_.stop(); - // Retrieve source item from index Node* i = static_cast(sort_model_.mapToSource(index).internalPointer()); // If the item is a folder, browse to it if (dynamic_cast(i) && (view_type() == ProjectToolbar::ListView || view_type() == ProjectToolbar::IconView)) { - BrowseToFolder(index); - } // Emit a signal @@ -333,16 +293,12 @@ void ProjectExplorer::DirUpSlot() } } -void ProjectExplorer::RenameTimerSlot() +void ProjectExplorer::RenameSelectedItem() { - // Start editing this index - CurrentView()->edit(clicked_index_); - - // Reset clicked index state - clicked_index_ = QModelIndex(); - - // Stop rename timer - rename_timer_.stop(); + auto indexes = CurrentView()->selectionModel()->selectedRows(); + if (!indexes.empty()) { + CurrentView()->edit(indexes.first()); + } } void ProjectExplorer::ShowContextMenu() @@ -392,6 +348,9 @@ void ProjectExplorer::ShowContextMenu() QAction* reveal_action = menu.addAction(reveal_text); connect(reveal_action, &QAction::triggered, this, &ProjectExplorer::RevealSelectedFootage); + QAction *replace_action = menu.addAction(tr("Replace Footage")); + connect(replace_action, &QAction::triggered, this, &ProjectExplorer::ReplaceSelectedFootage); + } menu.addSeparator(); @@ -439,6 +398,16 @@ void ProjectExplorer::ShowContextMenu() Q_UNUSED(all_items_are_footage_or_sequence) + if (context_menu_items_.size() == 1) { + menu.addSeparator(); + + auto rename_action = menu.addAction(tr("Rename")); + connect(rename_action, &QAction::triggered, this, &ProjectExplorer::RenameSelectedItem); + } + + auto delete_action = menu.addAction(tr("Delete")); + connect(delete_action, &QAction::triggered, this, &ProjectExplorer::DeleteSelected); + if (context_menu_items_.size() == 1) { menu.addSeparator(); @@ -497,6 +466,17 @@ void ProjectExplorer::RevealSelectedFootage() #endif } +void ProjectExplorer::ReplaceSelectedFootage() +{ + Footage* footage = static_cast(context_menu_items_.first()); + + QString file = QFileDialog::getOpenFileName(this, tr("Replace Footage")); + if (!file.isEmpty()) { + auto c = new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(footage, Footage::kFilenameInput)), file); + Core::instance()->undo_stack()->push(c); + } +} + void ProjectExplorer::OpenContextMenuItemInNewTab() { Core::instance()->main_window()->FolderOpen(project(), static_cast(context_menu_items_.first()), false); @@ -678,9 +658,11 @@ void ProjectExplorer::DeleteSelected() } } -bool ProjectExplorer::SelectItem(Node *n) +bool ProjectExplorer::SelectItem(Node *n, bool deselect_all_first) { - DeselectAll(); + if (deselect_all_first) { + DeselectAll(); + } QModelIndex index = model_.CreateIndexFromItem(n); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 30200a757..d69f1b8e3 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -85,13 +85,15 @@ public: void DeleteSelected(); - bool SelectItem(Node *n); + bool SelectItem(Node *n, bool deselect_all_first = true); public slots: void set_view_type(ProjectToolbar::ViewType type); void Edit(Node* item); + void RenameSelectedItem(); + signals: /** * @brief Emitted when an Item is double clicked @@ -160,15 +162,9 @@ private: QSortFilterProxyModel sort_model_; ProjectViewModel model_; - QModelIndex clicked_index_; - - QTimer rename_timer_; - QVector context_menu_items_; private slots: - void ItemClickedSlot(const QModelIndex& index); - void ViewEmptyAreaDoubleClickedSlot(); void ItemDoubleClickedSlot(const QModelIndex& index); @@ -177,14 +173,14 @@ private slots: void DirUpSlot(); - void RenameTimerSlot(); - void ShowContextMenu(); void ShowItemPropertiesDialog(); void RevealSelectedFootage(); + void ReplaceSelectedFootage(); + void OpenContextMenuItemInNewTab(); void OpenContextMenuItemInNewWindow(); diff --git a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp index 4672657bb..2d4f98ea2 100644 --- a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp +++ b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp @@ -31,36 +31,51 @@ namespace olive { ResizableTimelineScrollBar::ResizableTimelineScrollBar(QWidget* parent) : ResizableScrollBar(parent), - points_(nullptr), + markers_(nullptr), + workarea_(nullptr), scale_(1.0) { } ResizableTimelineScrollBar::ResizableTimelineScrollBar(Qt::Orientation orientation, QWidget* parent) : ResizableScrollBar(orientation, parent), - points_(nullptr), + markers_(nullptr), + workarea_(nullptr), scale_(1.0) { } -void ResizableTimelineScrollBar::ConnectTimelinePoints(TimelinePoints *points) +void ResizableTimelineScrollBar::ConnectMarkers(TimelineMarkerList *markers) { - if (points_) { - disconnect(points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&ResizableTimelineScrollBar::update)); - disconnect(points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); - disconnect(points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); - disconnect(points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); - disconnect(points_->markers(), &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); + if (markers_) { + disconnect(markers_, &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); + disconnect(markers_, &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); + disconnect(markers_, &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); } - points_ = points; + markers_ = markers; - if (points_) { - connect(points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&ResizableTimelineScrollBar::update)); - connect(points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); - connect(points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); - connect(points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); - connect(points_->markers(), &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); + if (markers_) { + connect(markers_, &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); + connect(markers_, &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); + connect(markers_, &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); + } + + update(); +} + +void ResizableTimelineScrollBar::ConnectWorkArea(TimelineWorkArea *workarea) +{ + if (workarea_) { + disconnect(workarea_, &TimelineWorkArea::RangeChanged, this, static_cast(&ResizableTimelineScrollBar::update)); + disconnect(workarea_, &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); + } + + workarea_ = workarea; + + if (workarea_) { + connect(workarea_, &TimelineWorkArea::RangeChanged, this, static_cast(&ResizableTimelineScrollBar::update)); + connect(workarea_, &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); } update(); @@ -77,9 +92,8 @@ void ResizableTimelineScrollBar::paintEvent(QPaintEvent *event) { ResizableScrollBar::paintEvent(event); - if (points_ - && !timebase().isNull() - && (points_->workarea()->enabled() || !points_->markers()->empty())) { + if (!timebase().isNull() && ((workarea_ && workarea_->enabled()) || (markers_ && !markers_->empty()))) { + // Draw workarea QStyleOptionSlider opt; initStyleOption(&opt); @@ -87,20 +101,20 @@ void ResizableTimelineScrollBar::paintEvent(QPaintEvent *event) QStyle::SC_ScrollBarGroove, this); double ratio = scale_ * double(gr.width()) / double(this->maximum() + gr.width()); - QPainter p(this); - if (points_->workarea()->enabled()) { + if (workarea_ && workarea_->enabled()) { + QColor workarea_color(this->palette().highlight().color()); workarea_color.setAlpha(128); - qint64 in = qMax(qint64(0), qRound64(ratio * TimeToScene(points_->workarea()->in()))); + qint64 in = qMax(qint64(0), qRound64(ratio * TimeToScene(workarea_->in()))); qint64 out; - if (points_->workarea()->out() == RATIONAL_MAX) { + if (workarea_->out() == RATIONAL_MAX) { out = gr.width(); } else { - out = qMin(qint64(gr.width()), qRound64(ratio * TimeToScene(points_->workarea()->out()))); + out = qMin(qint64(gr.width()), qRound64(ratio * TimeToScene(workarea_->out()))); } qint64 length = qMax(qint64(1), out-in); @@ -112,8 +126,9 @@ void ResizableTimelineScrollBar::paintEvent(QPaintEvent *event) workarea_color); } - if (!points_->markers()->empty()) { - for (auto it=points_->markers()->cbegin(); it!=points_->markers()->cend(); it++) { + // Draw markers + if (markers_ && !markers_->empty()) { + for (auto it=markers_->cbegin(); it!=markers_->cend(); it++) { TimelineMarker* marker = *it; QColor marker_color = ColorCoding::GetColor(marker->color()).toQColor(); diff --git a/app/widget/resizablescrollbar/resizabletimelinescrollbar.h b/app/widget/resizablescrollbar/resizabletimelinescrollbar.h index dcd8b10fc..93d7ec197 100644 --- a/app/widget/resizablescrollbar/resizabletimelinescrollbar.h +++ b/app/widget/resizablescrollbar/resizabletimelinescrollbar.h @@ -22,7 +22,8 @@ #define RESIZABLETIMELINESCROLLBAR_H #include "resizablescrollbar.h" -#include "timeline/timelinepoints.h" +#include "timeline/timelinemarker.h" +#include "timeline/timelineworkarea.h" #include "widget/timebased/timescaledobject.h" namespace olive { @@ -34,7 +35,8 @@ public: ResizableTimelineScrollBar(QWidget* parent = nullptr); ResizableTimelineScrollBar(Qt::Orientation orientation, QWidget* parent = nullptr); - void ConnectTimelinePoints(TimelinePoints* points); + void ConnectMarkers(TimelineMarkerList *markers); + void ConnectWorkArea(TimelineWorkArea *workarea); void SetScale(double d); @@ -42,7 +44,9 @@ protected: virtual void paintEvent(QPaintEvent* event) override; private: - TimelinePoints* points_; + TimelineMarkerList* markers_; + + TimelineWorkArea* workarea_; double scale_; diff --git a/app/widget/scope/histogram/histogram.cpp b/app/widget/scope/histogram/histogram.cpp index 26b92b711..a0ba2eaab 100644 --- a/app/widget/scope/histogram/histogram.cpp +++ b/app/widget/scope/histogram/histogram.cpp @@ -91,7 +91,7 @@ void HistogramScope::DrawScope(TexturePtr managed_tex, QVariant pipeline) renderer()->Blit(pipeline_secondary_, shader_job, texture_row_sums_->params()); // Draw line overlays - QPainter p(inner_widget()); + QPainter p(paint_device()); QFont font = p.font(); font.setPixelSize(10); QFontMetrics font_metrics = QFontMetrics(font); diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index a0b11769c..bb6902a64 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -85,7 +85,7 @@ void WaveformScope::DrawScope(TexturePtr managed_tex, QVariant pipeline) float waveform_end_dim_x = (width() - 1.0) - waveform_start_dim_x; // Draw line overlays - QPainter p(inner_widget()); + QPainter p(paint_device()); QFont font; font.setPixelSize(10); QFontMetrics font_metrics = QFontMetrics(font); diff --git a/app/widget/slider/base/numericsliderbase.cpp b/app/widget/slider/base/numericsliderbase.cpp index 4f0397967..d0a3e78f2 100644 --- a/app/widget/slider/base/numericsliderbase.cpp +++ b/app/widget/slider/base/numericsliderbase.cpp @@ -54,15 +54,15 @@ void NumericSliderBase::LabelPressed() { // Generate width hint drag_ladder_ = new SliderLadder(drag_multiplier_, ladder_element_count_, GetFormattedValueToString(99999999)); + connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &NumericSliderBase::LadderDragged); + connect(drag_ladder_, &SliderLadder::Released, this, &NumericSliderBase::LadderReleased); + drag_ladder_->SetValue(GetFormattedValueToString()); drag_ladder_->show(); drag_start_value_ = GetValueInternal(); QMetaObject::invokeMethod(this, "RepositionLadder", Qt::QueuedConnection); - - connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &NumericSliderBase::LadderDragged); - connect(drag_ladder_, &SliderLadder::Released, this, &NumericSliderBase::LadderReleased); } void NumericSliderBase::LadderDragged(int value, double multiplier) diff --git a/app/widget/slider/base/sliderladder.cpp b/app/widget/slider/base/sliderladder.cpp index 245c32ab9..5c927f61a 100644 --- a/app/widget/slider/base/sliderladder.cpp +++ b/app/widget/slider/base/sliderladder.cpp @@ -77,6 +77,14 @@ SliderLadder::SliderLadder(double drag_multiplier, int nb_outer_values, QString drag_timer_.setInterval(10); connect(&drag_timer_, &QTimer::timeout, this, &SliderLadder::TimerUpdate); + screen_ = nullptr; + foreach (QScreen *screen, qApp->screens()) { + if (screen->geometry().contains(QCursor::pos())) { + screen_ = screen; + break; + } + } + if (UsingLadders()) { drag_start_x_ = -1; wrap_count_ = 0; @@ -120,7 +128,7 @@ void SliderLadder::SetValue(const QString &s) void SliderLadder::StartListeningToMouseInput() { - drag_timer_.start(); + QMetaObject::invokeMethod(&drag_timer_, "start", Qt::QueuedConnection); } void SliderLadder::mouseReleaseEvent(QMouseEvent *event) @@ -198,21 +206,20 @@ void SliderLadder::TimerUpdate() emit DraggedByValue(now_pos - drag_start_x_, elements_.at(active_element_)->GetMultiplier()); // Determine if cursor is at desktop edge, if so wrap around to other side - int left = 0; - int right = 0; - foreach (QScreen *screen, qApp->screens()) { - left = qMin(left, screen->geometry().left()); - right = qMax(right, screen->geometry().right()); - } - if (now_pos == left || now_pos == right) { - if (now_pos == left) { - wrap_count_--; - now_pos = right-1; - } else { - wrap_count_++; - now_pos = left+1; + if (screen_) { + int left = screen_->geometry().left(); + int right = screen_->geometry().right(); + int width = right - left; + if (now_pos <= left || now_pos >= right) { + if (now_pos <= left) { + wrap_count_--; + now_pos += width; + } else { + wrap_count_++; + now_pos -= width; + } + QCursor::setPos(now_pos, QCursor::pos().y()); } - QCursor::setPos(now_pos, QCursor::pos().y()); } drag_start_x_ = now_pos; diff --git a/app/widget/slider/base/sliderladder.h b/app/widget/slider/base/sliderladder.h index 8426c1f22..16c1307ad 100644 --- a/app/widget/slider/base/sliderladder.h +++ b/app/widget/slider/base/sliderladder.h @@ -95,6 +95,8 @@ private: QTimer drag_timer_; + QScreen *screen_; + private slots: void TimerUpdate(); diff --git a/app/widget/standardcombos/videodividercombobox.h b/app/widget/standardcombos/videodividercombobox.h index 05c068458..caaeb74da 100644 --- a/app/widget/standardcombos/videodividercombobox.h +++ b/app/widget/standardcombos/videodividercombobox.h @@ -35,15 +35,7 @@ public: 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); + this->addItem(VideoParams::GetNameForDivider(d), d); } } diff --git a/app/widget/timebased/timebasedview.cpp b/app/widget/timebased/timebasedview.cpp index fe54459e2..310df8866 100644 --- a/app/widget/timebased/timebasedview.cpp +++ b/app/widget/timebased/timebasedview.cpp @@ -109,20 +109,24 @@ void TimeBasedView::ZoomIntoCursorPosition(QWheelEvent *event, double scale_mult } if (!only_vertical) { + double old_scroll = horizontalScrollBar()->value(); + double old_scale = GetScale(); emit ScaleChanged(old_scale * scale_multiplier); // Use GetScale so that if this value was clamped, we don't erroneously use an unclamped value - int new_x_scroll = qRound(double(cursor_pos.x() + horizontalScrollBar()->value()) / old_scale * GetScale() - cursor_pos.x()); + int new_x_scroll = qRound((cursor_pos.x() + old_scroll) / old_scale * GetScale() - cursor_pos.x()); horizontalScrollBar()->setValue(new_x_scroll); } if (!only_horizontal) { + double old_y_scroll = verticalScrollBar()->value(); + double old_y_scale = GetYScale(); SetYScale(old_y_scale * scale_multiplier); // Use GetYScale so that if this value was clamped, we don't erroneously use an unclamped value - int new_y_scroll = qRound(double(cursor_pos.y() + verticalScrollBar()->value()) / old_y_scale * GetYScale() - cursor_pos.y()); + int new_y_scroll = qRound((cursor_pos.y() + old_y_scroll) / old_y_scale * GetYScale() - cursor_pos.y()); verticalScrollBar()->setValue(new_y_scroll); } } diff --git a/app/widget/timebased/timebasedviewselectionmanager.h b/app/widget/timebased/timebasedviewselectionmanager.h index 7647ddc06..6d9e6962f 100644 --- a/app/widget/timebased/timebasedviewselectionmanager.h +++ b/app/widget/timebased/timebasedviewselectionmanager.h @@ -26,10 +26,12 @@ #include #include +#include "common/qtutils.h" #include "common/rational.h" #include "common/timecodefunctions.h" #include "timebasedview.h" #include "timebasedwidget.h" +#include "widget/timetarget/timetarget.h" namespace olive { @@ -158,8 +160,14 @@ public: return !dragging_.empty(); } - void DragStart(T *initial_item, QMouseEvent *event) + void DragStart(T *initial_item, QMouseEvent *event, TimeTargetObject *target = nullptr) { + if (event->button() != Qt::LeftButton) { + return; + } + + time_target_ = target; + initial_drag_item_ = initial_item; dragging_.resize(selected_.size()); @@ -170,6 +178,13 @@ public: snap_points_.resize(selected_.size()); } + if (target) { + time_targets_.resize(snap_points_.size()); + memset(time_targets_.data(), 0, time_targets_.size() * sizeof(Node*)); + } else { + time_targets_.clear(); + } + for (size_t i=0; itime().in(); snap_points_[i] = obj->time().in(); snap_points_[i+selected_.size()] = obj->time().out(); + + if (target) { + time_targets_[i] = time_targets_[i+selected_.size()] = QtUtils::GetParentOfType(obj); + } } else { dragging_[i] = obj->time(); snap_points_[i] = obj->time(); + + if (target) { + time_targets_[i] = QtUtils::GetParentOfType(obj); + } } } @@ -188,8 +211,18 @@ public: void SnapPoints(rational *movement) { + std::vector copy = snap_points_; + + if (time_target_) { + for (size_t i=0; iGetAdjustedTime(parent, time_target_->GetTimeTarget(), copy[i], false); + } + } + } + if (Core::instance()->snapping() && view_->GetSnapService()) { - view_->GetSnapService()->SnapPoint(snap_points_, movement, snap_mask_); + view_->GetSnapService()->SnapPoint(copy, movement, snap_mask_); } } @@ -287,6 +320,11 @@ public: QToolTip::showText(QCursor::pos(), tip); } + void DragMove(QMouseEvent *event, TimeTargetObject *target) + { + return DragMove(event, QString(), target); + } + void DragStop(MultiUndoCommand *command) { QToolTip::hideText(); @@ -399,6 +437,7 @@ private: std::vector dragging_; std::vector snap_points_; + std::vector time_targets_; T *initial_drag_item_; @@ -412,6 +451,8 @@ private: TimeBasedWidget::SnapMask snap_mask_; + TimeTargetObject *time_target_; + }; } diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index a812e6c69..a4beb3bfa 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -39,17 +39,24 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_statu viewer_node_(nullptr), auto_max_scrollbar_(false), toggle_show_all_(false), - auto_set_timebase_(true) + auto_set_timebase_(true), + workarea_(nullptr), + markers_(nullptr) { ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this); ConnectTimelineView(ruler_, true); ruler()->SetSnapService(this); + connect(ruler(), &TimeRuler::DragReleased, this, static_cast(&TimeBasedWidget::StopCatchUpScrollTimer)); scrollbar_ = new ResizableTimelineScrollBar(Qt::Horizontal, this); connect(scrollbar_, &ResizableScrollBar::ResizeBegan, this, &TimeBasedWidget::ScrollBarResizeBegan); connect(scrollbar_, &ResizableScrollBar::ResizeMoved, this, &TimeBasedWidget::ScrollBarResizeMoved); PassWheelEventsToScrollBar(ruler_); + + catchup_scroll_timer_ = new QTimer(this); + catchup_scroll_timer_->setInterval(250); // Hardcoded 1/4 scroll limit value + connect(catchup_scroll_timer_, &QTimer::timeout, this, &TimeBasedWidget::CatchUpTimerTimeout); } void TimeBasedWidget::SetScaleAndCenterOnPlayhead(const double &scale) @@ -98,8 +105,8 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) SetTimebase(rational()); // Disconnect ruler and scrollbar from timeline points - ruler()->ConnectTimelinePoints(nullptr); - scrollbar_->ConnectTimelinePoints(nullptr); + ConnectWorkArea(nullptr); + ConnectMarkers(nullptr); } // Call derivatives @@ -111,8 +118,8 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) connect(viewer_node_, &ViewerOutput::RemovedFromGraph, this, &TimeBasedWidget::ConnectedNodeRemovedFromGraph); // Connect ruler and scrollbar to timeline points - ruler()->ConnectTimelinePoints(viewer_node_->GetTimelinePoints()); - scrollbar_->ConnectTimelinePoints(viewer_node_->GetTimelinePoints()); + ConnectWorkArea(viewer_node_->GetWorkArea()); + ConnectMarkers(viewer_node_->GetMarkers()); // If we're setting the timebase, set it automatically based on the video and audio parameters if (auto_set_timebase_) { @@ -130,6 +137,20 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) emit ConnectedNodeChanged(old, node); } +void TimeBasedWidget::ConnectWorkArea(TimelineWorkArea *workarea) +{ + workarea_ = workarea; + ruler()->SetWorkArea(workarea); + scrollbar_->ConnectWorkArea(workarea); +} + +void TimeBasedWidget::ConnectMarkers(TimelineMarkerList *markers) +{ + markers_ = markers; + ruler()->SetMarkers(markers); + scrollbar_->ConnectMarkers(markers); +} + void TimeBasedWidget::UpdateMaximumScroll() { rational length = (viewer_node_) ? viewer_node_->GetLength() : 0; @@ -201,6 +222,15 @@ void TimeBasedWidget::CatchUpScrollToPoint(int point) PageScrollInternal(point, false); } +void TimeBasedWidget::CatchUpTimerTimeout() +{ + for (auto it=catchup_scroll_values_.cbegin(); it!=catchup_scroll_values_.cend(); it++) { + QScrollBar *sb = it.key(); + const CatchUpScrollData &d = it.value(); + PageScrollInternal(sb, d.maximum, sb->value() + d.value, false); + } +} + void TimeBasedWidget::AutoUpdateTimebase() { rational video_tb = viewer_node_->GetVideoParams().frame_rate_as_time_base(); @@ -285,11 +315,41 @@ void TimeBasedWidget::PassWheelEventsToScrollBar(QObject *object) object->installEventFilter(this); } +void TimeBasedWidget::SetCatchUpScrollValue(QScrollBar *b, int v, int maximum) +{ + CatchUpScrollData &cudata = catchup_scroll_values_[b]; + cudata.value = v; + cudata.maximum = maximum; + + static const qint64 min_cooldown = 100; // Hardcoded 1/10 sec cooldown + if (QDateTime::currentMSecsSinceEpoch() - cudata.last_forced >= min_cooldown) { + QMetaObject::invokeMethod(this, &TimeBasedWidget::CatchUpTimerTimeout, Qt::QueuedConnection); + cudata.last_forced = QDateTime::currentMSecsSinceEpoch(); + } + + if (!catchup_scroll_timer_->isActive()) { + catchup_scroll_timer_->start(); + } +} + +void TimeBasedWidget::SetCatchUpScrollValue(int v) +{ + SetCatchUpScrollValue(scrollbar_, v, ruler()->width()); +} + +void TimeBasedWidget::StopCatchUpScrollTimer(QScrollBar *b) +{ + catchup_scroll_values_.remove(b); + if (catchup_scroll_values_.empty()) { + catchup_scroll_timer_->stop(); + } +} + void TimeBasedWidget::SetTime(const rational &time) { if (UserIsDraggingPlayhead()) { // If the user is dragging the playhead, we will simply nudge over and not use autoscroll rules. - QMetaObject::invokeMethod(this, "CatchUpScrollToPlayhead", Qt::QueuedConnection); + SetCatchUpScrollValue(qRound(TimeToScene(time)) - scrollbar_->value()); } else { // Otherwise, assume we jumped to this out of nowhere and must now autoscroll switch (static_cast(OLIVE_CONFIG("Autoscroll").toInt())) { @@ -374,14 +434,14 @@ void TimeBasedWidget::GoToNextCut() rational closest_cut = RATIONAL_MAX; - foreach (Track* track, sequence->GetTracks()) { + for (Track* track : sequence->GetTracks()) { rational this_track_closest_cut = track->track_length(); if (this_track_closest_cut <= GetTime()) { this_track_closest_cut = RATIONAL_MAX; } - foreach (Block* block, track->Blocks()) { + for (Block* block : track->Blocks()) { if (block->in() > GetTime()) { this_track_closest_cut = block->in(); break; @@ -457,10 +517,10 @@ void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) } MultiUndoCommand* command = new MultiUndoCommand(); - TimelinePoints* points = viewer_node_->GetTimelinePoints(); + TimelineWorkArea* points = viewer_node_->GetWorkArea(); // Enable workarea if it isn't already enabled - if (!points->workarea()->enabled()) { + if (!points->enabled()) { command->add_child(new WorkareaSetEnabledCommand(viewer_node_->project(), points, true)); } @@ -470,23 +530,23 @@ void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) if (m == Timeline::kTrimIn) { in_point = time; - if (!points->workarea()->enabled() || points->workarea()->out() < in_point) { + if (!points->enabled() || points->out() < in_point) { out_point = TimelineWorkArea::kResetOut; } else { - out_point = points->workarea()->out(); + out_point = points->out(); } } else { out_point = time; - if (!points->workarea()->enabled() || points->workarea()->in() > out_point) { + if (!points->enabled() || points->in() > out_point) { in_point = TimelineWorkArea::kResetIn; } else { - in_point = points->workarea()->in(); + in_point = points->in(); } } // Set workarea - command->add_child(new WorkareaSetRangeCommand(points->workarea(), TimeRange(in_point, out_point))); + command->add_child(new WorkareaSetRangeCommand(points, TimeRange(in_point, out_point))); Core::instance()->undo_stack()->push(command); } @@ -497,13 +557,13 @@ void TimeBasedWidget::ResetPoint(Timeline::MovementMode m) return; } - TimelinePoints* points = GetConnectedNode()->GetTimelinePoints(); + TimelineWorkArea* points = GetConnectedNode()->GetWorkArea(); - if (!GetConnectedNode() || !points->workarea()->enabled()) { + if (!points->enabled()) { return; } - TimeRange r = points->workarea()->range(); + TimeRange r = points->range(); if (m == Timeline::kTrimIn) { r.set_in(TimelineWorkArea::kResetIn); @@ -511,7 +571,7 @@ void TimeBasedWidget::ResetPoint(Timeline::MovementMode m) r.set_out(TimelineWorkArea::kResetOut); } - Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(points->workarea(), r)); + Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(points, r)); } void TimeBasedWidget::PageScrollInternal(QScrollBar *bar, int maximum, int screen_position, bool whole_page_scroll) @@ -582,8 +642,7 @@ void TimeBasedWidget::ClearInOutPoints() return; } - - Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(GetConnectedNode()->project(), GetConnectedNode()->GetTimelinePoints(), false)); + Core::instance()->undo_stack()->push(new WorkareaSetEnabledCommand(GetConnectedNode()->project(), GetConnectedNode()->GetWorkArea(), false)); } void TimeBasedWidget::SetMarker() @@ -592,7 +651,7 @@ void TimeBasedWidget::SetMarker() return; } - TimelineMarkerList *markers = GetConnectedNode()->GetTimelinePoints()->markers(); + TimelineMarkerList *markers = GetConnectedNode()->GetMarkers(); if (TimelineMarker *existing = markers->GetMarkerAtTime(GetTime())) { // We already have a marker here, so pop open the edit dialog @@ -661,8 +720,8 @@ void TimeBasedWidget::ToggleShowAll() void TimeBasedWidget::GoToIn() { if (GetConnectedNode()) { - if (GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { - SetTimeAndSignal(GetConnectedNode()->GetTimelinePoints()->workarea()->in()); + if (GetConnectedNode()->GetWorkArea()->enabled()) { + SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->in()); } else { GoToStart(); } @@ -672,8 +731,8 @@ void TimeBasedWidget::GoToIn() void TimeBasedWidget::GoToOut() { if (GetConnectedNode()) { - if (GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { - SetTimeAndSignal(GetConnectedNode()->GetTimelinePoints()->workarea()->out()); + if (GetConnectedNode()->GetWorkArea()->enabled()) { + SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->out()); } else { GoToEnd(); } @@ -682,7 +741,7 @@ void TimeBasedWidget::GoToOut() void TimeBasedWidget::DeleteSelected() { - if (ruler_->underMouse()) { + if (ruler_->HasItemsSelected()) { ruler_->DeleteSelected(); } } @@ -750,7 +809,7 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration // Snap to clip markers too if (ClipBlock *clip = dynamic_cast(b)) { if (clip->connected_viewer()) { - TimelineMarkerList *markers = clip->connected_viewer()->GetTimelinePoints()->markers(); + TimelineMarkerList *markers = clip->connected_viewer()->GetMarkers(); for (auto jt=markers->cbegin(); jt!=markers->cend(); jt++) { TimelineMarker *marker = *jt; @@ -768,8 +827,8 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration } } - if ((snap_points & kSnapToMarkers) && ruler()->GetTimelinePoints()) { - for (auto it=ruler()->GetTimelinePoints()->markers()->cbegin(); it!=ruler()->GetTimelinePoints()->markers()->cend(); it++) { + if ((snap_points & kSnapToMarkers) && ruler()->GetMarkers()) { + for (auto it=ruler()->GetMarkers()->cbegin(); it!=ruler()->GetMarkers()->cend(); it++) { TimelineMarker* m = *it; // Ignore selected markers @@ -787,9 +846,9 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration } } - if ((snap_points & kSnapToWorkarea) && ruler()->GetTimelinePoints()) { - const rational &workarea_in = ruler()->GetTimelinePoints()->workarea()->in(); - const rational &workarea_out = ruler()->GetTimelinePoints()->workarea()->out(); + if ((snap_points & kSnapToWorkarea) && ruler()->GetWorkArea()) { + const rational &workarea_in = ruler()->GetWorkArea()->in(); + const rational &workarea_out = ruler()->GetWorkArea()->out(); AttemptSnap(potential_snaps, screen_pt, TimeToScene(workarea_in), start_times, workarea_in); AttemptSnap(potential_snaps, screen_pt, TimeToScene(workarea_out), start_times, workarea_out); @@ -806,9 +865,16 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration continue; } - qreal key_scene_pt = TimeToScene(key->time()); + rational time = key->time(); + if (const TimeTargetObject *target = GetKeyframeTimeTarget()) { + if (Node *parent = key->parent()) { + time = target->GetAdjustedTime(parent, target->GetTimeTarget(), time, false); + } + } - AttemptSnap(potential_snaps, screen_pt, key_scene_pt, start_times, key->time()); + qreal key_scene_pt = TimeToScene(time); + + AttemptSnap(potential_snaps, screen_pt, key_scene_pt, start_times, time); } } } diff --git a/app/widget/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index 82924160f..347c079c7 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -29,6 +29,7 @@ #include "widget/resizablescrollbar/resizabletimelinescrollbar.h" #include "widget/timebased/timescaledobject.h" #include "widget/timelinewidget/view/timelineview.h" +#include "widget/timetarget/timetarget.h" namespace olive { @@ -50,6 +51,11 @@ public: void ConnectViewerNode(ViewerOutput *node); + TimelineWorkArea *GetConnectedWorkArea() const { return workarea_; } + TimelineMarkerList *GetConnectedMarkers() const { return markers_; } + void ConnectWorkArea(TimelineWorkArea *workarea); + void ConnectMarkers(TimelineMarkerList *markers); + void SetScaleAndCenterOnPlayhead(const double& scale); TimeRuler* ruler() const; @@ -116,9 +122,6 @@ public slots: void DeleteSelected(); -protected slots: - void SetTimeAndSignal(const rational& t); - protected: ResizableTimelineScrollBar* scrollbar() const; @@ -130,6 +133,9 @@ protected: virtual void ConnectedNodeChangeEvent(ViewerOutput*){} + virtual void ConnectedWorkAreaChangeEvent(TimelineWorkArea *){} + virtual void ConnectedMarkersChangeEvent(TimelineMarkerList *){} + virtual void ConnectNodeEvent(ViewerOutput*){} virtual void DisconnectNodeEvent(ViewerOutput*){} @@ -142,8 +148,13 @@ protected: void PassWheelEventsToScrollBar(QObject* object); + void SetCatchUpScrollValue(QScrollBar *b, int v, int maximum); + void SetCatchUpScrollValue(int v); + void StopCatchUpScrollTimer(QScrollBar *b); + virtual const QVector *GetSnapBlocks() const { return nullptr; } virtual const QVector *GetSnapKeyframes() const { return nullptr; } + virtual const TimeTargetObject *GetKeyframeTimeTarget() const { return nullptr; } virtual const std::vector *GetSnapIgnoreKeyframes() const { return nullptr; } virtual const std::vector *GetSnapIgnoreMarkers() const { return nullptr; } @@ -161,6 +172,13 @@ protected slots: static void PageScrollInternal(QScrollBar* bar, int maximum, int screen_position, bool whole_page_scroll); + void SetTimeAndSignal(const olive::rational& t); + + void StopCatchUpScrollTimer() + { + StopCatchUpScrollTimer(scrollbar_); + } + signals: void TimeChanged(const rational&); @@ -217,6 +235,17 @@ private: double scrollbar_start_scale_; bool scrollbar_top_handle_; + TimelineWorkArea *workarea_; + TimelineMarkerList *markers_; + + QTimer *catchup_scroll_timer_; + struct CatchUpScrollData { + qint64 last_forced = 0; + int maximum; + int value; + }; + QMap catchup_scroll_values_; + private slots: void UpdateMaximumScroll(); @@ -236,6 +265,8 @@ private slots: void CatchUpScrollToPoint(int point); + void CatchUpTimerTimeout(); + void AutoUpdateTimebase(); void ConnectedNodeRemovedFromGraph(); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index aeb1a0678..62a071730 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -54,6 +54,7 @@ #include "undo/timelineundoworkarea.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" +#include "widget/nodeparamview/nodeparamview.h" #include "widget/nodeview/nodeviewundo.h" #include "widget/timeruler/timeruler.h" @@ -86,6 +87,9 @@ TimelineWidget::TimelineWidget(QWidget *parent) : ruler_and_time_layout->addWidget(ruler()); + ruler()->setFocusPolicy(Qt::TabFocus); + QWidget::setTabOrder(ruler(), timecode_label_); + // Create list of TimelineViews - these MUST correspond to the ViewType enum view_splitter_ = new QSplitter(Qt::Vertical); @@ -437,7 +441,7 @@ void TimelineWidget::ReplaceBlocksWithGaps(const QVector &blocks, void TimelineWidget::DeleteSelected(bool ripple) { - if (ruler()->hasFocus()) { + if (ruler()->HasItemsSelected()) { ruler()->DeleteSelected(); return; } @@ -568,6 +572,22 @@ void TimelineWidget::ToggleLinksOnSelected() Core::instance()->undo_stack()->push(new NodeLinkManyCommand(blocks, link)); } +void TimelineWidget::AddDefaultTransitionsToSelected() +{ + QVector blocks; + + foreach (Block* item, GetSelectedBlocks()) { + // Only clips can be linked + if (ClipBlock *clip = dynamic_cast(item)) { + blocks.append(clip); + } + } + + if (!blocks.isEmpty()) { + Core::instance()->undo_stack()->push(new TimelineAddDefaultTransitionCommand(blocks, timebase())); + } +} + bool TimelineWidget::CopySelected(bool cut) { if (super::CopySelected(cut)) { @@ -621,13 +641,23 @@ bool TimelineWidget::CopySelected(bool cut) bool TimelineWidget::Paste() { + // TimeRuler gets first chance (markers, etc.) if (super::Paste()) { return true; - } if (!GetConnectedNode()) { + } + + // Ensure we have a connected node + if (!GetConnectedNode()) { return false; } - return PasteInternal(false); + // Attempt regular clip pasting + if (PasteInternal(false)) { + return true; + } + + // Give last chance to NodeParamView + return NodeParamView::Paste(this, std::bind(&TimelineWidget::GenerateExistingPasteMap, this, std::placeholders::_1)); } void TimelineWidget::PasteInsert() @@ -638,7 +668,7 @@ void TimelineWidget::PasteInsert() void TimelineWidget::DeleteInToOut(bool ripple) { if (!GetConnectedNode() - || !GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { + || !GetConnectedNode()->GetWorkArea()->enabled()) { return; } @@ -648,8 +678,8 @@ void TimelineWidget::DeleteInToOut(bool ripple) command->add_child(new TimelineRippleRemoveAreaCommand( sequence(), - GetConnectedNode()->GetTimelinePoints()->workarea()->in(), - GetConnectedNode()->GetTimelinePoints()->workarea()->out())); + GetConnectedNode()->GetWorkArea()->in(), + GetConnectedNode()->GetWorkArea()->out())); } else { QVector unlocked_tracks = sequence()->GetUnlockedTracks(); @@ -657,7 +687,7 @@ void TimelineWidget::DeleteInToOut(bool ripple) foreach (Track* track, unlocked_tracks) { GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(GetConnectedNode()->GetTimelinePoints()->workarea()->length()); + gap->set_length_and_media_out(GetConnectedNode()->GetWorkArea()->length()); command->add_child(new NodeAddCommand(static_cast(track->parent()), gap)); @@ -665,17 +695,17 @@ void TimelineWidget::DeleteInToOut(bool ripple) command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track->type()), track->Index(), gap, - GetConnectedNode()->GetTimelinePoints()->workarea()->in())); + GetConnectedNode()->GetWorkArea()->in())); } } // Clear workarea after this command->add_child(new WorkareaSetEnabledCommand(GetConnectedNode()->project(), - GetConnectedNode()->GetTimelinePoints(), + GetConnectedNode()->GetWorkArea(), false)); if (ripple) { - SetTimeAndSignal(GetConnectedNode()->GetTimelinePoints()->workarea()->in()); + SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->in()); } Core::instance()->undo_stack()->push(command); @@ -890,8 +920,7 @@ void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event) UpdateViewports(); - QMetaObject::invokeMethod(this, "CatchUpScrollToPoint", Qt::QueuedConnection, - Q_ARG(int, qRound(event->GetSceneX()))); + SetCatchUpScrollValue(event->GetScreenPos().x()); } else { // Mouse is not down, attempt a hover event TimelineTool* hover_tool = GetActiveTool(); @@ -906,6 +935,8 @@ void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event) void TimelineWidget::ViewMouseReleased(TimelineViewMouseEvent *event) { + StopCatchUpScrollTimer(); + if (active_tool_) { if (GetConnectedNode()) { active_tool_->MouseRelease(event); @@ -940,16 +971,22 @@ void TimelineWidget::ViewDragMoved(TimelineViewMouseEvent *event) { import_tool_->DragMove(event); UpdateViewports(); + + SetCatchUpScrollValue(event->GetScreenPos().x()); } void TimelineWidget::ViewDragLeft(QDragLeaveEvent *event) { + StopCatchUpScrollTimer(); + import_tool_->DragLeave(event); UpdateViewports(); } void TimelineWidget::ViewDragDropped(TimelineViewMouseEvent *event) { + StopCatchUpScrollTimer(); + import_tool_->DragDrop(event); UpdateViewports(); } @@ -1073,14 +1110,18 @@ void TimelineWidget::ShowContextMenu() if (ClipBlock *clip = dynamic_cast(selected.first())) { if (clip->connected_viewer()) { + QAction *reveal_in_footage_viewer = menu.addAction(tr("Reveal in Footage Viewer")); + reveal_in_footage_viewer->setData(reinterpret_cast(clip->connected_viewer())); + reveal_in_footage_viewer->setProperty("range", QVariant::fromValue(clip->media_range())); + connect(reveal_in_footage_viewer, &QAction::triggered, this, &TimelineWidget::RevealInFootageViewer); + QAction *reveal_in_project = menu.addAction(tr("Reveal in Project")); reveal_in_project->setData(reinterpret_cast(clip->connected_viewer())); connect(reveal_in_project, &QAction::triggered, this, &TimelineWidget::RevealInProject); } } - QAction* rename_action = menu.addAction(tr("Rename")); - connect(rename_action, &QAction::triggered, this, &TimelineWidget::RenameSelectedBlocks); + menu.addSeparator(); QAction* properties_action = menu.addAction(tr("Properties")); connect(properties_action, &QAction::triggered, this, &TimelineWidget::ShowSpeedDurationDialogForSelectedClips); @@ -1098,9 +1139,6 @@ void TimelineWidget::ShowContextMenu() show_waveforms->setChecked(views_.first()->view()->GetShowWaveforms()); connect(show_waveforms, &QAction::triggered, this, &TimelineWidget::SetViewWaveformsEnabled); - QAction* scroll_zoom = views_.first()->view()->AddSetScrollZoomsByDefaultActionToMenu(&menu); - connect(scroll_zoom, &QAction::triggered, this, &TimelineWidget::SetScrollZoomsByDefaultOnAllViews); - menu.addSeparator(); QAction* properties_action = menu.addAction(tr("Properties")); @@ -1190,19 +1228,22 @@ void TimelineWidget::TrackIndexChanged(int old, int now) } } -void TimelineWidget::SetScrollZoomsByDefaultOnAllViews(bool e) -{ - foreach (TimelineAndTrackView* tview, views_) { - tview->view()->SetScrollZoomsByDefault(e); - } -} - void TimelineWidget::SignalBlockSelectionChange() { signal_block_change_timer_->stop(); signal_block_change_timer_->start(); } +void TimelineWidget::RevealInFootageViewer() +{ + QAction *a = static_cast(sender()); + + ViewerOutput *item_to_reveal = reinterpret_cast(a->data().value()); + TimeRange r = a->property("range").value(); + + emit RevealViewerInFootageViewer(item_to_reveal, r); +} + void TimelineWidget::RevealInProject() { QAction *a = static_cast(sender()); @@ -1273,6 +1314,9 @@ void TimelineWidget::NudgeInternal(rational amount) foreach (Block* b, selected_blocks_) { command->add_child(new TrackReplaceBlockWithGapCommand(b->track(), b, false)); + } + + foreach (Block* b, selected_blocks_) { command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(b->track()->type()), b->track()->Index(), b, b->in() + amount)); } @@ -1475,6 +1519,10 @@ QVector TimelineWidget::GetEditToInfo(const rational& play void TimelineWidget::RippleTo(Timeline::MovementMode mode) { + if (!GetConnectedNode()) { + return; + } + rational playhead_time = GetTime(); QVector tracks = GetEditToInfo(playhead_time, mode); @@ -1673,6 +1721,24 @@ TimelineAndTrackView *TimelineWidget::AddTimelineAndTrackView(Qt::Alignment alig return v; } +QHash TimelineWidget::GenerateExistingPasteMap(const ProjectSerializer::Result &r) +{ + QHash m; + + for (Node *n : r.GetLoadedNodes()) { + for (Block *b : qAsConst(this->selected_blocks_)) { + for (auto it=b->GetContextPositions().cbegin(); it!=b->GetContextPositions().cend(); it++) { + if (it.key()->id() == n->id() && !m.contains(it.key())) { + m.insert(it.key(), n); + break; + } + } + } + } + + return m; +} + QByteArray TimelineWidget::SaveSplitterState() const { return view_splitter_->saveState(); @@ -1789,6 +1855,10 @@ void TimelineWidget::SetSelections(const TimelineWidgetSelections &s, bool proce return; } + if (!GetConnectedNode()) { + return; + } + if (process_block_changes) { QVector deselected; QVector selected; diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index c7a38dfaa..494f19fba 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -28,6 +28,7 @@ #include "core.h" #include "node/block/transition/transition.h" #include "node/output/viewer/viewer.h" +#include "node/project/serializer/serializer.h" #include "timeline/timelinecommon.h" #include "timelineandtrackview.h" #include "widget/slider/rationalslider.h" @@ -79,6 +80,8 @@ public: void ToggleLinksOnSelected(); + void AddDefaultTransitionsToSelected(); + virtual bool CopySelected(bool cut) override; virtual bool Paste() override; @@ -272,11 +275,14 @@ public: public slots: void ClearTentativeSubtitleTrack(); + void RenameSelectedBlocks(); + signals: void BlockSelectionChanged(const QVector& selected_blocks); void RequestCaptureStart(const TimeRange &time, const Track::Reference &track); + void RevealViewerInFootageViewer(ViewerOutput *r, const TimeRange &range); void RevealViewerInProject(ViewerOutput *r); protected: @@ -306,6 +312,8 @@ private: TimelineAndTrackView *AddTimelineAndTrackView(Qt::Alignment alignment); + QHash GenerateExistingPasteMap(const ProjectSerializer::Result &r); + QPoint drag_origin_; QRubberBand rubberband_; @@ -421,14 +429,11 @@ private slots: void TrackIndexChanged(int old, int now); - void SetScrollZoomsByDefaultOnAllViews(bool e); - void SignalBlockSelectionChange(); + void RevealInFootageViewer(); void RevealInProject(); - void RenameSelectedBlocks(); - void TrackAboutToBeDeleted(Track *track); }; diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index 866a384d2..a765b0d73 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -130,9 +130,9 @@ Node *AddTool::CreateAddableClip(MultiUndoCommand *command, Sequence *sequence, clip = new SubtitleBlock(); } else { clip = new ClipBlock(); + clip->SetLabel(olive::Tool::GetAddableObjectName(Core::instance()->GetSelectedAddableObject())); } clip->set_length_and_media_out(length); - clip->SetLabel(olive::Tool::GetAddableObjectName(Core::instance()->GetSelectedAddableObject())); NodeGraph* graph = sequence->parent(); diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index bc1a5590a..3914e137f 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -231,7 +231,7 @@ void ImportTool::FootageToGhosts(rational ghost_start, const DraggedFootageData rational footage_duration; rational ghost_in; - TimelineWorkArea* wk = footage->GetTimelinePoints()->workarea(); + TimelineWorkArea* wk = footage->GetWorkArea(); if (wk->enabled()) { footage_duration = wk->length(); ghost_in = wk->in(); diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 7158cb906..3c8f9a7bf 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -383,6 +383,10 @@ void PointerTool::InitiateDragInternal(Block *clicked_item, } else { // Prepare for a standard pointer move by creating ghosts for them and any related blocks foreach (Block* block, clips) { + if (dynamic_cast(block)) { + continue; + } + // Create ghost for this block auto ghost = AddGhostFromBlock(block, trim_mode, true); Q_UNUSED(ghost) @@ -689,6 +693,8 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event) InsertGapsAtGhostDestination(command); } + QMap relinks; + // Now we can re-add each clip foreach (const GhostBlockPair& p, blocks_moving) { Block* block = p.block; @@ -696,7 +702,10 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event) if (duplicate_clips) { // Duplicate rather than move // Place the copy instead of the original block - block = static_cast(Node::CopyNodeInGraph(block, command)); + Block *new_block = static_cast(Node::CopyNodeInGraph(block, command)); + relinks.insert(block, new_block); + block = new_block; + if (ClipBlock *new_clip = dynamic_cast(block)) { new_clip->waveform() = static_cast(p.block)->waveform(); } @@ -709,6 +718,18 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event) p.ghost->GetAdjustedIn())); } + if (!relinks.empty()) { + for (auto it=relinks.cbegin(); it!=relinks.cend(); it++) { + for (auto jt=it.key()->links().cbegin(); jt!=it.key()->links().cend(); jt++) { + Node *link = *jt; + Node *copy_link = relinks.value(link); + if (copy_link) { + command->add_child(new NodeLinkCommand(it.value(), copy_link, true)); + } + } + } + } + // Adjust selections TimelineWidgetSelections new_sel = parent()->GetSelections(); new_sel.ShiftTime(blocks_moving.first().ghost->GetInAdjustment()); diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.cpp b/app/widget/timelinewidget/undo/timelineundogeneral.cpp index c90d8b078..634188e65 100644 --- a/app/widget/timelinewidget/undo/timelineundogeneral.cpp +++ b/app/widget/timelinewidget/undo/timelineundogeneral.cpp @@ -22,9 +22,11 @@ #include "node/block/clip/clip.h" #include "node/block/transition/transition.h" +#include "node/factory.h" #include "node/math/math/math.h" #include "node/math/merge/merge.h" #include "timelineundocommon.h" +#include "widget/timelinewidget/undo/timelineundotrack.h" namespace olive { @@ -287,8 +289,8 @@ void TrackListInsertGaps::prepare() QVector blocks_to_append_gap_to; QVector tracks_to_append_gap_to; - foreach (Track* track, working_tracks_) { - foreach (Block* b, track->Blocks()) { + for (Track* track : qAsConst(working_tracks_)) { + for (Block* b : track->Blocks()) { if (dynamic_cast(b) && b->in() <= point_ && b->out() >= point_) { // Found a gap at the location gaps_to_extend_.append(b); @@ -542,4 +544,121 @@ void TimelineRemoveTrackCommand::undo() remove_command_->undo_now(); } +void TimelineAddDefaultTransitionCommand::prepare() +{ + for (auto it=clips_.cbegin(); it!=clips_.cend(); it++) { + ClipBlock *c = *it; + + // Handle in transition + if (clips_.contains(static_cast(c->previous()))) { + // Do nothing, assume this will be handled by a dual transition from that clip + } else if (dynamic_cast(c->previous()) || !c->previous()) { + // Create in transition + AddTransition(c, kIn); + } + + // Handle out transition + if (clips_.contains(static_cast(c->next()))) { + AddTransition(c, kOutDual); + } else if (dynamic_cast(c->next()) || !c->next()) { + // Create out transition + AddTransition(c, kOut); + } + } +} + +void TimelineAddDefaultTransitionCommand::AddTransition(ClipBlock *c, CreateTransitionMode mode) +{ + if (Track *t = c->track()) { + Node *p = nullptr; + if (t->type() == Track::kVideo) { + p = NodeFactory::CreateFromID(OLIVE_CONFIG("DefaultVideoTransition").toString()); + } else if (t->type() == Track::kAudio) { + p = NodeFactory::CreateFromID(OLIVE_CONFIG("DefaultAudioTransition").toString()); + } + + rational transition_length = OLIVE_CONFIG("DefaultTransitionLength").value(); + + // Resize original clip + switch (mode) { + case kIn: + ValidateTransitionLength(c, transition_length); + + if (transition_length > 0) { + AdjustClipLength(c, transition_length, false); + } + break; + case kOut: + ValidateTransitionLength(c, transition_length); + + if (transition_length > 0) { + AdjustClipLength(c, transition_length, true); + } + break; + case kOutDual: + { + rational half_length = transition_length / 2; + + ValidateTransitionLength(static_cast(c->next()), half_length); + ValidateTransitionLength(c, half_length); + + transition_length = half_length * 2; + + if (transition_length > 0) { + AdjustClipLength(static_cast(c->next()), half_length, false); + AdjustClipLength(c, half_length, true); + } + break; + } + } + + if (transition_length > 0) { + if (TransitionBlock *transition = dynamic_cast(p)) { + transition->set_length_and_media_out(transition_length); + + // Add transition + commands_.append(new NodeAddCommand(c->parent(), transition)); + + // Insert block + Block *insert_after = (mode == kIn) ? c->previous() : c; + commands_.append(new TrackInsertBlockAfterCommand(c->track(), transition, insert_after)); + + // Connect + switch (mode) { + case kIn: + commands_.append(new NodeEdgeAddCommand(c, NodeInput(transition, TransitionBlock::kInBlockInput))); + break; + case kOutDual: + commands_.append(new NodeEdgeAddCommand(c->next(), NodeInput(transition, TransitionBlock::kInBlockInput))); + /* fall through */ + case kOut: + commands_.append(new NodeEdgeAddCommand(c, NodeInput(transition, TransitionBlock::kOutBlockInput))); + break; + } + } + } + } +} + +void TimelineAddDefaultTransitionCommand::AdjustClipLength(ClipBlock *c, const rational &transition_length, bool out) +{ + rational cur_len = lengths_.value(c, c->length()); + rational new_len = cur_len - transition_length; + if (out) { + commands_.append(new BlockResizeCommand(c, new_len)); + } else { + commands_.append(new BlockResizeWithMediaInCommand(c, new_len)); + } + lengths_.insert(c, new_len); +} + +void TimelineAddDefaultTransitionCommand::ValidateTransitionLength(ClipBlock *c, rational &transition_length) +{ + rational cur_len = lengths_.value(c, c->length()); + rational half_cur_len = cur_len/2; + if (transition_length >= half_cur_len) { + transition_length = half_cur_len - timebase_; + } +} + } diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.h b/app/widget/timelinewidget/undo/timelineundogeneral.h index 55f36b7b9..50d04fe09 100644 --- a/app/widget/timelinewidget/undo/timelineundogeneral.h +++ b/app/widget/timelinewidget/undo/timelineundogeneral.h @@ -358,6 +358,61 @@ private: }; +class TimelineAddDefaultTransitionCommand : public UndoCommand +{ +public: + TimelineAddDefaultTransitionCommand(const QVector &clips, const rational &timebase) : + clips_(clips), + timebase_(timebase) + {} + + virtual ~TimelineAddDefaultTransitionCommand() override + { + qDeleteAll(commands_); + } + + virtual Project* GetRelevantProject() const override + { + return clips_.empty() ? nullptr : clips_.first()->project(); + } + +protected: + virtual void prepare() override; + + virtual void redo() override + { + for (auto it=commands_.cbegin(); it!=commands_.cend(); it++) { + (*it)->redo_now(); + } + } + + virtual void undo() override + { + for (auto it=commands_.crbegin(); it!=commands_.crend(); it++) { + (*it)->undo_now(); + } + } + +private: + enum CreateTransitionMode { + kIn, + kOut, + kOutDual + }; + + void AddTransition(ClipBlock *c, CreateTransitionMode mode); + void AdjustClipLength(ClipBlock *c, const rational &transition_length, bool out); + void ValidateTransitionLength(ClipBlock *c, rational &transition_length); + + + QVector clips_; + rational timebase_; + QVector commands_; + + QHash lengths_; + +}; + } #endif // TIMELINEUNDOGENERAL_H diff --git a/app/widget/timelinewidget/undo/timelineundopointer.cpp b/app/widget/timelinewidget/undo/timelineundopointer.cpp index f8b51ca4c..f46f5a47e 100644 --- a/app/widget/timelinewidget/undo/timelineundopointer.cpp +++ b/app/widget/timelinewidget/undo/timelineundopointer.cpp @@ -340,7 +340,7 @@ void TrackPlaceBlockCommand::redo() // Place the Block at this point if (!ripple_remove_command_) { ripple_remove_command_ = new TrackRippleRemoveAreaCommand(track, TimeRange(in_, in_ + insert_->length())); - + ripple_remove_command_->SetAllowSplittingGaps(true); } ripple_remove_command_->redo_now(); diff --git a/app/widget/timelinewidget/undo/timelineundoripple.cpp b/app/widget/timelinewidget/undo/timelineundoripple.cpp index 3b7bdcac9..42ff4449e 100644 --- a/app/widget/timelinewidget/undo/timelineundoripple.cpp +++ b/app/widget/timelinewidget/undo/timelineundoripple.cpp @@ -30,6 +30,7 @@ namespace olive { TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range) : track_(track), range_(range), + allow_splitting_gaps_(false), splice_split_command_(nullptr) { trim_out_.block = nullptr; @@ -63,8 +64,15 @@ void TrackRippleRemoveAreaCommand::prepare() // If it's getting trimmed, determine if it's actually getting spliced if (first_block_is_out_trimmed && first_block_is_in_trimmed) { - // This block is getting spliced, so we'll handle that later - splice_split_command_ = new BlockSplitCommand(first_block, range_.in()); + if (!allow_splitting_gaps_ && dynamic_cast(first_block)) { + // As a rule, we don't split gaps, so we just treat it as a trim of the range requested + trim_out_ = {first_block, + first_block->length(), + first_block->length() - range_.length()}; + } else { + // This block is getting spliced, so we'll handle that later + splice_split_command_ = new BlockSplitCommand(first_block, range_.in()); + } } else { // It's just getting trimmed or removed, so we'll append that operation if (first_block_is_out_trimmed) { diff --git a/app/widget/timelinewidget/undo/timelineundoripple.h b/app/widget/timelinewidget/undo/timelineundoripple.h index 11e211575..bd17648cf 100644 --- a/app/widget/timelinewidget/undo/timelineundoripple.h +++ b/app/widget/timelinewidget/undo/timelineundoripple.h @@ -67,6 +67,11 @@ public: return nullptr; } + void SetAllowSplittingGaps(bool e) + { + allow_splitting_gaps_ = e; + } + protected: virtual void prepare() override; @@ -93,6 +98,7 @@ private: QVector removals_; TrimOperation trim_in_; Block* insert_previous_; + bool allow_splitting_gaps_; BlockSplitCommand* splice_split_command_; QVector remove_block_commands_; diff --git a/app/widget/timelinewidget/undo/timelineundoworkarea.h b/app/widget/timelinewidget/undo/timelineundoworkarea.h index b953fafa7..f83601747 100644 --- a/app/widget/timelinewidget/undo/timelineundoworkarea.h +++ b/app/widget/timelinewidget/undo/timelineundoworkarea.h @@ -22,16 +22,15 @@ #define TIMELINEUNDOWORKAREA_H #include "node/project/project.h" -#include "timeline/timelinepoints.h" namespace olive { class WorkareaSetEnabledCommand : public UndoCommand { public: - WorkareaSetEnabledCommand(Project *project, TimelinePoints* points, bool enabled) : + WorkareaSetEnabledCommand(Project *project, TimelineWorkArea* points, bool enabled) : project_(project), points_(points), - old_enabled_(points_->workarea()->enabled()), + old_enabled_(points_->enabled()), new_enabled_(enabled) { } @@ -44,18 +43,18 @@ public: protected: virtual void redo() override { - points_->workarea()->set_enabled(new_enabled_); + points_->set_enabled(new_enabled_); } virtual void undo() override { - points_->workarea()->set_enabled(old_enabled_); + points_->set_enabled(old_enabled_); } private: Project* project_; - TimelinePoints* points_; + TimelineWorkArea* points_; bool old_enabled_; diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index d3ee50c04..2c622fb4d 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -430,7 +430,8 @@ TimelineViewMouseEvent TimelineView::CreateMouseEvent(const QPoint& pos, Qt::Mou { QPointF scene_pt = mapToScene(pos); - return TimelineViewMouseEvent(scene_pt.x(), + return TimelineViewMouseEvent(scene_pt, + pos, GetScale(), timebase(), Track::Reference(ConnectedTrackType(), SceneToTrack(scene_pt.y())), @@ -485,11 +486,12 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q int text_height = fm.height(); int text_padding = text_height/4; // This ties into the track minimum height being 1.5 int text_total_height = text_height + text_padding + text_padding; + Q_UNUSED(text_total_height) if (foreground) { painter->setBrush(Qt::NoBrush); - QString using_label = block->GetLabel().isEmpty() ? block->Name() : block->GetLabel(); + QString using_label = block->GetLabelOrName(); QRectF text_rect = r.adjusted(text_padding, text_padding, -text_padding, -text_padding); painter->setPen(block->is_enabled() ? ColorCoding::GetUISelectorColor(block->color()) : Qt::lightGray); @@ -521,7 +523,7 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q if (ClipBlock *clip = dynamic_cast(block)) { // Draw waveform if (show_waveforms_) { - QRect waveform_rect = r.adjusted(0, text_total_height, 0, 0).toRect(); + QRect waveform_rect = r.toRect(); painter->setPen(shadow_color); AudioVisualWaveform::DrawWaveform(painter, waveform_rect, this->GetScale(), clip->waveform(), SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base())); @@ -530,24 +532,51 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q // Draw zebra stripes and markers if (clip->connected_viewer()) { if (!clip->connected_viewer()->GetLength().isNull()) { + painter->setPen(shadow_color); + if (clip->media_in() < 0) { - // Draw stripes for sections of clip < 0 - qreal zebra_right = TimeToScene(-clip->media_in()); - if (zebra_right > GetTimelineLeftBound()) { - DrawZebraStripes(painter, QRectF(block_left, block_top, zebra_right, block_height)); + qreal zebra_right = TimeToScene(clip->in() - clip->media_in()); + + switch (clip->loop_mode()) { + case Decoder::kLoopModeOff: + // Draw stripes for sections of clip < 0 + if (zebra_right > GetTimelineLeftBound()) { + DrawZebraStripes(painter, QRectF(block_left, block_top, zebra_right - block_left, block_height)); + } + break; + case Decoder::kLoopModeLoop: + for (qreal i=zebra_right; i>block_left; i-=TimeToScene(clip->connected_viewer()->GetLength())) { + painter->drawLine(i, block_top, i, block_top + block_height); + } + break; + case Decoder::kLoopModeClamp: + painter->drawLine(zebra_right, block_top, zebra_right, block_top + block_height); + break; } } if (clip->length() + clip->media_in() > clip->connected_viewer()->GetLength()) { - // Draw stripes for sections for clip > clip length qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength())); - if (zebra_left < GetTimelineRightBound()) { - DrawZebraStripes(painter, QRectF(zebra_left, block_top, block_right - zebra_left, block_height)); + switch (clip->loop_mode()) { + case Decoder::kLoopModeOff: + // Draw stripes for sections for clip > clip length + if (zebra_left < GetTimelineRightBound()) { + DrawZebraStripes(painter, QRectF(zebra_left, block_top, block_right - zebra_left, block_height)); + } + break; + case Decoder::kLoopModeLoop: + for (qreal i=zebra_left; iconnected_viewer()->GetLength())) { + painter->drawLine(i, block_top, i, block_top + block_height); + } + break; + case Decoder::kLoopModeClamp: + painter->drawLine(zebra_left, block_top, zebra_left, block_top + block_height); + break; } } } - TimelineMarkerList *marker_list = clip->connected_viewer()->GetTimelinePoints()->markers(); + TimelineMarkerList *marker_list = clip->connected_viewer()->GetMarkers(); if (!marker_list->empty()) { clip_marker_rects_.clear(); @@ -558,7 +587,7 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q if (marker->time().in() >= clip->media_in() && marker->time().out() <= clip->media_in() + clip->length()) { QPoint marker_pt(TimeToScene(clip->in() - clip->media_in() + marker->time().in()), block_top + block_height); painter->setClipRect(r); - QRect marker_rect = marker->Draw(painter, marker_pt, GetScale(), false); + QRect marker_rect = marker->Draw(painter, marker_pt, -1, GetScale(), false); clip_marker_rects_.insert(marker, marker_rect); painter->setClipping(false); } @@ -707,12 +736,14 @@ void TimelineView::ConnectTrackList(TrackList *list) { if (connected_track_list_) { disconnect(connected_track_list_, &TrackList::TrackListChanged, this, &TimelineView::TrackListChanged); + disconnect(connected_track_list_, &TrackList::TrackHeightChanged, this, &TimelineView::TrackListChanged); } connected_track_list_ = list; if (connected_track_list_) { connect(connected_track_list_, &TrackList::TrackListChanged, this, &TimelineView::TrackListChanged); + connect(connected_track_list_, &TrackList::TrackHeightChanged, this, &TimelineView::TrackListChanged); } } diff --git a/app/widget/timelinewidget/view/timelineviewmouseevent.h b/app/widget/timelinewidget/view/timelineviewmouseevent.h index 79bd99d85..a37a5a9d8 100644 --- a/app/widget/timelinewidget/view/timelineviewmouseevent.h +++ b/app/widget/timelinewidget/view/timelineviewmouseevent.h @@ -34,13 +34,15 @@ namespace olive { class TimelineViewMouseEvent { public: - TimelineViewMouseEvent(const qreal& scene_x, + TimelineViewMouseEvent(const QPointF& scene_pos, + const QPoint &screen_pos, const double& scale_x, const rational& timebase, const Track::Reference &track, const Qt::MouseButton &button, const Qt::KeyboardModifiers& modifiers = Qt::NoModifier) : - scene_x_(scene_x), + scene_pos_(scene_pos), + screen_pos_(screen_pos), scale_x_(scale_x), timebase_(timebase), track_(track), @@ -73,7 +75,7 @@ public: */ rational GetFrame(bool round = false) const { - return TimeScaledObject::SceneToTime(scene_x_, scale_x_, timebase_, round); + return TimeScaledObject::SceneToTime(GetSceneX(), scale_x_, timebase_, round); } const Track::Reference& GetTrack() const @@ -96,11 +98,14 @@ public: source_event_ = event; } - const qreal& GetSceneX() const + qreal GetSceneX() const { - return scene_x_; + return scene_pos_.x(); } + const QPointF &GetScenePos() const { return scene_pos_; } + const QPoint &GetScreenPos() const { return screen_pos_; } + const Qt::MouseButton& GetButton() const { return button_; @@ -122,7 +127,8 @@ public: void SetBypassImportBuffer(bool e) { bypass_import_buffer_ = e; } private: - qreal scene_x_; + QPointF scene_pos_; + QPoint screen_pos_; double scale_x_; rational timebase_; diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index 141a28b70..8dc21dace 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -41,13 +41,15 @@ namespace olive { SeekableWidget::SeekableWidget(QWidget* parent) : super(parent), - timeline_points_(nullptr), + markers_(nullptr), + workarea_(nullptr), dragging_(false), ignore_next_focus_out_(false), selection_manager_(this), resize_item_(nullptr), marker_top_(0), - marker_bottom_(0) + marker_bottom_(0), + marker_editing_enabled_(true) { QFontMetrics fm = fontMetrics(); @@ -63,26 +65,41 @@ SeekableWidget::SeekableWidget(QWidget* parent) : selection_manager_.SetSnapMask(TimeBasedWidget::kSnapAll); } -void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points) +void SeekableWidget::SetMarkers(TimelineMarkerList *markers) { - if (timeline_points_) { + if (markers_) { selection_manager_.ClearSelection(); - disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); - disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); - disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); - disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); - disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::update)); + disconnect(markers_, &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); + disconnect(markers_, &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); + disconnect(markers_, &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::update)); } - timeline_points_ = points; + markers_ = markers; - if (timeline_points_) { - connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); - connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); - connect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); - connect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); - connect(timeline_points_->markers(), &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::update)); + if (markers_) { + connect(markers_, &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); + connect(markers_, &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); + connect(markers_, &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::update)); + } + + viewport()->update(); +} + +void SeekableWidget::SetWorkArea(TimelineWorkArea *workarea) +{ + if (workarea_) { + selection_manager_.ClearSelection(); + + disconnect(workarea_, &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); + disconnect(workarea_, &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); + } + + workarea_ = workarea; + + if (workarea_) { + connect(workarea_, &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); + connect(workarea_, &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); } viewport()->update(); @@ -139,11 +156,11 @@ bool SeekableWidget::PasteMarkers() m->set_time(m->time().in() - min); - if (TimelineMarker *existing = timeline_points_->markers()->GetMarkerAtTime(m->time().in())) { + if (TimelineMarker *existing = markers_->GetMarkerAtTime(m->time().in())) { command->add_child(new MarkerRemoveCommand(existing)); } - command->add_child(new MarkerAddCommand(timeline_points_->markers(), m)); + command->add_child(new MarkerAddCommand(markers_, m)); } Core::instance()->undo_stack()->push(command); @@ -156,7 +173,11 @@ bool SeekableWidget::PasteMarkers() void SeekableWidget::mousePressEvent(QMouseEvent *event) { - if (resize_item_) { + TimelineMarker *initial; + + if (HandPress(event)) { + return; + } else if (resize_item_) { // Handle selection, even though we won't be using it for dragging if (!(event->modifiers() & Qt::ShiftModifier)) { selection_manager_.ClearSelection(); @@ -166,7 +187,7 @@ void SeekableWidget::mousePressEvent(QMouseEvent *event) } dragging_ = true; resize_start_ = mapToScene(event->pos()); - } else if (TimelineMarker *initial = selection_manager_.MousePress(event)) { + } else if (marker_editing_enabled_ && (initial = selection_manager_.MousePress(event))) { selection_manager_.DragStart(initial, event); } else if (!selection_manager_.GetObjectAtPoint(event->pos()) && event->button() == Qt::LeftButton) { SeekToScenePoint(mapToScene(event->pos()).x()); @@ -178,7 +199,9 @@ void SeekableWidget::mousePressEvent(QMouseEvent *event) void SeekableWidget::mouseMoveEvent(QMouseEvent *event) { - if (selection_manager_.IsDragging()) { + if (HandMove(event)) { + return; + } else if (selection_manager_.IsDragging()) { selection_manager_.DragMove(event); } else if (dragging_) { QPointF scene = mapToScene(event->pos()); @@ -187,7 +210,7 @@ void SeekableWidget::mouseMoveEvent(QMouseEvent *event) } else { SeekToScenePoint(scene.x()); } - } else if (timeline_points_) { + } else { // Look for resize points if (FindResizeHandle(event)) { setCursor(Qt::SizeHorCursor); @@ -199,6 +222,10 @@ void SeekableWidget::mouseMoveEvent(QMouseEvent *event) void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) { + if (HandRelease(event)) { + return; + } + if (selection_manager_.IsDragging()) { MultiUndoCommand *command = new MultiUndoCommand(); selection_manager_.DragStop(command); @@ -215,6 +242,7 @@ void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) } dragging_ = false; + emit DragReleased(); } void SeekableWidget::mouseDoubleClickEvent(QMouseEvent *event) @@ -238,6 +266,69 @@ void SeekableWidget::focusOutEvent(QFocusEvent *event) } } +void SeekableWidget::DrawMarkers(QPainter *p, int marker_bottom) +{ + selection_manager_.ClearDrawnObjects(); + + // Draw markers + if (markers_ && !markers_->empty() && marker_bottom > 0) { + int lim_left = GetLeftLimit(); + int lim_right = GetRightLimit(); + + for (auto it=markers_->cbegin(); it!=markers_->cend(); it++) { + TimelineMarker* marker = *it; + + int marker_right = TimeToScene(marker->time().out()); + if (marker_right < lim_left) { + continue; + } + + int marker_left = TimeToScene(marker->time().in()); + if (marker_left >= lim_right) { + break; + } + + int max_marker_right = lim_right; + { + // Check if there's a marker next + auto next = it; + next++; + if (next != markers_->cend()) { + max_marker_right = std::min(max_marker_right, int(TimeToScene((*next)->time().in()))); + } + } + + QRect marker_rect = marker->Draw(p, QPoint(marker_left, marker_bottom), max_marker_right, GetScale(), selection_manager_.IsSelected(marker)); + marker_top_ = marker_rect.top(); + selection_manager_.DeclareDrawnObject(marker, marker_rect); + } + } + + marker_bottom_ = marker_bottom; +} + +void SeekableWidget::DrawWorkArea(QPainter *p) +{ + // Draw in/out workarea + if (workarea_ && workarea_->enabled()) { + int lim_left = GetLeftLimit(); + int lim_right = GetRightLimit(); + + int workarea_left = qMax(qreal(lim_left), TimeToScene(workarea_->in())); + int workarea_right; + + if (workarea_->out() == TimelineWorkArea::kResetOut) { + workarea_right = lim_right; + } else { + workarea_right = qMin(qreal(lim_right), TimeToScene(workarea_->out())); + } + + QColor translucent_highlight = palette().highlight().color(); + translucent_highlight.setAlpha(96); + p->fillRect(workarea_left, 0, workarea_right - workarea_left, height(), translucent_highlight); + } +} + void SeekableWidget::DeselectAllMarkers() { selection_manager_.ClearSelection(); @@ -309,61 +400,15 @@ void SeekableWidget::SelectionManagerDeselectEvent(void *obj) viewport()->update(); } -void SeekableWidget::DrawTimelinePoints(QPainter* p, int marker_bottom) -{ - if (!GetTimelinePoints()) { - return; - } - - int lim_left = GetScroll(); - int lim_right = lim_left + width(); - - selection_manager_.ClearDrawnObjects(); - - // Draw in/out workarea - if (GetTimelinePoints()->workarea()->enabled()) { - int workarea_left = qMax(qreal(lim_left), TimeToScene(GetTimelinePoints()->workarea()->in())); - int workarea_right; - - if (GetTimelinePoints()->workarea()->out() == TimelineWorkArea::kResetOut) { - workarea_right = lim_right; - } else { - workarea_right = qMin(qreal(lim_right), TimeToScene(GetTimelinePoints()->workarea()->out())); - } - - p->fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); - } - - // Draw markers - if (marker_bottom > 0 && !GetTimelinePoints()->markers()->empty()) { - for (auto it=GetTimelinePoints()->markers()->cbegin(); it!=GetTimelinePoints()->markers()->cend(); it++) { - TimelineMarker* marker = *it; - - int marker_right = TimeToScene(marker->time().out()); - if (marker_right < lim_left) { - continue; - } - - int marker_left = TimeToScene(marker->time().in()); - if (marker_left >= lim_right) { - break; - } - - QRect marker_rect = marker->Draw(p, QPoint(marker_left, marker_bottom), GetScale(), selection_manager_.IsSelected(marker)); - marker_top_ = marker_rect.top(); - selection_manager_.DeclareDrawnObject(marker, marker_rect); - } - } - - marker_bottom_ = marker_bottom; -} - void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) { int half_width = playhead_width_ / 2; - if (x + half_width < 0 || x - half_width > width()) { - return; + { + int test = x - this->GetScroll(); + if (test + half_width < 0 || test - half_width > width()) { + return; + } } p->setRenderHint(QPainter::Antialiasing); @@ -384,9 +429,19 @@ void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) p->setRenderHint(QPainter::Antialiasing, false); } +int SeekableWidget::GetLeftLimit() const +{ + return GetScroll(); +} + +int SeekableWidget::GetRightLimit() const +{ + return GetLeftLimit() + width(); +} + bool SeekableWidget::ShowContextMenu(const QPoint &p) { - if (selection_manager_.GetObjectAtPoint(p) && !selection_manager_.GetSelectedObjects().empty()) { + if (marker_editing_enabled_ && selection_manager_.GetObjectAtPoint(p) && !selection_manager_.GetSelectedObjects().empty()) { // Show marker-specific menu Menu m; @@ -413,6 +468,10 @@ bool SeekableWidget::ShowContextMenu(const QPoint &p) bool SeekableWidget::FindResizeHandle(QMouseEvent *event) { + if (!marker_editing_enabled_) { + return false; + } + resize_item_ = nullptr; resize_mode_ = kResizeNone; @@ -422,32 +481,38 @@ bool SeekableWidget::FindResizeHandle(QMouseEvent *event) rational max = SceneToTimeNoGrid(scene.x() + border); // Test for workarea - if (timeline_points_->workarea()->in() >= min && timeline_points_->workarea()->in() < max) { - resize_mode_ = kResizeIn; - } else if (timeline_points_->workarea()->out() >= min && timeline_points_->workarea()->out() < max) { - resize_mode_ = kResizeOut; + if (workarea_) { + if (workarea_->in() >= min && workarea_->in() < max) { + resize_mode_ = kResizeIn; + } else if (workarea_->out() >= min && workarea_->out() < max) { + resize_mode_ = kResizeOut; + } } if (resize_mode_ != kResizeNone) { - resize_item_ = timeline_points_->workarea(); - resize_item_range_ = timeline_points_->workarea()->range(); - resize_snap_mask_ = TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToWorkarea; + if (workarea_) { + resize_item_ = workarea_; + resize_item_range_ = workarea_->range(); + resize_snap_mask_ = TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToWorkarea; + } } else if (event->pos().y() >= marker_top_ && event->pos().y() < marker_bottom_) { - // Check for markers - for (auto it=timeline_points_->markers()->cbegin(); it!=timeline_points_->markers()->cend(); it++) { - TimelineMarker *m = *it; - if (m->time().in() != m->time().out()) { - if (m->time().in() >= min && m->time().in() < max) { - resize_mode_ = kResizeIn; - } else if (m->time().out() >= min && m->time().out() < max) { - resize_mode_ = kResizeOut; - } + if (markers_) { + // Check for markers + for (auto it=markers_->cbegin(); it!=markers_->cend(); it++) { + TimelineMarker *m = *it; + if (m->time().in() != m->time().out()) { + if (m->time().in() >= min && m->time().in() < max) { + resize_mode_ = kResizeIn; + } else if (m->time().out() >= min && m->time().out() < max) { + resize_mode_ = kResizeOut; + } - if (resize_mode_ != kResizeNone) { - resize_item_ = m; - resize_item_range_ = m->time(); - resize_snap_mask_ = TimeBasedWidget::kSnapAll; - break; + if (resize_mode_ != kResizeNone) { + resize_item_ = m; + resize_item_range_ = m->time(); + resize_snap_mask_ = TimeBasedWidget::kSnapAll; + break; + } } } } diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h index dc28e9dbe..ef90f872e 100644 --- a/app/widget/timeruler/seekablewidget.h +++ b/app/widget/timeruler/seekablewidget.h @@ -25,7 +25,6 @@ #include #include "common/rational.h" -#include "timeline/timelinepoints.h" #include "widget/menu/menu.h" #include "widget/timebased/timebasedviewselectionmanager.h" @@ -42,14 +41,20 @@ public: return horizontalScrollBar()->value(); } - TimelinePoints* GetTimelinePoints() const { return timeline_points_; } - void ConnectTimelinePoints(TimelinePoints* points); + TimelineMarkerList *GetMarkers() const { return markers_; } + TimelineWorkArea *GetWorkArea() const { return workarea_; } + + void SetMarkers(TimelineMarkerList *markers); + void SetWorkArea(TimelineWorkArea *workarea); bool IsDraggingPlayhead() const { return dragging_; } + bool IsMarkerEditingEnabled() const { return marker_editing_enabled_; } + void SetMarkerEditingEnabled(bool e) { marker_editing_enabled_ = e; } + void DeleteSelected(); bool CopySelected(bool cut); @@ -60,6 +65,11 @@ public: void SeekToScenePoint(qreal scene); + bool HasItemsSelected() const + { + return !selection_manager_.GetSelectedObjects().empty(); + } + const std::vector &GetSelectedMarkers() const { return selection_manager_.GetSelectedObjects(); @@ -76,6 +86,9 @@ public slots: virtual void TimebaseChangedEvent(const rational &) override; +signals: + void DragReleased(); + protected: virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseMoveEvent(QMouseEvent *event) override; @@ -84,7 +97,8 @@ protected: virtual void focusOutEvent(QFocusEvent *event) override; - void DrawTimelinePoints(QPainter *p, int marker_bottom = 0); + void DrawMarkers(QPainter *p, int marker_bottom = 0); + void DrawWorkArea(QPainter *p); void DrawPlayhead(QPainter* p, int x, int y); @@ -96,6 +110,9 @@ protected: return playhead_width_; } + int GetLeftLimit() const; + int GetRightLimit() const; + protected slots: virtual bool ShowContextMenu(const QPoint &p); @@ -112,7 +129,8 @@ private: void CommitResizeHandle(); - TimelinePoints* timeline_points_; + TimelineMarkerList* markers_; + TimelineWorkArea* workarea_; int text_height_; @@ -133,6 +151,8 @@ private: int marker_top_; int marker_bottom_; + bool marker_editing_enabled_; + private slots: void SetMarkerColor(int c); diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 642e160df..39d356006 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -103,9 +103,8 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) // Draw timeline points if connected int marker_height = TimelineMarker::GetMarkerHeight(p->fontMetrics()); - if (GetTimelinePoints()) { - DrawTimelinePoints(p, marker_height); - } + DrawMarkers(p, marker_height); + DrawWorkArea(p); double width_of_frame = timebase_dbl() * GetScale(); double width_of_second = 0; diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 92dc7e500..073edc0d8 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -82,7 +82,8 @@ void AudioWaveformView::drawForeground(QPainter *p, const QRectF &rect) } // Draw in/out points - DrawTimelinePoints(p); + DrawWorkArea(p); + DrawMarkers(p); // Draw waveform p->setPen(QColor(64, 255, 160)); // FIXME: Hardcoded color diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index 9ede7b066..b643f6740 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -38,12 +38,29 @@ FootageViewerWidget::FootageViewerWidget(QWidget *parent) : controls_->SetAudioVideoDragButtonsVisible(true); connect(controls_, &PlaybackControls::VideoPressed, this, &FootageViewerWidget::StartVideoDrag); connect(controls_, &PlaybackControls::AudioPressed, this, &FootageViewerWidget::StartAudioDrag); + + override_workarea_ = new TimelineWorkArea(this); +} + +void FootageViewerWidget::OverrideWorkArea(const TimeRange &r) +{ + override_workarea_->set_enabled(true); + override_workarea_->set_range(r); + this->ConnectWorkArea(override_workarea_); +} + +void FootageViewerWidget::ResetWorkArea() +{ + if (GetConnectedWorkArea() == override_workarea_) { + this->ConnectWorkArea(GetConnectedNode() ? GetConnectedNode()->GetWorkArea() : nullptr); + } } void FootageViewerWidget::ConnectNodeEvent(ViewerOutput *n) { super::ConnectNodeEvent(n); + IgnoreNextScrubEvent(); SetTime(cached_timestamps_.value(n, 0)); } @@ -54,6 +71,7 @@ void FootageViewerWidget::DisconnectNodeEvent(ViewerOutput *n) super::DisconnectNodeEvent(n); + IgnoreNextScrubEvent(); SetTime(0); } diff --git a/app/widget/viewer/footageviewer.h b/app/widget/viewer/footageviewer.h index e9ef774f3..de98866c8 100644 --- a/app/widget/viewer/footageviewer.h +++ b/app/widget/viewer/footageviewer.h @@ -32,6 +32,9 @@ class FootageViewerWidget : public ViewerWidget public: FootageViewerWidget(QWidget* parent = nullptr); + void OverrideWorkArea(const TimeRange &r); + void ResetWorkArea(); + protected: virtual void ConnectNodeEvent(ViewerOutput *) override; @@ -42,6 +45,8 @@ private: QHash cached_timestamps_; + TimelineWorkArea *override_workarea_; + private slots: void StartFootageDrag(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 35eb765d0..fb8fde0da 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -21,6 +21,7 @@ #include "viewer.h" #include +#include #include #include #include @@ -32,7 +33,6 @@ #include "audio/audiomanager.h" #include "common/clamp.h" -#include "common/power.h" #include "common/ratiodialog.h" #include "common/timecodefunctions.h" #include "config/config.h" @@ -41,10 +41,10 @@ #include "node/generator/shape/shapenodebase.h" #include "node/project/project.h" #include "render/rendermanager.h" -#include "task/taskmanager.h" #include "viewerpreventsleep.h" +#include "widget/audiomonitor/audiomonitor.h" #include "widget/menu/menu.h" -#include "window/mainwindow/mainwindow.h" +#include "widget/nodeparamview/nodeparamviewundo.h" #include "widget/timelinewidget/tool/add.h" #include "widget/timeruler/timeruler.h" @@ -72,22 +72,20 @@ ViewerWidget::ViewerWidget(QWidget *parent) : record_armed_(false), recording_(false), first_requeue_watcher_(nullptr), - enable_audio_scrubbing_(true) + enable_audio_scrubbing_(true), + waveform_mode_(kWFAutomatic), + ignore_scrub_(0) { // Set up main layout QVBoxLayout* layout = new QVBoxLayout(this); layout->setMargin(0); - // Set up stacked widget to allow switching away from the viewer widget - stack_ = new QStackedWidget(); - layout->addWidget(stack_); - // Create main OpenGL-based view and sizer sizer_ = new ViewerSizer(); - stack_->addWidget(sizer_); + sizer_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + layout->addWidget(sizer_); display_widget_ = new ViewerDisplayWidget(); - display_widget_->setAcceptDrops(true); display_widget_->SetShowWidgetBackground(true); playback_devices_.append(display_widget_); connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); @@ -114,7 +112,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : waveform_view_ = new AudioWaveformView(); ConnectTimelineView(waveform_view_, true); PassWheelEventsToScrollBar(waveform_view_); - stack_->addWidget(waveform_view_); + layout->addWidget(waveform_view_); // Create time ruler layout->addWidget(ruler()); @@ -149,7 +147,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : instances_.append(this); - setAcceptDrops(true); + UpdateWaveformViewFromMode(); connect(Core::instance(), &Core::ColorPickerEnabled, this, &ViewerWidget::SetSignalCursorColorEnabled); connect(this, &ViewerWidget::CursorColor, Core::instance(), &Core::ColorPickerColorEmitted); @@ -207,9 +205,10 @@ void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); connect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot); connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters); + connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection); connect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters); connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); - connect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateStack); + connect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode); VideoParams vp = n->GetVideoParams(); @@ -230,10 +229,9 @@ void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) dw->ConnectColorManager(color_manager); } - UpdateStack(); + UpdateWaveformViewFromMode(); waveform_view_->SetViewer(GetConnectedNode()->audio_playback_cache()); - waveform_view_->ConnectTimelinePoints(GetConnectedNode()->GetTimelinePoints()); UpdateRendererVideoParameters(); UpdateRendererAudioParameters(); @@ -251,11 +249,13 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); disconnect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot); disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters); + disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateTextureFromNode); disconnect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters); disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); - disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateStack); + disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode); CloseAudioProcessor(); + audio_scrub_watchers_.clear(); SetDisplayImage(QVariant()); @@ -269,10 +269,9 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) } waveform_view_->SetViewer(nullptr); - waveform_view_->ConnectTimelinePoints(nullptr); // Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected - QMetaObject::invokeMethod(this, &ViewerWidget::UpdateStack, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &ViewerWidget::UpdateWaveformViewFromMode, Qt::QueuedConnection); SetGizmos(nullptr); } @@ -283,6 +282,16 @@ void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n) display_widget_->SetSubtitleTracks(dynamic_cast(n)); } +void ViewerWidget::ConnectedWorkAreaChangeEvent(TimelineWorkArea *workarea) +{ + waveform_view_->SetWorkArea(workarea); +} + +void ViewerWidget::ConnectedMarkersChangeEvent(TimelineMarkerList *markers) +{ + waveform_view_->SetMarkers(markers); +} + void ViewerWidget::ScaleChangedEvent(const double &s) { super::ScaleChangedEvent(s); @@ -378,8 +387,8 @@ void ViewerWidget::CacheEntireSequence() void ViewerWidget::CacheSequenceInOut() { - if (GetConnectedNode() && GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { - auto_cacher_.ForceCacheRange(GetConnectedNode()->GetTimelinePoints()->workarea()->range()); + if (GetConnectedNode() && GetConnectedNode()->GetWorkArea()->enabled()) { + auto_cacher_.ForceCacheRange(GetConnectedNode()->GetWorkArea()->range()); } else { QMessageBox::warning(this, tr("Error"), @@ -477,7 +486,7 @@ void ViewerWidget::DisarmRecording() void ViewerWidget::UpdateAudioProcessor() { if (GetConnectedNode()) { - audio_processor_.Close(); + CloseAudioProcessor(); AudioParams ap = GetConnectedNode()->GetAudioParams(); AudioParams packed(OLIVE_CONFIG("AudioOutputSampleRate").toInt(), @@ -530,11 +539,78 @@ void ViewerWidget::CreateAddableAt(const QRectF &f) } } +void ViewerWidget::HandleFirstRequeueDestroy() +{ + // Extra protection to ensure we don't reference a destroyed object + if (first_requeue_watcher_ == sender()) { + first_requeue_watcher_ = nullptr; + } +} + +void ViewerWidget::ShowSubtitleProperties() +{ + QFont f(OLIVE_CONFIG("DefaultSubtitleFamily").toString(), OLIVE_CONFIG("DefaultSubtitleSize").toInt(), OLIVE_CONFIG("DefaultSubtitleWeight").toInt()); + QFontDialog fd(f, this); + + if (fd.exec() == QDialog::Accepted) { + f = fd.selectedFont(); + OLIVE_CONFIG("DefaultSubtitleSize") = f.pointSize(); + OLIVE_CONFIG("DefaultSubtitleFamily") = f.family(); + OLIVE_CONFIG("DefaultSubtitleWeight") = f.weight(); + display_widget_->update(); + } +} + +void ViewerWidget::DryRunFinished() +{ + RenderTicketWatcher *w = static_cast(sender()); + + if (dry_run_watchers_.contains(w)) { + RequestNextDryRun(); + } + + delete w; +} + +void ViewerWidget::RequestNextDryRun() +{ + if (IsPlaying()) { + rational next_time = Timecode::timestamp_to_time(dry_run_next_frame_, timebase()); + if (FrameExistsAtTime(next_time)) { + if (next_time > GetTime() + RenderManager::kDryRunInterval) { + QTimer::singleShot(timebase().toDouble() / playback_speed_, this, &ViewerWidget::RequestNextDryRun); + } else { + RenderTicketWatcher *watcher = new RenderTicketWatcher(this); + connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::DryRunFinished); + watcher->SetTicket(auto_cacher_.GetSingleFrame(next_time, true)); + dry_run_next_frame_ += playback_speed_; + dry_run_watchers_.append(watcher); + } + } + } +} + void ViewerWidget::CloseAudioProcessor() { audio_processor_.Close(); } +void ViewerWidget::SetWaveformMode(WaveformMode wf) +{ + waveform_mode_ = wf; + UpdateWaveformViewFromMode(); +} + +void ViewerWidget::UpdateWaveformViewFromMode() +{ + bool prefer_waveform = ShouldForceWaveform(); + + sizer_->setVisible(waveform_mode_ == kWFViewerAndWaveform || waveform_mode_ == kWFViewerOnly || (waveform_mode_ == kWFAutomatic && !prefer_waveform)); + waveform_view_->setVisible(waveform_mode_ == kWFViewerAndWaveform || waveform_mode_ == kWFWaveformOnly || (waveform_mode_ == kWFAutomatic && prefer_waveform)); + + waveform_view_->setSizePolicy(QSizePolicy::Expanding, waveform_mode_ == kWFViewerAndWaveform ? QSizePolicy::Maximum : QSizePolicy::Expanding); +} + void ViewerWidget::QueueNextAudioBuffer() { rational queue_end = audio_playback_queue_time_ + (kAudioPlaybackInterval * playback_speed_); @@ -553,7 +629,7 @@ void ViewerWidget::QueueNextAudioBuffer() RenderTicketWatcher *watcher = new RenderTicketWatcher(this); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForPlayback); audio_playback_queue_.push_back(watcher); - watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(audio_playback_queue_time_, queue_end), RenderTicketPriority::kHigh)); + watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(audio_playback_queue_time_, queue_end))); audio_playback_queue_time_ = queue_end; } @@ -604,30 +680,33 @@ void ViewerWidget::ReceivedAudioBufferForPlayback() void ViewerWidget::ReceivedAudioBufferForScrubbing() { - // NOTE: Might be good to organize a queue for this in the event that audio takes a long time to - // keep the scrubbed chunks ordered, similar to the playback_queue_ or audio_playback_queue_ - RenderTicketWatcher *watcher = static_cast(sender()); - if (watcher->HasResult()) { - SampleBuffer samples = watcher->Get().value(); - if (samples.is_allocated()) { - if (samples.audio_params().channel_count() > 0) { - AudioProcessor::Buffer buf; - int r = audio_processor_.Convert(samples.to_raw_ptrs().data(), samples.sample_count(), &buf); + while (!audio_scrub_watchers_.empty() && audio_scrub_watchers_.front() != watcher) { + audio_scrub_watchers_.pop_front(); + } - if (r >= 0) { - if (!buf.empty()) { - QString error; - const QByteArray &packed = buf.at(0); - AudioManager::instance()->ClearBufferedOutput(); - if (!AudioManager::instance()->PushToOutput(audio_processor_.to(), packed, &error)) { - Core::instance()->ShowStatusBarMessage(tr("Audio scrubbing failed: %1").arg(error)); + if (!audio_scrub_watchers_.empty()) { + if (watcher->HasResult()) { + SampleBuffer samples = watcher->Get().value(); + if (samples.is_allocated()) { + if (samples.audio_params().channel_count() > 0) { + AudioProcessor::Buffer buf; + int r = audio_processor_.Convert(samples.to_raw_ptrs().data(), samples.sample_count(), &buf); + + if (r >= 0) { + if (!buf.empty()) { + QString error; + const QByteArray &packed = buf.at(0); + AudioManager::instance()->ClearBufferedOutput(); + if (!AudioManager::instance()->PushToOutput(audio_processor_.to(), packed, &error)) { + Core::instance()->ShowStatusBarMessage(tr("Audio scrubbing failed: %1").arg(error)); + } + AudioMonitor::PushSampleBufferOnAll(samples); } - AudioMonitor::PushSampleBufferOnAll(samples); + } else { + qCritical() << "Failed to process audio for scrubbing:" << r; } - } else { - qCritical() << "Failed to process audio for scrubbing:" << r; } } } @@ -676,6 +755,7 @@ void ViewerWidget::ForceRequeueFromCurrentTime() RenderTicketWatcher *watcher = RequestNextFrameForQueue(); if (!first_requeue_watcher_) { first_requeue_watcher_ = watcher; + connect(first_requeue_watcher_, &RenderTicketWatcher::destroyed, this, &ViewerWidget::HandleFirstRequeueDestroy); } } } @@ -709,7 +789,7 @@ void ViewerWidget::UpdateTextureFromNode() ClearVideoAutoCacherQueue(); } - watcher->SetTicket(GetFrame(time, RenderTicketPriority::kHigh)); + watcher->SetTicket(GetFrame(time)); } else { // There is definitely no frame here, we can immediately flip to showing nothing nonqueue_watchers_.clear(); @@ -742,6 +822,8 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) viewer->auto_cacher_.SetAudioPaused(true); } + RenderManager::instance()->SetAggressiveGarbageCollection(true); + // Disarm recording if armed if (record_armed_) { DisarmRecording(); @@ -762,7 +844,7 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) playback_speed_ = speed; play_in_to_out_only_ = in_to_out_only; - playback_queue_next_frame_ = GetTimestamp(); + playback_queue_next_frame_ = GetTimestamp() + playback_speed_; controls_->ShowPauseButton(); @@ -776,18 +858,12 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) prequeuing_video_ = true; prequeue_count_ = 0; - // We "prioritize" the frames, which means they're pushed to the top of the render queue, - // we queue in reverse so that they're still queued in order - - playback_queue_next_frame_ += playback_speed_ * prequeue_length_; - int64_t temp = playback_queue_next_frame_; - for (int i=0; iSetAggressiveGarbageCollection(false); } prequeuing_video_ = false; prequeuing_audio_ = 0; + dry_run_watchers_.clear(); // Reset screen timeout timer PreventSleep(false); @@ -859,16 +939,23 @@ void ViewerWidget::PauseInternal() void ViewerWidget::PushScrubbedAudio() { if (!IsPlaying() && GetConnectedNode() && OLIVE_CONFIG("AudioScrubbing").toBool() && enable_audio_scrubbing_) { - // Get audio src device from renderer - const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters(); + if (ignore_scrub_ > 0) { + ignore_scrub_--; + } - if (params.is_valid()) { - // NOTE: Hardcoded scrubbing interval (20ms) - rational interval = rational(20, 1000); + if (ignore_scrub_ == 0) { + // Get audio src device from renderer + const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters(); - RenderTicketWatcher *watcher = new RenderTicketWatcher(); - connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForScrubbing); - watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(GetTime(), GetTime() + interval), RenderTicketPriority::kHigh)); + if (params.is_valid()) { + // NOTE: Hardcoded scrubbing interval (20ms) + rational interval = rational(20, 1000); + + RenderTicketWatcher *watcher = new RenderTicketWatcher(); + connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::ReceivedAudioBufferForScrubbing); + audio_scrub_watchers_.push_back(watcher); + watcher->SetTicket(auto_cacher_.GetRangeOfAudio(TimeRange(GetTime(), GetTime() + interval))); + } } } } @@ -918,7 +1005,7 @@ void ViewerWidget::SetDisplayImage(QVariant frame) } } -RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(RenderTicketPriority priority, bool increment) +RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(bool increment) { RenderTicketWatcher *watcher = nullptr; @@ -934,19 +1021,19 @@ RenderTicketWatcher *ViewerWidget::RequestNextFrameForQueue(RenderTicketPriority watcher->setProperty("time", QVariant::fromValue(next_time)); connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrameForQueue); queue_watchers_.append(watcher); - watcher->SetTicket(GetFrame(next_time, priority)); + watcher->SetTicket(GetFrame(next_time)); } return watcher; } -RenderTicketPtr ViewerWidget::GetFrame(const rational &t, RenderTicketPriority priority) +RenderTicketPtr ViewerWidget::GetFrame(const rational &t) { QString cache_fn = GetConnectedNode()->video_frame_cache()->GetValidCacheFilename(t); if (!QFileInfo::exists(cache_fn)) { // Frame hasn't been cached, start render job - return auto_cacher_.GetSingleFrame(t, priority); + return auto_cacher_.GetSingleFrame(t); } else { // Frame has been cached, grab the frame RenderTicketPtr ticket = std::make_shared(); @@ -1006,7 +1093,7 @@ int ViewerWidget::DeterminePlaybackQueueSize() end_ts = 0; } - int remaining_frames = (end_ts - GetTimestamp()) / playback_speed_; + int remaining_frames = (end_ts - GetTimestamp() - 1) / playback_speed_; // Generate maximum queue int max_frames = qCeil(kVideoPlaybackInterval.toDouble() / timebase().toDouble()); @@ -1014,33 +1101,22 @@ int ViewerWidget::DeterminePlaybackQueueSize() return qMin(max_frames, remaining_frames); } -void ViewerWidget::UpdateStack() -{ - rational new_tb; - - if (ShouldForceWaveform()) { - // If we have a node AND video is disconnected AND audio is connected, show waveform view - stack_->setCurrentWidget(waveform_view_); - //new_tb = GetConnectedNode()->audio_params().time_base(); - } else { - // Otherwise show regular display - stack_->setCurrentWidget(sizer_); - - /*if (GetConnectedNode()) { - new_tb = GetConnectedNode()->video_params().time_base(); - }*/ - } - - /*if (new_tb != timebase()) { - SetTimebase(new_tb); - }*/ -} - void ViewerWidget::ContextMenuSetFullScreen(QAction *action) { SetFullScreen(QGuiApplication::screens().at(action->data().toInt())); } +void ViewerWidget::ContextMenuSetPlaybackRes(QAction *action) +{ + int div = action->data().toInt(); + + auto vp = GetConnectedNode()->GetVideoParams(); + vp.set_divider(div); + + auto c = new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(GetConnectedNode(), ViewerOutput::kVideoParamsInput, 0)), QVariant::fromValue(vp)); + Core::instance()->undo_stack()->push(c); +} + void ViewerWidget::ContextMenuDisableSafeMargins() { context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(false)); @@ -1108,11 +1184,18 @@ void ViewerWidget::RendererGeneratedFrameForQueue() foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->queue()->AppendTimewise({ts, frame}, playback_speed_); } - prequeue_count_++; - if (prequeuing_video_ && prequeue_count_ == prequeue_length_) { - prequeuing_video_ = false; - FinishPlayPreprocess(); + if (prequeuing_video_) { + prequeue_count_++; + + if (prequeue_count_ == prequeue_length_) { + prequeuing_video_ = false; + FinishPlayPreprocess(); + } else { + // This call was mostly necessary to keep the threads busy between prequeue and playback. + // If we only have a single render thread, it's no longer necessary. + //RequestNextFrameForQueue(); + } } } } @@ -1191,6 +1274,18 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) connect(full_screen_menu, &QMenu::triggered, this, &ViewerWidget::ContextMenuSetFullScreen); } + { + // Playback Resolution Menu + Menu *playback_res_menu = new Menu(tr("Playback Resolution"), &menu); + menu.addMenu(playback_res_menu); + + for (int d : VideoParams::kSupportedDividers) { + playback_res_menu->AddActionWithData(VideoParams::GetNameForDivider(d), d, GetConnectedNode()->GetVideoParams().divider()); + } + + connect(playback_res_menu, &QMenu::triggered, this, &ViewerWidget::ContextMenuSetPlaybackRes); + } + { // Deinterlace Option if (GetConnectedNode()->GetVideoParams().interlacing() != VideoParams::kInterlaceNone) { @@ -1254,11 +1349,14 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) } { - QAction* show_waveform_action = menu.addAction(tr("Show Audio Waveform")); - show_waveform_action->setCheckable(true); - show_waveform_action->setChecked(stack_->currentWidget() == waveform_view_); - show_waveform_action->setEnabled(!ShouldForceWaveform()); - connect(show_waveform_action, &QAction::triggered, this, &ViewerWidget::ManualSwitchToWaveform); + auto waveform_menu = new Menu(tr("Audio Waveform"), &menu); + menu.addMenu(waveform_menu); + + waveform_menu->AddActionWithData(tr("Automatically Show/Hide"), kWFAutomatic, waveform_mode_); + waveform_menu->AddActionWithData(tr("Show Waveform Only"), kWFWaveformOnly, waveform_mode_); + waveform_menu->AddActionWithData(tr("Show Both Viewer And Waveform"), kWFViewerAndWaveform, waveform_mode_); + + connect(waveform_menu, &Menu::triggered, this, &ViewerWidget::UpdateWaveformModeFromMenu); } { @@ -1269,10 +1367,26 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) } if (context_menu_widget_ == display_widget_) { - QAction* show_subtitles_action = menu.addAction(tr("Show Subtitles")); + auto subtitle_menu = new Menu(tr("Subtitles"), &menu); + menu.addMenu(subtitle_menu); + + QAction* show_subtitles_action = subtitle_menu->addAction(tr("Show Subtitles")); show_subtitles_action->setCheckable(true); show_subtitles_action->setChecked(display_widget_->GetShowSubtitles()); connect(show_subtitles_action, &QAction::triggered, display_widget_, &ViewerDisplayWidget::SetShowSubtitles); + + subtitle_menu->addSeparator(); + + auto subtitle_font_properties = subtitle_menu->addAction(tr("Subtitle Properties")); + connect(subtitle_font_properties, &QAction::triggered, this, &ViewerWidget::ShowSubtitleProperties); + + auto subtitle_antialias = subtitle_menu->addAction(tr("Use Anti-aliasing")); + subtitle_antialias->setCheckable(true); + subtitle_antialias->setChecked(OLIVE_CONFIG("AntialiasSubtitles").toBool()); + connect(subtitle_antialias, &QAction::triggered, this, [this](bool e){ + OLIVE_CONFIG("AntialiasSubtitles") = e; + display_widget_->update(); + }); } menu.exec(static_cast(sender())->mapToGlobal(pos)); @@ -1282,9 +1396,9 @@ void ViewerWidget::Play(bool in_to_out_only) { if (in_to_out_only) { if (GetConnectedNode() - && GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { + && GetConnectedNode()->GetWorkArea()->enabled()) { // Jump to in point - SetTimeAndSignal(GetConnectedNode()->GetTimelinePoints()->workarea()->in()); + SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->in()); } else { in_to_out_only = false; } @@ -1412,11 +1526,11 @@ void ViewerWidget::PlaybackTimerUpdate() min_time = recording_range_.in(); max_time = recording_range_.out(); - } else if (play_in_to_out_only_ && GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { + } else if (play_in_to_out_only_ && GetConnectedNode()->GetWorkArea()->enabled()) { // If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea - min_time = GetConnectedNode()->GetTimelinePoints()->workarea()->in(); - max_time = GetConnectedNode()->GetTimelinePoints()->workarea()->out(); + min_time = GetConnectedNode()->GetWorkArea()->in(); + max_time = GetConnectedNode()->GetWorkArea()->out(); } else { @@ -1492,7 +1606,7 @@ void ViewerWidget::PlaybackTimerUpdate() } if (IsPlaying()) { - while (queue_watchers_.size() < DeterminePlaybackQueueSize()) { + while ((int(display_widget_->queue()->size()) + queue_watchers_.size()) < DeterminePlaybackQueueSize()) { if (!RequestNextFrameForQueue()) { // Prevent infinite loop break; @@ -1574,13 +1688,9 @@ void ViewerWidget::ViewerInvalidatedVideoRange(const TimeRange &range) } } -void ViewerWidget::ManualSwitchToWaveform(bool e) +void ViewerWidget::UpdateWaveformModeFromMenu(QAction *a) { - if (e) { - stack_->setCurrentWidget(waveform_view_); - } else { - stack_->setCurrentWidget(sizer_); - } + SetWaveformMode(static_cast(a->data().toInt())); } void ViewerWidget::DragEntered(QDragEnterEvent* event) diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 6c58ed194..a6c2fec22 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -34,7 +34,6 @@ #include "node/output/viewer/viewer.h" #include "render/previewaudiodevice.h" #include "render/previewautocacher.h" -#include "threading/threadticketwatcher.h" #include "viewerdisplay.h" #include "viewersizer.h" #include "viewerwindow.h" @@ -51,6 +50,13 @@ class ViewerWidget : public TimeBasedWidget { Q_OBJECT public: + enum WaveformMode { + kWFAutomatic, + kWFViewerOnly, + kWFWaveformOnly, + kWFViewerAndWaveform + }; + ViewerWidget(QWidget* parent = nullptr); virtual ~ViewerWidget() override; @@ -157,6 +163,8 @@ protected: virtual void ConnectNodeEvent(ViewerOutput *) override; virtual void DisconnectNodeEvent(ViewerOutput *) override; virtual void ConnectedNodeChangeEvent(ViewerOutput *) override; + virtual void ConnectedWorkAreaChangeEvent(TimelineWorkArea *) override; + virtual void ConnectedMarkersChangeEvent(TimelineMarkerList *) override; virtual void ScaleChangedEvent(const double& s) override; @@ -169,6 +177,11 @@ protected: return display_widget_; } + void IgnoreNextScrubEvent() + { + ignore_scrub_++; + } + private: int64_t GetTimestamp() const { @@ -195,9 +208,9 @@ private: void SetDisplayImage(QVariant frame); - RenderTicketWatcher *RequestNextFrameForQueue(RenderTicketPriority priority = RenderTicketPriority::kNormal, bool increment = true); + RenderTicketWatcher *RequestNextFrameForQueue(bool increment = true); - RenderTicketPtr GetFrame(const rational& t, RenderTicketPriority priority); + RenderTicketPtr GetFrame(const rational& t); void FinishPlayPreprocess(); @@ -223,7 +236,7 @@ private: void CloseAudioProcessor(); - QStackedWidget* stack_; + void SetWaveformMode(WaveformMode wf); ViewerSizer* sizer_; @@ -248,6 +261,7 @@ private: QTimer playback_backup_timer_; int64_t playback_queue_next_frame_; + int64_t dry_run_next_frame_; QVector playback_devices_; bool prequeuing_video_; @@ -272,6 +286,8 @@ private: static QVector instances_; + std::list audio_scrub_watchers_; + bool record_armed_; bool recording_; TimelineWidget *recording_callback_; @@ -284,6 +300,12 @@ private: bool enable_audio_scrubbing_; + WaveformMode waveform_mode_; + + QVector dry_run_watchers_; + + int ignore_scrub_; + private slots: void PlaybackTimerUpdate(); @@ -299,10 +321,12 @@ private slots: void SetZoomFromMenu(QAction* action); - void UpdateStack(); + void UpdateWaveformViewFromMode(); void ContextMenuSetFullScreen(QAction* action); + void ContextMenuSetPlaybackRes(QAction* action); + void ContextMenuDisableSafeMargins(); void ContextMenuSetSafeMargins(); @@ -317,7 +341,7 @@ private slots: void ViewerInvalidatedVideoRange(const olive::TimeRange &range); - void ManualSwitchToWaveform(bool e); + void UpdateWaveformModeFromMenu(QAction *a); void DragEntered(QDragEnterEvent* event); @@ -338,6 +362,14 @@ private slots: void CreateAddableAt(const QRectF &f); + void HandleFirstRequeueDestroy(); + + void ShowSubtitleProperties(); + + void DryRunFinished(); + + void RequestNextDryRun(); + }; } diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index cdbe6fdc6..8999385ae 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -34,8 +34,6 @@ #include #include -#include "common/define.h" -#include "common/functiontimer.h" #include "common/html.h" #include "common/qtutils.h" #include "config/config.h" @@ -45,7 +43,6 @@ #include "node/gizmo/point.h" #include "node/gizmo/polygon.h" #include "node/gizmo/screen.h" -#include "viewertexteditor.h" #include "window/mainwindow/mainwindow.h" namespace olive { @@ -68,18 +65,19 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) : show_widget_background_(false), playback_speed_(0), push_mode_(kPushNull), - add_band_(nullptr), - queue_starved_(false) + add_band_(false), + queue_starved_(false), + text_edit_(nullptr) { connect(Core::instance(), &Core::ToolChanged, this, &ViewerDisplayWidget::ToolChanged); - connect(this, &ViewerDisplayWidget::InnerWidgetMouseMove, this, &ViewerDisplayWidget::EmitColorAtCursor); - // Initializes cursor based on tool UpdateCursor(); const int kFrameRateAverageCount = 8; frame_rate_averages_.resize(kFrameRateAverageCount); + + inner_widget()->setAcceptDrops(true); } void ViewerDisplayWidget::SetMatrixTranslate(const QMatrix4x4 &mat) @@ -106,18 +104,18 @@ void ViewerDisplayWidget::SetMatrixCrop(const QMatrix4x4 &mat) void ViewerDisplayWidget::UpdateCursor() { if (Core::instance()->tool() == Tool::kHand) { - setCursor(Qt::OpenHandCursor); + this->inner_widget()->setCursor(Qt::OpenHandCursor); } else if (Core::instance()->tool() == Tool::kAdd) { - setCursor(Qt::CrossCursor); + this->inner_widget()->setCursor(Qt::CrossCursor); } else { - unsetCursor(); + this->inner_widget()->unsetCursor(); } } void ViewerDisplayWidget::SetSignalCursorColorEnabled(bool e) { signal_cursor_color_ = e; - inner_widget()->setMouseTracking(e); + SetInnerMouseTracking(e); } void ViewerDisplayWidget::SetImage(const QVariant &buffer) @@ -242,185 +240,115 @@ void ViewerDisplayWidget::IncrementSkippedFrames() Core::instance()->ShowStatusBarMessage(tr("%n skipped frame(s) detected during playback", nullptr, frames_skipped_), 10000); } -void ViewerDisplayWidget::mousePressEvent(QMouseEvent *event) +bool ViewerDisplayWidget::eventFilter(QObject *o, QEvent *e) { - if (event->button() == Qt::LeftButton && Core::instance()->tool() == Tool::kAdd - && (Core::instance()->GetSelectedAddableObject() == Tool::kAddableShape || Core::instance()->GetSelectedAddableObject() == Tool::kAddableTitle)) { - - add_band_start_ = event->pos(); - - add_band_ = new QRubberBand(QRubberBand::Rectangle, this); - add_band_->setGeometry(QRect(add_band_start_, add_band_start_)); - add_band_->show(); - - } else if (event->button() == Qt::LeftButton && gizmos_ - && (gizmo_last_draw_transform_inverted_ = gizmo_last_draw_transform_.inverted(), - current_gizmo_ = TryGizmoPress(gizmo_db_, gizmo_last_draw_transform_inverted_.map(event->pos())))) { - - // Handle gizmo click - gizmo_start_drag_ = event->pos(); - gizmo_last_drag_ = gizmo_start_drag_; - current_gizmo_->SetGlobals(NodeTraverser::GenerateGlobals(gizmo_params_, GenerateGizmoTime())); - - } else if (IsHandDrag(event)) { - - // Handle hand drag - hand_last_drag_pos_ = event->pos(); - hand_dragging_ = true; - emit HandDragStarted(); - setCursor(Qt::ClosedHandCursor); - - } else { - - if (event->button() == Qt::LeftButton) { - // Handle standard drag - emit DragStarted(); - } - - super::mousePressEvent(event); - - } -} - -void ViewerDisplayWidget::mouseMoveEvent(QMouseEvent *event) -{ - // Handle hand dragging - if (hand_dragging_) { - - // Emit movement - emit HandDragMoved(event->x() - hand_last_drag_pos_.x(), - event->y() - hand_last_drag_pos_.y()); - - hand_last_drag_pos_ = event->pos(); - - } else if (add_band_) { - - add_band_->setGeometry(QRect(event->pos(), add_band_start_).normalized()); - - } else if (current_gizmo_) { - - // Signal movement - if (DraggableGizmo *draggable = dynamic_cast(current_gizmo_)) { - if (!gizmo_drag_started_) { - QPointF start = gizmo_start_drag_ * gizmo_last_draw_transform_inverted_; - - rational gizmo_time = GetGizmoTime(); - NodeTraverser t; - t.SetCacheVideoParams(gizmo_params_); - NodeValueRow row = t.GenerateRow(gizmos_, TimeRange(gizmo_time, gizmo_time + gizmo_params_.frame_rate_as_time_base())); - - draggable->DragStart(row, start.x(), start.y(), gizmo_time); - gizmo_drag_started_ = true; - } - - QPointF v = event->pos() * gizmo_last_draw_transform_inverted_; - switch (draggable->GetDragValueBehavior()) { - case DraggableGizmo::kAbsolute: - // Above value is correct - break; - case DraggableGizmo::kDeltaFromPrevious: - v -= gizmo_last_drag_ * gizmo_last_draw_transform_inverted_; - gizmo_last_drag_ = event->pos(); - break; - case DraggableGizmo::kDeltaFromStart: - v -= gizmo_start_drag_ * gizmo_last_draw_transform_inverted_; - break; - } - - draggable->DragMove(v.x(), v.y(), event->modifiers()); - } - - } else { - - // Default behavior - super::mouseMoveEvent(event); - - } -} - -void ViewerDisplayWidget::mouseReleaseEvent(QMouseEvent *event) -{ - if (hand_dragging_) { - - // Handle hand drag - emit HandDragEnded(); - hand_dragging_ = false; - UpdateCursor(); - - } else if (add_band_) { - - const QRect &band_rect = add_band_->geometry(); - if (band_rect.width() > 1 && band_rect.height() > 1) { - QRectF r = GenerateDisplayTransform().inverted().mapRect(add_band_->geometry()); - emit CreateAddableAt(r); - } - - add_band_->deleteLater(); - add_band_ = nullptr; - - } else if (current_gizmo_) { - - // Handle gizmo - if (gizmo_drag_started_) { - MultiUndoCommand *command = new MultiUndoCommand(); - if (DraggableGizmo *draggable = dynamic_cast(current_gizmo_)) { - draggable->DragEnd(command); - } - Core::instance()->undo_stack()->pushIfHasChildren(command); - gizmo_drag_started_ = false; - } - current_gizmo_ = nullptr; - - } else { - - // Default behavior - super::mouseReleaseEvent(event); - - } -} - -void ViewerDisplayWidget::mouseDoubleClickEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton && gizmos_) { - QPointF ptr = TransformViewerSpaceToBufferSpace(event->pos()); - foreach (NodeGizmo *g, gizmos_->GetGizmos()) { - if (TextGizmo *text = dynamic_cast(g)) { - if (text->GetRect().contains(ptr)) { - OpenTextGizmo(text, event); - break; + if (o == this->inner_widget()) { + switch (e->type()) { + case QEvent::MouseButtonPress: + { + QMouseEvent *mouse = static_cast(e); + if (!(mouse->flags() & Qt::MouseEventCreatedDoubleClick)) { + if (OnMousePress(mouse)) { + return true; } } + break; + } + case QEvent::MouseMove: + EmitColorAtCursor(static_cast(e)); + if (OnMouseMove(static_cast(e))) { + return true; + } + break; + case QEvent::MouseButtonRelease: + if (OnMouseRelease(static_cast(e))) { + return true; + } + break; + case QEvent::MouseButtonDblClick: + if (OnMouseDoubleClick(static_cast(e))) { + return true; + } + break; + case QEvent::ShortcutOverride: + case QEvent::KeyPress: + if (OnKeyPress(static_cast(e))) { + return true; + } + break; + case QEvent::KeyRelease: + if (OnKeyRelease(static_cast(e))) { + return true; + } + break; + case QEvent::DragEnter: + { + auto drag_enter = static_cast(e); + if (text_edit_) { + ForwardDragEventToTextEdit(drag_enter); + } else { + emit DragEntered(drag_enter); + } + + if (drag_enter->isAccepted()) { + return true; + } + break; + } + case QEvent::DragMove: + { + auto drag_move = static_cast(e); + if (text_edit_) { + ForwardDragEventToTextEdit(drag_move); + } + + if (drag_move->isAccepted()) { + return true; + } + break; + } + case QEvent::DragLeave: + { + auto drag_leave = static_cast(e); + if (text_edit_) { + ForwardDragEventToTextEdit(drag_leave); + } else { + emit DragLeft(drag_leave); + } + + if (drag_leave->isAccepted()) { + return true; + } + break; + } + case QEvent::Drop: + { + auto drop = static_cast(e); + if (text_edit_) { + ForwardDragEventToTextEdit(drop); + } else { + emit Dropped(drop); + } + + if (drop->isAccepted()) { + return true; + } + break; + } + default: + break; + } + } else if (o == text_edit_) { + switch (e->type()) { + case QEvent::Paint: + update(); + return true; + default: + break; } } - super::mouseDoubleClickEvent(event); -} - -void ViewerDisplayWidget::dragEnterEvent(QDragEnterEvent *event) -{ - emit DragEntered(event); - - if (!event->isAccepted()) { - super::dragEnterEvent(event); - } -} - -void ViewerDisplayWidget::dragLeaveEvent(QDragLeaveEvent *event) -{ - emit DragLeft(event); - - if (!event->isAccepted()) { - super::dragLeaveEvent(event); - } -} - -void ViewerDisplayWidget::dropEvent(QDropEvent *event) -{ - emit Dropped(event); - - if (!event->isAccepted()) { - super::dropEvent(event); - } + return super::eventFilter(o, e); } void ViewerDisplayWidget::OnPaint() @@ -511,7 +439,7 @@ void ViewerDisplayWidget::OnPaint() TimeRange range = GenerateGizmoTime(); gizmo_db_ = gt.GenerateRow(gizmos_, range); - QPainter p(inner_widget()); + QPainter p(paint_device()); gizmo_last_draw_transform_ = GenerateGizmoTransform(gt, range); p.setWorldTransform(gizmo_last_draw_transform_); @@ -521,11 +449,21 @@ void ViewerDisplayWidget::OnPaint() gizmo->Draw(&p); } } + + if (text_edit_) { + QPixmap pm(text_edit_->width(), text_edit_->height()); + pm.fill(Qt::transparent); + + QPainter pixp(&pm); + text_edit_->Paint(&pixp, active_text_gizmo_->GetVerticalAlignment()); + + p.drawPixmap(text_edit_pos_, pm); + } } // Draw action/title safe areas if (safe_margin_.is_enabled()) { - QPainter p(inner_widget()); + QPainter p(paint_device()); p.setWorldTransform(GenerateWorldTransform()); p.setPen(QPen(Qt::lightGray, 0)); @@ -575,7 +513,7 @@ void ViewerDisplayWidget::OnPaint() } if (frame_rate_average_count_ >= frame_rate_averages_.size()) { - QPainter p(inner_widget()); + QPainter p(paint_device()); double average = 0.0; for (int i=0; irect(), tr("%1 FPS").arg(QString::number(average, 'f', 1))); + DrawTextWithCrudeShadow(&p, GetInnerRect(), tr("%1 FPS").arg(QString::number(average, 'f', 1))); if (frames_skipped_ > 0) { - DrawTextWithCrudeShadow(&p, inner_widget()->rect().adjusted(0, p.fontMetrics().height(), 0, 0), + DrawTextWithCrudeShadow(&p, GetInnerRect().adjusted(0, p.fontMetrics().height(), 0, 0), tr("%1 frames skipped").arg(frames_skipped_)); } } } // Extraordinarily basic subtitle renderer. Hoping to swap this out with libass at some point. - if (show_subtitles_ && subtitle_tracks_) { - const QVector &subtitle_tracklist = subtitle_tracks_->track_list(Track::kSubtitle)->GetTracks(); + DrawSubtitleTracks(); - if (!subtitle_tracklist.empty()) { - QPainter p(inner_widget()); - - QTransform transform = GenerateWorldTransform(); - QRect bounding_box = transform.mapRect(rect()); - - bounding_box.adjust(bounding_box.width()/10, bounding_box.height()/10, -bounding_box.width()/10, -bounding_box.height()/10); - - QFont f = p.font(); - int font_sz = bounding_box.height() / 18; - f.setStyleHint(QFont::SansSerif); - f.setFamily(f.defaultFamily()); - f.setPointSize(font_sz); - f.setWeight(QFont::Bold); - p.setFont(f); - p.setPen(Qt::white); - - QPainterPath path; - - int text_line = 1; - - for (int j=subtitle_tracklist.size()-1; j>=0; j--) { - Track *sub_track = subtitle_tracklist.at(j); - if (!sub_track->IsMuted()) { - if (SubtitleBlock *sub = dynamic_cast(sub_track->BlockAtTime(time_))) { - // Split into lines - QStringList list = QtUtils::WordWrapString(sub->GetText(), p.fontMetrics(), bounding_box.width()); - - for (int i=list.size()-1; i>=0; i--) { - int w = QtUtils::QFontMetricsWidth(p.fontMetrics(), list.at(i)); - path.addText(bounding_box.x() + bounding_box.width()/2 - w/2, bounding_box.y() + bounding_box.height() - p.fontMetrics().height() * text_line + p.fontMetrics().ascent(), p.font(), list.at(i)); - text_line++; - } - } - } - } - - p.setPen(QPen(Qt::black, font_sz / 16)); - p.setBrush(Qt::white); - p.drawPath(path); - } + if (add_band_) { + QPainter p(paint_device()); + QColor highlight = palette().highlight().color(); + p.setPen(highlight); + highlight.setAlpha(128); + p.setBrush(highlight); + p.drawRect(QRect(add_band_start_, add_band_end_).normalized()); } } void ViewerDisplayWidget::OnDestroy() { - renderer()->DestroyNativeShader(deinterlace_shader_); - deinterlace_shader_.clear(); - renderer()->DestroyNativeShader(blank_shader_); - blank_shader_.clear(); + if (!deinterlace_shader_.isNull()) { + renderer()->DestroyNativeShader(deinterlace_shader_); + deinterlace_shader_.clear(); + } + if (!blank_shader_.isNull()) { + renderer()->DestroyNativeShader(blank_shader_); + blank_shader_.clear(); + } super::OnDestroy(); @@ -789,58 +696,302 @@ NodeGizmo *ViewerDisplayWidget::TryGizmoPress(const NodeValueRow &row, const QPo void ViewerDisplayWidget::OpenTextGizmo(TextGizmo *text, QMouseEvent *event) { - QTransform gizmo_transform = GenerateDisplayTransform(); + active_text_gizmo_ = text; + text_transform_ = GenerateGizmoTransform(); + text_transform_inverted_ = text_transform_.inverted(); - ViewerTextEditor *text_edit = new ViewerTextEditor(gizmo_transform.m11(), this); - Html::HtmlToDoc(text_edit->document(), text->GetHtml()); - text_edit->setProperty("gizmo", reinterpret_cast(text)); + // Create text editor + text_edit_ = new ViewerTextEditor(text_transform_.m11(), this); - QRectF transformed_geom = gizmo_transform.map(text->GetRect()).boundingRect(); - text_edit->setGeometry(transformed_geom.toRect()); + // Set text editor's gizmo property for later use + text_edit_->setProperty("gizmo", reinterpret_cast(text)); - ViewerTextEditorToolBar *toolbar = new ViewerTextEditorToolBar(this); + // Install ourselves as event filter so we can receive the text editor's paint events + text_edit_->installEventFilter(this); - QPoint pos = mapToGlobal(QPoint(transformed_geom.x(), transformed_geom.y() - toolbar->height())); - for (QScreen *screen : qApp->screens()) { - if (screen->geometry().contains(pos)) { - if (pos.x() + toolbar->width() > screen->geometry().right()) { - pos.setX(screen->geometry().right() - toolbar->width()); - } - break; + // Disable focus on text editor + text_edit_->setFocusPolicy(Qt::NoFocus); + + // Disable mouse events on text editor + text_edit_->setAttribute(Qt::WA_TransparentForMouseEvents); + + // "Show" text editor so that it throws paint events, even though its paint event is disabled + text_edit_->show(); + + // Convert HTML to Qt document + Html::HtmlToDoc(text_edit_->document(), text->GetHtml()); + + // Connect text change event to propagate back to node + connect(text_edit_, &ViewerTextEditor::textChanged, this, &ViewerDisplayWidget::TextEditChanged); + + // Connect destroyed signal to cleanup after destruction + connect(text_edit_, &ViewerTextEditor::destroyed, this, &ViewerDisplayWidget::TextEditDestroyed); + + // Set text editor's size to logical size + QRectF text_rect = text->GetRect(); + text_edit_pos_ = text_rect.topLeft(); + text_edit_->setGeometry(text_rect.toRect()); + + // Emit text gizmo activation signal + emit text->Activated(); + + // Create toolbar + text_toolbar_ = new ViewerTextEditorToolBar(text_edit_); + text_toolbar_->setWindowFlags(Qt::Dialog| Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint); + connect(text_toolbar_, &ViewerTextEditorToolBar::VerticalAlignmentChanged, text, &TextGizmo::SetVerticalAlignment); + connect(text, &TextGizmo::VerticalAlignmentChanged, text_toolbar_, &ViewerTextEditorToolBar::SetVerticalAlignment); + text_toolbar_->SetVerticalAlignment(text->GetVerticalAlignment()); + text_edit_->ConnectToolBar(text_toolbar_); + + QPoint toolbar_pos = mapToGlobal(text_transform_.map(text_edit_pos_).toPoint()); + if (QScreen *screen = qApp->screenAt(toolbar_pos)) { + // Determine whether to anchor to the top of the rect of the bottom + if (toolbar_pos.y() - text_toolbar_->height() >= screen->geometry().top()) { + toolbar_pos.setY(toolbar_pos.y() - text_toolbar_->height()); + } else { + toolbar_pos.setY(toolbar_pos.y() + text_transform_.map(text_rect).boundingRect().height()); } + + // Clamp X + if (toolbar_pos.x() + text_toolbar_->width() > screen->geometry().right()) { + toolbar_pos.setX(screen->geometry().right() - text_toolbar_->width()); + } + + // Clamp Y + if (toolbar_pos.y() + text_toolbar_->height() > screen->geometry().bottom()) { + toolbar_pos.setY(screen->geometry().bottom() - text_toolbar_->height()); + } + } else { + // Fallback + toolbar_pos.setY(toolbar_pos.y() - text_toolbar_->height()); } - toolbar->move(pos); - toolbar->show(); - text_edit->show(); + text_toolbar_->move(toolbar_pos); + text_toolbar_->show(); - connect(text_edit, &ViewerTextEditor::textChanged, this, &ViewerDisplayWidget::TextEditChanged); + // Allow widget to take keyboard focus + inner_widget()->setFocusPolicy(Qt::StrongFocus); + inner_widget()->setMouseTracking(true); - text_edit->ConnectToolBar(toolbar); + connect(qApp, &QApplication::focusChanged, this, &ViewerDisplayWidget::FocusChanged); - QPoint text_edit_pos; + // Start text cursor where the user clicked if (event) { - text_edit_pos = text_edit->mapFrom(this, event->pos()); + QPoint click_pos = text_transform_inverted_.map(event->pos()) - text_edit_pos_.toPoint(); + text_edit_->setTextCursor(text_edit_->cursorForPosition(click_pos)); } - // Ensure text edit is actually focused rather than the toolbar - connect(toolbar, &ViewerTextEditorToolBar::FirstPaint, this, [this, text_edit, text_edit_pos]{ - // Grab focus back from the toolbar - this->raise(); - this->activateWindow(); - text_edit->setFocus(); + // Grab focus back from the toolbar + connect(text_toolbar_, &ViewerTextEditorToolBar::FirstPaint, this, [this]{ + Core::instance()->main_window()->activateWindow(); + inner_widget()->setFocus(); + }); +} + +bool ViewerDisplayWidget::OnMousePress(QMouseEvent *event) +{ + if (IsHandDrag(event)) { + + // Handle hand drag + hand_last_drag_pos_ = event->pos(); + hand_dragging_ = true; + emit HandDragStarted(); + inner_widget()->setCursor(Qt::ClosedHandCursor); + + return true; + + } else if (text_edit_) { + + return ForwardMouseEventToTextEdit(event, true); + + } else if (event->button() == Qt::LeftButton) { + + if (Core::instance()->tool() == Tool::kAdd + && (Core::instance()->GetSelectedAddableObject() == Tool::kAddableShape || Core::instance()->GetSelectedAddableObject() == Tool::kAddableTitle)) { + + add_band_start_ = event->pos(); + add_band_end_ = add_band_start_; + add_band_ = true; + + } else if (gizmos_ + && (gizmo_last_draw_transform_inverted_ = gizmo_last_draw_transform_.inverted(), + current_gizmo_ = TryGizmoPress(gizmo_db_, gizmo_last_draw_transform_inverted_.map(event->pos())))) { + + // Handle gizmo click + gizmo_start_drag_ = event->pos(); + gizmo_last_drag_ = gizmo_start_drag_; + current_gizmo_->SetGlobals(NodeTraverser::GenerateGlobals(gizmo_params_, GenerateGizmoTime())); + + } else { + + // Handle standard drag + emit DragStarted(); - // Start text cursor where the user clicked - if (!text_edit_pos.isNull()) { - text_edit->setTextCursor(text_edit->cursorForPosition(text_edit_pos)); } - // HACK: On macOS, for some reason the QDockWidget receives focus before the - // ViewerTextEditor, causing the editor to close prematurely. However this only - // happens the first time the editor receives focus and not subsequent times, so - // if we get it to only listen after the first one, this solves the problem. - text_edit->SetListenToFocusEvents(true); - }); + return true; + + } + + return false; +} + +bool ViewerDisplayWidget::OnMouseMove(QMouseEvent *event) +{ + // Handle hand dragging + if (hand_dragging_) { + + // Emit movement + emit HandDragMoved(event->x() - hand_last_drag_pos_.x(), + event->y() - hand_last_drag_pos_.y()); + + hand_last_drag_pos_ = event->pos(); + + return true; + + } else if (text_edit_) { + + if (event->buttons() == Qt::NoButton) { + QPointF mapped = text_transform_inverted_.map(event->pos()) - text_edit_pos_; + if (mapped.x() >= 0 && mapped.y() >= 0 && mapped.x() < text_edit_->width() && mapped.y() < text_edit_->height()) { + inner_widget()->setCursor(Qt::IBeamCursor); + } else { + inner_widget()->unsetCursor(); + } + } + + return ForwardMouseEventToTextEdit(event); + + } else if (add_band_) { + + add_band_end_ = event->pos(); + update(); + return true; + + } else if (current_gizmo_) { + + // Signal movement + if (DraggableGizmo *draggable = dynamic_cast(current_gizmo_)) { + if (!gizmo_drag_started_) { + QPointF start = gizmo_start_drag_ * gizmo_last_draw_transform_inverted_; + + rational gizmo_time = GetGizmoTime(); + NodeTraverser t; + t.SetCacheVideoParams(gizmo_params_); + NodeValueRow row = t.GenerateRow(gizmos_, TimeRange(gizmo_time, gizmo_time + gizmo_params_.frame_rate_as_time_base())); + + draggable->DragStart(row, start.x(), start.y(), gizmo_time); + gizmo_drag_started_ = true; + } + + QPointF v = event->pos() * gizmo_last_draw_transform_inverted_; + switch (draggable->GetDragValueBehavior()) { + case DraggableGizmo::kAbsolute: + // Above value is correct + break; + case DraggableGizmo::kDeltaFromPrevious: + v -= gizmo_last_drag_ * gizmo_last_draw_transform_inverted_; + gizmo_last_drag_ = event->pos(); + break; + case DraggableGizmo::kDeltaFromStart: + v -= gizmo_start_drag_ * gizmo_last_draw_transform_inverted_; + break; + } + + draggable->DragMove(v.x(), v.y(), event->modifiers()); + + return true; + } + + } + + return false; +} + +bool ViewerDisplayWidget::OnMouseRelease(QMouseEvent *e) +{ + if (hand_dragging_) { + + // Handle hand drag + emit HandDragEnded(); + hand_dragging_ = false; + UpdateCursor(); + + return true; + + } else if (text_edit_) { + + return ForwardMouseEventToTextEdit(e); + + } else if (add_band_) { + + QRect band_rect = QRect(add_band_start_, add_band_end_).normalized(); + if (band_rect.width() > 1 && band_rect.height() > 1) { + QRectF r = GenerateDisplayTransform().inverted().mapRect(band_rect); + emit CreateAddableAt(r); + } + + add_band_ = false; + return true; + + } else if (current_gizmo_) { + + // Handle gizmo + if (gizmo_drag_started_) { + MultiUndoCommand *command = new MultiUndoCommand(); + if (DraggableGizmo *draggable = dynamic_cast(current_gizmo_)) { + draggable->DragEnd(command); + } + Core::instance()->undo_stack()->pushIfHasChildren(command); + gizmo_drag_started_ = false; + } + current_gizmo_ = nullptr; + + return true; + + } + + return false; +} + +bool ViewerDisplayWidget::OnMouseDoubleClick(QMouseEvent *event) +{ + if (text_edit_) { + return ForwardMouseEventToTextEdit(event); + } else if (event->button() == Qt::LeftButton && gizmos_) { + QPointF ptr = TransformViewerSpaceToBufferSpace(event->pos()); + foreach (NodeGizmo *g, gizmos_->GetGizmos()) { + if (TextGizmo *text = dynamic_cast(g)) { + if (text->GetRect().contains(ptr)) { + OpenTextGizmo(text, event); + return true; + } + } + } + } + + return false; +} + +bool ViewerDisplayWidget::OnKeyPress(QKeyEvent *e) +{ + if (text_edit_) { + if (e->key() == Qt::Key_Escape) { + CloseTextEditor(); + return true; + } else { + return ForwardEventToTextEdit(e); + } + } + return false; +} + +bool ViewerDisplayWidget::OnKeyRelease(QKeyEvent *e) +{ + if (text_edit_) { + return ForwardEventToTextEdit(e); + } + return false; } void ViewerDisplayWidget::EmitColorAtCursor(QMouseEvent *e) @@ -853,6 +1004,8 @@ void ViewerDisplayWidget::EmitColorAtCursor(QMouseEvent *e) QPointF pixel_pos = GenerateDisplayTransform().inverted().map(e->pos()); pixel_pos /= texture_->params().divider(); + makeCurrent(); + reference = renderer()->GetPixelFromTexture(texture_.get(), pixel_pos); display = color_service()->ConvertColor(reference); } @@ -861,6 +1014,176 @@ void ViewerDisplayWidget::EmitColorAtCursor(QMouseEvent *e) } } +void ViewerDisplayWidget::DrawSubtitleTracks() +{ + if (!show_subtitles_ || !subtitle_tracks_) { + return; + } + + const QVector &subtitle_tracklist = subtitle_tracks_->track_list(Track::kSubtitle)->GetTracks(); + if (subtitle_tracklist.empty()) { + return; + } + + // Scale font size by transform + QTransform display_transform = GenerateDisplayTransform(); + qreal font_sz = OLIVE_CONFIG("DefaultSubtitleSize").toInt(); + font_sz *= display_transform.m11(); + if (qIsNaN(font_sz)) { + return; + } + + QPainterPath path; + + QTransform transform = GenerateWorldTransform(); + QRect bounding_box = transform.mapRect(rect()); + + QFont f; + f.setPointSizeF(font_sz); + + QString family = OLIVE_CONFIG("DefaultSubtitleFamily").toString(); + if (!family.isEmpty()) { + f.setFamily(family); + } + + f.setWeight(OLIVE_CONFIG("DefaultSubtitleWeight").toInt()); + + bounding_box.adjust(bounding_box.width()/10, bounding_box.height()/10, -bounding_box.width()/10, -bounding_box.height()/10); + + QFontMetrics fm(f); + + for (int j=subtitle_tracklist.size()-1; j>=0; j--) { + Track *sub_track = subtitle_tracklist.at(j); + if (!sub_track->IsMuted()) { + if (SubtitleBlock *sub = dynamic_cast(sub_track->BlockAtTime(time_))) { + // Split into lines + QStringList list = QtUtils::WordWrapString(sub->GetText(), fm, bounding_box.width()); + + for (int i=list.size()-1; i>=0; i--) { + int w = QtUtils::QFontMetricsWidth(fm, list.at(i)); + path.addText(bounding_box.width()/2 - w/2, bounding_box.height() - fm.height() * (list.size() - i) + fm.ascent(), f, list.at(i)); + } + } + } + } + + bool antialias = OLIVE_CONFIG("AntialiasSubtitles").toBool(); + + QPixmap *aa_pixmap; + QPainter *text_painter; + if (antialias) { + // QPainter only supports anti-aliasing in software, so to achieve it, we draw to a + // software buffer first and then draw that onto the hardware + aa_pixmap = new QPixmap(bounding_box.width(), bounding_box.height()); + aa_pixmap->fill(Qt::transparent); + text_painter = new QPainter(aa_pixmap); + } else { + // Just draw straight to the hardware + text_painter = new QPainter(paint_device()); + + // Offset path by however much is necessary + path.translate(bounding_box.x(), bounding_box.y()); + } + + text_painter->setPen(QPen(Qt::black, f.pointSizeF() / 16)); + text_painter->setBrush(Qt::white); + text_painter->setRenderHint(QPainter::Antialiasing); + + text_painter->drawPath(path); + + delete text_painter; + + if (antialias) { + // We just drew to a software buffer, now draw this image onto the hardware device + QPainter p(paint_device()); + p.drawPixmap(bounding_box.x(), bounding_box.y(), *aa_pixmap); + delete aa_pixmap; + } +} + +template +void ViewerDisplayWidget::ForwardDragEventToTextEdit(T *e) +{ + // HACK: Absolutely filthy hack. We need to be able to transform the mouse coordinates for our + // proxied QTextEdit, however unlike QMouseEvents, Qt's drag events don't allow modifying + // the position after construction. Unhelpfully, Qt also explicitly forbids users creating + // their own drag events because they "rely on Qt's internal state". So in order to forward + // drag events, we defy this by creating our own events, but DON'T process them through Qt's + // event queue and instead just send them directly to the widget (requiring its protected + // drag events to be made public). That way Qt stays happy, because as far as it's + // concerned it's only interfacing with this widget, and the QTextEdit gets to receive + // transformed events. It's a terrible hack, but seems to work. + + if constexpr (std::is_same_v) { + text_edit_->dragLeaveEvent(e); + } else { + T relay(AdjustPosByVAlign(GetVirtualPosForTextEdit(e->posF())).toPoint(), + e->possibleActions(), + e->mimeData(), + e->mouseButtons(), + e->keyboardModifiers()); + + if (e->type() == QEvent::DragEnter) { + text_edit_->dragEnterEvent(static_cast(&relay)); + } else if (e->type() == QEvent::DragMove) { + text_edit_->dragMoveEvent(static_cast(&relay)); + } else if (e->type() == QEvent::Drop) { + text_edit_->dropEvent(&relay); + } + + if (relay.isAccepted()) { + e->accept(); + } + } +} + +bool ViewerDisplayWidget::ForwardMouseEventToTextEdit(QMouseEvent *event, bool check_if_outside) +{ + // Transform screen mouse coords to world mouse coords + QPointF local_pos = GetVirtualPosForTextEdit(event->localPos()); + + if (check_if_outside) { + if (local_pos.x() < 0 || local_pos.x() >= text_edit_->width() || local_pos.y() < 0 || local_pos.y() >= text_edit_->height()) { + CloseTextEditor(); + return true; + } + } + + local_pos = AdjustPosByVAlign(local_pos); + + event->setLocalPos(local_pos); + return ForwardEventToTextEdit(event); +} + +bool ViewerDisplayWidget::ForwardEventToTextEdit(QEvent *event) +{ + qApp->sendEvent(text_edit_->viewport(), event); + return event->isAccepted(); +} + +QPointF ViewerDisplayWidget::AdjustPosByVAlign(QPointF p) +{ + switch (active_text_gizmo_->GetVerticalAlignment()) { + case Qt::AlignTop: + // Do nothing + break; + case Qt::AlignVCenter: + p.setY(p.y() - text_edit_->height()/2 + text_edit_->document()->size().height()/2); + break; + case Qt::AlignBottom: + p.setY(p.y() - text_edit_->height() + text_edit_->document()->size().height()); + break; + } + + return p; +} + +void ViewerDisplayWidget::CloseTextEditor() +{ + text_edit_->deleteLater(); + text_edit_ = nullptr; +} + void ViewerDisplayWidget::SetShowFPS(bool e) { show_fps_ = e; @@ -968,6 +1291,18 @@ void ViewerDisplayWidget::TextEditChanged() gizmo->UpdateInputHtml(html, GetGizmoTime()); } +void ViewerDisplayWidget::TextEditDestroyed() +{ + TextGizmo *gizmo = reinterpret_cast(sender()->property("gizmo").value()); + emit gizmo->Deactivated(); + text_edit_ = nullptr; + text_toolbar_ = nullptr; + inner_widget()->setMouseTracking(false); + inner_widget()->setFocusPolicy(Qt::NoFocus); + UpdateCursor(); + disconnect(qApp, &QApplication::focusChanged, this, &ViewerDisplayWidget::FocusChanged); +} + void ViewerDisplayWidget::SubtitlesChanged(const TimeRange &r) { if (time_ >= r.in() && time_ < r.out()) { @@ -975,4 +1310,27 @@ void ViewerDisplayWidget::SubtitlesChanged(const TimeRange &r) } } +void ViewerDisplayWidget::FocusChanged(QWidget *old, QWidget *now) +{ + if (!now) { + // Ignore this + return; + } + + bool unfocused = true; + + while (now) { + if (now == text_toolbar_ || now == this) { + unfocused = false; + break; + } else { + now = now->parentWidget(); + } + } + + if (unfocused) { + CloseTextEditor(); + } +} + } diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index 7359087f6..ab39a2b10 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -22,7 +22,6 @@ #define VIEWERGLWIDGET_H #include -#include #include #include "node/color/colormanager/colormanager.h" @@ -35,6 +34,7 @@ #include "viewerplaybacktimer.h" #include "viewerqueue.h" #include "viewersafemargininfo.h" +#include "viewertexteditor.h" #include "widget/manageddisplay/manageddisplay.h" #include "widget/timetarget/timetarget.h" @@ -131,6 +131,8 @@ public: return &timer_; } + virtual bool eventFilter(QObject *o, QEvent *e) override; + public slots: /** * @brief Set the transformation matrix to draw with @@ -216,30 +218,6 @@ signals: void CreateAddableAt(const QRectF &rect); -protected: - /** - * @brief Override the mouse press event for the DragStarted() signal and gizmos - */ - virtual void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Override mouse move to signal for the pixel sampler and gizmos - */ - virtual void mouseMoveEvent(QMouseEvent* event) override; - - /** - * @brief Override mouse release event for gizmos - */ - virtual void mouseReleaseEvent(QMouseEvent* event) override; - - virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - - virtual void dragEnterEvent(QDragEnterEvent* event) override; - - virtual void dragLeaveEvent(QDragLeaveEvent* event) override; - - virtual void dropEvent(QDropEvent* event) override; - protected slots: /** * @brief Paint function to display the texture (received in SetTexture()) on screen. @@ -268,6 +246,12 @@ private: QTransform GenerateDisplayTransform(); QTransform GenerateGizmoTransform(NodeTraverser >, const TimeRange &range); + QTransform GenerateGizmoTransform() + { + NodeTraverser t; + t.SetCacheVideoParams(gizmo_params_); + return GenerateGizmoTransform(t, GenerateGizmoTime()); + } TimeRange GenerateGizmoTime() { @@ -279,6 +263,33 @@ private: void OpenTextGizmo(TextGizmo *text, QMouseEvent *event = nullptr); + bool OnMousePress(QMouseEvent *e); + bool OnMouseMove(QMouseEvent *e); + bool OnMouseRelease(QMouseEvent *e); + bool OnMouseDoubleClick(QMouseEvent *e); + + bool OnKeyPress(QKeyEvent *e); + bool OnKeyRelease(QKeyEvent *e); + + void EmitColorAtCursor(QMouseEvent* e); + + void DrawSubtitleTracks(); + + QPointF GetVirtualPosForTextEdit(const QPointF &p) + { + return text_transform_inverted_.map(p) - text_edit_pos_; + } + + template + void ForwardDragEventToTextEdit(T *event); + + bool ForwardMouseEventToTextEdit(QMouseEvent *event, bool check_if_outside = false); + bool ForwardEventToTextEdit(QEvent *event); + + QPointF AdjustPosByVAlign(QPointF p); + + void CloseTextEditor(); + /** * @brief Internal reference to the OpenGL texture to draw. Set in SetTexture() and used in paintGL(). */ @@ -385,20 +396,29 @@ private: rational playback_timebase_; - QRubberBand *add_band_; + bool add_band_; QPoint add_band_start_; + QPoint add_band_end_; bool queue_starved_; -private slots: - void EmitColorAtCursor(QMouseEvent* e); + TextGizmo *active_text_gizmo_; + QPointF text_edit_pos_; + ViewerTextEditor *text_edit_; + ViewerTextEditorToolBar *text_toolbar_; + QTransform text_transform_; + QTransform text_transform_inverted_; +private slots: void UpdateFromQueue(); void TextEditChanged(); + void TextEditDestroyed(); void SubtitlesChanged(const TimeRange &r); + void FocusChanged(QWidget *old, QWidget *now); + }; diff --git a/app/widget/viewer/viewertexteditor.cpp b/app/widget/viewer/viewertexteditor.cpp index 76fd94037..99da81967 100644 --- a/app/widget/viewer/viewertexteditor.cpp +++ b/app/widget/viewer/viewertexteditor.cpp @@ -24,11 +24,13 @@ #include #include #include +#include #include +#include +#include #include "common/qtutils.h" #include "ui/icons/icons.h" -#include "widget/colorbutton/colorbutton.h" namespace olive { @@ -38,7 +40,8 @@ ViewerTextEditor::ViewerTextEditor(double scale, QWidget *parent) : super(parent), transparent_clone_(nullptr), block_update_toolbar_signal_(false), - listen_to_focus_events_(false) + listen_to_focus_events_(false), + forced_default_(false) { // Ensure default text color is white QPalette p = palette(); @@ -47,6 +50,9 @@ ViewerTextEditor::ViewerTextEditor(double scale, QWidget *parent) : document()->setDefaultStyleSheet(QStringLiteral("body { color: white; }")); + // Ensure cursor is visible at this scale + setCursorWidth(std::ceil(1.0 / scale)); + viewport()->setAutoFillBackground(false); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -56,12 +62,11 @@ ViewerTextEditor::ViewerTextEditor(double scale, QWidget *parent) : // Force DPI to the same one that we're using in the actual render dpi_force_ = QImage(1, 1, QImage::Format_RGBA8888_Premultiplied); - const int dpm = 3780 * scale; + const int dpm = 3780; dpi_force_.setDotsPerMeterX(dpm); dpi_force_.setDotsPerMeterY(dpm); document()->documentLayout()->setPaintDevice(&dpi_force_); - connect(qApp, &QApplication::focusChanged, this, &ViewerTextEditor::FocusChanged); connect(this, &QTextEdit::currentCharFormatChanged, this, &ViewerTextEditor::FormatChanged); connect(document(), &QTextDocument::contentsChanged, this, &ViewerTextEditor::DocumentChanged, Qt::QueuedConnection); @@ -70,8 +75,6 @@ ViewerTextEditor::ViewerTextEditor(double scale, QWidget *parent) : void ViewerTextEditor::ConnectToolBar(ViewerTextEditorToolBar *toolbar) { - connect(this, &ViewerTextEditor::destroyed, toolbar, &ViewerTextEditorToolBar::deleteLater); - connect(toolbar, &ViewerTextEditorToolBar::FamilyChanged, this, &ViewerTextEditor::SetFamily); connect(toolbar, &ViewerTextEditorToolBar::SizeChanged, this, &ViewerTextEditor::setFontPointSize); connect(toolbar, &ViewerTextEditorToolBar::StyleChanged, this, &ViewerTextEditor::SetStyle); @@ -94,25 +97,13 @@ void ViewerTextEditor::ConnectToolBar(ViewerTextEditorToolBar *toolbar) toolbars_.append(toolbar); } -void ViewerTextEditor::keyPressEvent(QKeyEvent *event) +void ViewerTextEditor::Paint(QPainter *p, Qt::Alignment valign) { - super::keyPressEvent(event); - - if (event->key() == Qt::Key_Escape) { - deleteLater(); - } -} - -void ViewerTextEditor::paintEvent(QPaintEvent *e) -{ - QPainter p(this->viewport()); - QAbstractTextDocumentLayout::PaintContext ctx; - QRect r = e->rect(); - if (r.isValid()) - p.setClipRect(r, Qt::IntersectClip); - ctx.clip = r; + QRect clip = this->rect(); + p->setClipRect(clip, Qt::IntersectClip); + ctx.clip = clip; ctx.cursorPosition = this->textCursor().position(); @@ -134,7 +125,29 @@ void ViewerTextEditor::paintEvent(QPaintEvent *e) ctx.selections.append(selection); } - transparent_clone_->documentLayout()->draw(&p, ctx); + switch (valign) { + case Qt::AlignTop: + // Do nothing + break; + case Qt::AlignVCenter: + p->translate(0, clip.height()/2-document()->size().height()/2); + break; + case Qt::AlignBottom: + p->translate(0, clip.height()-document()->size().height()); + break; + } + + const bool use_transparent_clone = true; + if (transparent_clone_ && use_transparent_clone) { + transparent_clone_->documentLayout()->draw(p, ctx); + } else { + document()->documentLayout()->draw(p, ctx); + } +} + +void ViewerTextEditor::paintEvent(QPaintEvent *e) +{ + // Disable painting } void ViewerTextEditor::UpdateToolBar(ViewerTextEditorToolBar *toolbar, const QTextCharFormat &f, const QTextBlockFormat &b, Qt::Alignment alignment) @@ -176,34 +189,6 @@ void ViewerTextEditor::UpdateToolBar(ViewerTextEditorToolBar *toolbar, const QTe toolbar->SetLineHeight(b.lineHeight() == 0.0 ? 100 : b.lineHeight()); } -void ViewerTextEditor::FocusChanged(QWidget *old, QWidget *now) -{ - if (!listen_to_focus_events_) { - return; - } - - QWidget *test = now; - - if (!test) { - // Ignore null focuses because that could be one of the toolbar widgets simply losing focus - // and that would be undesirable to close the text editor from - return; - } - - while (test) { - if (test == this - || dynamic_cast(test) - || dynamic_cast(test)) { - return; - } - - test = test->parentWidget(); - } - - // If we didn't return in the loop, the user must have focused on something else - deleteLater(); -} - void ViewerTextEditor::FormatChanged(const QTextCharFormat &f) { if (!block_update_toolbar_signal_) { @@ -211,6 +196,10 @@ void ViewerTextEditor::FormatChanged(const QTextCharFormat &f) UpdateToolBar(toolbar, f, textCursor().blockFormat(), this->alignment()); } } + + if (!(document()->blockCount() == 1 && document()->firstBlock().text().isEmpty())) { + default_fmt_ = f; + } } void ViewerTextEditor::SetFamily(const QString &s) @@ -273,6 +262,7 @@ void ViewerTextEditor::MergeCharFormat(const QTextCharFormat &fmt) // this can be undesirable if the user is currently typing a font block_update_toolbar_signal_ = true; mergeCurrentCharFormat(fmt); + //default_fmt_ = this->currentCharFormat(); block_update_toolbar_signal_ = false; } @@ -301,6 +291,16 @@ void ViewerTextEditor::LockScrollBarMaximumToZero() void ViewerTextEditor::DocumentChanged() { + if (document()->blockCount() == 1 && document()->firstBlock().text().isEmpty()) { + if (!forced_default_) { + QTextCursor c(document()->firstBlock()); + c.setBlockCharFormat(default_fmt_); + forced_default_ = true; + } + } else { + forced_default_ = false; + } + // HACK: We want to show the text cursor and selections without necessarily rendering the text, // because the text is already being rendered underneath the gizmo (and rendering twice will // alter the overall look of the text while editing). This is something that Qt does not @@ -313,6 +313,7 @@ void ViewerTextEditor::DocumentChanged() delete transparent_clone_; transparent_clone_ = document()->clone(this); transparent_clone_->documentLayout()->setPaintDevice(&dpi_force_); + transparent_clone_->documentLayout()->setProperty("cursorWidth", document()->documentLayout()->property("cursorWidth")); QTextCursor cursor(transparent_clone_); cursor.select(QTextCursor::Document); @@ -323,133 +324,161 @@ void ViewerTextEditor::DocumentChanged() } ViewerTextEditorToolBar::ViewerTextEditorToolBar(QWidget *parent) : - QWidget(parent, Qt::Tool | Qt::FramelessWindowHint), - painted_(false) + QWidget(parent), + painted_(false), + drag_enabled_(true) { QVBoxLayout *outer_layout = new QVBoxLayout(this); - outer_layout->setSpacing(0); - QHBoxLayout *basic_layout = new QHBoxLayout(); - outer_layout->addLayout(basic_layout); + const int advanced_slider_width = QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("9999.9%")); - int advanced_slider_width = QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("9999.9%")); + { + QHBoxLayout *row_layout = new QHBoxLayout(); + row_layout->setSpacing(0); + outer_layout->addLayout(row_layout); - font_combo_ = new QFontComboBox(); - connect(font_combo_, &QFontComboBox::currentTextChanged, this, &ViewerTextEditorToolBar::UpdateFontStyleListAndEmitFamilyChanged); - basic_layout->addWidget(font_combo_); + font_combo_ = new QFontComboBox(); + connect(font_combo_, &QFontComboBox::currentTextChanged, this, &ViewerTextEditorToolBar::UpdateFontStyleListAndEmitFamilyChanged); + row_layout->addWidget(font_combo_); - font_sz_slider_ = new FloatSlider(); - font_sz_slider_->SetMinimum(0.1); - font_sz_slider_->SetMaximum(9999.9); - font_sz_slider_->SetDecimalPlaces(1); - font_sz_slider_->SetAlignment(Qt::AlignCenter); - font_sz_slider_->setFixedWidth(advanced_slider_width); - connect(font_sz_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::SizeChanged); - font_sz_slider_->SetLadderElementCount(2); - basic_layout->addWidget(font_sz_slider_); + font_sz_slider_ = new FloatSlider(); + font_sz_slider_->SetMinimum(0.1); + font_sz_slider_->SetMaximum(9999.9); + font_sz_slider_->SetDecimalPlaces(1); + font_sz_slider_->SetAlignment(Qt::AlignCenter); + font_sz_slider_->setFixedWidth(advanced_slider_width); + connect(font_sz_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::SizeChanged); + font_sz_slider_->SetLadderElementCount(2); + row_layout->addWidget(font_sz_slider_); - style_combo_ = new QComboBox(); - connect(style_combo_, &QComboBox::currentTextChanged, this, &ViewerTextEditorToolBar::StyleChanged); - basic_layout->addWidget(style_combo_); + style_combo_ = new QComboBox(); + connect(style_combo_, &QComboBox::currentTextChanged, this, &ViewerTextEditorToolBar::StyleChanged); + row_layout->addWidget(style_combo_); - underline_btn_ = new QPushButton(); - connect(underline_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::UnderlineChanged); - underline_btn_->setCheckable(true); - underline_btn_->setIcon(icon::TextUnderline); - basic_layout->addWidget(underline_btn_); + underline_btn_ = new QPushButton(); + connect(underline_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::UnderlineChanged); + underline_btn_->setCheckable(true); + underline_btn_->setIcon(icon::TextUnderline); + row_layout->addWidget(underline_btn_); - strikethrough_btn_ = new QPushButton(); - connect(strikethrough_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::StrikethroughChanged); - strikethrough_btn_->setCheckable(true); - strikethrough_btn_->setIcon(icon::TextStrikethrough); - basic_layout->addWidget(strikethrough_btn_); + strikethrough_btn_ = new QPushButton(); + connect(strikethrough_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::StrikethroughChanged); + strikethrough_btn_->setCheckable(true); + strikethrough_btn_->setIcon(icon::TextStrikethrough); + row_layout->addWidget(strikethrough_btn_); - basic_layout->addWidget(QtUtils::CreateVerticalLine()); + AddSpacer(row_layout); - align_left_btn_ = new QPushButton(); - align_left_btn_->setCheckable(true); - align_left_btn_->setIcon(icon::TextAlignLeft); - connect(align_left_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignLeft);}); - basic_layout->addWidget(align_left_btn_); + color_btn_ = new QPushButton(); + color_btn_->setAutoFillBackground(true); + connect(color_btn_, &QPushButton::clicked, this, [this]{ + QColor c = color_btn_->property("color").value(); - align_center_btn_ = new QPushButton(); - align_center_btn_->setCheckable(true); - align_center_btn_->setIcon(icon::TextAlignCenter); - connect(align_center_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignCenter);}); - basic_layout->addWidget(align_center_btn_); + QColorDialog cd(c, this); + if (cd.exec() == QDialog::Accepted) { + c = cd.selectedColor(); + SetColor(c); + emit ColorChanged(c); + } + }); + row_layout->addWidget(color_btn_); - align_right_btn_ = new QPushButton(); - align_right_btn_->setCheckable(true); - align_right_btn_->setIcon(icon::TextAlignRight); - connect(align_right_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignRight);}); - basic_layout->addWidget(align_right_btn_); + row_layout->addStretch(); + } - align_justify_btn_ = new QPushButton(); - align_justify_btn_->setCheckable(true); - align_justify_btn_->setIcon(icon::TextAlignJustify); - connect(align_justify_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignJustify);}); - basic_layout->addWidget(align_justify_btn_); + { + QHBoxLayout *row_layout = new QHBoxLayout(); + row_layout->setSpacing(0); + outer_layout->addLayout(row_layout); - basic_layout->addWidget(QtUtils::CreateVerticalLine()); + align_left_btn_ = new QPushButton(); + align_left_btn_->setCheckable(true); + align_left_btn_->setIcon(icon::TextAlignLeft); + connect(align_left_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignLeft);}); + row_layout->addWidget(align_left_btn_); - color_btn_ = new QPushButton(); - color_btn_->setAutoFillBackground(true); - connect(color_btn_, &QPushButton::clicked, this, [this]{ - QColor c = color_btn_->property("color").value(); + align_center_btn_ = new QPushButton(); + align_center_btn_->setCheckable(true); + align_center_btn_->setIcon(icon::TextAlignCenter); + connect(align_center_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignHCenter);}); + row_layout->addWidget(align_center_btn_); - QColorDialog cd(c, this); - if (cd.exec() == QDialog::Accepted) { - c = cd.selectedColor(); - SetColor(c); - emit ColorChanged(c); - } - }); - basic_layout->addWidget(color_btn_); + align_right_btn_ = new QPushButton(); + align_right_btn_->setCheckable(true); + align_right_btn_->setIcon(icon::TextAlignRight); + connect(align_right_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignRight);}); + row_layout->addWidget(align_right_btn_); - basic_layout->addStretch(); + align_justify_btn_ = new QPushButton(); + align_justify_btn_->setCheckable(true); + align_justify_btn_->setIcon(icon::TextAlignJustify); + connect(align_justify_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignJustify);}); + row_layout->addWidget(align_justify_btn_); - QHBoxLayout *advanced_layout = new QHBoxLayout(); - outer_layout->addLayout(advanced_layout); + AddSpacer(row_layout); - advanced_layout->addWidget(new QLabel(tr("Stretch: "))); // FIXME: Procure icon + align_top_btn_ = new QPushButton(); + align_top_btn_->setCheckable(true); + align_top_btn_->setIcon(icon::TextAlignTop); + connect(align_top_btn_, &QPushButton::clicked, this, [this]{emit VerticalAlignmentChanged(Qt::AlignTop);}); + row_layout->addWidget(align_top_btn_); - stretch_slider_ = new IntegerSlider(); - stretch_slider_->SetMinimum(0); - stretch_slider_->SetDefaultValue(100); - stretch_slider_->setFixedWidth(advanced_slider_width); - stretch_slider_->SetFormat(tr("%1%")); - connect(stretch_slider_, &IntegerSlider::ValueChanged, this, &ViewerTextEditorToolBar::StretchChanged); - advanced_layout->addWidget(stretch_slider_); + align_middle_btn_ = new QPushButton(); + align_middle_btn_->setCheckable(true); + align_middle_btn_->setIcon(icon::TextAlignMiddle); + connect(align_middle_btn_, &QPushButton::clicked, this, [this]{emit VerticalAlignmentChanged(Qt::AlignVCenter);}); + row_layout->addWidget(align_middle_btn_); - advanced_layout->addWidget(new QLabel(tr("Kerning: "))); // FIXME: Procure icon + align_bottom_btn_ = new QPushButton(); + align_bottom_btn_->setCheckable(true); + align_bottom_btn_->setIcon(icon::TextAlignBottom); + connect(align_bottom_btn_, &QPushButton::clicked, this, [this]{emit VerticalAlignmentChanged(Qt::AlignBottom);}); + row_layout->addWidget(align_bottom_btn_); - kerning_slider_ = new FloatSlider(); - kerning_slider_->SetMinimum(0); - kerning_slider_->SetDefaultValue(100); - kerning_slider_->SetDecimalPlaces(1); - kerning_slider_->setFixedWidth(advanced_slider_width); - kerning_slider_->SetFormat(tr("%1%")); - connect(kerning_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::KerningChanged); - advanced_layout->addWidget(kerning_slider_); + AddSpacer(row_layout); - advanced_layout->addWidget(new QLabel(tr("Line Height: "))); // FIXME: Procure icon + small_caps_btn_ = new QPushButton(); + small_caps_btn_->setIcon(icon::TextSmallCaps); + small_caps_btn_->setCheckable(true); + connect(small_caps_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::SmallCapsChanged); + row_layout->addWidget(small_caps_btn_); - line_height_slider_ = new FloatSlider(); - line_height_slider_->SetMinimum(0); - line_height_slider_->SetDefaultValue(100); - line_height_slider_->SetDecimalPlaces(1); - line_height_slider_->setFixedWidth(advanced_slider_width); - line_height_slider_->SetFormat(tr("%1%")); - connect(line_height_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::LineHeightChanged); - advanced_layout->addWidget(line_height_slider_); + AddSpacer(row_layout); - small_caps_btn_ = new QPushButton(); - small_caps_btn_->setIcon(icon::TextSmallCaps); - small_caps_btn_->setCheckable(true); - connect(small_caps_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::SmallCapsChanged); - advanced_layout->addWidget(small_caps_btn_); + row_layout->addWidget(new QLabel(tr("Stretch: "))); // FIXME: Procure icon - advanced_layout->addStretch(); + stretch_slider_ = new IntegerSlider(); + stretch_slider_->SetMinimum(0); + stretch_slider_->SetDefaultValue(100); + stretch_slider_->setFixedWidth(advanced_slider_width); + stretch_slider_->SetFormat(tr("%1%")); + connect(stretch_slider_, &IntegerSlider::ValueChanged, this, &ViewerTextEditorToolBar::StretchChanged); + row_layout->addWidget(stretch_slider_); + + row_layout->addWidget(new QLabel(tr("Kerning: "))); // FIXME: Procure icon + + kerning_slider_ = new FloatSlider(); + kerning_slider_->SetMinimum(0); + kerning_slider_->SetDefaultValue(100); + kerning_slider_->SetDecimalPlaces(1); + kerning_slider_->setFixedWidth(advanced_slider_width); + kerning_slider_->SetFormat(tr("%1%")); + connect(kerning_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::KerningChanged); + row_layout->addWidget(kerning_slider_); + + row_layout->addWidget(new QLabel(tr("Line Height: "))); // FIXME: Procure icon + + line_height_slider_ = new FloatSlider(); + line_height_slider_->SetMinimum(0); + line_height_slider_->SetDefaultValue(100); + line_height_slider_->SetDecimalPlaces(1); + line_height_slider_->setFixedWidth(advanced_slider_width); + line_height_slider_->SetFormat(tr("%1%")); + connect(line_height_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::LineHeightChanged); + row_layout->addWidget(line_height_slider_); + + row_layout->addStretch(); + } setAutoFillBackground(true); @@ -459,11 +488,18 @@ ViewerTextEditorToolBar::ViewerTextEditorToolBar(QWidget *parent) : void ViewerTextEditorToolBar::SetAlignment(Qt::Alignment a) { align_left_btn_->setChecked(a == Qt::AlignLeft); - align_center_btn_->setChecked(a == Qt::AlignCenter); + align_center_btn_->setChecked(a == Qt::AlignHCenter); align_right_btn_->setChecked(a == Qt::AlignRight); align_justify_btn_->setChecked(a == Qt::AlignJustify); } +void ViewerTextEditorToolBar::SetVerticalAlignment(Qt::Alignment a) +{ + align_top_btn_->setChecked(a == Qt::AlignTop); + align_middle_btn_->setChecked(a == Qt::AlignVCenter); + align_bottom_btn_->setChecked(a == Qt::AlignBottom); +} + void ViewerTextEditorToolBar::SetColor(const QColor &c) { color_btn_->setProperty("color", c); @@ -484,6 +520,20 @@ void ViewerTextEditorToolBar::paintEvent(QPaintEvent *event) QWidget::paintEvent(event); } +void ViewerTextEditorToolBar::AddSpacer(QLayout *l) +{ + const int spacing = this->fontMetrics().height()/4; + QWidget *a = new QWidget(); + a->setFixedSize(spacing, 1); + l->addWidget(a); + + l->addWidget(QtUtils::CreateVerticalLine()); + + QWidget *b = new QWidget(); + b->setFixedSize(spacing, 1); + l->addWidget(b); +} + void ViewerTextEditorToolBar::UpdateFontStyleList(const QString &family) { QString temp = style_combo_->currentText(); @@ -509,7 +559,7 @@ void ViewerTextEditorToolBar::mousePressEvent(QMouseEvent *event) { QWidget::mousePressEvent(event); - if (event->button() == Qt::LeftButton) { + if (event->button() == Qt::LeftButton && drag_enabled_) { drag_anchor_ = event->pos(); } } @@ -518,7 +568,7 @@ void ViewerTextEditorToolBar::mouseMoveEvent(QMouseEvent *event) { QWidget::mouseMoveEvent(event); - if (event->buttons() & Qt::LeftButton) { + if ((event->buttons() & Qt::LeftButton) && drag_enabled_) { this->move(mapToParent(QPoint(event->pos() - drag_anchor_))); } } diff --git a/app/widget/viewer/viewertexteditor.h b/app/widget/viewer/viewertexteditor.h index 8df6a5285..8bef901b9 100644 --- a/app/widget/viewer/viewertexteditor.h +++ b/app/widget/viewer/viewertexteditor.h @@ -68,6 +68,7 @@ public slots: void SetUnderline(bool e) { underline_btn_->setChecked(e); } void SetStrikethrough(bool e) { strikethrough_btn_->setChecked(e); } void SetAlignment(Qt::Alignment a); + void SetVerticalAlignment(Qt::Alignment a); void SetColor(const QColor &c); void SetSmallCaps(bool e) { small_caps_btn_->setChecked(e); } void SetStretch(int i) { stretch_slider_->SetValue(i); } @@ -81,6 +82,7 @@ signals: void UnderlineChanged(bool e); void StrikethroughChanged(bool e); void AlignmentChanged(Qt::Alignment alignment); + void VerticalAlignmentChanged(Qt::Alignment alignment); void ColorChanged(const QColor &c); void SmallCapsChanged(bool e); void StretchChanged(int i); @@ -101,6 +103,8 @@ protected: virtual void paintEvent(QPaintEvent *event) override; private: + void AddSpacer(QLayout *l); + QPoint drag_anchor_; QFontComboBox *font_combo_; @@ -117,6 +121,10 @@ private: QPushButton *align_right_btn_; QPushButton *align_justify_btn_; + QPushButton *align_top_btn_; + QPushButton *align_middle_btn_; + QPushButton *align_bottom_btn_; + IntegerSlider *stretch_slider_; FloatSlider *kerning_slider_; FloatSlider *line_height_slider_; @@ -126,6 +134,8 @@ private: bool painted_; + bool drag_enabled_; + private slots: void UpdateFontStyleList(const QString &family); @@ -143,9 +153,14 @@ public: void SetListenToFocusEvents(bool e) { listen_to_focus_events_ = e; } -protected: - virtual void keyPressEvent(QKeyEvent *event) override; + void Paint(QPainter *p, Qt::Alignment valign); + virtual void dragEnterEvent(QDragEnterEvent *e) override { return QTextEdit::dragEnterEvent(e); } + virtual void dragMoveEvent(QDragMoveEvent *e) override { return QTextEdit::dragMoveEvent(e); } + virtual void dragLeaveEvent(QDragLeaveEvent *e) override { return QTextEdit::dragLeaveEvent(e); } + virtual void dropEvent(QDropEvent *e) override { return QTextEdit::dropEvent(e); } + +protected: virtual void paintEvent(QPaintEvent *event) override; private: @@ -165,9 +180,10 @@ private: bool listen_to_focus_events_; -private slots: - void FocusChanged(QWidget *old, QWidget *now); + bool forced_default_; + QTextCharFormat default_fmt_; +private slots: void FormatChanged(const QTextCharFormat &f); void SetFamily(const QString &s); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 176575e40..5b76ed1b0 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #ifdef Q_OS_LINUX #include @@ -32,6 +33,7 @@ #include "dialog/about/about.h" #include "mainmenu.h" #include "mainstatusbar.h" +#include "widget/timelinewidget/undo/timelineundoworkarea.h" namespace olive { @@ -43,7 +45,9 @@ MainWindow::MainWindow(QWidget *parent) : // window beforehand works around that issue and we just set it to whatever size is available. // * On Linux, it seems the window starts off at a vastly different size and then maximizes // which throws off the proportions and makes the resulting layout wonky. - resize(qApp->desktop()->availableGeometry(this).size()); + if (!qApp->screens().empty()) { + resize(qApp->screens().at(0)->availableSize()); + } #ifdef Q_OS_WINDOWS // Set up taskbar button progress bar (used for some modal tasks like exporting) @@ -182,7 +186,7 @@ TimelinePanel* MainWindow::OpenSequence(Sequence *sequence, bool enable_focus) panel = timeline_panels_.first(); } else { panel = AppendTimelinePanel(); - enable_focus = false; + //enable_focus = false; } panel->ConnectViewerNode(sequence); @@ -310,9 +314,18 @@ void MainWindow::ToggleMaximizedPanel() } } } else { + // Preserve currently focused panel + auto currently_focused_panel = PanelManager::instance()->CurrentlyFocused(false); + // Assume we are currently maximized, restore the state + PanelManager::instance()->SetSuppressChangedSignal(true); restoreState(premaximized_state_); premaximized_state_.clear(); + + currently_focused_panel->raise(); + currently_focused_panel->setFocus(); + + PanelManager::instance()->SetSuppressChangedSignal(false); } } @@ -406,6 +419,16 @@ void MainWindow::SetApplicationProgressValue(int value) #endif } +void MainWindow::SelectFootage(const QVector &e) +{ + for (ProjectPanel *p : project_panels_) { + SelectFootageForProjectPanel(e, p); + } + for (ProjectPanel *p : folder_panels_) { + SelectFootageForProjectPanel(e, p); + } +} + void MainWindow::closeEvent(QCloseEvent *e) { // Try to close all projects (this will return false if the user chooses not to close) @@ -486,6 +509,20 @@ void MainWindow::RevealViewerInProject(ViewerOutput *r) } } +void MainWindow::RevealViewerInFootageViewer(ViewerOutput *r, const TimeRange &range) +{ + footage_viewer_panel_->ConnectViewerNode(r); + + auto command = new MultiUndoCommand(); + if (!r->GetWorkArea()->enabled()) { + command->add_child(new WorkareaSetEnabledCommand(r->project(), r->GetWorkArea(), true)); + } + command->add_child(new WorkareaSetRangeCommand(r->GetWorkArea(), range)); + Core::instance()->undo_stack()->push(command); + + footage_viewer_panel_->SetTime(range.in()); +} + #ifdef Q_OS_LINUX void MainWindow::ShowNouveauWarning() { @@ -565,6 +602,7 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &TimelinePanel::RequestCaptureStart, sequence_viewer_panel_, &SequenceViewerPanel::StartCapture); connect(panel, &TimelinePanel::BlockSelectionChanged, this, &MainWindow::TimelinePanelSelectionChanged); connect(panel, &TimelinePanel::RevealViewerInProject, this, &MainWindow::RevealViewerInProject); + connect(panel, &TimelinePanel::RevealViewerInFootageViewer, this, &MainWindow::RevealViewerInFootageViewer); connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime); connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime); connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, panel, &TimelinePanel::SetTime); @@ -740,6 +778,16 @@ void MainWindow::UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel) param_panel_->SetContexts(context); } +void MainWindow::SelectFootageForProjectPanel(const QVector &e, ProjectPanel *p) +{ + p->DeselectAll(); + for (Footage *f : e) { + if (p->get_root()->HasChildRecursive(f)) { + p->SelectItem(f, false); + } + } +} + void MainWindow::FocusedPanelChanged(PanelWidget *panel) { // Update audio monitor panel diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 53d6bb6c0..1f6504c6f 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -92,6 +92,8 @@ public: */ void SetApplicationProgressValue(int value); + void SelectFootage(const QVector &e); + public slots: void ProjectOpen(Project *p); @@ -142,6 +144,8 @@ private: void UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel); + void SelectFootageForProjectPanel(const QVector &e, ProjectPanel *p); + QByteArray premaximized_state_; // Standard panels @@ -196,6 +200,7 @@ private slots: void ShowWelcomeDialog(); void RevealViewerInProject(ViewerOutput *r); + void RevealViewerInFootageViewer(ViewerOutput *r, const TimeRange &range); };