diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f57448568..f83b3a78f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -287,7 +287,7 @@ jobs: compiler-name: Clang LLVM os-name: macOS os-arch: x86_64 - os: macos-10.15 + os: macos-11.0 cmake-gen: Ninja min-deploy: 10.13 - build-type: RelWithDebInfo diff --git a/CMakeLists.txt b/CMakeLists.txt index 61f4adef4..04fc8ce8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,7 +94,7 @@ find_package(OpenEXR REQUIRED) list(APPEND OLIVE_LIBRARIES ${OPENEXR_LIBRARIES}) list(APPEND OLIVE_INCLUDE_DIRS ${OPENEXR_INCLUDES}) -# Link Qt 5 +# Link Qt set(QT_LIBRARIES Core Gui @@ -106,23 +106,44 @@ set(QT_LIBRARIES if (UNIX AND NOT APPLE) list(APPEND QT_LIBRARIES DBus) endif() -find_package(Qt5 5.6 REQUIRED +find_package(QT + NAMES + Qt6 + Qt5 + REQUIRED COMPONENTS ${QT_LIBRARIES} OPTIONAL_COMPONENTS Network ) -if (NOT Qt5Network_FOUND) - message(" Qt5::Network module not found, crash reporting will be disabled.") +find_package(Qt${QT_VERSION_MAJOR} REQUIRED + COMPONENTS + ${QT_LIBRARIES} + OPTIONAL_COMPONENTS + Network +) +if (NOT Qt${QT_VERSION_MAJOR}Network_FOUND) + message(" Qt${QT_VERSION_MAJOR}::Network module not found, crash reporting will be disabled.") endif() list(APPEND OLIVE_LIBRARIES - Qt5::Core - Qt5::Gui - Qt5::Widgets - Qt5::OpenGL - Qt5::Concurrent + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::OpenGL + Qt${QT_VERSION_MAJOR}::Concurrent ) +if (${QT_VERSION_MAJOR} EQUAL "6") + find_package(Qt${QT_VERSION_MAJOR} + REQUIRED + OpenGLWidgets + ) + + list(APPEND OLIVE_LIBRARIES + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + ) +endif() + # Link FFmpeg find_package(FFMPEG 3.0 REQUIRED COMPONENTS @@ -186,7 +207,7 @@ if (WIN32) elseif (APPLE) list(APPEND OLIVE_LIBRARIES "-framework IOKit") elseif(UNIX) - list(APPEND OLIVE_LIBRARIES Qt5::DBus) + list(APPEND OLIVE_LIBRARIES Qt${QT_VERSION_MAJOR}::DBus) endif() # Generate Git hash diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 35a818bc0..e8cf1ec2c 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -44,7 +44,7 @@ add_subdirectory(widget) add_subdirectory(window) # Add translations -qt5_add_translation(OLIVE_QM_FILES ${OLIVE_TS_FILES}) +qt_add_translation(OLIVE_QM_FILES ${OLIVE_TS_FILES}) set(QRC_BODY "") foreach(QM_FILE ${OLIVE_QM_FILES}) @@ -64,7 +64,7 @@ add_library(olive-version-obj version.cpp version.h ) -target_link_libraries(olive-version-obj PRIVATE Qt5::Core) +target_link_libraries(olive-version-obj PRIVATE Qt${QT_VERSION_MAJOR}::Core) target_compile_options(olive-version-obj PRIVATE -DAPPVERSION="${PROJECT_VERSION}" -DAPPVERSIONLONG="${PROJECT_LONG_VERSION}" ) # Add main library @@ -142,6 +142,6 @@ target_include_directories(olive-editor PRIVATE ${OLIVE_INCLUDE_DIRS}) target_include_directories(libolive-editor PRIVATE ${OLIVE_INCLUDE_DIRS}) # Add crash handler -if (GoogleCrashpad_FOUND AND Qt5Network_FOUND) +if (GoogleCrashpad_FOUND AND Qt${QT_VERSION_MAJOR}Network_FOUND) add_subdirectory(crashhandler) endif() diff --git a/app/codec/decoder.cpp b/app/codec/decoder.cpp index 434376c06..6fb2a2676 100644 --- a/app/codec/decoder.cpp +++ b/app/codec/decoder.cpp @@ -112,12 +112,13 @@ TexturePtr Decoder::RetrieveVideo(const RetrieveVideoParams &p) return nullptr; } - if (cached_texture_ && cached_time_ == p.time) { + if (cached_texture_ && cached_time_ == p.time && cached_divider_ == p.divider) { return cached_texture_; } cached_texture_ = RetrieveVideoInternal(p); cached_time_ = p.time; + cached_divider_ = p.divider; return cached_texture_; } @@ -292,7 +293,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 == kLoopModeLoop) { + if (loop_mode == LoopMode::kLoopModeLoop) { while (read_index >= input.size()) { read_index -= input.size(); } diff --git a/app/codec/decoder.h b/app/codec/decoder.h index d0c974846..471958bc9 100644 --- a/app/codec/decoder.h +++ b/app/codec/decoder.h @@ -31,12 +31,11 @@ extern "C" { #include #include -#include "codec/frame.h" #include "codec/samplebuffer.h" #include "common/rational.h" #include "node/block/block.h" #include "node/project/footage/footagedescription.h" -#include "task/task.h" +#include "render/cancelatom.h" namespace olive { @@ -71,12 +70,6 @@ public: kIndexUnavailable }; - enum LoopMode { - kLoopModeOff, - kLoopModeLoop, - kLoopModeClamp - }; - Decoder(); /** @@ -316,6 +309,7 @@ private: TexturePtr cached_texture_; rational cached_time_; + int cached_divider_; }; diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index d2292f002..5e2bdddbe 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -49,15 +49,10 @@ extern "C" { namespace olive { QVariant Yuv2RgbShader; +QVariant DeinterlaceShader; FFmpegDecoder::FFmpegDecoder() : - filter_graph_(nullptr), - buffersrc_ctx_(nullptr), - buffersink_ctx_(nullptr), - input_fmt_(AV_PIX_FMT_NONE), - native_internal_pix_fmt_(VideoParams::kFormatInvalid), - native_output_pix_fmt_(VideoParams::kFormatInvalid), - working_frame_(nullptr), + sws_ctx_(nullptr), working_packet_(nullptr), cache_at_zero_(false), cache_at_eof_(false) @@ -72,217 +67,195 @@ bool FFmpegDecoder::OpenInternal() // Store one second in the source's timebase second_ts_ = qRound64(av_q2d(av_inv_q(s->time_base))); - working_frame_ = av_frame_alloc(); working_packet_ = av_packet_alloc(); - - frame_rate_tb_ = rational::NaN; return true; } return false; } -/*FramePtr FFmpegDecoder::RetrieveStillImage(const rational &timecode, const int ÷r) +TexturePtr FFmpegDecoder::ProcessFrameIntoTexture(AVFramePtr f, const RetrieveVideoParams &p, const AVFramePtr original) { - // This is a still image - QString img_filename = stream().filename(); + // Determine native format + AVPixelFormat ideal_fmt = FFmpegUtils::GetCompatiblePixelFormat(static_cast(f->format)); + VideoParams::Format native_fmt = GetNativePixelFormat(ideal_fmt); + int native_channels = GetNativeChannelCount(ideal_fmt); - int64_t ts; + // Set up video params + VideoParams vp(original->width, + original->height, + native_fmt, + native_channels, + av_guess_sample_aspect_ratio(instance_.fmt_ctx(), instance_.avstream(), nullptr), + VideoParams::kInterlaceNone, + p.divider); - // If it's an image sequence, we'll probably need to transform the filename - if (stream().GetStream().video_type() == Track::kVideoTypeImageSequence) { - ts = stream().GetTimeInTimebaseUnits(timecode); + // Create texture + TexturePtr tex = p.renderer->CreateTexture(vp); - img_filename = TransformImageSequenceFileName(stream().filename(), ts); - } else { - ts = 0; + switch (f->format) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUV420P10LE: + case AV_PIX_FMT_YUV422P10LE: + case AV_PIX_FMT_YUV444P10LE: + case AV_PIX_FMT_YUV420P12LE: + case AV_PIX_FMT_YUV422P12LE: + case AV_PIX_FMT_YUV444P12LE: + { + // Run through YUV to RGB shader + if (Yuv2RgbShader.isNull()) { + // Compile shader + Yuv2RgbShader = p.renderer->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/yuv2rgb.frag")))); + if (Yuv2RgbShader.isNull()) { + return nullptr; + } + } + + int px_size; + int bits_per_pixel; + switch (f->format) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUV444P: + default: + px_size = 1; + bits_per_pixel = 8; + break; + case AV_PIX_FMT_YUV420P10LE: + case AV_PIX_FMT_YUV422P10LE: + case AV_PIX_FMT_YUV444P10LE: + px_size = 2; + bits_per_pixel = 10; + break; + case AV_PIX_FMT_YUV420P12LE: + case AV_PIX_FMT_YUV422P12LE: + case AV_PIX_FMT_YUV444P12LE: + px_size = 2; + bits_per_pixel = 12; + break; + } + + AVFrame *hw_in = f.get(); + + VideoParams plane_params = vp; + plane_params.set_channel_count(1); + plane_params.set_format(native_fmt); + + TexturePtr y_plane = p.renderer->CreateTexture(plane_params, hw_in->data[0], hw_in->linesize[0] / px_size); + + switch (f->format) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUV420P10LE: + case AV_PIX_FMT_YUV422P10LE: + case AV_PIX_FMT_YUV420P12LE: + case AV_PIX_FMT_YUV422P12LE: + plane_params.set_width(plane_params.width()/2); + break; + } + + switch (f->format) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUV420P10LE: + case AV_PIX_FMT_YUV420P12LE: + plane_params.set_height(plane_params.height()/2); + break; + } + + TexturePtr u_plane = p.renderer->CreateTexture(plane_params, hw_in->data[1], hw_in->linesize[1] / px_size); + TexturePtr v_plane = p.renderer->CreateTexture(plane_params, hw_in->data[2], hw_in->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, hw_in->color_range == AVCOL_RANGE_JPEG)); + + const int *yuv_coeffs = sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(hw_in->colorspace)); + job.Insert(QStringLiteral("yuv_crv"), NodeValue(NodeValue::kFloat, yuv_coeffs[0]/65536.0)); + job.Insert(QStringLiteral("yuv_cgu"), NodeValue(NodeValue::kFloat, yuv_coeffs[2]/65536.0)); + job.Insert(QStringLiteral("yuv_cgv"), NodeValue(NodeValue::kFloat, yuv_coeffs[3]/65536.0)); + job.Insert(QStringLiteral("yuv_cbu"), NodeValue(NodeValue::kFloat, yuv_coeffs[1]/65536.0)); + + tex = p.renderer->CreateTexture(vp); + p.renderer->BlitToTexture(Yuv2RgbShader, job, tex.get(), false); + break; + } + case AV_PIX_FMT_RGBA: + case AV_PIX_FMT_RGBA64LE: + // RGBA can be uploaded directly to the texture + tex->Upload(f->data[0], f->linesize[0] / vp.GetBytesPerPixel()); + break; } - AVPacket* pkt = av_packet_alloc(); - AVFrame* frame = av_frame_alloc(); - FramePtr output_frame = nullptr; + // Deinterlace if necessary + if (p.src_interlacing != VideoParams::kInterlaceNone) { + if (DeinterlaceShader.isNull()) { + // Compile shader + DeinterlaceShader = p.renderer->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/deinterlace2.frag")))); + if (DeinterlaceShader.isNull()) { + return nullptr; + } + } - Instance i; - i.Open(img_filename.toUtf8(), stream().GetRealStreamIndex()); + rational frame_rate_tb = av_guess_frame_rate(instance_.fmt_ctx(), instance_.avstream(), original.get()); - int ret = i.GetFrame(pkt, frame); + // Double frame rate for interlaced fields + frame_rate_tb *= 2; - if (ret >= 0) { - VideoParams video_params = stream().video_params(); + // Flip frame rate so it can be used as a timebase + frame_rate_tb.flip(); - // Create frame to return - output_frame = Frame::Create(); - output_frame->set_video_params(VideoParams(frame->width, - frame->height, - native_pix_fmt_, - native_channel_count_, - video_params.pixel_aspect_ratio(), - video_params.interlacing(), - divider)); - output_frame->set_timestamp(timecode); - output_frame->allocate(); + int64_t req = Timecode::time_to_timestamp(p.time + rational(instance_.fmt_ctx()->start_time, AV_TIME_BASE), frame_rate_tb); + int64_t frm = Timecode::rescale_timestamp(original->pts, instance_.avstream()->time_base, frame_rate_tb); - uint8_t* copy_data = reinterpret_cast(output_frame->data()); - int copy_linesize = output_frame->linesize_bytes(); + bool first = (req == frm); + bool top_first = (p.src_interlacing == VideoParams::kInterlacedTopFirst); - FFmpegBufferToNativeBuffer(frame->data, frame->linesize, ©_data, ©_linesize); - } else { - qWarning() << "Failed to retrieve still image from decoder"; + int interlacing = (first == top_first) ? 1 : 2; + + TexturePtr deinterlaced = p.renderer->CreateTexture(tex->params()); + + ShaderJob job; + job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, tex)); + job.Insert(QStringLiteral("interlacing"), NodeValue(NodeValue::kInt, interlacing)); + job.Insert(QStringLiteral("pixel_height"), NodeValue(NodeValue::kInt, original->height)); + + p.renderer->BlitToTexture(DeinterlaceShader, job, deinterlaced.get(), false); + + tex = deinterlaced; } - i.Close(); - - av_frame_free(&frame); - av_packet_free(&pkt); - - return output_frame; -}*/ + return tex; +} TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) { - if (AVFramePtr f = RetrieveFrame(p.time, p.src_interlacing, p.cancelled)) { + if (AVFramePtr f = RetrieveFrame(p.time, p.cancelled)) { if (p.cancelled && p.cancelled->IsCancelled()) { return nullptr; } - int &src_fmt = f.get()->format; - src_fmt = FFmpegUtils::ConvertJPEGSpaceToRegularSpace(static_cast(src_fmt)); + AVFramePtr original = f; + // Disregard "JPEG" pixel formats because we allow the user to override that + f->format = FFmpegUtils::ConvertJPEGSpaceToRegularSpace(static_cast(f->format)); + + // Force frame's color range to whatever it's set to in Olive 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, - p.divider); - - TexturePtr tex = nullptr; - - // Attempt to use GLSL shader for faster YUV to RGB conversion - if (IsPixelFormatGLSLCompatible(static_cast(src_fmt))) { - if (Yuv2RgbShader.isNull()) { - // Compile shader - Yuv2RgbShader = p.renderer->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/yuv2rgb.frag")))); - } - - if (!Yuv2RgbShader.isNull()) { - int px_size; - int bits_per_pixel; - switch (src_fmt) { - case AV_PIX_FMT_YUV420P: - case AV_PIX_FMT_YUV422P: - case AV_PIX_FMT_YUV444P: - default: - px_size = 1; - bits_per_pixel = 8; - break; - case AV_PIX_FMT_YUV420P10LE: - case AV_PIX_FMT_YUV422P10LE: - case AV_PIX_FMT_YUV444P10LE: - px_size = 2; - bits_per_pixel = 10; - break; - case AV_PIX_FMT_YUV420P12LE: - case AV_PIX_FMT_YUV422P12LE: - case AV_PIX_FMT_YUV444P12LE: - px_size = 2; - bits_per_pixel = 12; - break; - } - - AVFrame *hw_in = f.get(); - - VideoParams plane_params = vp; - plane_params.set_channel_count(1); - plane_params.set_format(native_internal_pix_fmt_); - - if (p.divider != 1) { - ApplyScaler(f.get()); - hw_in = working_frame_; - } else { - // Fallback: shouldn't ever really get here, but just in case - plane_params.set_divider(1); - } - - TexturePtr y_plane = p.renderer->CreateTexture(plane_params, hw_in->data[0], hw_in->linesize[0] / px_size); - - if (src_fmt == AV_PIX_FMT_YUV420P - || src_fmt == AV_PIX_FMT_YUV422P - || src_fmt == AV_PIX_FMT_YUV420P10LE - || src_fmt == AV_PIX_FMT_YUV422P10LE - || src_fmt == AV_PIX_FMT_YUV420P12LE - || src_fmt == AV_PIX_FMT_YUV422P12LE) { - plane_params.set_width(plane_params.width()/2); - } - - if (src_fmt == AV_PIX_FMT_YUV420P - || src_fmt == AV_PIX_FMT_YUV420P10LE - || src_fmt == AV_PIX_FMT_YUV420P12LE) { - plane_params.set_height(plane_params.height()/2); - } - - TexturePtr u_plane = p.renderer->CreateTexture(plane_params, hw_in->data[1], hw_in->linesize[1] / px_size); - TexturePtr v_plane = p.renderer->CreateTexture(plane_params, hw_in->data[2], hw_in->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, hw_in->color_range == AVCOL_RANGE_JPEG)); - - const int *yuv_coeffs = sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(hw_in->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(), hw_in); - - // 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(hw_in->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); - - av_frame_unref(working_frame_); - } - } - - if (!tex) { - // Fallback to software pixel format conversion - if (!ApplyScaler(f.get())) { - return nullptr; - } - - tex = p.renderer->CreateTexture(vp, working_frame_->data[0], working_frame_->linesize[0] / vp.GetBytesPerPixel()); - - av_frame_unref(working_frame_); - } - - return tex; + // Perform any CPU processing required + f = PreProcessFrame(f, p); + if (!f) { + // Error occurred while software scaling + return nullptr; } + + // Finally, perform any GPU processing required + return ProcessFrameIntoTexture(f, p, original); } return nullptr; @@ -295,19 +268,10 @@ void FFmpegDecoder::CloseInternal() working_packet_ = nullptr; } - if (working_frame_) { - av_frame_free(&working_frame_); - working_frame_ = nullptr; - } - ClearFrameCache(); FreeScaler(); instance_.Close(); - - input_fmt_ = AV_PIX_FMT_NONE; - native_internal_pix_fmt_ = VideoParams::kFormatInvalid; - native_output_pix_fmt_ = VideoParams::kFormatInvalid; } rational FFmpegDecoder::GetAudioStartOffset() const @@ -350,6 +314,11 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, CancelAtom *can int64_t footage_duration = fmt_ctx->duration; + bool duration_guessed_from_bitrate = (fmt_ctx->duration_estimation_method == AVFMT_DURATION_FROM_BITRATE); + if (duration_guessed_from_bitrate) { + qWarning() << "Unreliable duration detected - we will manually determine it ourselves (this may take some time)"; + } + // Dump it into the Footage object for (unsigned int i=0;inb_streams;i++) { @@ -409,15 +378,15 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, CancelAtom *can if (ret >= 0) { // Check if we need a manual duration - if (avstream->duration == AV_NOPTS_VALUE) { - if (footage_duration == AV_NOPTS_VALUE) { + if (avstream->duration == AV_NOPTS_VALUE || duration_guessed_from_bitrate) { + if (footage_duration == AV_NOPTS_VALUE || duration_guessed_from_bitrate) { // Manually read through file for duration int64_t new_dur; do { new_dur = frame->best_effort_timestamp; - } while (instance.GetFrame(pkt, frame) >= 0); + } while (instance.GetFrame(pkt, frame) >= 0 && (!cancelled || !cancelled->IsCancelled())); avstream->duration = new_dur; @@ -468,9 +437,9 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, CancelAtom *can channel_layout = static_cast(av_get_default_channel_layout(avstream->codecpar->channels)); } - if (avstream->duration == AV_NOPTS_VALUE) { + if (avstream->duration == AV_NOPTS_VALUE || duration_guessed_from_bitrate) { // Loop through stream until we get the whole duration - if (footage_duration == AV_NOPTS_VALUE) { + if (footage_duration == AV_NOPTS_VALUE || duration_guessed_from_bitrate) { Instance instance; instance.Open(filename_c, avstream->index); @@ -481,7 +450,7 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, CancelAtom *can do { new_dur = frame->best_effort_timestamp; - } while (instance.GetFrame(pkt, frame) >= 0); + } while (instance.GetFrame(pkt, frame) >= 0 && (!cancelled || !cancelled->IsCancelled())); avstream->duration = new_dur; @@ -721,73 +690,25 @@ const char *FFmpegDecoder::GetInterlacingModeInFFmpeg(VideoParams::Interlacing i bool FFmpegDecoder::IsPixelFormatGLSLCompatible(AVPixelFormat f) { - return f == AV_PIX_FMT_YUV420P - || f == AV_PIX_FMT_YUV422P - || f == AV_PIX_FMT_YUV444P - || f == AV_PIX_FMT_YUV420P10LE - || f == AV_PIX_FMT_YUV422P10LE - || f == AV_PIX_FMT_YUV444P10LE - || f == AV_PIX_FMT_YUV420P12LE - || f == AV_PIX_FMT_YUV422P12LE - || f == AV_PIX_FMT_YUV444P12LE; -} - -/* OLD UNUSED CODE: Keeping this around in case the code proves useful - -void FFmpegDecoder::CacheFrameToDisk(AVFrame *f) -{ - QFile save_frame(GetIndexFilename().append(QString::number(f->pts))); - if (save_frame.open(QFile::WriteOnly)) { - - // Save frame to media index - int cached_buffer_sz = av_image_get_buffer_size(static_cast(f->format), - f->width, - f->height, - 1); - - QByteArray cached_frame(cached_buffer_sz, Qt::Uninitialized); - - av_image_copy_to_buffer(reinterpret_cast(cached_frame.data()), - cached_frame.size(), - f->data, - f->linesize, - static_cast(f->format), - f->width, - f->height, - 1); - - save_frame.write(qCompress(cached_frame, 1)); - save_frame.close(); - - DiskManager::instance()->CreatedFile(save_frame.fileName(), QByteArray()); - } - - // See if we stored this frame in the disk cache - - QByteArray frame_loader; - if (!got_frame) { - QFile compressed_frame(GetIndexFilename().append(QString::number(target_ts))); - if (compressed_frame.exists() - && compressed_frame.size() > 0 - && compressed_frame.open(QFile::ReadOnly)) { - DiskManager::instance()->Accessed(compressed_frame.fileName()); - - // Read data - frame_loader = qUncompress(compressed_frame.readAll()); - - av_image_fill_arrays(input_data, - input_linesize, - reinterpret_cast(frame_loader.data()), - static_cast(avstream_->codecpar->format), - avstream_->codecpar->width, - avstream_->codecpar->height, - 1); - - got_frame = true; - } + // NOTE: We don't include RGB24 or RGB48 here because those are slow on the GPU and performance + // should be better if we convert to RGBA on the CPU beforehand + switch (f) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUV420P10LE: + case AV_PIX_FMT_YUV422P10LE: + case AV_PIX_FMT_YUV444P10LE: + case AV_PIX_FMT_YUV420P12LE: + case AV_PIX_FMT_YUV422P12LE: + case AV_PIX_FMT_YUV444P12LE: + case AV_PIX_FMT_RGBA: + case AV_PIX_FMT_RGBA64LE: + return true; + default: + return false; } } -*/ void FFmpegDecoder::ClearFrameCache() { @@ -798,14 +719,98 @@ void FFmpegDecoder::ClearFrameCache() } } -AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, VideoParams::Interlacing interlacing, CancelAtom *cancelled) +AVFramePtr FFmpegDecoder::PreProcessFrame(AVFramePtr f, const RetrieveVideoParams &p) +{ + // In pre-processing, we try to achieve the following: + // - If a divider is being used, scale down the image + // - If a pixel format is not compatible with the GLSL shader, convert it to RGBA ourselves + + if (p.divider == 1 && IsPixelFormatGLSLCompatible(static_cast(f->format))) { + // No CPU processing required, the user wants this in full resolution and the pixel format can + // be converted on the GPU + return f; + } + + // Some scaling and/or format conversion needs to be done + AVFramePtr dest = CreateAVFramePtr(); + + dest->width = f->width; + dest->height = f->height; + dest->format = f->format; + dest->color_range = f->color_range; + dest->colorspace = f->colorspace; + + if (p.divider > 1) { + dest->width = VideoParams::GetScaledDimension(dest->width, p.divider); + dest->height = VideoParams::GetScaledDimension(dest->height, p.divider); + } + + if (!IsPixelFormatGLSLCompatible(static_cast(dest->format))) { + dest->format = FFmpegUtils::GetCompatiblePixelFormat(static_cast(dest->format), p.maximum_format); + } + + int r = av_frame_get_buffer(dest.get(), 0); + if (r < 0) { + FFmpegError(r); + return nullptr; + } + + if (!sws_ctx_ + || sws_src_width_ != f->width + || sws_src_height_ != f->height + || sws_src_format_ != f->format + || sws_dst_width_ != dest->width + || sws_dst_height_ != dest->height + || sws_dst_format_ != dest->format + || sws_colrange_ != dest->color_range + || sws_colspace_ != dest->colorspace) { + // SwsContext must be recreated, destroy current if it exists + FreeScaler(); + + // Cache info + sws_src_width_ = f->width; + sws_src_height_ = f->height; + sws_src_format_ = static_cast(f->format); + sws_dst_width_ = dest->width; + sws_dst_height_ = dest->height; + sws_dst_format_ = static_cast(dest->format); + sws_colrange_ = dest->color_range; + sws_colspace_ = dest->colorspace; + + // Create new scaler + sws_ctx_ = sws_getContext(sws_src_width_, + sws_src_height_, + sws_src_format_, + sws_dst_width_, + sws_dst_height_, + sws_dst_format_, + SWS_POINT, + nullptr, + nullptr, + nullptr); + + // Set swscale's colorspace details + sws_setColorspaceDetails(sws_ctx_, + sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(dest->colorspace)), + dest->color_range == AVCOL_RANGE_JPEG ? 1 : 0, + sws_getCoefficients(FFmpegUtils::GetSwsColorspaceFromAVColorSpace(dest->colorspace)), + dest->color_range == AVCOL_RANGE_JPEG ? 1 : 0, + 0, 0x10000, 0x10000); + } + + r = sws_scale(sws_ctx_, f->data, f->linesize, 0, f->height, dest->data, dest->linesize); + if (r < 0) { + FFmpegError(r); + return nullptr; + } + + return dest; +} + +AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, CancelAtom *cancelled) { int64_t target_ts = Timecode::time_to_timestamp(time, instance_.avstream()->time_base); - if (interlacing != VideoParams::kInterlaceNone && !IsPixelFormatGLSLCompatible(static_cast(instance_.avstream()->codecpar->format))) { - target_ts *= 2; - } - if (instance_.fmt_ctx()->start_time != AV_NOPTS_VALUE) { target_ts += av_rescale_q(instance_.fmt_ctx()->start_time, {1, AV_TIME_BASE}, instance_.avstream()->time_base); } @@ -820,9 +825,6 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, VideoParams::Inter || (target_ts < cached_frames_.front()->pts || target_ts > cached_frames_.back()->pts + 2*second_ts_)) { ClearFrameCache(); - // Filter graph may rely on "continuous" video frames, so we free the scaler here - //ResetScaler(); - instance_.Seek(seek_ts); if (seek_ts == min_seek) { cache_at_zero_ = true; @@ -849,7 +851,7 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, VideoParams::Inter } if (!filtered) { - filtered = CreateAVFramePtr(av_frame_alloc()); + filtered = CreateAVFramePtr(); } // Pull from the decoder @@ -938,143 +940,11 @@ AVFramePtr FFmpegDecoder::RetrieveFrame(const rational& time, VideoParams::Inter return return_frame; } -bool FFmpegDecoder::InitScaler(AVFrame *input, const RetrieveVideoParams& params) -{ - 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; - } - - // We need to (re)create the filter, delete current if necessary - ClearFrameCache(); - FreeScaler(); - - // Set our params to this - filter_params_ = params; - input_fmt_ = static_cast(input->format); - if (input_fmt_ == AV_PIX_FMT_NONE) { - return false; - } - - // Get an Olive compatible AVPixelFormat - AVPixelFormat ideal_pix_fmt = FFmpegUtils::GetCompatiblePixelFormat(static_cast(input_fmt_), params.maximum_format); - - // Determine which Olive native pixel format we retrieved - // Note that FFmpeg doesn't support float formats - native_output_pix_fmt_ = GetNativePixelFormat(ideal_pix_fmt); - native_channel_count_ = GetNativeChannelCount(ideal_pix_fmt); - - AVPixelFormat ideal_internal_pix_fmt = FFmpegUtils::GetCompatiblePixelFormat(static_cast(input_fmt_)); - native_internal_pix_fmt_ = GetNativePixelFormat(ideal_internal_pix_fmt); - - if (native_output_pix_fmt_ == VideoParams::kFormatInvalid - || native_internal_pix_fmt_ == VideoParams::kFormatInvalid - || native_channel_count_ == 0) { - qCritical() << "Failed to find valid native pixel format for" << ideal_pix_fmt; - return false; - } - - // Allocate filter graph - filter_graph_ = avfilter_graph_alloc(); - if (!filter_graph_) { - qWarning() << "Failed to allocate filter graph"; - return false; - } - - AVStream* s = instance_.avstream(); - - int src_width = s->codecpar->width; - int src_height = s->codecpar->height; - - // Define filter parameters - static const int kFilterArgSz = 1024; - char filter_args[kFilterArgSz]; - snprintf(filter_args, kFilterArgSz, "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", - src_width, - src_height, - input->format, - s->time_base.num, - s->time_base.den, - s->codecpar->sample_aspect_ratio.num, - s->codecpar->sample_aspect_ratio.den); - - // Create path in and out of the filter graph (the buffer in and the buffersink out) - avfilter_graph_create_filter(&buffersrc_ctx_, avfilter_get_by_name("buffer"), "in", filter_args, nullptr, filter_graph_); - avfilter_graph_create_filter(&buffersink_ctx_, avfilter_get_by_name("buffersink"), "out", nullptr, nullptr, filter_graph_); - - // Link filters as necessary - AVFilterContext *last_filter = buffersrc_ctx_; - - bool glsl_available = IsPixelFormatGLSLCompatible(static_cast(input->format)); - - // Add deinterlace filter if necessary - if (filter_params_.src_interlacing != VideoParams::kInterlaceNone && !glsl_available) { - 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 - if (filter_params_.divider > 1) { - AVFilterContext* scale_filter; - - int dst_width, dst_height; - dst_width = VideoParams::GetScaledDimension(src_width, filter_params_.divider); - dst_height = VideoParams::GetScaledDimension(src_height, filter_params_.divider); - - snprintf(filter_args, kFilterArgSz, "w=%d:h=%d:flags=fast_bilinear:interl=0", - dst_width, - dst_height); - - avfilter_graph_create_filter(&scale_filter, avfilter_get_by_name("scale"), "scale", filter_args, nullptr, filter_graph_); - - avfilter_link(last_filter, 0, scale_filter, 0); - last_filter = scale_filter; - } - - // Add format filter if necessary - if (ideal_pix_fmt != input->format && !glsl_available) { - AVFilterContext* format_filter; - - snprintf(filter_args, kFilterArgSz, "pix_fmts=%u", ideal_pix_fmt); - - avfilter_graph_create_filter(&format_filter, avfilter_get_by_name("format"), "format", filter_args, nullptr, filter_graph_); - - avfilter_link(last_filter, 0, format_filter, 0); - last_filter = format_filter; - } - - // Finally, link the last filter with the buffersink - avfilter_link(last_filter, 0, buffersink_ctx_, 0); - - // Configure graph - if (int ret = avfilter_graph_config(filter_graph_, nullptr) < 0) { - qCritical() << "Failed to configure graph:" << FFmpegError(ret); - return false; - } - - return true; -} - void FFmpegDecoder::FreeScaler() { - if (filter_graph_) { - avfilter_graph_free(&filter_graph_); - filter_graph_ = nullptr; - buffersrc_ctx_ = nullptr; - buffersink_ctx_ = nullptr; + if (sws_ctx_) { + sws_freeContext(sws_ctx_); + sws_ctx_ = nullptr; } } @@ -1119,22 +989,6 @@ void FFmpegDecoder::RemoveFirstFrame() cache_at_zero_ = false; } -bool FFmpegDecoder::ApplyScaler(AVFrame *in) -{ - int r; - - r = av_buffersrc_add_frame_flags(buffersrc_ctx_, in, AV_BUFFERSRC_FLAG_KEEP_REF); - if (r < 0) { - return false; - } - r = av_buffersink_get_frame(buffersink_ctx_, working_frame_); - if (r < 0) { - return false; - } - - return true; -} - int FFmpegDecoder::MaximumQueueSize() { // Fairly arbitrary size. This used to need to be the number of current threads to ensure any diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index cd60c5d05..a695f0b31 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -134,7 +134,6 @@ private: */ static QString FFmpegError(int error_code); - bool InitScaler(AVFrame *input, const RetrieveVideoParams ¶ms); void FreeScaler(); static VideoParams::Format GetNativePixelFormat(AVPixelFormat pix_fmt); @@ -150,25 +149,26 @@ private: void ClearFrameCache(); - AVFramePtr RetrieveFrame(const rational &time, VideoParams::Interlacing interlacing, CancelAtom *cancelled); + AVFramePtr PreProcessFrame(AVFramePtr f, const RetrieveVideoParams &p); + + TexturePtr ProcessFrameIntoTexture(AVFramePtr f, const RetrieveVideoParams &p, const AVFramePtr original); + + AVFramePtr RetrieveFrame(const rational &time, CancelAtom *cancelled); void RemoveFirstFrame(); - bool ApplyScaler(AVFrame *in); - static int MaximumQueueSize(); - RetrieveVideoParams filter_params_; - AVFilterGraph* filter_graph_; - AVFilterContext* buffersrc_ctx_; - AVFilterContext* buffersink_ctx_; - AVPixelFormat input_fmt_; - VideoParams::Format native_internal_pix_fmt_; - VideoParams::Format native_output_pix_fmt_; - int native_channel_count_; - rational frame_rate_tb_; + SwsContext *sws_ctx_; + int sws_src_width_; + int sws_src_height_; + AVPixelFormat sws_src_format_; + int sws_dst_width_; + int sws_dst_height_; + AVPixelFormat sws_dst_format_; + AVColorRange sws_colrange_; + AVColorSpace sws_colspace_; - AVFrame *working_frame_; AVPacket *working_packet_; int64_t second_ts_; diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index 192e03305..9d56fd220 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -280,28 +280,43 @@ bool FFmpegEncoder::WriteFrame(FramePtr frame, rational time) bool FFmpegEncoder::WriteAudio(const SampleBuffer &audio) { - bool result = true; - - // Create input buffer - int input_sample_count = 0; - uint8_t** input_data = nullptr; - if (audio.is_allocated()) { - input_sample_count = audio.sample_count(); - int input_linesize; - - av_samples_alloc_array_and_samples(&input_data, &input_linesize, audio.audio_params().channel_count(), - input_sample_count, FFmpegUtils::GetFFmpegSampleFormat(audio.audio_params().format()), 0); - - for (int i=0; i(input_data), input_sample_count); + bool result = true; - if (input_data) { - av_freep(&input_data[0]); - av_freep(&input_data); + size_t start = 0; + size_t end = audio.sample_count(); + const size_t max_frame = 48000; + + while (result && start < end) { + // Create input buffer + uint8_t** input_data = nullptr; + size_t input_sample_count = std::min(end - start, max_frame); + int input_linesize; + + int r = av_samples_alloc_array_and_samples(&input_data, &input_linesize, audio.audio_params().channel_count(), + input_sample_count, FFmpegUtils::GetFFmpegSampleFormat(audio.audio_params().format()), 0); + + if (r < 0) { + FFmpegError(tr("Failed to allocate sample array"), r); + return false; + } else { + int bpsc = audio.audio_params().bytes_per_sample_per_channel(); + for (int i=0; i(input_data), input_sample_count); + + if (input_data) { + av_freep(&input_data[0]); + av_freep(&input_data); + } } return result; diff --git a/app/common/ffmpegutils.cpp b/app/common/ffmpegutils.cpp index 565f0269c..489fdf080 100644 --- a/app/common/ffmpegutils.cpp +++ b/app/common/ffmpegutils.cpp @@ -24,7 +24,7 @@ namespace olive { AVPixelFormat FFmpegUtils::GetCompatiblePixelFormat(const AVPixelFormat &pix_fmt, VideoParams::Format maximum) { - std::vector possible_pix_fmts(3); + AVPixelFormat possible_pix_fmts[3]; possible_pix_fmts[0] = AV_PIX_FMT_RGBA; @@ -35,7 +35,7 @@ AVPixelFormat FFmpegUtils::GetCompatiblePixelFormat(const AVPixelFormat &pix_fmt possible_pix_fmts[2] = AV_PIX_FMT_NONE; } - return avcodec_find_best_pix_fmt_of_list(possible_pix_fmts.data(), + return avcodec_find_best_pix_fmt_of_list(possible_pix_fmts, pix_fmt, 1, nullptr); diff --git a/app/common/ffmpegutils.h b/app/common/ffmpegutils.h index 7d13b8642..66fafdd49 100644 --- a/app/common/ffmpegutils.h +++ b/app/common/ffmpegutils.h @@ -82,6 +82,10 @@ inline AVFramePtr CreateAVFramePtr(AVFrame *f) { return std::shared_ptr(f, [](AVFrame *g){ av_frame_free(&g); }); } +inline AVFramePtr CreateAVFramePtr() +{ + return CreateAVFramePtr(av_frame_alloc()); +} } diff --git a/app/common/html.cpp b/app/common/html.cpp index d9ad84a28..74cc805f9 100644 --- a/app/common/html.cpp +++ b/app/common/html.cpp @@ -12,22 +12,7 @@ const QVector Html::kBlockTags = { QStringLiteral("div") }; -inline bool StrEquals(const QString &a, const QStringRef &b) -{ - return !a.compare(b, Qt::CaseInsensitive); -} - -inline bool StrEquals(const QString &a, const QString &b) -{ - return !a.compare(b, Qt::CaseInsensitive); -} - -inline bool StrEquals(const QStringRef &a, const QString &b) -{ - return !a.compare(b, Qt::CaseInsensitive); -} - -inline bool StrEquals(const QStringRef &a, const QStringRef &b) +inline bool StrEquals(const QStringView &a, const QStringView &b) { return !a.compare(b, Qt::CaseInsensitive); } @@ -223,8 +208,9 @@ void Html::WriteCSSProperty(QString *style, const QString &key, const QStringLis void Html::WriteCharFormat(QString *style, const QTextCharFormat &fmt) { - if (!fmt.fontFamily().isEmpty()) { - WriteCSSProperty(style, QStringLiteral("font-family"), fmt.fontFamily()); + QStringList families = fmt.fontFamilies().toStringList(); + if (!families.isEmpty()) { + WriteCSSProperty(style, QStringLiteral("font-family"), families.first()); } if (fmt.hasProperty(QTextFormat::FontPointSize)) { @@ -302,7 +288,7 @@ QTextCharFormat Html::ReadCharFormat(const QXmlStreamAttributes &attributes) const QString &first_val = it.value().first(); if (it.key() == QStringLiteral("font-family")) { - fmt.setFontFamily(first_val); + fmt.setFontFamilies({first_val}); } else if (it.key() == QStringLiteral("font-size")) { if (first_val.endsWith(QStringLiteral("pt"), Qt::CaseInsensitive)) { fmt.setFontPointSize(first_val.chopped(2).toDouble()); @@ -421,7 +407,7 @@ QMap Html::GetCSSFromStyle(const QString &s) // match. Also commas should be filtered out. QStringList values; const QString &val = kv.at(1); - QChar in_quote = 0; + QChar in_quote(0); QString current_str; for (int i=0; i Html::GetCSSFromStyle(const QString &s) if (!in_quote.isNull()) { // If inside quotes and character isn't quote, indiscriminately append char if (current_char == in_quote) { - in_quote = 0; + in_quote = QChar(0); } else { current_str.append(current_char); } diff --git a/app/common/memorypool.h b/app/common/memorypool.h index a932ad5f3..270e3a0a0 100644 --- a/app/common/memorypool.h +++ b/app/common/memorypool.h @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include diff --git a/app/common/ratiodialog.cpp b/app/common/ratiodialog.cpp index fd216b6a1..ee85582fa 100644 --- a/app/common/ratiodialog.cpp +++ b/app/common/ratiodialog.cpp @@ -22,6 +22,7 @@ #include #include +#include namespace olive { @@ -49,7 +50,7 @@ double GetFloatRatioFromUser(QWidget* parent, return qSNaN(); } - QStringList ratio_components = s.split(QRegExp(QStringLiteral(":|;|\\/"))); + QStringList ratio_components = s.split(QRegularExpression(QStringLiteral(":|;|\\/"))); if (ratio_components.size() == 1) { bool float_ok; diff --git a/app/common/timecodefunctions.cpp b/app/common/timecodefunctions.cpp index cbd9d4074..ca6e8503e 100644 --- a/app/common/timecodefunctions.cpp +++ b/app/common/timecodefunctions.cpp @@ -24,6 +24,7 @@ extern "C" { #include } +#include #include #include "config/config.h" @@ -34,16 +35,13 @@ QString padded(int64_t arg, int padding) { return QStringLiteral("%1").arg(arg, padding, 10, QChar('0')); } -QString Timecode::timestamp_to_timecode(const int64_t ×tamp, - const rational& timebase, - const Display& display, - bool show_plus_if_positive) +QString Timecode::time_to_timecode(const rational &time, const rational &timebase, const Timecode::Display &display, bool show_plus_if_positive) { if (timebase.isNull()) { return QStringLiteral("INVALID TIMEBASE"); } - double timestamp_dbl = (rational(timestamp) * timebase).toDouble(); + double time_dbl = time.toDouble(); switch (display) { case kTimecodeNonDropFrame: @@ -52,21 +50,21 @@ QString Timecode::timestamp_to_timecode(const int64_t ×tamp, { QString prefix; - if (timestamp_dbl < 0) { + if (time_dbl < 0) { prefix = "-"; } else if (show_plus_if_positive) { prefix = "+"; } if (display == kTimecodeSeconds) { - timestamp_dbl = qAbs(timestamp_dbl); + time_dbl = qAbs(time_dbl); - int64_t total_seconds = qFloor(timestamp_dbl); + int64_t total_seconds = qFloor(time_dbl); int64_t hours = total_seconds / 3600; int64_t mins = total_seconds / 60 - hours * 60; int64_t secs = total_seconds - mins * 60; - int64_t fraction = qRound64((timestamp_dbl - static_cast(total_seconds)) * 1000); + int64_t fraction = qRound64((time_dbl - static_cast(total_seconds)) * 1000); return QStringLiteral("%1%2:%3:%4.%5").arg(prefix, padded(hours, 2), @@ -79,7 +77,7 @@ QString Timecode::timestamp_to_timecode(const int64_t ×tamp, double frame_rate = timebase.flipped().toDouble(); int rounded_frame_rate = qRound(frame_rate); int64_t frames, secs, mins, hours; - int64_t f = qAbs(timestamp); + int64_t f = qAbs(time_to_timestamp(time, timebase)); if (display == kTimecodeDropFrame && TimebaseIsDropFrame(timebase)) { frame_token = ";"; @@ -127,18 +125,36 @@ QString Timecode::timestamp_to_timecode(const int64_t ×tamp, } } case kFrames: - return QString::number(timestamp); + return QString::number(time_to_timestamp(time, timebase)); case kMilliseconds: - return QString::number(qRound(timestamp_dbl * 1000)); + return QString::number(qRound(time_dbl * 1000)); } return QStringLiteral("INVALID TIMECODE MODE"); } -int64_t Timecode::timecode_to_timestamp(const QString &timecode, const rational &timebase, const Display &display, bool* ok) +int64_t StrToInt64EmptyTolerant(const QString &s, bool *ok) { - double timebase_dbl = timebase.toDouble(); + if (s.isEmpty()) { + if (ok) *ok = true; + return 0; + } else { + return s.toLongLong(ok); + } +} +double StrToDoubleEmptyTolerant(const QString &s, bool *ok) +{ + if (s.isEmpty()) { + if (ok) *ok = true; + return 0; + } else { + return s.toDouble(ok); + } +} + +rational Timecode::timecode_to_time(const QString &timecode, const rational &timebase, const Timecode::Display &display, bool *ok) +{ if (timecode.isEmpty()) { goto err_fatal; } @@ -148,71 +164,73 @@ int64_t Timecode::timecode_to_timestamp(const QString &timecode, const rational case kTimecodeDropFrame: case kTimecodeSeconds: { - const int kTimecodeElementCount = 4; - QStringList timecode_split = timecode.split(QRegExp("(:)|(;)|(\\.)")); + QStringList timecode_split = timecode.split(QRegularExpression("(:)|(;)")); - bool valid; + const int element_count = display == kTimecodeSeconds ? 3 : 4; - // We only deal with HH, MM, SS, and FF. Any values after that are ignored. - while (timecode_split.size() > kTimecodeElementCount) { + // Remove excess tokens (we're only interested in HH:MM:SS.FF) + while (timecode_split.size() > element_count) { timecode_split.removeLast(); } - // Convert values to integers - QList timecode_numbers; + // For easier index calculations, ensure minimum size + while (timecode_split.size() < element_count) { + timecode_split.prepend(QString()); + } bool negative = timecode.trimmed().startsWith('-'); - foreach (const QString& element, timecode_split) { - valid = true; - - timecode_numbers.append((element.isEmpty()) ? 0 : qAbs(element.toLong(&valid))); - - // If element cannot be converted to a number, - if (!valid) { - goto err_fatal; - } - } - - // Ensure value size is always 4 - while (timecode_numbers.size() < 4) { - timecode_numbers.prepend(0); - } - double frame_rate = timebase.flipped().toDouble(); int rounded_frame_rate = qRound(frame_rate); - int64_t hours = timecode_numbers.at(0); - int64_t mins = timecode_numbers.at(1); - int64_t secs = timecode_numbers.at(2); - int64_t frames = timecode_numbers.at(3); + bool valid; + rational time; - int64_t sec_count = (hours*3600 + mins*60 + secs); - int64_t timestamp = sec_count*rounded_frame_rate + frames; + int64_t hours = StrToInt64EmptyTolerant(timecode_split.at(0), &valid); + if (!valid) goto err_fatal; + int64_t mins = StrToInt64EmptyTolerant(timecode_split.at(1), &valid); + if (!valid) goto err_fatal; - if (display == kTimecodeDropFrame && TimebaseIsDropFrame(timebase)) { + if (display == kTimecodeSeconds) { + double secs = StrToDoubleEmptyTolerant(timecode_split.at(2), &valid); + if (!valid) goto err_fatal; - // Number of frames to drop on the minute marks is the nearest integer to 6% of the framerate - int64_t dropFrames = qRound64(frame_rate * (2.0/30.0)); + time = rational::fromDouble(hours * 3600 + mins * 60 + secs); + } else { + int64_t secs = StrToInt64EmptyTolerant(timecode_split.at(2), &valid); + if (!valid) goto err_fatal; + int64_t frames = StrToInt64EmptyTolerant(timecode_split.at(3), &valid); + if (!valid) goto err_fatal; - // d and m need to be calculated from - int64_t real_fr_ts = qRound64(static_cast(sec_count)*frame_rate) + frames; + int64_t sec_count = (hours*3600 + mins*60 + secs); + int64_t frame_count = sec_count*rounded_frame_rate + frames; - int64_t framesPer10Minutes = qRound(frame_rate * 600); - int64_t d = real_fr_ts / framesPer10Minutes; - int64_t m = real_fr_ts % framesPer10Minutes; + if (display == kTimecodeDropFrame && TimebaseIsDropFrame(timebase)) { - if (m > dropFrames) { - timestamp -= dropFrames * ((m - dropFrames) / (qRound(frame_rate)*60 - dropFrames)); + // Number of frames to drop on the minute marks is the nearest integer to 6% of the framerate + int64_t dropFrames = qRound64(frame_rate * (2.0/30.0)); + + // d and m need to be calculated from + int64_t real_fr_ts = qRound64(static_cast(sec_count)*frame_rate) + frames; + + int64_t framesPer10Minutes = qRound(frame_rate * 600); + int64_t d = real_fr_ts / framesPer10Minutes; + int64_t m = real_fr_ts % framesPer10Minutes; + + if (m > dropFrames) { + frame_count -= dropFrames * ((m - dropFrames) / (qRound(frame_rate)*60 - dropFrames)); + } + frame_count -= dropFrames*9*d; } - timestamp -= dropFrames*9*d; + + time = timestamp_to_time(frame_count, timebase); } if (ok) *ok = true; - if (negative) timestamp = -timestamp; + if (negative) time = -time; - return timestamp; + return time; } case kMilliseconds: { @@ -223,18 +241,23 @@ int64_t Timecode::timecode_to_timestamp(const QString &timecode, const rational // Convert milliseconds to seconds timecode_secs *= 0.001; - // Convert seconds to frames - timecode_secs /= timebase_dbl; - - if (ok) *ok = true; - return qRound(timecode_secs); + // Convert seconds to rational + return rational::fromDouble(timecode_secs, ok); } else { goto err_fatal; } } case kFrames: + { + bool valid; + int64_t ts = timecode.toLongLong(&valid); + if (!valid) { + goto err_fatal; + } + if (ok) *ok = true; - return timecode.toLong(ok); + return timestamp_to_time(ts, timebase); + } } err_fatal: @@ -242,12 +265,6 @@ err_fatal: return 0; } -rational Timecode::timecode_to_time(const QString &timecode, const rational &timebase, const Timecode::Display &display, bool *ok) -{ - int64_t timestamp = timecode_to_timestamp(timecode, timebase, display, ok); - return timestamp_to_time(timestamp, timebase); -} - rational Timecode::snap_time_to_timebase(const rational &time, const rational &timebase, Rounding floor) { // Just convert to a timestamp in timebase units and back @@ -268,11 +285,6 @@ rational Timecode::timestamp_to_time(const int64_t ×tamp, const rational &t return rational(num_r, den_r); } -QString Timecode::time_to_timecode(const rational &time, const rational &timebase, const Timecode::Display &display, bool show_plus_if_positive) -{ - return timestamp_to_timecode(time_to_timestamp(time, timebase), timebase, display, show_plus_if_positive); -} - bool Timecode::TimebaseIsDropFrame(const rational &timebase) { return (timebase.numerator() != 1); diff --git a/app/common/timecodefunctions.h b/app/common/timecodefunctions.h index fe5273acb..0cdabd242 100644 --- a/app/common/timecodefunctions.h +++ b/app/common/timecodefunctions.h @@ -56,9 +56,7 @@ public: /** * @brief Convert a timestamp (according to a rational timebase) to a user-friendly string representation */ - static QString timestamp_to_timecode(const int64_t ×tamp, const rational& timebase, const Display &display, bool show_plus_if_positive = false); - - static int64_t timecode_to_timestamp(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); + static QString time_to_timecode(const rational& time, const rational& timebase, const Display &display, bool show_plus_if_positive = false); static rational timecode_to_time(const QString& timecode, const rational& timebase, const Display& display, bool *ok = nullptr); static rational snap_time_to_timebase(const rational& time, const rational& timebase, Rounding floor = kRound); @@ -71,8 +69,6 @@ public: static rational timestamp_to_time(const int64_t& timestamp, const rational& timebase); - static QString time_to_timecode(const rational& time, const rational& timebase, const Display &display, bool show_plus_if_positive = false); - static bool TimebaseIsDropFrame(const rational& timebase); static QString TimeToString(int64_t ms); diff --git a/app/config/config.cpp b/app/config/config.cpp index c9e814a16..192fcdb74 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -82,7 +82,7 @@ void Config::SetDefaults() SetEntryInternal(QStringLiteral("InvertTimelineScrollAxes"), NodeValue::kBoolean, true); SetEntryInternal(QStringLiteral("SelectAlsoSeeks"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("PasteSeeks"), NodeValue::kBoolean, true); - SetEntryInternal(QStringLiteral("SelectAlsoSeeks"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("SeekAlsoSelects"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("SetNameWithMarker"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("AutoSeekToBeginning"), NodeValue::kBoolean, true); SetEntryInternal(QStringLiteral("DropFileOnMediaToReplace"), NodeValue::kBoolean, false); @@ -104,6 +104,7 @@ void Config::SetDefaults() SetEntryInternal(QStringLiteral("UseLegacyColorInInputTab"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("ReassocLinToNonLin"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("PreviewNonFloatDontAskAgain"), NodeValue::kBoolean, false); + SetEntryInternal(QStringLiteral("UseGLFinish"), NodeValue::kBoolean, false); SetEntryInternal(QStringLiteral("TimelineThumbnailMode"), NodeValue::kInt, Timeline::kThumbnailInOut); SetEntryInternal(QStringLiteral("TimelineWaveformMode"), NodeValue::kInt, Timeline::kWaveformsEnabled); diff --git a/app/core.cpp b/app/core.cpp index a4aaef6fc..07684d63c 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -87,6 +87,7 @@ Core::Core(const CoreParams& params) : addable_object_(Tool::kAddableEmpty), snapping_(true), core_params_(params), + magic_(false), pixel_sampling_users_(0), shown_cache_full_warning_(false) { @@ -198,15 +199,6 @@ void Core::Stop() // Save Config Config::Save(); - // Save recently opened projects - { - QFile recent_projects_file(GetRecentProjectsFilePath()); - if (recent_projects_file.open(QFile::WriteOnly | QFile::Text)) { - recent_projects_file.write(recent_projects_.join('\n').toUtf8()); - recent_projects_file.close(); - } - } - ProjectSerializer::Destroy(); ConformManager::DestroyInstance(); @@ -292,6 +284,7 @@ void Core::SetSelectedTransitionObject(const QString &obj) void Core::ClearOpenRecentList() { recent_projects_.clear(); + SaveRecentProjectsList(); emit OpenRecentListChanged(); } @@ -387,11 +380,8 @@ void Core::DialogProjectPropertiesShow() void Core::DialogExportShow() { - ViewerOutput* viewer; - rational time; - - if (GetSequenceToExport(&viewer, &time)) { - OpenExportDialogForViewer(viewer, time, false); + if (ViewerOutput* viewer = GetSequenceToExport()) { + OpenExportDialogForViewer(viewer, false); } } @@ -478,7 +468,7 @@ void Core::CreateNewSequence() } } -void Core::AddOpenProject(Project* p) +void Core::AddOpenProject(Project* p, bool add_to_recents) { // Ensure project is not open at the moment foreach (Project* already_open, open_projects_) { @@ -497,12 +487,14 @@ void Core::AddOpenProject(Project* p) connect(p, &Project::ModifiedChanged, this, &Core::ProjectWasModified); open_projects_.append(p); - PushRecentlyOpenedProject(p->filename()); + if (!p->filename().isEmpty() && add_to_recents) { + PushRecentlyOpenedProject(p->filename()); + } emit ProjectOpened(p); } -bool Core::AddOpenProjectFromTask(Task *task) +bool Core::AddOpenProjectFromTask(Task *task, bool add_to_recents) { ProjectLoadBaseTask* load_task = static_cast(task); @@ -510,7 +502,7 @@ bool Core::AddOpenProjectFromTask(Task *task) Project* project = load_task->GetLoadedProject(); if (ValidateFootageInLoadedProject(project, project->GetSavedURL())) { - AddOpenProject(project); + AddOpenProject(project, add_to_recents); main_window_->LoadLayout(project->GetLayoutInfo()); return true; @@ -733,7 +725,7 @@ void Core::OpenStartupProject() void Core::AddRecoveryProjectFromTask(Task *task) { - if (AddOpenProjectFromTask(task)) { + if (AddOpenProjectFromTask(task, false)) { ProjectLoadBaseTask* load_task = static_cast(task); Project* project = load_task->GetLoadedProject(); @@ -858,7 +850,7 @@ void Core::SaveProjectInternal(Project* project, const QString& override_filenam psm->deleteLater(); } -bool Core::GetSequenceToExport(ViewerOutput **viewer, rational *time) +ViewerOutput *Core::GetSequenceToExport() { // First try the most recently focused time based window TimeBasedPanel* time_panel = PanelManager::instance()->MostRecentlyFocused(); @@ -876,9 +868,7 @@ bool Core::GetSequenceToExport(ViewerOutput **viewer, rational *time) tr("This Sequence is empty. There is nothing to export."), QMessageBox::Ok); } else { - *viewer = time_panel->GetConnectedViewer(); - *time = time_panel->GetTime(); - return true; + return time_panel->GetConnectedViewer(); } } else { QMessageBox::critical(main_window_, @@ -887,7 +877,7 @@ bool Core::GetSequenceToExport(ViewerOutput **viewer, rational *time) QMessageBox::Ok); } - return false; + return nullptr; } QString Core::GetAutoRecoveryIndexFilename() @@ -960,6 +950,16 @@ bool Core::RevertProjectInternal(Project *p, bool by_opening_existing) return false; } +void Core::SaveRecentProjectsList() +{ + // Save recently opened projects + QFile recent_projects_file(GetRecentProjectsFilePath()); + if (recent_projects_file.open(QFile::WriteOnly | QFile::Text)) { + recent_projects_file.write(recent_projects_.join('\n').toUtf8()); + recent_projects_file.close(); + } +} + void Core::SaveAutorecovery() { if (OLIVE_CONFIG("AutorecoveryEnabled").toBool()) { @@ -1040,6 +1040,8 @@ void Core::ProjectSaveSucceeded(Task* task) autorecovered_projects_.removeOne(p->GetUuid()); SaveUnrecoveredList(); + + ShowStatusBarMessage(tr("Saved to \"%1\" successfully").arg(p->filename())); } Project* Core::GetActiveProject() const @@ -1247,10 +1249,9 @@ void Core::OpenNodeInViewer(ViewerOutput *viewer) main_window_->OpenNodeInViewer(viewer); } -void Core::OpenExportDialogForViewer(ViewerOutput *viewer, const rational &time, bool start_still_image) +void Core::OpenExportDialogForViewer(ViewerOutput *viewer, bool start_still_image) { ExportDialog* ed = new ExportDialog(viewer, start_still_image, main_window_); - ed->SetTime(time); connect(ed, &ExportDialog::finished, ed, &ExportDialog::deleteLater); ed->open(); connect(ed, &ExportDialog::RequestImportFile, this, &Core::ImportSingleFile); @@ -1372,6 +1373,8 @@ void Core::PushRecentlyOpenedProject(const QString& s) } } + SaveRecentProjectsList(); + emit OpenRecentListChanged(); } @@ -1418,7 +1421,7 @@ void Core::OpenProjectInternal(const QString &filename, bool recovery_project) if (recovery_project) { connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddRecoveryProjectFromTask); } else { - connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask); + connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTaskAndAddToRecents); } task_dialog->open(); @@ -1523,6 +1526,8 @@ void Core::OpenProjectFromRecentList(int index) QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { recent_projects_.removeAt(index); + SaveRecentProjectsList(); + emit OpenRecentListChanged(); } } @@ -1688,37 +1693,63 @@ void Core::CacheActiveSequence(bool in_out_only) } } +QString StripWindowsDriveLetter(QString s) +{ + // HACK: On Windows, absolute paths are saved with a drive letter (e.g. "C:\video.mp4"). Below, + // we use Qt's relative path system to resolve when an entire project may be in a different + // folder, but the files are all in the same place relatively to the project. Unfortunately, + // Qt chooses not to understand paths from Windows on non-Windows platforms, which causes + // this to break when a project is moving from Windows to non-Windows. To resolve that, if + // we're on a non-Windows platform and we detect a Windows path (i.e. a path with a drive + // letter at the start), we strip it off. We also convert any back-slashes to forward-slashes + // because on Windows they are interchangeable and on non-Windows they are not. +#ifndef Q_OS_WINDOWS + if (s.size() >= 2) { + if (s.at(0).isLetter() && s.at(1) == ':') { + s = s.mid(2); + s.replace('\\', '/'); + } + } +#endif + + return s; +} + bool Core::ValidateFootageInLoadedProject(Project* project, const QString& project_saved_url) { - QVector project_footage = project->root()->ListChildrenOfType(); QVector footage_we_couldnt_validate; - foreach (Footage* footage, project_footage) { - if (!QFileInfo::exists(footage->filename()) && !project_saved_url.isEmpty()) { - // If the footage doesn't exist, it might have moved with the project - const QString& project_current_url = project->filename(); + for (Node *n : project->nodes()) { + if (Footage *footage = dynamic_cast(n)) { + QString footage_fn = StripWindowsDriveLetter(footage->filename()); + QString project_fn = StripWindowsDriveLetter(project_saved_url); - if (project_current_url != project_saved_url) { - // Project has definitely moved, try to resolve relative paths - QDir saved_dir(QFileInfo(project_saved_url).dir()); - QDir true_dir(QFileInfo(project_current_url).dir()); + if (!QFileInfo::exists(footage_fn) && !project_saved_url.isEmpty()) { + // If the footage doesn't exist, it might have moved with the project + const QString& project_current_url = project->filename(); - QString relative_filename = saved_dir.relativeFilePath(footage->filename()); - QString transformed_abs_filename = true_dir.filePath(relative_filename); + if (project_current_url != project_fn) { + // Project has definitely moved, try to resolve relative paths + QDir saved_dir(QFileInfo(project_fn).dir()); + QDir true_dir(QFileInfo(project_current_url).dir()); - if (QFileInfo::exists(transformed_abs_filename)) { - // Use this file instead - qInfo() << "Resolved" << footage->filename() << "relatively to" << transformed_abs_filename; - footage->set_filename(transformed_abs_filename); + QString relative_filename = saved_dir.relativeFilePath(footage_fn); + QString transformed_abs_filename = true_dir.filePath(relative_filename); + + if (QFileInfo::exists(transformed_abs_filename)) { + // Use this file instead + qInfo() << "Resolved" << footage_fn << "relatively to" << transformed_abs_filename; + footage->set_filename(transformed_abs_filename); + } } } - } - if (QFileInfo::exists(footage->filename())) { - // Assume valid - footage->SetValid(); - } else { - footage_we_couldnt_validate.append(footage); + if (QFileInfo::exists(footage->filename())) { + // Assume valid + footage->SetValid(); + } else { + footage_we_couldnt_validate.append(footage); + } } } diff --git a/app/core.h b/app/core.h index 0d9d8df0d..297db9a47 100644 --- a/app/core.h +++ b/app/core.h @@ -317,7 +317,9 @@ public: void OpenNodeInViewer(ViewerOutput* viewer); - void OpenExportDialogForViewer(ViewerOutput *viewer, const rational &time, bool start_still_image); + void OpenExportDialogForViewer(ViewerOutput *viewer, bool start_still_image); + + bool IsMagicEnabled() const { return magic_; } public slots: /** @@ -449,6 +451,11 @@ public slots: void WarnCacheFull(); + void SetMagic(bool e) + { + magic_ = e; + } + signals: /** * @brief Signal emitted when a project is opened @@ -551,7 +558,7 @@ private: /** * @brief Retrieves the currently most active sequence for exporting */ - bool GetSequenceToExport(ViewerOutput **viewer, rational *time); + ViewerOutput *GetSequenceToExport(); static QString GetAutoRecoveryIndexFilename(); @@ -559,6 +566,15 @@ private: bool RevertProjectInternal(Project *p, bool by_opening_existing); + void SaveRecentProjectsList(); + + /** + * @brief Adds a project to the "open projects" list + */ + void AddOpenProject(olive::Project* p, bool add_to_recents = false); + + bool AddOpenProjectFromTask(Task* task, bool add_to_recents); + /** * @brief Internal main window object */ @@ -624,6 +640,11 @@ private: */ QVector autorecovered_projects_; + /** + * @brief Do something debug related + */ + bool magic_; + /** * @brief How many widgets currently need pixel sampling access */ @@ -636,12 +657,10 @@ private slots: void ProjectSaveSucceeded(Task *task); - /** - * @brief Adds a project to the "open projects" list - */ - void AddOpenProject(olive::Project* p); - - bool AddOpenProjectFromTask(Task* task); + bool AddOpenProjectFromTaskAndAddToRecents(Task* task) + { + return AddOpenProjectFromTask(task, true); + } void ImportTaskComplete(Task *task); diff --git a/app/crashhandler/CMakeLists.txt b/app/crashhandler/CMakeLists.txt index 0a023275b..f835c73ae 100644 --- a/app/crashhandler/CMakeLists.txt +++ b/app/crashhandler/CMakeLists.txt @@ -39,10 +39,10 @@ target_include_directories( target_link_libraries( olive-crashhandler PRIVATE - Qt5::Core - Qt5::Gui - Qt5::Widgets - Qt5::Network + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Network ${CRASHPAD_LIBRARIES} ) diff --git a/app/dialog/about/about.cpp b/app/dialog/about/about.cpp index bb584af42..4fb526944 100644 --- a/app/dialog/about/about.cpp +++ b/app/dialog/about/about.cpp @@ -44,10 +44,10 @@ AboutDialog::AboutDialog(bool welcome_dialog, QWidget *parent) : QFontMetrics fm = fontMetrics(); QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(fm.height()); + layout->setContentsMargins(fm.height(), fm.height(), fm.height(), fm.height()); QHBoxLayout *horiz_layout = new QHBoxLayout(); - horiz_layout->setMargin(fm.height()); + horiz_layout->setContentsMargins(fm.height(), fm.height(), fm.height(), fm.height()); horiz_layout->setSpacing(fm.height()*2); QLabel* icon = new QLabel(QStringLiteral("")); @@ -108,7 +108,7 @@ AboutDialog::AboutDialog(bool welcome_dialog, QWidget *parent) : layout->addWidget(new QLabel()); QHBoxLayout *btn_layout = new QHBoxLayout(); - btn_layout->setMargin(0); + btn_layout->setContentsMargins(0, 0, 0, 0); btn_layout->setSpacing(0); if (welcome_dialog) { diff --git a/app/dialog/export/codec/cineformsection.cpp b/app/dialog/export/codec/cineformsection.cpp index da83eabed..9cae58409 100644 --- a/app/dialog/export/codec/cineformsection.cpp +++ b/app/dialog/export/codec/cineformsection.cpp @@ -30,7 +30,7 @@ CineformSection::CineformSection(QWidget *parent) : { QGridLayout *layout = new QGridLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); int row = 0; diff --git a/app/dialog/export/codec/h264section.cpp b/app/dialog/export/codec/h264section.cpp index 732aef346..0d50312e4 100644 --- a/app/dialog/export/codec/h264section.cpp +++ b/app/dialog/export/codec/h264section.cpp @@ -39,7 +39,7 @@ H264Section::H264Section(int default_crf, QWidget *parent) : CodecSection(parent) { QGridLayout* layout = new QGridLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); int row = 0; layout->addWidget(new QLabel(tr("Encode Speed:")), row, 0); @@ -173,7 +173,7 @@ H264CRFSection::H264CRFSection(int default_crf, QWidget *parent) : QWidget(parent) { QHBoxLayout* layout = new QHBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); crf_slider_ = new QSlider(Qt::Horizontal); crf_slider_->setMinimum(kMinimumCRF); @@ -207,7 +207,7 @@ H264BitRateSection::H264BitRateSection(QWidget *parent) : QWidget(parent) { QGridLayout* layout = new QGridLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); int row = 0; @@ -261,7 +261,7 @@ H264FileSizeSection::H264FileSizeSection(QWidget *parent) : QWidget(parent) { QGridLayout* layout = new QGridLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); int row = 0; diff --git a/app/dialog/export/codec/imagesection.cpp b/app/dialog/export/codec/imagesection.cpp index b2bf50fd8..50be54175 100644 --- a/app/dialog/export/codec/imagesection.cpp +++ b/app/dialog/export/codec/imagesection.cpp @@ -29,7 +29,7 @@ ImageSection::ImageSection(QWidget* parent) : CodecSection(parent) { QGridLayout* layout = new QGridLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); int row = 0; @@ -47,6 +47,7 @@ ImageSection::ImageSection(QWidget* parent) : frame_slider_->SetMinimum(0); frame_slider_->SetValue(0); frame_slider_->SetDisplayType(RationalSlider::kTime); + connect(frame_slider_, &RationalSlider::ValueChanged, this, &ImageSection::TimeChanged); layout->addWidget(frame_slider_, row, 1); } diff --git a/app/dialog/export/codec/imagesection.h b/app/dialog/export/codec/imagesection.h index 3575ea5a2..b1d6cac61 100644 --- a/app/dialog/export/codec/imagesection.h +++ b/app/dialog/export/codec/imagesection.h @@ -59,6 +59,9 @@ public: frame_slider_->SetValue(t); } +signals: + void TimeChanged(const rational &t); + private: QCheckBox* image_sequence_checkbox_; diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 64573cf4f..46f74eb7c 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -58,7 +58,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode, QWi preferences_area_ = new QWidget(); QGridLayout* preferences_layout = new QGridLayout(preferences_area_); - preferences_layout->setMargin(0); + preferences_layout->setContentsMargins(0, 0, 0, 0); int row = 0; @@ -148,6 +148,11 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode, QWi video_tab_ = new ExportVideoTab(color_manager_); AddPreferencesTab(video_tab_, tr("Video")); + // Set video tab time and make connections + connect(viewer_node, &ViewerOutput::PlayheadChanged, video_tab_, &ExportVideoTab::SetTime); + connect(video_tab_, &ExportVideoTab::TimeChanged, viewer_node, &ViewerOutput::SetPlayhead); + video_tab_->SetTime(viewer_node->GetPlayhead()); + audio_tab_ = new ExportAudioTab(); AddPreferencesTab(audio_tab_, tr("Audio")); @@ -183,7 +188,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode, QWi row++; QHBoxLayout *btn_layout = new QHBoxLayout(); - btn_layout->setMargin(0); + btn_layout->setContentsMargins(0, 0, 0, 0); preferences_layout->addLayout(btn_layout, row, 0, 1, 4); btn_layout->addStretch(); @@ -206,7 +211,6 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode, QWi 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_); splitter->addWidget(preview_area); @@ -391,7 +395,7 @@ void ExportDialog::ExportFinished() // If this task was cancelled, we stay open so the user can potentially queue another export } else { // Accept this dialog and close - if (import_file_after_export_) { + if (import_file_after_export_->isEnabled() && import_file_after_export_->isChecked()) { QString filename = filename_edit_->text().trimmed(); emit RequestImportFile(filename); } @@ -437,7 +441,7 @@ void ExportDialog::PresetComboBoxChanged() if (loading_presets_) { return; } - + QComboBox *c = static_cast(sender()); int preset_number = c->currentData().toInt(); diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index 594b16380..b0afc83ac 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -51,14 +51,6 @@ public: rational GetSelectedTimebase() const; void SetSelectedTimebase(const rational &r); - void SetTime(const rational &time) - { - preview_viewer_->SetAudioScrubbingEnabled(false); - preview_viewer_->SetTime(time); - video_tab_->SetTime(time); - preview_viewer_->SetAudioScrubbingEnabled(true); - } - EncodingParams GenerateParams() const; void SetParams(const EncodingParams &e); diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index 8eefefee1..f0c130d02 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -184,6 +184,7 @@ QWidget *ExportVideoTab::SetupCodecSection() codec_layout->addWidget(codec_stack_, row, 0, 1, 2); image_section_ = new ImageSection(); + connect(image_section_, &ImageSection::TimeChanged, this, &ExportVideoTab::TimeChanged); codec_stack_->addWidget(image_section_); h264_section_ = new H264Section(); diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index 67d0cfef0..4c0f6fd13 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -167,6 +167,8 @@ signals: void ImageSequenceCheckBoxChanged(bool e); + void TimeChanged(const rational &time); + private: QWidget* SetupResolutionSection(); QWidget* SetupColorSection(); diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 6ed1e486f..e2b223621 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -38,7 +38,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) video_premultiply_alpha_(nullptr) { QGridLayout* video_layout = new QGridLayout(this); - video_layout->setMargin(0); + video_layout->setContentsMargins(0, 0, 0, 0); int row = 0; diff --git a/app/dialog/footagerelink/footagerelinkdialog.cpp b/app/dialog/footagerelink/footagerelinkdialog.cpp index 2f89de75e..066c163e6 100644 --- a/app/dialog/footagerelink/footagerelinkdialog.cpp +++ b/app/dialog/footagerelink/footagerelinkdialog.cpp @@ -102,8 +102,16 @@ void FootageRelinkDialog::BrowseForFootage() QString new_fn = QFileDialog::getOpenFileName(this, tr("Relink \"%1\"").arg(f->GetLabel()), - info.absolutePath(), - QStringLiteral("%1;;%2 (**)").arg(info.fileName(), tr("All Files"))); + info.absolutePath()); + + // Originally, this function would attempt to filter to the exact filename of the missing file. + // However, this would break on Windows if the filename had any spaces in it. The reason is + // Windows separates its extensions with ';' while Qt separates them with ' '. Qt isn't + // intelligent enough to determine whether it's a list of extensions or a single filename with a + // space in it, it just does a global replace of ' ' to ';'. There's no way around it, outside of + // bypassing Qt entirely and using Win32's GetOpenFileName() directly. As annoying as it is, I've + // just disabled it for now. + //QStringLiteral("%1 (\"%1\");;%2 (*)").arg(info.fileName(), tr("All Files"))); // We received a new filename if (!new_fn.isEmpty()) { diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index 6f73dbedc..b498e53ae 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -36,7 +36,7 @@ PreferencesAudioTab::PreferencesAudioTab() { // Backend Layout QGridLayout* main_layout = new QGridLayout(); - main_layout->setMargin(0); + main_layout->setContentsMargins(0, 0, 0, 0); int row = 0; diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp index df9917a84..ad184c7d1 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp @@ -73,7 +73,7 @@ PreferencesBehaviorTab::PreferencesBehaviorTab() tr("Hold ALT on any UI element to switch scrolling axes"), timeline_group); AddItem(tr("Seek Also Selects"), - QStringLiteral("SelectAlsoSeeks"), + QStringLiteral("SeekAlsoSelects"), timeline_group); AddItem(tr("Seek to the End of Pastes"), QStringLiteral("PasteSeeks"), @@ -107,6 +107,11 @@ PreferencesBehaviorTab::PreferencesBehaviorTab() tr("Multiple clips can share the same nodes. Disable this to automatically share node " "dependencies among clips when copying or splitting them."), node_group); + + QTreeWidgetItem* opengl_group = AddParent(tr("OpenGL")); + AddItem(tr("Use glFinish"), + QStringLiteral("UseGLFinish"), + opengl_group); } void PreferencesBehaviorTab::Accept(MultiUndoCommand *command) diff --git a/app/dialog/progress/progress.cpp b/app/dialog/progress/progress.cpp index c63372f94..45d374498 100644 --- a/app/dialog/progress/progress.cpp +++ b/app/dialog/progress/progress.cpp @@ -55,7 +55,7 @@ ProgressDialog::ProgressDialog(const QString& message, const QString& title, QWi QHBoxLayout* cancel_layout = new QHBoxLayout(); layout->addLayout(cancel_layout); - cancel_layout->setMargin(0); + cancel_layout->setContentsMargins(0, 0, 0, 0); cancel_layout->setSpacing(0); cancel_layout->addStretch(); diff --git a/app/dialog/sequence/sequencedialogpresettab.cpp b/app/dialog/sequence/sequencedialogpresettab.cpp index eb3942068..68446fd16 100644 --- a/app/dialog/sequence/sequencedialogpresettab.cpp +++ b/app/dialog/sequence/sequencedialogpresettab.cpp @@ -47,7 +47,7 @@ SequenceDialogPresetTab::SequenceDialogPresetTab(QWidget* parent) : PresetManager(this, QStringLiteral("sequencepresets")) { QVBoxLayout* outer_layout = new QVBoxLayout(this); - outer_layout->setMargin(0); + outer_layout->setContentsMargins(0, 0, 0, 0); preset_tree_ = new QTreeWidget(); preset_tree_->setColumnCount(1); diff --git a/app/dialog/speedduration/speeddurationdialog.cpp b/app/dialog/speedduration/speeddurationdialog.cpp index 7214ca27c..8f186ed20 100644 --- a/app/dialog/speedduration/speeddurationdialog.cpp +++ b/app/dialog/speedduration/speeddurationdialog.cpp @@ -100,9 +100,9 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, cons 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_combo_->addItem(tr("None"), int(LoopMode::kLoopModeOff)); + loop_combo_->addItem(tr("Loop"), int(LoopMode::kLoopModeLoop)); + loop_combo_->addItem(tr("Clamp"), int(LoopMode::kLoopModeClamp)); loop_layout->addWidget(loop_combo_, row, 1); } @@ -117,7 +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(); + start_loop_ = int(clips.first()->loop_mode()); for (int i=1; i &clips, cons start_maintain_audio_pitch_ = -1; } - if (start_loop_ != -1 && c->loop_mode() != start_loop_) { + if (start_loop_ != -1 && int(c->loop_mode()) != start_loop_) { start_loop_ = -1; } } diff --git a/app/dialog/task/task.cpp b/app/dialog/task/task.cpp index 41cfc55a4..aab2b2979 100644 --- a/app/dialog/task/task.cpp +++ b/app/dialog/task/task.cpp @@ -56,7 +56,13 @@ void TaskDialog::showEvent(QShowEvent *e) this, &TaskDialog::TaskFinished, Qt::QueuedConnection); // Run task in another thread with QtConcurrent - task_watcher->setFuture(QtConcurrent::run(task_, &Task::Start)); + task_watcher->setFuture( +#if QT_VERSION_MAJOR >= 6 + QtConcurrent::run(&Task::Start, task_) +#else + QtConcurrent::run(task_, &Task::Start) +#endif + ); already_shown_ = true; } diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index f007eee42..fb030e5b5 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -39,7 +39,7 @@ Block::Block() : track_(nullptr), index_(-1) { - AddInput(kLengthInput, NodeValue::kRational, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + AddInput(kLengthInput, NodeValue::kRational, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagHidden)); SetInputProperty(kLengthInput, QStringLiteral("min"), QVariant::fromValue(rational(0, 1))); SetInputProperty(kLengthInput, QStringLiteral("view"), RationalSlider::kTime); SetInputProperty(kLengthInput, QStringLiteral("viewlock"), true); diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 110a26ee4..7ce334697 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -21,6 +21,7 @@ #include "clip.h" #include "config/config.h" +#include "node/block/transition/transition.h" #include "node/output/track/track.h" #include "node/output/viewer/viewer.h" #include "widget/slider/floatslider.h" @@ -453,10 +454,12 @@ void ClipBlock::InputValueChangedEvent(const QString &input, int element) } } } + } else if (input == kLoopModeInput) { + emit PreviewChanged(); } } -TimeRange ClipBlock::InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const +TimeRange ClipBlock::InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time, bool clamp) const { Q_UNUSED(element) @@ -464,7 +467,7 @@ TimeRange ClipBlock::InputTimeAdjustment(const QString& input, int element, cons return TimeRange(SequenceToMediaTime(input_time.in()), SequenceToMediaTime(input_time.out())); } - return super::InputTimeAdjustment(input, element, input_time); + return super::InputTimeAdjustment(input, element, input_time, clamp); } TimeRange ClipBlock::OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const @@ -538,7 +541,7 @@ void ClipBlock::ConnectedToPreviewEvent() TimeRange ClipBlock::media_range() const { - return InputTimeAdjustment(kBufferIn, -1, TimeRange(0, length())); + return InputTimeAdjustment(kBufferIn, -1, TimeRange(0, length()), false); } MultiCamNode *ClipBlock::FindMulticam() diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index 15cadcfb7..19987c7fb 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -68,7 +68,7 @@ public: virtual void InvalidateCache(const TimeRange& range, const QString& from, int element, InvalidateCacheOptions options) override; - virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; + virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time, bool clamp) const override; virtual TimeRange OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; @@ -188,12 +188,12 @@ public: /** * @brief Get currently set loop mode */ - Decoder::LoopMode loop_mode() const + LoopMode loop_mode() const { - return static_cast(GetStandardValue(kLoopModeInput).toInt()); + return static_cast(GetStandardValue(kLoopModeInput).toInt()); } - void set_loop_mode(Decoder::LoopMode l) + void set_loop_mode(LoopMode l) { SetStandardValue(kLoopModeInput, int(l)); } diff --git a/app/node/block/transition/diptocolor/diptocolortransition.cpp b/app/node/block/transition/diptocolor/diptocolortransition.cpp index 5b262288e..42d04dd94 100644 --- a/app/node/block/transition/diptocolor/diptocolortransition.cpp +++ b/app/node/block/transition/diptocolor/diptocolortransition.cpp @@ -24,6 +24,8 @@ namespace olive { const QString DipToColorTransition::kColorInput = QStringLiteral("color_in"); +#define super TransitionBlock + DipToColorTransition::DipToColorTransition() { AddInput(kColorInput, NodeValue::kColor, QVariant::fromValue(Color(0, 0, 0))); @@ -56,6 +58,13 @@ ShaderCode DipToColorTransition::GetShaderCode(const ShaderRequest &request) con return ShaderCode(FileFunctions::ReadFileAsString(":/shaders/diptoblack.frag"), QString()); } +void DipToColorTransition::Retranslate() +{ + super::Retranslate(); + + SetInputName(kColorInput, tr("Color")); +} + void DipToColorTransition::ShaderJobEvent(const NodeValueRow &value, ShaderJob *job) const { job->Insert(kColorInput, value); diff --git a/app/node/block/transition/diptocolor/diptocolortransition.h b/app/node/block/transition/diptocolor/diptocolortransition.h index f0554f6c7..8c84134eb 100644 --- a/app/node/block/transition/diptocolor/diptocolortransition.h +++ b/app/node/block/transition/diptocolor/diptocolortransition.h @@ -40,6 +40,8 @@ public: virtual ShaderCode GetShaderCode(const ShaderRequest &request) const override; + virtual void Retranslate() override; + static const QString kColorInput; protected: diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index d5c048eeb..1b05f3ba7 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -47,6 +47,8 @@ TransitionBlock::TransitionBlock() : AddInput(kCenterInput, NodeValue::kRational, InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable)); SetInputProperty(kCenterInput, QStringLiteral("view"), RationalSlider::kTime); SetInputProperty(kCenterInput, QStringLiteral("viewlock"), true); + + SetFlags(GetFlags() & ~kDontShowInParamView); } void TransitionBlock::Retranslate() @@ -171,10 +173,14 @@ void TransitionBlock::Value(const NodeValueRow &value, const NodeGlobals &global if (out_buffer.type() != NodeValue::kNone) { job.Insert(kOutBlockInput, out_buffer); + } else { + job.Insert(kOutBlockInput, NodeValue(NodeValue::kTexture, nullptr)); } if (in_buffer.type() != NodeValue::kNone) { job.Insert(kInBlockInput, in_buffer); + } else { + job.Insert(kInBlockInput, NodeValue(NodeValue::kTexture, nullptr)); } job.Insert(kCurveInput, value); @@ -281,16 +287,17 @@ void TransitionBlock::InputDisconnectedEvent(const QString &input, int element, } } -TimeRange TransitionBlock::InputTimeAdjustment(const QString &input, int element, const TimeRange &input_time) const +TimeRange TransitionBlock::InputTimeAdjustment(const QString &input, int element, const TimeRange &input_time, bool clamp) const { if (input == kInBlockInput || input == kOutBlockInput) { Block* block = dynamic_cast(GetConnectedOutput(input)); if (block) { + // Retransform time as if it came from the track return input_time + in() - block->in(); } } - return super::InputTimeAdjustment(input, element, input_time); + return super::InputTimeAdjustment(input, element, input_time, clamp); } TimeRange TransitionBlock::OutputTimeAdjustment(const QString &input, int element, const TimeRange &input_time) const diff --git a/app/node/block/transition/transition.h b/app/node/block/transition/transition.h index 554a7c469..d16a195bd 100644 --- a/app/node/block/transition/transition.h +++ b/app/node/block/transition/transition.h @@ -83,7 +83,7 @@ protected: virtual void InputDisconnectedEvent(const QString& input, int element, Node *output) override; - virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; + virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time, bool clamp) const override; virtual TimeRange OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; diff --git a/app/node/distort/transform/transformdistortnode.cpp b/app/node/distort/transform/transformdistortnode.cpp index b19934615..26e1e3577 100644 --- a/app/node/distort/transform/transformdistortnode.cpp +++ b/app/node/distort/transform/transformdistortnode.cpp @@ -297,7 +297,7 @@ QMatrix4x4 TransformDistortNode::AdjustMatrixByResolutions(const QMatrix4x4 &mat adjusted_matrix.scale(2.0 / sequence_res.x(), 2.0 / sequence_res.y(), 1.0); // Apply offset if applicable - adjusted_matrix.translate(offset); + adjusted_matrix.translate(offset.x(), offset.y()); // Adjust by the matrix we generated earlier adjusted_matrix *= mat; @@ -358,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.scale(sequence_half_res.x(), sequence_half_res.y()); rectangle_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, false, false, false, row[kParentInput].toMatrix()), sequence_res, tex_sz, @@ -378,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.scale(sequence_half_res.x(), sequence_half_res.y()); anchor_matrix *= AdjustMatrixByResolutions(GenerateMatrix(row, true, false, false, row[kParentInput].toMatrix()), sequence_res, tex_sz, diff --git a/app/node/effect/opacity/opacityeffect.cpp b/app/node/effect/opacity/opacityeffect.cpp index cc0f2d2c0..ee58fb632 100644 --- a/app/node/effect/opacity/opacityeffect.cpp +++ b/app/node/effect/opacity/opacityeffect.cpp @@ -39,15 +39,22 @@ void OpacityEffect::Retranslate() ShaderCode OpacityEffect::GetShaderCode(const ShaderRequest &request) const { - Q_UNUSED(request) - return ShaderCode(FileFunctions::ReadFileAsString(":/shaders/opacity.frag")); + if (request.id == QStringLiteral("rgbmult")) { + return ShaderCode(FileFunctions::ReadFileAsString(":/shaders/opacity_rgb.frag")); + } else { + return ShaderCode(FileFunctions::ReadFileAsString(":/shaders/opacity.frag")); + } } void OpacityEffect::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeValueTable *table) const { // If there's no texture, no need to run an operation if (TexturePtr tex = value[kTextureInput].toTexture()) { - if (!qFuzzyCompare(value[kValueInput].toDouble(), 1.0)) { + if (TexturePtr opacity_tex = value[kValueInput].toTexture()) { + ShaderJob job(value); + job.SetShaderID(QStringLiteral("rgbmult")); + table->Push(NodeValue::kTexture, tex->toJob(job), this); + } else if (!qFuzzyCompare(value[kValueInput].toDouble(), 1.0)) { table->Push(NodeValue::kTexture, tex->toJob(ShaderJob(value)), this); } else { // 1.0 float is a no-op, so just push the texture diff --git a/app/node/generator/matrix/matrix.cpp b/app/node/generator/matrix/matrix.cpp index 81dbdb8f6..0d8a112d4 100644 --- a/app/node/generator/matrix/matrix.cpp +++ b/app/node/generator/matrix/matrix.cpp @@ -128,7 +128,7 @@ QMatrix4x4 MatrixGenerator::GenerateMatrix(const QVector2D& pos, QMatrix4x4 mat) { // Position - mat.translate(pos); + mat.translate(pos.x(), pos.y()); // Rotation mat.rotate(rot, 0, 0, 1); @@ -143,7 +143,7 @@ QMatrix4x4 MatrixGenerator::GenerateMatrix(const QVector2D& pos, mat.scale(full_scale); // Anchor Point - mat.translate(-anchor); + mat.translate(-anchor.x(), -anchor.y()); return mat; } diff --git a/app/node/globals.h b/app/node/globals.h index c7ec1ce59..9c8eb26a9 100644 --- a/app/node/globals.h +++ b/app/node/globals.h @@ -25,6 +25,7 @@ #include "common/timerange.h" #include "render/audioparams.h" +#include "render/loopmode.h" #include "render/videoparams.h" namespace olive { @@ -34,10 +35,16 @@ class NodeGlobals public: NodeGlobals(){} - NodeGlobals(const VideoParams &vparam, const AudioParams &aparam, const TimeRange &time) : + NodeGlobals(const VideoParams &vparam, const AudioParams &aparam, const TimeRange &time, LoopMode loop_mode) : video_params_(vparam), audio_params_(aparam), - time_(time) + time_(time), + loop_mode_(loop_mode) + { + } + + NodeGlobals(const VideoParams &vparam, const AudioParams &aparam, const rational &time, LoopMode loop_mode) : + NodeGlobals(vparam, aparam, TimeRange(time, time + vparam.frame_rate_as_time_base()), loop_mode) { } @@ -46,11 +53,13 @@ public: const AudioParams &aparams() const { return audio_params_; } const VideoParams &vparams() const { return video_params_; } const TimeRange &time() const { return time_; } + LoopMode loop_mode() const { return loop_mode_; } private: VideoParams video_params_; AudioParams audio_params_; TimeRange time_; + LoopMode loop_mode_; }; diff --git a/app/node/input/multicam/multicamnode.cpp b/app/node/input/multicam/multicamnode.cpp index 2ff887069..e34220906 100644 --- a/app/node/input/multicam/multicamnode.cpp +++ b/app/node/input/multicam/multicamnode.cpp @@ -13,11 +13,7 @@ const QString MultiCamNode::kSequenceTypeInput = QStringLiteral("sequence_type_i MultiCamNode::MultiCamNode() { - AddInput(kCurrentInput, NodeValue::kInt, InputFlags(kInputFlagStatic)); - - // Make current index start at 1 instead of 0 - SetInputProperty(kCurrentInput, QStringLiteral("offset"), 1); - SetInputProperty(kCurrentInput, QStringLiteral("min"), 0); + AddInput(kCurrentInput, NodeValue::kCombo, InputFlags(kInputFlagStatic)); AddInput(kSourcesInput, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable | kInputFlagArray)); SetInputProperty(kSourcesInput, QStringLiteral("arraystart"), 1); @@ -135,6 +131,18 @@ void MultiCamNode::Retranslate() SetInputName(kSequenceInput, tr("Sequence")); SetInputName(kSequenceTypeInput, tr("Sequence Type")); SetComboBoxStrings(kSequenceTypeInput, {tr("Video"), tr("Audio")}); + + QStringList names; + int name_count = GetSourceCount(); + names.reserve(name_count); + for (int i=0; iName(); + } + names.append(tr("%1: %2").arg(QString::number(i+1), src_name)); + } + SetComboBoxStrings(kCurrentInput, names); } int MultiCamNode::GetSourceCount() const diff --git a/app/node/math/math/math.cpp b/app/node/math/math/math.cpp index 4ddbe2e12..82b07e6e8 100644 --- a/app/node/math/math/math.cpp +++ b/app/node/math/math/math.cpp @@ -44,6 +44,14 @@ MathNode::MathNode() QString MathNode::Name() const { + // Default to naming after the operation + if (parent()) { + QString op_name = GetOperationName(GetOperation()); + if (!op_name.isEmpty()) { + return op_name; + } + } + return tr("Math"); } @@ -70,12 +78,12 @@ void MathNode::Retranslate() SetInputName(kParamAIn, tr("Value")); SetInputName(kParamBIn, tr("Value")); - QStringList operations = {tr("Add"), - tr("Subtract"), - tr("Multiply"), - tr("Divide"), + QStringList operations = {GetOperationName(kOpAdd), + GetOperationName(kOpSubtract), + GetOperationName(kOpMultiply), + GetOperationName(kOpDivide), QString(), - tr("Power")}; + GetOperationName(kOpPower)}; SetComboBoxStrings(kMethodIn, operations); } diff --git a/app/node/math/math/mathbase.cpp b/app/node/math/math/mathbase.cpp index 4ed0ea5f6..2ae9594ae 100644 --- a/app/node/math/math/mathbase.cpp +++ b/app/node/math/math/mathbase.cpp @@ -141,9 +141,9 @@ QVector4D MathNodeBase::RetrieveVector(const NodeValue &val) // QVariant doesn't know that QVector*D can convert themselves so we do it here switch (val.type()) { case NodeValue::kVec2: - return val.toVec2(); + return QVector4D(val.toVec2()); case NodeValue::kVec3: - return val.toVec3(); + return QVector4D(val.toVec3()); case NodeValue::kVec4: default: return val.toVec4(); @@ -167,6 +167,19 @@ void MathNodeBase::PushVector(NodeValueTable *output, olive::NodeValue::Type typ } } +QString MathNodeBase::GetOperationName(Operation o) +{ + switch (o) { + case kOpAdd: return tr("Add"); + case kOpSubtract: return tr("Subtract"); + case kOpMultiply: return tr("Multiply"); + case kOpDivide: return tr("Divide"); + case kOpPower: return tr("Power"); + } + + return QString(); +} + void MathNodeBase::PerformAllOnFloatBuffer(Operation operation, float *a, float b, int start, int end) { for (int j=start;jOutputsTo(n, recursively, ignore_edges, added_edge)) { - return true; - } else if (added_edge.first == this) { - Node *proposed_connected = added_edge.second.node(); - - if (proposed_connected == n) { - return true; - } else if (recursively && proposed_connected->OutputsTo(n, recursively, ignore_edges, added_edge)) { - return true; - } - } - } - - return false; -} - -bool Node::OutputsTo(const QString &id, bool recursively) const -{ - for (const OutputConnection& conn : output_connections_) { - Node* connected = conn.second.node(); - - if (connected->id() == id) { - return true; - } else if (recursively && connected->OutputsTo(id, recursively)) { - return true; - } - } - - return false; -} - -bool Node::OutputsTo(const NodeInput &input, bool recursively) const -{ - for (const OutputConnection& conn : output_connections_) { - const NodeInput& connected = conn.second; - - if (connected == input) { - return true; - } else if (recursively && connected.node()->OutputsTo(input, recursively)) { - return true; - } - } - - return false; -} - bool Node::InputsFrom(Node *n, bool recursively) const { for (auto it=input_connections_.cbegin(); it!=input_connections_.cend(); it++) { @@ -1643,28 +1585,6 @@ bool Node::InputsFrom(const QString &id, bool recursively) const return false; } -int Node::GetNumberOfRoutesTo(Node *n) const -{ - bool outputs_directly = false; - int routes = 0; - - foreach (const OutputConnection& conn, output_connections_) { - Node* connected_node = conn.second.node(); - - if (connected_node == n) { - outputs_directly = true; - } else { - routes += connected_node->GetNumberOfRoutesTo(n); - } - } - - if (outputs_directly) { - routes++; - } - - return routes; -} - void Node::DisconnectAll() { // Disconnect inputs (copy map since internal map will change as we disconnect) @@ -1712,42 +1632,33 @@ QString Node::GetCategoryName(const CategoryID &c) return tr("Uncategorized"); } -QVector Node::TransformTimeTo(const TimeRange &time, Node *target, bool input_dir) +TimeRange Node::TransformTimeTo(TimeRange time, Node *target, TransformTimeDirection dir, int path_index) { - QVector paths_found; + Node *from = this; + Node *to = target; - if (input_dir) { - // If this input is connected, traverse it to see if we stumble across the specified `node` - for (auto it=input_connections_.cbegin(); it!=input_connections_.cend(); it++) { - TimeRange input_adjustment = InputTimeAdjustment(it->first.input(), it->first.element(), time); - Node* connected = it->second; + if (dir == kTransformTowardsInput) { + std::swap(from, to); + } - if (connected == target) { - // We found the target, no need to keep traversing - if (!paths_found.contains(input_adjustment)) { - paths_found.append(input_adjustment); - } - } else { - // We did NOT find the target, traverse this - paths_found.append(connected->TransformTimeTo(input_adjustment, target, input_dir)); + std::list path = FindPath(from, to, path_index); + + if (!path.empty()) { + if (dir == kTransformTowardsInput) { + for (auto it=path.crbegin(); it!=path.crend(); it++) { + const NodeInput &i = (*it); + time = i.node()->InputTimeAdjustment(i.input(), i.element(), time, false); } - } - } else { - // If this input is connected, traverse it to see if we stumble across the specified `node` - foreach (const OutputConnection& conn, output_connections_) { - Node* connected_node = conn.second.node(); - - TimeRange output_adjustment = connected_node->OutputTimeAdjustment(conn.second.input(), conn.second.element(), time); - - if (connected_node == target) { - paths_found.append(output_adjustment); - } else { - paths_found.append(connected_node->TransformTimeTo(output_adjustment, target, input_dir)); + } else { + // Traverse in output direction + for (auto it=path.cbegin(); it!=path.cend(); it++) { + const NodeInput &i = (*it); + time = i.node()->OutputTimeAdjustment(i.input(), i.element(), time); } } } - return paths_found; + return time; } QVariant Node::PtrToValue(void *ptr) @@ -2011,46 +1922,39 @@ void Node::SetValueAtTime(const NodeInput &input, const rational &time, const QV } } -void FindPathInternal(std::list &vec, Node *to, int &path_index) +bool FindPathInternal(std::list &vec, Node *from, Node *to, int &path_index) { - Node *from = vec.back(); + for (auto it=from->output_connections().cbegin(); it!=from->output_connections().cend(); it++) { + const NodeInput &next = it->second; - for (auto it=from->input_connections().cbegin(); it!=from->input_connections().cend(); it++) { - vec.push_back(it->second); - if (it->second == to) { - // Found a path, determine if it's the one we want + vec.push_back(next); + + if (next.node() == to) { + // Found a path! Determine if it's the index we want if (path_index == 0) { // It is! - break; + return true; } else { + // It isn't, keep looking... path_index--; } } - // Recurse to see if we can find it here - FindPathInternal(vec, to, path_index); - if (vec.back() == to) { - // Found through recursion - break; - } else { - // Must not be available through this path - vec.pop_back(); + if (FindPathInternal(vec, next.node(), to, path_index)) { + return true; } + + vec.pop_back(); } + + return false; } -std::list Node::FindPath(Node *from, Node *to, int path_index) +std::list Node::FindPath(Node *from, Node *to, int path_index) { - std::list v; + std::list v; - v.push_back(from); - - FindPathInternal(v, to, path_index); - - if (v.size() == 1) { - // Failed to find path, return empty list - v.pop_back(); - } + FindPathInternal(v, from, to, path_index); return v; } diff --git a/app/node/node.h b/app/node/node.h index 0dbfdaa3e..8fa933979 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -786,30 +786,6 @@ public: */ virtual void GenerateFrame(FramePtr frame, const GenerateJob &job) const; - /** - * @brief Returns whether this Node outputs to `n` - * - * @param n - * - * The node instance to check. - * - * @param recursively - * - * Whether to keep traversing down outputs to find this node (TRUE) or stick to immediate outputs - * (FALSE). - */ - bool OutputsTo(Node* n, bool recursively, const OutputConnections &ignore_edges = OutputConnections(), const OutputConnection &added_edge = OutputConnection()) const; - - /** - * @brief Same as OutputsTo(Node*), but for a node ID rather than a specific instance. - */ - bool OutputsTo(const QString& id, bool recursively) const; - - /** - * @brief Same as OutputsTo(Node*), but for a specific node input rather than just a node. - */ - bool OutputsTo(const NodeInput &input, bool recursively) const; - /** * @brief Returns whether this node ever receives an input from a particular node instance */ @@ -820,7 +796,6 @@ public: */ bool InputsFrom(const QString& id, bool recursively) const; - /** * @brief Find inputs that `output` outputs to in order to arrive at this node * @@ -829,11 +804,6 @@ public: */ QVector FindWaysNodeArrivesHere(const Node *output) const; - /** - * @brief Determines how many paths go from this node out to another node - */ - int GetNumberOfRoutesTo(Node* n) const; - /** * @brief Severs all input and output connections */ @@ -844,10 +814,15 @@ public: */ static QString GetCategoryName(const CategoryID &c); + enum TransformTimeDirection { + kTransformTowardsInput, + kTransformTowardsOutput + }; + /** * @brief Transforms time from this node through the connections it takes to get to the specified node */ - QVector TransformTimeTo(const TimeRange& time, Node* target, bool input_dir); + TimeRange TransformTimeTo(TimeRange time, Node* target, TransformTimeDirection dir, int path_index); /** * @brief Find nodes of a certain type that this Node takes inputs from @@ -861,12 +836,6 @@ public: template static QVector FindInputNodesConnectedToInput(const NodeInput &input, int maximum = 0); - template - /** - * @brief Find a node of a certain type that this Node outputs to - */ - QVector FindOutputNode(); - /** * @brief Convert a pointer to a value that can be sent between NodeParams */ @@ -902,7 +871,7 @@ public: * If this node modifies the `time` (i.e. a clip converting sequence time to media time), this function should be * overridden to do so. Also make sure to override OutputTimeAdjustment() to provide the inverse function. */ - virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const; + virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time, bool clamp) const; /** * @brief The inverse of InputTimeAdjustment() @@ -1147,7 +1116,10 @@ public: static void SetValueAtTime(const NodeInput &input, const rational &time, const QVariant &value, int track, MultiUndoCommand *command, bool insert_on_all_tracks_if_no_key); - static std::list FindPath(Node *from, Node *to, int path_index = 0); + /** + * @brief Find path starting at `from` that outputs to arrive at `to` + */ + static std::list FindPath(Node *from, Node *to, int path_index); static const QString kEnabledInput; @@ -1405,9 +1377,6 @@ private: template static void FindInputNodeInternal(const Node* n, QVector& list, int maximum); - template - static void FindOutputNodeInternal(const Node* n, QVector& list); - QVector GetDependenciesInternal(bool traverse, bool exclusive_only) const; void ParameterValueChanged(const QString &input, int element, const olive::TimeRange &range); @@ -1565,31 +1534,6 @@ T* Node::ValueToPtr(const QVariant &ptr) return reinterpret_cast(ptr.value()); } -template -void Node::FindOutputNodeInternal(const Node* n, QVector& list) -{ - foreach (const OutputConnection& output, n->output_connections_) { - Node* connected = output.second.node(); - T* cast_test = dynamic_cast(connected); - - if (cast_test) { - list.append(cast_test); - } - - FindOutputNodeInternal(connected, list); - } -} - -template -QVector Node::FindOutputNode() -{ - QVector list; - - FindOutputNodeInternal(this, list); - - return list; -} - using NodePtr = std::shared_ptr; class NodeSetPositionCommand : public UndoCommand diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index f2bed9064..e1bdde7f3 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -113,6 +113,10 @@ Node::ActiveElements Track::GetActiveElementsAtTime(const QString &input, const end = blocks_.size()-1; } + if (blocks_.at(end)->in() == r.out()) { + end--; + } + ActiveElements a; for (int i=start; i<=end; i++) { Block *b = blocks_.at(i); @@ -146,17 +150,24 @@ void Track::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeVal } } -TimeRange Track::InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const +TimeRange Track::InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time, bool clamp) const { if (input == kBlockInput && element >= 0) { int cache_index = GetCacheIndexFromArrayIndex(element); if (cache_index > -1) { - return TransformRangeForBlock(blocks_.at(cache_index), input_time); + TimeRange r = input_time; + Block *b = blocks_.at(cache_index); + + if (clamp) { + r.set_range(std::max(r.in(), b->in()), std::min(r.out(), b->out())); + } + + return TransformRangeForBlock(b, r); } } - return Node::InputTimeAdjustment(input, element, input_time); + return Node::InputTimeAdjustment(input, element, input_time, clamp); } TimeRange Track::OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const @@ -180,7 +191,7 @@ const double &Track::GetTrackHeight() const void Track::SetTrackHeight(const double &height) { track_height_ = height; - emit TrackHeightChangedInPixels(GetTrackHeightInPixels()); + emit TrackHeightChanged(track_height_); } void Track::InputConnectedEvent(const QString &input, int element, Node *output) @@ -647,16 +658,16 @@ void Track::ProcessAudioTrack(const NodeValueRow &value, const NodeGlobals &glob TimeRange range_for_block(qMax(b->in(), range.in()), qMin(b->out(), range.out())); + qint64 source_offset = 0; qint64 destination_offset = globals.aparams().time_to_samples(range_for_block.in() - range.in()); qint64 max_dest_sz = globals.aparams().time_to_samples(range_for_block.length()); // Destination buffer SampleBuffer samples_from_this_block = it->second.toSamples(); - ClipBlock *clip_cast = dynamic_cast(b); if (samples_from_this_block.is_allocated()) { // If this is a clip, we might have extra speed/reverse information - if (clip_cast) { + if (ClipBlock *clip_cast = dynamic_cast(b)) { double speed_value = clip_cast->speed(); bool reversed = clip_cast->reverse(); @@ -711,11 +722,11 @@ void Track::ProcessAudioTrack(const NodeValueRow &value, const NodeGlobals &glob } } - qint64 copy_length = qMin(max_dest_sz, qint64(samples_from_this_block.sample_count() - destination_offset)); + qint64 copy_length = qMin(max_dest_sz, qint64(samples_from_this_block.sample_count() - source_offset)); // Copy samples into destination buffer for (int i=0; i(sender()), height); + connect(track, &Track::TrackHeightChanged, this, [this](){ + Track *t = static_cast(sender()); + emit TrackHeightChanged(t, t->GetTrackHeightInPixels()); }); track->set_type(type_); diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 50d90f0b3..fc8490a2a 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -227,11 +227,11 @@ void ViewerOutput::InvalidateCache(const TimeRange& range, const QString& from, if (from == kTextureInput) { //connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly); if (autocache_input_video_) { - TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetVideoLength())); + TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetVideoLength()), false); connected->video_frame_cache()->Request(range.Intersected(max_range)); } } else if (from == kSamplesInput) { - TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetAudioLength())); + TimeRange max_range = InputTimeAdjustment(from, element, TimeRange(0, GetAudioLength()), false); if (waveform_requests_enabled_) { connected->waveform_cache()->Request(range.Intersected(max_range)); } @@ -316,6 +316,12 @@ void ViewerOutput::VerifyLength() } } +void ViewerOutput::SetPlayhead(const rational &t) +{ + playhead_ = t; + emit PlayheadChanged(t); +} + void ViewerOutput::InputConnectedEvent(const QString &input, int element, Node *output) { if (input == kTextureInput) { @@ -394,7 +400,7 @@ void ViewerOutput::SetWaveformEnabled(bool e) { if ((waveform_requests_enabled_ = e)) { if (Node *connected = this->GetConnectedSampleOutput()) { - TimeRange max_range = InputTimeAdjustment(kSamplesInput, -1, TimeRange(0, GetAudioLength())); + TimeRange max_range = InputTimeAdjustment(kSamplesInput, -1, TimeRange(0, GetAudioLength()), false); TimeRangeList invalid = connected->waveform_cache()->GetInvalidatedRanges(max_range); for (const TimeRange &r : invalid) { connected->waveform_cache()->Request(r); diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index b99ebc30b..93df8d46b 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -97,6 +97,8 @@ public: } } + const rational &GetPlayhead() { return playhead_; } + void SetVideoParams(const VideoParams &video, int index = 0) { SetStandardValue(kVideoParamsInput, QVariant::fromValue(video), index); @@ -219,9 +221,13 @@ signals: void ConnectedWaveformChanged(); + void PlayheadChanged(const rational &t); + public slots: void VerifyLength(); + void SetPlayhead(const rational &t); + protected: virtual void InputConnectedEvent(const QString &input, int element, Node *output) override; @@ -253,6 +259,8 @@ private: bool waveform_requests_enabled_; + rational playhead_; + }; } diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 210d448f5..f99ea8bc5 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -265,7 +265,7 @@ void Footage::Value(const NodeValueRow &value, const NodeGlobals &globals, NodeV // Push each stream as a footage job for (int i=0; iPush(NodeValue::kTexture, Texture::Job(vp, job), this, ref.ToString()); - } else { + } else if (ref.type() == Track::kAudio) { AudioParams ap = GetAudioParams(ref.index()); job.set_audio_params(ap); job.set_cache_path(project()->cache_path()); @@ -337,7 +337,7 @@ bool TimeIsOutOfBounds(const rational& time, const rational& length) return time < 0 || time >= length; } -rational Footage::AdjustTimeByLoopMode(rational time, Decoder::LoopMode loop_mode, const rational &length, VideoParams::Type type, const rational& timebase) +rational Footage::AdjustTimeByLoopMode(rational time, LoopMode loop_mode, const rational &length, VideoParams::Type type, const rational& timebase) { if (type == VideoParams::kVideoTypeStill) { // No looping for still images @@ -346,15 +346,15 @@ rational Footage::AdjustTimeByLoopMode(rational time, Decoder::LoopMode loop_mod if (TimeIsOutOfBounds(time, length)) { switch (loop_mode) { - case Decoder::kLoopModeOff: + case LoopMode::kLoopModeOff: // Return no time to indicate no frame should be shown here time = rational::NaN; break; - case Decoder::kLoopModeClamp: + case LoopMode::kLoopModeClamp: // Clamp footage time to length time = clamp(time, rational(0), length - timebase); break; - case Decoder::kLoopModeLoop: + case LoopMode::kLoopModeLoop: // Loop footage time around job length do { if (time >= length) { @@ -474,8 +474,10 @@ void Footage::Reprobe() } } - if (!footage_info.Save(meta_cache_file)) { - qWarning() << "Failed to save stream cache, footage will have to be re-probed"; + if (!cancelled_ || !cancelled_->HeardCancel()) { + if (!footage_info.Save(meta_cache_file)) { + qWarning() << "Failed to save stream cache, footage will have to be re-probed"; + } } } diff --git a/app/node/project/footage/footage.h b/app/node/project/footage/footage.h index 7eb38f3c5..321afa829 100644 --- a/app/node/project/footage/footage.h +++ b/app/node/project/footage/footage.h @@ -173,7 +173,7 @@ public: virtual Node *GetConnectedSampleOutput() override; - static rational AdjustTimeByLoopMode(rational time, Decoder::LoopMode loop_mode, const rational& length, VideoParams::Type type, const rational &timebase); + static rational AdjustTimeByLoopMode(rational time, LoopMode loop_mode, const rational& length, VideoParams::Type type, const rational &timebase); virtual void LoadFinishedEvent() override; diff --git a/app/node/project/serializer/serializer220403.cpp b/app/node/project/serializer/serializer220403.cpp index 513990302..6708911df 100644 --- a/app/node/project/serializer/serializer220403.cpp +++ b/app/node/project/serializer/serializer220403.cpp @@ -570,13 +570,13 @@ void ProjectSerializer220403::LoadNode(Node *node, XMLNodeData &xml_node_data, Q } else if (reader->name() == QStringLiteral("caches")) { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("audio")) { - node->audio_playback_cache()->SetUuid(reader->readElementText()); + node->audio_playback_cache()->SetUuid(QUuid::fromString(reader->readElementText())); } else if (reader->name() == QStringLiteral("video")) { - node->video_frame_cache()->SetUuid(reader->readElementText()); + node->video_frame_cache()->SetUuid(QUuid::fromString(reader->readElementText())); } else if (reader->name() == QStringLiteral("thumb")) { - node->thumbnail_cache()->SetUuid(reader->readElementText()); + node->thumbnail_cache()->SetUuid(QUuid::fromString(reader->readElementText())); } else if (reader->name() == QStringLiteral("waveform")) { - node->waveform_cache()->SetUuid(reader->readElementText()); + node->waveform_cache()->SetUuid(QUuid::fromString(reader->readElementText())); } else { reader->skipCurrentElement(); } diff --git a/app/node/time/timeoffset/timeoffsetnode.cpp b/app/node/time/timeoffset/timeoffsetnode.cpp index 98fc2c6d0..1a6201b5e 100644 --- a/app/node/time/timeoffset/timeoffsetnode.cpp +++ b/app/node/time/timeoffset/timeoffsetnode.cpp @@ -46,12 +46,12 @@ void TimeOffsetNode::Retranslate() SetInputName(kInputInput, QStringLiteral("Input")); } -TimeRange TimeOffsetNode::InputTimeAdjustment(const QString &input, int element, const TimeRange &input_time) const +TimeRange TimeOffsetNode::InputTimeAdjustment(const QString &input, int element, const TimeRange &input_time, bool clamp) const { if (input == kInputInput) { return TimeRange(GetRemappedTime(input_time.in()), GetRemappedTime(input_time.out())); } else { - return super::InputTimeAdjustment(input, element, input_time); + return super::InputTimeAdjustment(input, element, input_time, clamp); } } diff --git a/app/node/time/timeoffset/timeoffsetnode.h b/app/node/time/timeoffset/timeoffsetnode.h index 56f1c0711..f1890924f 100644 --- a/app/node/time/timeoffset/timeoffsetnode.h +++ b/app/node/time/timeoffset/timeoffsetnode.h @@ -52,7 +52,7 @@ public: return tr("Offset time passing through the graph."); } - virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; + virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time, bool clamp) const override; virtual TimeRange OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; virtual void Retranslate() override; diff --git a/app/node/time/timeremap/timeremap.cpp b/app/node/time/timeremap/timeremap.cpp index 2c1d0d2c5..d3035bb3d 100644 --- a/app/node/time/timeremap/timeremap.cpp +++ b/app/node/time/timeremap/timeremap.cpp @@ -58,12 +58,12 @@ QString TimeRemapNode::Description() const return tr("Arbitrarily remap time through the nodes."); } -TimeRange TimeRemapNode::InputTimeAdjustment(const QString &input, int element, const TimeRange &input_time) const +TimeRange TimeRemapNode::InputTimeAdjustment(const QString &input, int element, const TimeRange &input_time, bool clamp) const { if (input == kInputInput) { return TimeRange(GetRemappedTime(input_time.in()), GetRemappedTime(input_time.out())); } else { - return super::InputTimeAdjustment(input, element, input_time); + return super::InputTimeAdjustment(input, element, input_time, clamp); } } diff --git a/app/node/time/timeremap/timeremap.h b/app/node/time/timeremap/timeremap.h index 8efd60ed0..3ba9cd8e1 100644 --- a/app/node/time/timeremap/timeremap.h +++ b/app/node/time/timeremap/timeremap.h @@ -38,7 +38,7 @@ public: virtual QVector Category() const override; virtual QString Description() const override; - virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; + virtual TimeRange InputTimeAdjustment(const QString& input, int element, const TimeRange& input_time, bool clamp) const override; virtual TimeRange OutputTimeAdjustment(const QString& input, int element, const TimeRange& input_time) const override; virtual void Retranslate() override; diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index ee1fa0fbd..86e91ea2d 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -32,7 +32,7 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa NodeValueDatabase database; // HACK: Pick up loop mode from clips - Decoder::LoopMode old_loop_mode = loop_mode_; + LoopMode old_loop_mode = loop_mode_; if (const ClipBlock *clip = dynamic_cast(node)) { loop_mode_ = clip->loop_mode(); } @@ -184,17 +184,12 @@ void NodeTraverser::Transform(QTransform *transform, const Node *start, const No transform_ = nullptr; } -NodeGlobals NodeTraverser::GenerateGlobals(const VideoParams &vparams, const AudioParams &aparams, const TimeRange &time) -{ - return NodeGlobals(vparams, aparams, time); -} - NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& input, const TimeRange& range) { // If input is connected, retrieve value directly if (node->IsInputConnectedForRender(input)) { - TimeRange adjusted_range = node->InputTimeAdjustment(input, -1, range); + TimeRange adjusted_range = node->InputTimeAdjustment(input, -1, range, true); // Value will equal something from the connected node, follow it Node *output = node->GetConnectedRenderOutput(input); @@ -229,7 +224,7 @@ NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& inpu } else { // Not connected or an array, just pull the immediate - TimeRange adjusted_range = node->InputTimeAdjustment(input, -1, range); + TimeRange adjusted_range = node->InputTimeAdjustment(input, -1, range, true); return_val = node->GetValueAtTime(input, adjusted_range.in()); @@ -245,7 +240,7 @@ NodeValueTable NodeTraverser::ProcessInput(const Node* node, const QString& inpu void NodeTraverser::ProcessInputElement(NodeValueTableArray &array_tbl, const Node *node, const QString &input, int element, const TimeRange &range) { NodeValueTable& sub_tbl = array_tbl[element]; - TimeRange adjusted_range = node->InputTimeAdjustment(input, element, range); + TimeRange adjusted_range = node->InputTimeAdjustment(input, element, range, true); if (node->IsInputConnectedForRender(input, element)) { Node *output = node->GetConnectedRenderOutput(input, element); @@ -259,7 +254,7 @@ void NodeTraverser::ProcessInputElement(NodeValueTableArray &array_tbl, const No NodeTraverser::NodeTraverser() : cancel_(nullptr), transform_(nullptr), - loop_mode_(Decoder::kLoopModeOff) + loop_mode_(LoopMode::kLoopModeOff) { } @@ -309,7 +304,7 @@ NodeValueTable NodeTraverser::GenerateTable(const Node *n, const TimeRange& rang table = database.Merge(); // By this point, the node should have all the inputs it needs to render correctly - NodeGlobals globals = GenerateGlobals(video_params_, audio_params_, range); + NodeGlobals globals(video_params_, audio_params_, range, loop_mode_); n->Value(row, globals, &table); // `transform_now_` is the next node in the path that needs to be traversed. It only ever goes @@ -430,7 +425,7 @@ void NodeTraverser::ResolveJobs(NodeValue &val) } else if (FootageJob *fj = dynamic_cast(base_job)) { - rational footage_time = Footage::AdjustTimeByLoopMode(fj->time().in(), loop_mode_, fj->length(), fj->video_params().video_type(), fj->video_params().frame_rate_as_time_base()); + rational footage_time = Footage::AdjustTimeByLoopMode(fj->time().in(), fj->loop_mode(), fj->length(), fj->video_params().video_type(), fj->video_params().frame_rate_as_time_base()); TexturePtr tex; diff --git a/app/node/traverser.h b/app/node/traverser.h index 4f2041d57..84284a0d8 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -54,12 +54,6 @@ public: void Transform(QTransform *transform, const Node *start, const Node *end, const TimeRange &range); - static NodeGlobals GenerateGlobals(const VideoParams &vparams, const AudioParams &aparams, const TimeRange &time); - static NodeGlobals GenerateGlobals(const VideoParams &vparams, const AudioParams &aparams, const rational &time) - { - return GenerateGlobals(vparams, aparams, TimeRange(time, time + vparams.frame_rate_as_time_base())); - } - const VideoParams& GetCacheVideoParams() const { return video_params_; @@ -144,7 +138,7 @@ protected: return block_stack_.empty() ? nullptr : block_stack_.back(); } - Decoder::LoopMode loop_mode() const { return loop_mode_; } + LoopMode loop_mode() const { return loop_mode_; } virtual bool UseCache() const { return false; } @@ -163,7 +157,7 @@ private: std::list block_stack_; - Decoder::LoopMode loop_mode_; + LoopMode loop_mode_; QHash > value_cache_; QHash resolved_texture_cache_; diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index dcbb72fe3..1b1bf5167 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -40,7 +40,7 @@ ProjectPanel::ProjectPanel(QWidget *parent) : // Create main widget and its layout QWidget* central_widget = new QWidget(this); QVBoxLayout* layout = new QVBoxLayout(central_widget); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); SetWidgetWithPadding(central_widget); diff --git a/app/panel/scope/scope.cpp b/app/panel/scope/scope.cpp index 27700bb5c..9117734d3 100644 --- a/app/panel/scope/scope.cpp +++ b/app/panel/scope/scope.cpp @@ -36,7 +36,7 @@ ScopePanel::ScopePanel(QWidget* parent) : QVBoxLayout* layout = new QVBoxLayout(central); QHBoxLayout* toolbar_layout = new QHBoxLayout(); - toolbar_layout->setMargin(0); + toolbar_layout->setContentsMargins(0, 0, 0, 0); scope_type_combobox_ = new QComboBox(); diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index b10220dab..948f254c8 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -29,9 +29,9 @@ TimeBasedPanel::TimeBasedPanel(const QString &object_name, QWidget *parent) : { } -rational TimeBasedPanel::GetTime() +TimeBasedPanel::~TimeBasedPanel() { - return widget_->GetTime(); + delete widget_; } const rational& TimeBasedPanel::timebase() @@ -74,11 +74,6 @@ void TimeBasedPanel::SetTimebase(const rational &timebase) widget_->SetTimebase(timebase); } -void TimeBasedPanel::SetTime(const rational &time) -{ - widget_->SetTime(time); -} - void TimeBasedPanel::GoToPrevCut() { widget_->GoToPrevCut(); @@ -122,16 +117,12 @@ void TimeBasedPanel::ConnectViewerNode(ViewerOutput *node) void TimeBasedPanel::SetTimeBasedWidget(TimeBasedWidget *widget) { if (widget_) { - disconnect(widget_, &TimeBasedWidget::TimeChanged, this, &TimeBasedPanel::TimeChanged); - disconnect(widget_, &TimeBasedWidget::TimebaseChanged, this, &TimeBasedPanel::TimebaseChanged); disconnect(widget_, &TimeBasedWidget::ConnectedNodeChanged, this, &TimeBasedPanel::ConnectedNodeChanged); } widget_ = widget; if (widget_) { - connect(widget_, &TimeBasedWidget::TimeChanged, this, &TimeBasedPanel::TimeChanged); - connect(widget_, &TimeBasedWidget::TimebaseChanged, this, &TimeBasedPanel::TimebaseChanged); connect(widget_, &TimeBasedWidget::ConnectedNodeChanged, this, &TimeBasedPanel::ConnectedNodeChanged); } diff --git a/app/panel/timebased/timebased.h b/app/panel/timebased/timebased.h index 4101e6b09..566412b66 100644 --- a/app/panel/timebased/timebased.h +++ b/app/panel/timebased/timebased.h @@ -32,6 +32,8 @@ class TimeBasedPanel : public PanelWidget public: TimeBasedPanel(const QString& object_name, QWidget *parent = nullptr); + virtual ~TimeBasedPanel() override; + void ConnectViewerNode(ViewerOutput *node); void DisconnectViewerNode() @@ -39,8 +41,6 @@ public: ConnectViewerNode(nullptr); } - rational GetTime(); - // Get the timebase of this panels widget const rational& timebase(); @@ -111,13 +111,7 @@ public: public slots: void SetTimebase(const rational& timebase); - void SetTime(const rational &time); - signals: - void TimeChanged(const rational& time); - - void TimebaseChanged(const rational& timebase); - void PlayPauseRequested(); void PlayInToOutRequested(); diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index 2c779e2e8..e2e310fcc 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -38,6 +38,7 @@ set(OLIVE_SOURCES render/framehashcache.h render/framemanager.cpp render/framemanager.h + render/loopmode.h render/managedcolor.cpp render/managedcolor.h render/playbackcache.cpp @@ -46,6 +47,8 @@ set(OLIVE_SOURCES render/previewaudiodevice.h render/previewautocacher.cpp render/previewautocacher.h + render/projectcopier.cpp + render/projectcopier.h render/renderer.cpp render/renderer.h render/rendercache.h diff --git a/app/render/job/footagejob.h b/app/render/job/footagejob.h index dcafee554..3fbd7e267 100644 --- a/app/render/job/footagejob.h +++ b/app/render/job/footagejob.h @@ -33,12 +33,13 @@ public: { } - FootageJob(const TimeRange &time, const QString& decoder, const QString& filename, Track::Type type, const rational& length) : + FootageJob(const TimeRange &time, const QString& decoder, const QString& filename, Track::Type type, const rational& length, LoopMode loop_mode) : time_(time), decoder_(decoder), filename_(filename), type_(type), - length_(length) + length_(length), + loop_mode_(loop_mode) { } @@ -99,6 +100,9 @@ public: const TimeRange &time() const { return time_; } + LoopMode loop_mode() const { return loop_mode_; } + void set_loop_mode(LoopMode m) { loop_mode_ = m; } + private: TimeRange time_; @@ -116,6 +120,8 @@ private: rational length_; + LoopMode loop_mode_; + }; } diff --git a/app/render/loopmode.h b/app/render/loopmode.h new file mode 100644 index 000000000..9e4927727 --- /dev/null +++ b/app/render/loopmode.h @@ -0,0 +1,14 @@ +#ifndef LOOPMODE_H +#define LOOPMODE_H + +namespace olive { + +enum class LoopMode { + kLoopModeOff, + kLoopModeLoop, + kLoopModeClamp +}; + +} + +#endif // LOOPMODE_H diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index b0c91d7ab..3340c64d4 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -25,6 +25,8 @@ #include #include +#include "config/config.h" + namespace olive { const int OpenGLRenderer::kTextureCacheMaxSize = 5000; @@ -364,7 +366,11 @@ void OpenGLRenderer::Flush() { GL_PREAMBLE; - functions_->glFlush(); + if (OLIVE_CONFIG("UseGLFinish").toBool()) { + functions_->glFinish(); + } else { + functions_->glFlush(); + } } Color OpenGLRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt) diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 08624be67..827b02c98 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -28,11 +28,6 @@ #include "node/inputdragger.h" #include "node/project/project.h" #include "render/diskmanager.h" -#include "render/renderprocessor.h" -#include "task/customcache/customcachetask.h" -#include "task/taskmanager.h" -#include "widget/slider/base/numericsliderbase.h" -#include "widget/viewer/viewer.h" namespace olive { @@ -47,6 +42,10 @@ PreviewAutoCacher::PreviewAutoCacher(QObject *parent) : multicam_(nullptr), ignore_cache_requests_(false) { + copier_ = new ProjectCopier(this); + connect(copier_, &ProjectCopier::AddedNode, this, &PreviewAutoCacher::ConnectToNodeCache); + connect(copier_, &ProjectCopier::RemovedNode, this, &PreviewAutoCacher::DisconnectFromNodeCache); + // Set defaults SetPlayhead(0); @@ -157,7 +156,7 @@ void PreviewAutoCacher::AudioRendered() if (running_audio_tasks_.removeOne(watcher)) { // Assume that a "result" is a fully completed image and a non-result is a cancelled ticket TimeRange range = watcher->property("time").value(); - Node *node = copy_map_.key(Node::ValueToPtr(watcher->property("node"))); + Node *node = copier_->GetOriginal(Node::ValueToPtr(watcher->property("node"))); if (watcher->HasResult() && node) { if (PlaybackCache *cache = Node::ValueToPtr(watcher->property("cache"))) { @@ -252,140 +251,12 @@ void PreviewAutoCacher::VideoRendered() delete watcher; } -void PreviewAutoCacher::ProcessUpdateQueue() -{ - // Iterate everything that happened to the graph and do the same thing on our end - while (!graph_update_queue_.empty()) { - QueuedJob job = graph_update_queue_.front(); - graph_update_queue_.pop_front(); - - switch (job.type) { - case QueuedJob::kNodeAdded: - AddNode(job.node); - break; - case QueuedJob::kNodeRemoved: - RemoveNode(job.node); - break; - case QueuedJob::kEdgeAdded: - AddEdge(job.output, job.input); - break; - case QueuedJob::kEdgeRemoved: - RemoveEdge(job.output, job.input); - break; - case QueuedJob::kValueChanged: - CopyValue(job.input); - break; - case QueuedJob::kValueHintChanged: - CopyValueHint(job.input); - break; - } - } - - // Indicate that we have synchronized to this point, which is compared with the graph change - // time to see if our copied graph is up to date - UpdateLastSyncedValue(); -} - -void PreviewAutoCacher::AddNode(Node *node) -{ - if (dynamic_cast(node)) { - // Group nodes are just dummy nodes, no need to copy them - return; - } - - // Copy node - Node* copy = node->copy(); - - // Add to project - copy->setParent(&copied_project_); - - // Disable caches for copy - copy->SetCachesEnabled(false); - - // Copy cache UUIDs - copy->CopyCacheUuidsFrom(node); - - // Insert into map - InsertIntoCopyMap(node, copy); - - // Keep track of our nodes - created_nodes_.append(copy); -} - -void PreviewAutoCacher::RemoveNode(Node *node) -{ - // Find our copy and remove it - Node* copy = copy_map_.take(node); - - // Disconnect from node's caches - DisconnectFromNodeCache(node); - - // Remove from created list - created_nodes_.removeOne(copy); - - // Delete it - delete copy; -} - -void PreviewAutoCacher::AddEdge(Node *output, const NodeInput &input) -{ - // Create same connection with our copied graph - Node* our_output = copy_map_.value(output); - Node* our_input = copy_map_.value(input.node()); - - Node::ConnectEdge(our_output, NodeInput(our_input, input.input(), input.element())); -} - -void PreviewAutoCacher::RemoveEdge(Node *output, const NodeInput &input) -{ - // Remove same connection with our copied graph - Node* our_output = copy_map_.value(output); - Node* our_input = copy_map_.value(input.node()); - - Node::DisconnectEdge(our_output, NodeInput(our_input, input.input(), input.element())); -} - -void PreviewAutoCacher::CopyValue(const NodeInput &input) -{ - if (dynamic_cast(input.node())) { - // Group nodes are just dummy nodes, no need to copy them - return; - } - - // Copy all values to our graph - Node* our_input = copy_map_.value(input.node()); - Node::CopyValuesOfElement(input.node(), our_input, input.input(), input.element()); -} - -void PreviewAutoCacher::CopyValueHint(const NodeInput &input) -{ - if (dynamic_cast(input.node())) { - // Group nodes are just dummy nodes, no need to copy them - return; - } - - // Copy value hint to our graph - Node* our_input = copy_map_.value(input.node()); - Node::ValueHint hint = input.node()->GetValueHintForInput(input.input(), input.element()); - our_input->SetValueHintForInput(input.input(), hint, input.element()); -} - -void PreviewAutoCacher::InsertIntoCopyMap(Node *node, Node *copy) -{ - // Insert into map - copy_map_.insert(node, copy); - - // Copy parameters - Node::CopyInputs(node, copy, false); - - // Connect to node's cache - if (!ignore_cache_requests_) { - ConnectToNodeCache(node); - } -} - void PreviewAutoCacher::ConnectToNodeCache(Node *node) { + if (ignore_cache_requests_) { + return; + } + connect(node->video_frame_cache(), &PlaybackCache::Requested, this, @@ -455,16 +326,6 @@ void PreviewAutoCacher::DisconnectFromNodeCache(Node *node) &PreviewAutoCacher::CancelForCache); } -void PreviewAutoCacher::UpdateGraphChangeValue() -{ - graph_changed_time_.Acquire(); -} - -void PreviewAutoCacher::UpdateLastSyncedValue() -{ - last_update_time_.Acquire(); -} - void PreviewAutoCacher::CancelQueuedSingleFrameRender() { if (single_frame_render_) { @@ -477,7 +338,7 @@ void PreviewAutoCacher::CancelQueuedSingleFrameRender() void PreviewAutoCacher::StartCachingRange(const TimeRange &range, TimeRangeList *range_list, RenderJobTracker *tracker) { range_list->insert(range); - tracker->insert(range, graph_changed_time_); + tracker->insert(range, copier_->GetGraphChangeTime()); } void PreviewAutoCacher::StartCachingVideoRange(PlaybackCache *cache, const TimeRange &range) @@ -494,7 +355,7 @@ void PreviewAutoCacher::StartCachingVideoRange(PlaybackCache *cache, const TimeR TimeRangeListFrameIterator iterator({range}, using_tb); pending_video_jobs_.push_back({node, cache, range, iterator}); - video_cache_data_[cache].job_tracker.insert(TimeRange(iterator.Snap(range.in()), range.out()), graph_changed_time_); + video_cache_data_[cache].job_tracker.insert(TimeRange(iterator.Snap(range.in()), range.out()), copier_->GetGraphChangeTime()); TryRender(); } @@ -505,12 +366,17 @@ void PreviewAutoCacher::StartCachingAudioRange(PlaybackCache *cache, const TimeR cache->ClearRequestRange(range); pending_audio_jobs_.push_back({node, cache, range}); - audio_cache_data_[cache].job_tracker.insert(range, graph_changed_time_); + audio_cache_data_[cache].job_tracker.insert(range, copier_->GetGraphChangeTime()); TryRender(); } void PreviewAutoCacher::VideoInvalidatedFromNode(PlaybackCache *cache, const TimeRange &range) { + // Ignore render requests if no video is present + if (!viewer_node_ || !viewer_node_->GetVideoParams().is_valid()) { + return; + } + // Stop any current render tasks because a) they might be out of date now anyway, and b) we // want to dedicate all our rendering power to realtime feedback for the user //CancelVideoTasks(node); @@ -525,6 +391,11 @@ void PreviewAutoCacher::VideoInvalidatedFromNode(PlaybackCache *cache, const Tim void PreviewAutoCacher::AudioInvalidatedFromNode(PlaybackCache *cache, const TimeRange &range) { + // Ignore render requests if no video is present + if (!viewer_node_ || !viewer_node_->GetAudioParams().is_valid()) { + return; + } + // We don't stop rendering audio because currently there's no system of requeuing audio if it's // cancelled, so some areas may end up unrendered forever // ClearAudioQueue(); @@ -592,47 +463,11 @@ void PreviewAutoCacher::SetThumbnailsPaused(bool e) } } -void PreviewAutoCacher::NodeAdded(Node *node) -{ - graph_update_queue_.push_back({QueuedJob::kNodeAdded, node, NodeInput(), nullptr}); - UpdateGraphChangeValue(); -} - -void PreviewAutoCacher::NodeRemoved(Node *node) -{ - graph_update_queue_.push_back({QueuedJob::kNodeRemoved, node, NodeInput(), nullptr}); - UpdateGraphChangeValue(); -} - -void PreviewAutoCacher::EdgeAdded(Node *output, const NodeInput &input) -{ - graph_update_queue_.push_back({QueuedJob::kEdgeAdded, nullptr, input, output}); - UpdateGraphChangeValue(); -} - -void PreviewAutoCacher::EdgeRemoved(Node *output, const NodeInput &input) -{ - graph_update_queue_.push_back({QueuedJob::kEdgeRemoved, nullptr, input, output}); - UpdateGraphChangeValue(); -} - -void PreviewAutoCacher::ValueChanged(const NodeInput &input) -{ - graph_update_queue_.push_back({QueuedJob::kValueChanged, nullptr, input, nullptr}); - UpdateGraphChangeValue(); -} - -void PreviewAutoCacher::ValueHintChanged(const NodeInput &input) -{ - graph_update_queue_.push_back({QueuedJob::kValueHintChanged, nullptr, input, nullptr}); - UpdateGraphChangeValue(); -} - void PreviewAutoCacher::TryRender() { delayed_requeue_timer_.stop(); - if (!graph_update_queue_.empty()) { + if (copier_->HasUpdatesInQueue()) { // Check if we have jobs running in other threads that shouldn't be interrupted right now // NOTE: We don't check for downloads because, while they run in another thread, they don't // require any access to the graph and therefore don't risk race conditions. @@ -642,7 +477,7 @@ void PreviewAutoCacher::TryRender() } // No jobs are active, we can process the update queue - ProcessUpdateQueue(); + copier_->ProcessUpdateQueue(); } if (single_frame_render_) { @@ -653,7 +488,7 @@ void PreviewAutoCacher::TryRender() // Check if already caching this Node *n = Node::ValueToPtr(t->property("node")); - Node *copy = copy_map_.value(n); + Node *copy = copier_->GetCopy(n); if (copy) { RenderTicketWatcher *watcher = RenderFrame(copy, @@ -676,7 +511,7 @@ void PreviewAutoCacher::TryRender() while (!pending_video_jobs_.empty()) { VideoJob &d = pending_video_jobs_.front(); - if (Node *copy = copy_map_.value(d.node)) { + if (Node *copy = copier_->GetCopy(d.node)) { // Queue next frames rational t; while (running_video_tasks_.size() < max_tasks && d.iterator.GetNext(&t)) { @@ -707,7 +542,7 @@ void PreviewAutoCacher::TryRender() bool pop = true; // Start job - if (Node *copy = copy_map_.value(d.node)) { + if (Node *copy = copier_->GetCopy(d.node)) { TimeRange &queued_range = d.range; TimeRange use_range = queued_range; @@ -736,7 +571,7 @@ void PreviewAutoCacher::TryRender() RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& time, PlaybackCache *cache, bool dry) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); - watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + watcher->setProperty("job", QVariant::fromValue(copier_->GetLastUpdateTime())); watcher->setProperty("cache", Node::PtrToValue(cache)); watcher->setProperty("time", QVariant::fromValue(time)); connect(watcher, &RenderTicketWatcher::Finished, this, &PreviewAutoCacher::VideoRendered); @@ -768,7 +603,7 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& rvp.use_cache = true; // Multicam - rvp.multicam = static_cast(copy_map_.value(multicam_)); + rvp.multicam = copier_->GetCopy(multicam_); watcher->SetTicket(RenderManager::instance()->RenderFrame(rvp)); @@ -778,7 +613,7 @@ RenderTicketWatcher* PreviewAutoCacher::RenderFrame(Node *node, const rational& RenderTicketPtr PreviewAutoCacher::RenderAudio(Node *node, const TimeRange &r, PlaybackCache *cache) { RenderTicketWatcher* watcher = new RenderTicketWatcher(); - watcher->setProperty("job", QVariant::fromValue(last_update_time_)); + watcher->setProperty("job", QVariant::fromValue(copier_->GetLastUpdateTime())); watcher->setProperty("node", Node::PtrToValue(node)); watcher->setProperty("cache", Node::PtrToValue(cache)); watcher->setProperty("time", QVariant::fromValue(r)); @@ -861,16 +696,13 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) video_immediate_passthroughs_.clear(); // Disconnect from all node cache's - for (auto it=copy_map_.cbegin(); it!=copy_map_.cend(); it++) { + for (auto it=copier_->GetNodeMap().cbegin(); it!=copier_->GetNodeMap().cend(); it++) { DisconnectFromNodeCache(it.key()); } // Delete all of our copied nodes - qDeleteAll(created_nodes_); - created_nodes_.clear(); - copy_map_.clear(); + copier_->SetProject(nullptr); copied_viewer_node_ = nullptr; - graph_update_queue_.clear(); // Ensure all cache data is cleared video_cache_data_.clear(); @@ -878,59 +710,25 @@ void PreviewAutoCacher::SetViewerNode(ViewerOutput *viewer_node) // Clear multicam reference multicam_ = nullptr; - - // Disconnect signals for future node additions/deletions - NodeGraph* graph = viewer_node_->parent(); - - disconnect(graph, &NodeGraph::NodeAdded, this, &PreviewAutoCacher::NodeAdded); - disconnect(graph, &NodeGraph::NodeRemoved, this, &PreviewAutoCacher::NodeRemoved); - disconnect(graph, &NodeGraph::InputConnected, this, &PreviewAutoCacher::EdgeAdded); - disconnect(graph, &NodeGraph::InputDisconnected, this, &PreviewAutoCacher::EdgeRemoved); - disconnect(graph, &NodeGraph::ValueChanged, this, &PreviewAutoCacher::ValueChanged); - disconnect(graph, &NodeGraph::InputValueHintChanged, this, &PreviewAutoCacher::ValueHintChanged); } viewer_node_ = viewer_node; if (viewer_node_) { - // Copy graph - NodeGraph* graph = viewer_node_->parent(); + // Copy graph (this should always be a Project) + Project* graph = static_cast(viewer_node_->parent()); SetRendersPaused(true); - // Add all nodes - for (int i=0; inodes().at(i), copied_project_.nodes().at(i)); - } - for (int i=copied_project_.nodes().size(); inodes().size(); i++) { - AddNode(graph->nodes().at(i)); - } + copier_->SetProject(graph); + for (int i=0; inodes().size(); i++) { graph->nodes().at(i)->ConnectedToPreviewEvent(); } // Find copied viewer node - copied_viewer_node_ = static_cast(copy_map_.value(viewer_node_)); - copied_color_manager_ = static_cast(copy_map_.value(viewer_node_->project()->color_manager())); - - // Add all connections - foreach (Node* node, graph->nodes()) { - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - AddEdge(it->second, it->first); - } - } - - // Ensure graph change value is just before the sync value - UpdateGraphChangeValue(); - UpdateLastSyncedValue(); - - // Connect signals for future node additions/deletions - connect(graph, &NodeGraph::NodeAdded, this, &PreviewAutoCacher::NodeAdded, Qt::DirectConnection); - connect(graph, &NodeGraph::NodeRemoved, this, &PreviewAutoCacher::NodeRemoved, Qt::DirectConnection); - connect(graph, &NodeGraph::InputConnected, this, &PreviewAutoCacher::EdgeAdded, Qt::DirectConnection); - connect(graph, &NodeGraph::InputDisconnected, this, &PreviewAutoCacher::EdgeRemoved, Qt::DirectConnection); - connect(graph, &NodeGraph::ValueChanged, this, &PreviewAutoCacher::ValueChanged, Qt::DirectConnection); - connect(graph, &NodeGraph::InputValueHintChanged, this, &PreviewAutoCacher::ValueHintChanged, Qt::DirectConnection); + copied_viewer_node_ = copier_->GetCopy(viewer_node_); + copied_color_manager_ = copier_->GetCopy(graph->color_manager()); SetRendersPaused(false); } diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index 21efb9912..4aabca518 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -31,6 +31,7 @@ #include "node/output/viewer/viewer.h" #include "node/project/project.h" #include "render/audioparams.h" +#include "render/projectcopier.h" #include "render/renderjobtracker.h" #include "render/rendermanager.h" @@ -113,29 +114,9 @@ private: RenderTicketPtr RenderAudio(Node *node, const TimeRange &range, PlaybackCache *cache); - /** - * @brief Process all changes to internal NodeGraph copy - * - * PreviewAutoCacher staggers updates to its internal NodeGraph copy, only applying them when the - * RenderManager is not reading from it. This function is called when such an opportunity arises. - */ - void ProcessUpdateQueue(); - - void AddNode(Node* node); - void RemoveNode(Node* node); - void AddEdge(Node *output, const NodeInput& input); - void RemoveEdge(Node *output, const NodeInput& input); - void CopyValue(const NodeInput& input); - void CopyValueHint(const NodeInput& input); - - void InsertIntoCopyMap(Node* node, Node* copy); - void ConnectToNodeCache(Node *node); void DisconnectFromNodeCache(Node *node); - void UpdateGraphChangeValue(); - void UpdateLastSyncedValue(); - void CancelQueuedSingleFrameRender(); void StartCachingRange(const TimeRange &range, TimeRangeList *range_list, RenderJobTracker *tracker); @@ -145,33 +126,9 @@ private: void VideoInvalidatedFromNode(PlaybackCache *cache, const olive::TimeRange &range); void AudioInvalidatedFromNode(PlaybackCache *cache, const olive::TimeRange &range); - class QueuedJob { - public: - enum Type { - kNodeAdded, - kNodeRemoved, - kEdgeAdded, - kEdgeRemoved, - kValueChanged, - kValueHintChanged - }; - - Type type; - Node* node; - NodeInput input; - Node *output; - }; - ViewerOutput* viewer_node_; - Project copied_project_; - - std::list graph_update_queue_; - QHash copy_map_; - QHash graph_map_; - ViewerOutput* copied_viewer_node_; - ColorManager* copied_color_manager_; - QVector created_nodes_; + ProjectCopier *copier_; TimeRange cache_range_; @@ -184,9 +141,6 @@ private: RenderTicketPtr single_frame_render_; QMap > video_immediate_passthroughs_; - JobTime graph_changed_time_; - JobTime last_update_time_; - QTimer delayed_requeue_timer_; JobTime last_conform_task_; @@ -194,6 +148,9 @@ private: QVector running_video_tasks_; QVector running_audio_tasks_; + ViewerOutput* copied_viewer_node_; + ColorManager* copied_color_manager_; + struct VideoJob { Node *node; PlaybackCache *cache; @@ -251,18 +208,6 @@ private slots: */ void VideoRendered(); - void NodeAdded(Node* node); - - void NodeRemoved(Node* node); - - void EdgeAdded(Node *output, const NodeInput& input); - - void EdgeRemoved(Node *output, const NodeInput& input); - - void ValueChanged(const NodeInput& input); - - void ValueHintChanged(const NodeInput &input); - /** * @brief Generic function called whenever the frames to render need to be (re)queued */ diff --git a/app/render/projectcopier.cpp b/app/render/projectcopier.cpp new file mode 100644 index 000000000..89ff9f1e4 --- /dev/null +++ b/app/render/projectcopier.cpp @@ -0,0 +1,259 @@ +/*** + + 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 "projectcopier.h" + +namespace olive { + +ProjectCopier::ProjectCopier(QObject *parent) : + QObject(parent) +{ + original_ = nullptr; + copy_ = new Project(); + copy_->setParent(this); +} + +void ProjectCopier::SetProject(Project *project) +{ + if (original_) { + // Clear current project + qDeleteAll(created_nodes_); + created_nodes_.clear(); + copy_map_.clear(); + graph_update_queue_.clear(); + + disconnect(original_, &NodeGraph::NodeAdded, this, &ProjectCopier::QueueNodeAdd); + disconnect(original_, &NodeGraph::NodeRemoved, this, &ProjectCopier::QueueNodeRemove); + disconnect(original_, &NodeGraph::InputConnected, this, &ProjectCopier::QueueEdgeAdd); + disconnect(original_, &NodeGraph::InputDisconnected, this, &ProjectCopier::QueueEdgeRemove); + disconnect(original_, &NodeGraph::ValueChanged, this, &ProjectCopier::QueueValueChange); + disconnect(original_, &NodeGraph::InputValueHintChanged, this, &ProjectCopier::QueueValueHintChange); + } + + original_ = project; + + if (original_) { + // Add all nodes + for (int i=0; inodes().size(); i++) { + InsertIntoCopyMap(original_->nodes().at(i), copy_->nodes().at(i)); + } + + for (int i=copy_->nodes().size(); inodes().size(); i++) { + DoNodeAdd(original_->nodes().at(i)); + } + + // Add all connections + foreach (Node* node, original_->nodes()) { + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + DoEdgeAdd(it->second, it->first); + } + } + + // Ensure graph change value is just before the sync value + UpdateGraphChangeValue(); + UpdateLastSyncedValue(); + + // Connect signals for future node additions/deletions + connect(original_, &NodeGraph::NodeAdded, this, &ProjectCopier::QueueNodeAdd, Qt::DirectConnection); + connect(original_, &NodeGraph::NodeRemoved, this, &ProjectCopier::QueueNodeRemove, Qt::DirectConnection); + connect(original_, &NodeGraph::InputConnected, this, &ProjectCopier::QueueEdgeAdd, Qt::DirectConnection); + connect(original_, &NodeGraph::InputDisconnected, this, &ProjectCopier::QueueEdgeRemove, Qt::DirectConnection); + connect(original_, &NodeGraph::ValueChanged, this, &ProjectCopier::QueueValueChange, Qt::DirectConnection); + connect(original_, &NodeGraph::InputValueHintChanged, this, &ProjectCopier::QueueValueHintChange, Qt::DirectConnection); + } +} + +void ProjectCopier::ProcessUpdateQueue() +{ + // Iterate everything that happened to the graph and do the same thing on our end + while (!graph_update_queue_.empty()) { + QueuedJob job = graph_update_queue_.front(); + graph_update_queue_.pop_front(); + + switch (job.type) { + case QueuedJob::kNodeAdded: + DoNodeAdd(job.node); + break; + case QueuedJob::kNodeRemoved: + DoNodeRemove(job.node); + break; + case QueuedJob::kEdgeAdded: + DoEdgeAdd(job.output, job.input); + break; + case QueuedJob::kEdgeRemoved: + DoEdgeRemove(job.output, job.input); + break; + case QueuedJob::kValueChanged: + DoValueChange(job.input); + break; + case QueuedJob::kValueHintChanged: + DoValueHintChange(job.input); + break; + } + } + + // Indicate that we have synchronized to this point, which is compared with the graph change + // time to see if our copied graph is up to date + UpdateLastSyncedValue(); +} + +void ProjectCopier::DoNodeAdd(Node *node) +{ + if (dynamic_cast(node)) { + // Group nodes are just dummy nodes, no need to copy them + return; + } + + // Copy node + Node* copy = node->copy(); + + // Add to project + copy->setParent(copy_); + + // Disable caches for copy + copy->SetCachesEnabled(false); + + // Copy cache UUIDs + copy->CopyCacheUuidsFrom(node); + + // Insert into map + InsertIntoCopyMap(node, copy); + + // Keep track of our nodes + created_nodes_.append(copy); +} + +void ProjectCopier::DoNodeRemove(Node *node) +{ + // Find our copy and remove it + Node* copy = copy_map_.take(node); + + // Disconnect from node's caches + emit RemovedNode(node); + + // Remove from created list + created_nodes_.removeOne(copy); + + // Delete it + delete copy; +} + +void ProjectCopier::DoEdgeAdd(Node *output, const NodeInput &input) +{ + // Create same connection with our copied graph + Node* our_output = copy_map_.value(output); + Node* our_input = copy_map_.value(input.node()); + + Node::ConnectEdge(our_output, NodeInput(our_input, input.input(), input.element())); +} + +void ProjectCopier::DoEdgeRemove(Node *output, const NodeInput &input) +{ + // Remove same connection with our copied graph + Node* our_output = copy_map_.value(output); + Node* our_input = copy_map_.value(input.node()); + + Node::DisconnectEdge(our_output, NodeInput(our_input, input.input(), input.element())); +} + +void ProjectCopier::DoValueChange(const NodeInput &input) +{ + if (dynamic_cast(input.node())) { + // Group nodes are just dummy nodes, no need to copy them + return; + } + + // Copy all values to our graph + Node* our_input = copy_map_.value(input.node()); + Node::CopyValuesOfElement(input.node(), our_input, input.input(), input.element()); +} + +void ProjectCopier::DoValueHintChange(const NodeInput &input) +{ + if (dynamic_cast(input.node())) { + // Group nodes are just dummy nodes, no need to copy them + return; + } + + // Copy value hint to our graph + Node* our_input = copy_map_.value(input.node()); + Node::ValueHint hint = input.node()->GetValueHintForInput(input.input(), input.element()); + our_input->SetValueHintForInput(input.input(), hint, input.element()); +} + +void ProjectCopier::InsertIntoCopyMap(Node *node, Node *copy) +{ + // Insert into map + copy_map_.insert(node, copy); + + // Copy parameters + Node::CopyInputs(node, copy, false); + + // Connect to node's cache + emit AddedNode(node); +} + +void ProjectCopier::QueueNodeAdd(Node *node) +{ + graph_update_queue_.push_back({QueuedJob::kNodeAdded, node, NodeInput(), nullptr}); + UpdateGraphChangeValue(); +} + +void ProjectCopier::QueueNodeRemove(Node *node) +{ + graph_update_queue_.push_back({QueuedJob::kNodeRemoved, node, NodeInput(), nullptr}); + UpdateGraphChangeValue(); +} + +void ProjectCopier::QueueEdgeAdd(Node *output, const NodeInput &input) +{ + graph_update_queue_.push_back({QueuedJob::kEdgeAdded, nullptr, input, output}); + UpdateGraphChangeValue(); +} + +void ProjectCopier::QueueEdgeRemove(Node *output, const NodeInput &input) +{ + graph_update_queue_.push_back({QueuedJob::kEdgeRemoved, nullptr, input, output}); + UpdateGraphChangeValue(); +} + +void ProjectCopier::QueueValueChange(const NodeInput &input) +{ + graph_update_queue_.push_back({QueuedJob::kValueChanged, nullptr, input, nullptr}); + UpdateGraphChangeValue(); +} + +void ProjectCopier::QueueValueHintChange(const NodeInput &input) +{ + graph_update_queue_.push_back({QueuedJob::kValueHintChanged, nullptr, input, nullptr}); + UpdateGraphChangeValue(); +} + +void ProjectCopier::UpdateGraphChangeValue() +{ + graph_changed_time_.Acquire(); +} + +void ProjectCopier::UpdateLastSyncedValue() +{ + last_update_time_.Acquire(); +} + +} diff --git a/app/render/projectcopier.h b/app/render/projectcopier.h new file mode 100644 index 000000000..42fd14daa --- /dev/null +++ b/app/render/projectcopier.h @@ -0,0 +1,125 @@ +/*** + + 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 PROJECTCOPIER_H +#define PROJECTCOPIER_H + +#include "node/project/project.h" + +namespace olive { + +class ProjectCopier : public QObject +{ + Q_OBJECT +public: + ProjectCopier(QObject *parent = nullptr); + + void SetProject(Project *project); + + template + T *GetCopy(T *original) + { + return static_cast(copy_map_.value(original)); + } + + template + T *GetOriginal(T *copy) + { + return static_cast(copy_map_.key(copy)); + } + + const QHash &GetNodeMap() const { return copy_map_; } + + const JobTime &GetGraphChangeTime() const { return graph_changed_time_; } + const JobTime &GetLastUpdateTime() const { return last_update_time_; } + + bool HasUpdatesInQueue() const { return !graph_update_queue_.empty(); } + + /** + * @brief Process all changes to internal NodeGraph copy + * + * PreviewAutoCacher staggers updates to its internal NodeGraph copy, only applying them when the + * RenderManager is not reading from it. This function is called when such an opportunity arises. + */ + void ProcessUpdateQueue(); + +signals: + void AddedNode(Node *n); + void RemovedNode(Node *n); + +private: + void DoNodeAdd(Node* node); + void DoNodeRemove(Node* node); + void DoEdgeAdd(Node *output, const NodeInput& input); + void DoEdgeRemove(Node *output, const NodeInput& input); + void DoValueChange(const NodeInput& input); + void DoValueHintChange(const NodeInput& input); + + void InsertIntoCopyMap(Node* node, Node* copy); + + void UpdateGraphChangeValue(); + void UpdateLastSyncedValue(); + + Project *original_; + Project *copy_; + + class QueuedJob { + public: + enum Type { + kNodeAdded, + kNodeRemoved, + kEdgeAdded, + kEdgeRemoved, + kValueChanged, + kValueHintChanged + }; + + Type type; + Node* node; + NodeInput input; + Node *output; + }; + + std::list graph_update_queue_; + QHash copy_map_; + QHash graph_map_; + QVector created_nodes_; + + JobTime graph_changed_time_; + JobTime last_update_time_; + +private slots: + void QueueNodeAdd(Node* node); + + void QueueNodeRemove(Node* node); + + void QueueEdgeAdd(Node *output, const NodeInput& input); + + void QueueEdgeRemove(Node *output, const NodeInput& input); + + void QueueValueChange(const NodeInput& input); + + void QueueValueHintChange(const NodeInput &input); + +}; + +} + +#endif // PROJECTCOPIER_H diff --git a/app/render/texture.h b/app/render/texture.h index 942c0c4f6..a184edb15 100644 --- a/app/render/texture.h +++ b/app/render/texture.h @@ -22,6 +22,7 @@ #define RENDERTEXTURE_H #include +#include #include "render/videoparams.h" diff --git a/app/shaders/deinterlace2.frag b/app/shaders/deinterlace2.frag new file mode 100644 index 000000000..37b6002ce --- /dev/null +++ b/app/shaders/deinterlace2.frag @@ -0,0 +1,21 @@ +uniform sampler2D ove_maintex; + +uniform int interlacing; +uniform int pixel_height; + +in vec2 ove_texcoord; +out vec4 frag_color; + +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; + } + + frag_color = texture(ove_maintex, real_coord); +} diff --git a/app/shaders/diptoblack.frag b/app/shaders/diptoblack.frag index 30a9c918d..a5c00a16f 100644 --- a/app/shaders/diptoblack.frag +++ b/app/shaders/diptoblack.frag @@ -1,8 +1,13 @@ +#define LINEAR_CURVE 0 +#define EXPONENTIAL_CURVE 1 +#define LOGARITHMIC_CURVE 2 + uniform sampler2D out_block_in; uniform sampler2D in_block_in; uniform bool out_block_in_enabled; uniform bool in_block_in_enabled; uniform vec4 color_in; +uniform int curve_in; uniform float ove_tprog_all; uniform float ove_tprog_out; @@ -11,16 +16,27 @@ uniform float ove_tprog_in; in vec2 ove_texcoord; out vec4 frag_color; +float TransformCurve(float linear) { + if (curve_in == EXPONENTIAL_CURVE) { + return linear * linear; + } else if (curve_in == LOGARITHMIC_CURVE) { + return sqrt(linear); + } else { + return linear; + } +} + void main(void) { if (out_block_in_enabled && in_block_in_enabled) { - vec4 out_block_col = mix(texture(out_block_in, ove_texcoord), color_in, ove_tprog_out); - vec4 in_block_col = mix(texture(in_block_in, ove_texcoord), color_in, 1.0 - ove_tprog_in); + // mix(x, y , a): a(1-x) + b(x) + vec4 out_block_col = ove_tprog_out == 0.0 ? vec4(0.0) : mix(color_in, texture(out_block_in, ove_texcoord),TransformCurve(ove_tprog_out)); + vec4 in_block_col = ove_tprog_out != 0.0 ? vec4(0.0) : mix(color_in, texture(in_block_in, ove_texcoord), TransformCurve(ove_tprog_in)); frag_color = out_block_col + in_block_col; } else if (out_block_in_enabled) { - frag_color = mix(texture(out_block_in, ove_texcoord), color_in, ove_tprog_all); + frag_color = mix(color_in, texture(out_block_in, ove_texcoord), TransformCurve(ove_tprog_out)); } else if (in_block_in_enabled) { - frag_color = mix(texture(in_block_in, ove_texcoord), color_in, 1.0 - ove_tprog_all); + frag_color = mix(texture(in_block_in, ove_texcoord), color_in, TransformCurve(1.0 - ove_tprog_in)); } else { frag_color = vec4(0.0); } diff --git a/app/shaders/opacity_rgb.frag b/app/shaders/opacity_rgb.frag new file mode 100644 index 000000000..6d529023e --- /dev/null +++ b/app/shaders/opacity_rgb.frag @@ -0,0 +1,27 @@ +// Inputs +uniform sampler2D tex_in; +uniform sampler2D opacity_in; + +// Input texture coordinate +in vec2 ove_texcoord; +out vec4 frag_color; + +vec3 rgb2hsv(vec3 c) +{ + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +void main() { + vec4 value = texture(opacity_in, ove_texcoord); + float v = rgb2hsv(value.rgb).b; + + vec4 c = texture(tex_in, ove_texcoord); + c *= v; + frag_color = c; +} diff --git a/app/shaders/yuv2rgb.frag b/app/shaders/yuv2rgb.frag index 8c40b5918..7f0d87a16 100644 --- a/app/shaders/yuv2rgb.frag +++ b/app/shaders/yuv2rgb.frag @@ -5,40 +5,36 @@ 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; +uniform float yuv_crv; +uniform float yuv_cgu; +uniform float yuv_cgv; +uniform float yuv_cbu; in vec2 ove_texcoord; out vec4 frag_color; 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) { + if (bits_per_pixel == 8) { + // Convert 0.0-1.0 to -0.5-0.5 + yuv.gb -= (128.0/255.0); + } else if (bits_per_pixel == 10) { + // Convert 0.0-1.0 to -0.5-0.5 + yuv.gb -= (512.0/1023.0); + yuv *= 64.0; } else if (bits_per_pixel == 12) { + // Convert 0.0-1.0 to -0.5-0.5 + yuv.gb -= (2048.0/4095.0); + yuv *= 16.0; } @@ -46,20 +42,11 @@ void main() 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; - // 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; + rgba.r = yuv.r + yuv_crv * yuv.b; + rgba.g = yuv.r - yuv_cgu * yuv.g - yuv_cgv * yuv.b; + rgba.b = yuv.r + yuv_cbu * yuv.g; // If the expected value is full range, transform to full range here if (full_range) { diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 017fc59fb..397a0eda8 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -28,10 +28,14 @@ namespace olive { ExportTask::ExportTask(ViewerOutput *viewer_node, ColorManager* color_manager, const EncodingParams& params) : - color_manager_(color_manager), params_(params) { - set_viewer(viewer_node); + // Create a copy of the project + copier_ = new ProjectCopier(this); + copier_->SetProject(viewer_node->project()); + + set_viewer(copier_->GetCopy(viewer_node)); + color_manager_ = copier_->GetCopy(color_manager); // Adjust video params to have no divider VideoParams vp = viewer_node->GetVideoParams(); diff --git a/app/task/export/export.h b/app/task/export/export.h index 7dcd8cf99..7a367c6b2 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -24,6 +24,7 @@ #include "codec/encoder.h" #include "node/output/viewer/viewer.h" #include "render/colorprocessor.h" +#include "render/projectcopier.h" #include "task/render/render.h" #include "task/task.h" @@ -52,6 +53,8 @@ protected: private: bool WriteAudioLoop(const TimeRange &time, const SampleBuffer &samples); + ProjectCopier *copier_; + QHash time_map_; QHash audio_map_; diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index db2fd434f..91572757b 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -35,7 +35,7 @@ ProjectImportTask::ProjectImportTask(Folder *folder, const QStringList &filename folder_(folder) { foreach (const QString& f, filenames) { - filenames_.append(f); + filenames_.append(QFileInfo(f)); } file_count_ = Core::CountFilesInFileList(filenames_); @@ -106,10 +106,15 @@ void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counte } else { - Footage* footage = new Footage(file_info.absoluteFilePath()); + Footage* footage = new Footage(); + footage->SetCancelPointer(this->GetCancelAtom()); + + footage->set_filename(file_info.absoluteFilePath()); footage->SetLabel(file_info.fileName()); + footage->SetCancelPointer(nullptr); + if (footage->IsValid()) { // See if this footage is an image sequence ValidateImageSequence(footage, import, i); diff --git a/app/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 202b71a5e..d6c2802fb 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -61,7 +61,8 @@ bool LoadOTIOTask::Run() auto root = OTIO::SerializableObjectWithMetadata::from_json_file(GetFilename().toStdString(), &es); if (es.outcome != OTIO::ErrorStatus::Outcome::OK) { - SetError(tr("Failed to load OpenTimelineIO from file \"%1\"").arg(GetFilename())); + SetError(tr("Failed to load OpenTimelineIO from file \"%1\" \n\nOpenTimelineIO Error:\n\n%2") + .arg(GetFilename(), QString::fromStdString(es.full_description))); return false; } diff --git a/app/task/task.h b/app/task/task.h index 835c009fd..a2b516c1f 100644 --- a/app/task/task.h +++ b/app/task/task.h @@ -93,6 +93,7 @@ public slots: bool Start() { start_time_ = QDateTime::currentMSecsSinceEpoch(); + emit Started(start_time_); bool ret = Run(); @@ -150,6 +151,8 @@ protected: } signals: + void Started(qint64 start_time); + /** * @brief Signal emitted whenever progress is made * diff --git a/app/task/taskmanager.cpp b/app/task/taskmanager.cpp index 26730a0f8..9165bb135 100644 --- a/app/task/taskmanager.cpp +++ b/app/task/taskmanager.cpp @@ -29,6 +29,7 @@ TaskManager* TaskManager::instance_ = nullptr; TaskManager::TaskManager() { + thread_pool_.setMaxThreadCount(1); } TaskManager::~TaskManager() @@ -93,7 +94,13 @@ void TaskManager::AddTask(Task* t) tasks_.insert(watcher, t); // Run task concurrently - watcher->setFuture(QtConcurrent::run(t, &Task::Start)); + watcher->setFuture( +#if QT_VERSION_MAJOR >= 6 + QtConcurrent::run(&thread_pool_, &Task::Start, t) +#else + QtConcurrent::run(&thread_pool_, t, &Task::Start) +#endif + ); // Emit signal that a Task was added emit TaskAdded(t); diff --git a/app/ui/style/style.cpp b/app/ui/style/style.cpp index a8038c4c3..3cf32e4bf 100644 --- a/app/ui/style/style.cpp +++ b/app/ui/style/style.cpp @@ -36,7 +36,6 @@ namespace olive { const char* StyleManager::kDefaultStyle = "olive-dark"; QString StyleManager::current_style_; QMap StyleManager::available_themes_; -QPalette StyleManager::platform_palette_; QPalette StyleManager::ParsePalette(const QString& ini_path) { @@ -127,10 +126,6 @@ void StyleManager::ParsePaletteColor(QSettings *ini, QPalette *palette, QPalette void StyleManager::Init() { - // Store standard palette before replacing it with our own - platform_palette_ = qApp->palette(); - platform_palette_.resolve(-1); - qApp->setStyle(QStyleFactory::create("Fusion")); available_themes_.insert(QStringLiteral("olive-dark"), QStringLiteral("Olive Dark")); diff --git a/app/ui/style/style.h b/app/ui/style/style.h index 0ae18332d..860693914 100644 --- a/app/ui/style/style.h +++ b/app/ui/style/style.h @@ -54,8 +54,6 @@ private: static QMap available_themes_; - static QPalette platform_palette_; - }; } diff --git a/app/widget/colorwheel/colorvalueswidget.cpp b/app/widget/colorwheel/colorvalueswidget.cpp index 4ba54df49..bfdaf7d1e 100644 --- a/app/widget/colorwheel/colorvalueswidget.cpp +++ b/app/widget/colorwheel/colorvalueswidget.cpp @@ -44,7 +44,7 @@ ColorValuesWidget::ColorValuesWidget(ColorManager *manager, QWidget *parent) : { QHBoxLayout* preview_layout = new QHBoxLayout(); - preview_layout->setMargin(0); + preview_layout->setContentsMargins(0, 0, 0, 0); preview_layout->addWidget(new QLabel(tr("Preview"))); diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index 10300b7ff..6f105a22d 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -519,7 +519,7 @@ void CurveView::ZoomToFitInternal(bool selected_only) rational transformed_time = GetAdjustedTime(key->parent(), GetTimeTarget(), key->time(), - false); + Node::kTransformTowardsOutput); qreal key_y = GetUnscaledItemYFromKeyframeValue(key); diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index d85e6d168..ed5a1b79c 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -53,13 +53,12 @@ CurveWidget::CurveWidget(QWidget *parent) : QWidget* workarea = new QWidget(); QVBoxLayout* layout = new QVBoxLayout(workarea); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); splitter->addWidget(workarea); QHBoxLayout* top_controls = new QHBoxLayout(); key_control_ = new NodeParamViewKeyframeControl(false); - connect(key_control_, &NodeParamViewKeyframeControl::RequestSetTime, this, &CurveWidget::SetTimeAndSignal); top_controls->addWidget(key_control_); top_controls->addStretch(); @@ -86,7 +85,7 @@ CurveWidget::CurveWidget(QWidget *parent) : // We use a separate layout for the ruler+view combination so that there's no spacing between them QVBoxLayout* ruler_view_layout = new QVBoxLayout(); - ruler_view_layout->setMargin(0); + ruler_view_layout->setContentsMargins(0, 0, 0, 0); ruler_view_layout->setSpacing(0); ruler_view_layout->addWidget(ruler()); @@ -99,15 +98,12 @@ CurveWidget::CurveWidget(QWidget *parent) : layout->addLayout(ruler_view_layout); // Connect ruler and view together - connect(view_, &CurveView::TimeChanged, this, &CurveWidget::SetTimeAndSignal); 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()); - connect(view_->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); // Disable collapsing the main curve view (but allow collapsing the tree) splitter->setCollapsible(1, false); @@ -193,14 +189,6 @@ void CurveWidget::SetNodes(const QVector &nodes) } } -void CurveWidget::TimeChangedEvent(const rational &time) -{ - super::TimeChangedEvent(time); - - view_->SetTime(time); - UpdateBridgeTime(time); -} - void CurveWidget::TimebaseChangedEvent(const rational &timebase) { super::TimebaseChangedEvent(timebase); @@ -215,7 +203,7 @@ void CurveWidget::ScaleChangedEvent(const double &scale) view_->SetScale(scale); } -void CurveWidget::TimeTargetChangedEvent(Node *target) +void CurveWidget::TimeTargetChangedEvent(ViewerOutput *target) { TimeTargetObject::TimeTargetChangedEvent(target); @@ -228,6 +216,8 @@ void CurveWidget::ConnectedNodeChangeEvent(ViewerOutput *n) { super::ConnectedNodeChangeEvent(n); + key_control_->SetTimeTarget(n); + SetTimeTarget(n); } @@ -252,11 +242,6 @@ void CurveWidget::SetKeyframeButtonCheckedFromType(NodeKeyframe::Type type) hold_button_->setChecked(type == NodeKeyframe::kHold); } -void CurveWidget::UpdateBridgeTime(const rational &time) -{ - key_control_->SetTime(time); -} - void CurveWidget::ConnectInput(Node *node, const QString &input, int element) { if (element == -1 && node->InputIsArray(input)) { diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index eef0ab62f..5ade8eb31 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -65,11 +65,10 @@ public slots: void SetNodes(const QVector &nodes); protected: - virtual void TimeChangedEvent(const rational &) override; virtual void TimebaseChangedEvent(const rational &) override; virtual void ScaleChangedEvent(const double &) override; - virtual void TimeTargetChangedEvent(Node* target) override; + virtual void TimeTargetChangedEvent(ViewerOutput *target) override; virtual void ConnectedNodeChangeEvent(ViewerOutput* n) override; @@ -95,8 +94,6 @@ private: void SetKeyframeButtonCheckedFromType(NodeKeyframe::Type type); - void UpdateBridgeTime(const rational &time); - void ConnectInput(Node *node, const QString &input, int element); void ConnectInputInternal(Node *node, const QString &input, int element); diff --git a/app/widget/filefield/filefield.cpp b/app/widget/filefield/filefield.cpp index dde16dd36..4d40eae7c 100644 --- a/app/widget/filefield/filefield.cpp +++ b/app/widget/filefield/filefield.cpp @@ -34,7 +34,7 @@ FileField::FileField(QWidget* parent) : { QHBoxLayout* layout = new QHBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); line_edit_ = new QLineEdit(); connect(line_edit_, &QLineEdit::textChanged, this, &FileField::LineEditChanged); diff --git a/app/widget/flowlayout/flowlayout.cpp b/app/widget/flowlayout/flowlayout.cpp index 2c9aed8ac..c226a2f2f 100644 --- a/app/widget/flowlayout/flowlayout.cpp +++ b/app/widget/flowlayout/flowlayout.cpp @@ -146,7 +146,7 @@ QSize FlowLayout::minimumSize() const foreach (item, itemList) size = size.expandedTo(item->minimumSize()); - size += QSize(2*margin(), 2*margin()); + size += QSize(2*contentsMargins().left(), 2*contentsMargins().top()); return size; } diff --git a/app/widget/handmovableview/handmovableview.cpp b/app/widget/handmovableview/handmovableview.cpp index a44858ad8..83052dea3 100644 --- a/app/widget/handmovableview/handmovableview.cpp +++ b/app/widget/handmovableview/handmovableview.cpp @@ -31,7 +31,8 @@ namespace olive { HandMovableView::HandMovableView(QWidget* parent) : super(parent), - dragging_hand_(false) + dragging_hand_(false), + is_timeline_axes_(false) { connect(Core::instance(), &Core::ToolChanged, this, &HandMovableView::ApplicationToolChanged); } @@ -60,7 +61,7 @@ bool HandMovableView::HandPress(QMouseEvent *event) // Transform mouse event to act like the left button is pressed QMouseEvent transformed(event->type(), - event->localPos(), + event->pos(), Qt::LeftButton, Qt::LeftButton, event->modifiers()); @@ -82,15 +83,15 @@ bool HandMovableView::HandMove(QMouseEvent *event) QPoint adjustment(0, 0); QMouseEvent transformed(event->type(), - event->localPos() - transformed_pos_, + event->pos() - transformed_pos_, Qt::LeftButton, Qt::LeftButton, event->modifiers()); - if (event->localPos().x() < 0) { + if (event->pos().x() < 0) { transformed_pos_.setX(transformed_pos_.x() + width()); adjustment.setX(width()); - } else if (event->localPos().x() >= width()) { + } else if (event->pos().x() >= width()) { transformed_pos_.setX(transformed_pos_.x() - width()); adjustment.setX(-width()); } @@ -118,9 +119,12 @@ bool HandMovableView::HandRelease(QMouseEvent *event) // Transform mouse event to act like the left button is pressed QMouseEvent transformed(event->type(), event->localPos(), + event->windowPos(), + event->screenPos(), Qt::LeftButton, Qt::LeftButton, - event->modifiers()); + event->modifiers(), + event->source()); super::mouseReleaseEvent(&transformed); @@ -146,16 +150,21 @@ const HandMovableView::DragMode &HandMovableView::GetDefaultDragMode() const return default_drag_mode_; } -bool HandMovableView::WheelEventIsAZoomEvent(QWheelEvent *event) const +bool HandMovableView::WheelEventIsAZoomEvent(QWheelEvent *event) { return (static_cast(event->modifiers() & Qt::ControlModifier) == !OLIVE_CONFIG("ScrollZooms").toBool()); } +qreal HandMovableView::GetScrollZoomMultiplier(QWheelEvent *event) +{ + return 1.0 + (static_cast(event->angleDelta().x() + event->angleDelta().y()) * 0.001); +} + void HandMovableView::wheelEvent(QWheelEvent *event) { if (WheelEventIsAZoomEvent(event)) { if (!event->angleDelta().isNull()) { - qreal multiplier = 1.0 + (static_cast(event->angleDelta().x() + event->angleDelta().y()) * 0.001); + qreal multiplier = GetScrollZoomMultiplier(event); QPointF cursor_pos; #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) @@ -166,6 +175,54 @@ void HandMovableView::wheelEvent(QWheelEvent *event) ZoomIntoCursorPosition(event, multiplier, cursor_pos); } + } else if (is_timeline_axes_) { +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + + QPoint angle_delta = event->angleDelta(); + + if (OLIVE_CONFIG("InvertTimelineScrollAxes").toBool() // Check if config is set to invert timeline axes + && event->source() != Qt::MouseEventSynthesizedBySystem) { // Never flip axes on Apple trackpads though + angle_delta = QPoint(angle_delta.y(), angle_delta.x()); + } + + QWheelEvent e( + #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + event->position(), + event->globalPosition(), + #else + event->pos(), + event->globalPos(), + #endif + event->pixelDelta(), + angle_delta, + event->buttons(), + event->modifiers(), + event->phase(), + event->inverted(), + event->source() + ); + +#else + + Qt::Orientation orientation = event->orientation(); + + if (OLIVE_CONFIG("InvertTimelineScrollAxes").toBool()) { + orientation = (orientation == Qt::Horizontal) ? Qt::Vertical : Qt::Horizontal; + } + + QWheelEvent e( + event->pos(), + event->globalPos(), + event->pixelDelta(), + event->angleDelta(), + event->delta(), + orientation, + event->buttons(), + event->modifiers() + ); +#endif + + super::wheelEvent(&e); } else { super::wheelEvent(event); } diff --git a/app/widget/handmovableview/handmovableview.h b/app/widget/handmovableview/handmovableview.h index e8c01599b..15fd8fc38 100644 --- a/app/widget/handmovableview/handmovableview.h +++ b/app/widget/handmovableview/handmovableview.h @@ -34,6 +34,10 @@ class HandMovableView : public QGraphicsView public: HandMovableView(QWidget* parent = nullptr); + static bool WheelEventIsAZoomEvent(QWheelEvent* event); + + static qreal GetScrollZoomMultiplier(QWheelEvent* event); + protected: virtual void ToolChangedEvent(Tool::Item tool){Q_UNUSED(tool)} @@ -44,12 +48,12 @@ protected: void SetDefaultDragMode(DragMode mode); const DragMode& GetDefaultDragMode() const; - bool WheelEventIsAZoomEvent(QWheelEvent* event) const; - virtual void wheelEvent(QWheelEvent* event) override; virtual void ZoomIntoCursorPosition(QWheelEvent* event, double multiplier, const QPointF &cursor_pos); + void SetIsTimelineAxes(bool e) { is_timeline_axes_ = e; } + private: bool dragging_hand_; DragMode pre_hand_drag_mode_; @@ -58,6 +62,8 @@ private: QPointF transformed_pos_; + bool is_timeline_axes_; + private slots: void ApplicationToolChanged(Tool::Item tool); diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index 9fead0fe1..0811f416f 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -204,6 +204,10 @@ bool KeyframeView::CopySelected(bool cut) bool KeyframeView::Paste(std::function find_node_function) { + if (!GetViewerNode()) { + return false; + } + ProjectSerializer::Result res = ProjectSerializer::Paste(QStringLiteral("keyframes")); if (res == ProjectSerializer::kSuccess) { const ProjectSerializer::SerializedKeyframes &keys = res.GetLoadData().keyframes; @@ -216,7 +220,7 @@ bool KeyframeView::Paste(std::function find_node_functi min = std::min(min, key->time()); } } - min -= GetTime(); + min -= GetViewerNode()->GetPlayhead(); for (auto it=keys.cbegin(); it!=keys.cend(); it++) { const QString &paste_id = it.key(); @@ -228,7 +232,7 @@ bool KeyframeView::Paste(std::function find_node_functi for (NodeKeyframe *key : it.value()) { // Adjust sequence time to node's time rational t = key->time() - min; - t = GetAdjustedTime(GetTimeTarget(), node_with_id, t, true); + t = GetAdjustedTime(GetTimeTarget(), node_with_id, t, Node::kTransformTowardsInput); key->set_time(t); if (NodeKeyframe *existing = node_with_id->GetKeyframeAtTimeOnTrack(key->input(), key->time(), key->track(), key->element())) { @@ -454,7 +458,7 @@ void KeyframeView::ScaleChangedEvent(const double &scale) Redraw(); } -void KeyframeView::TimeTargetChangedEvent(Node *target) +void KeyframeView::TimeTargetChangedEvent(ViewerOutput *v) { Redraw(); } @@ -491,12 +495,12 @@ void KeyframeView::DeselectKeyframe(NodeKeyframe *key) rational KeyframeView::GetUnadjustedKeyframeTime(NodeKeyframe *key, const rational &time) { - return GetAdjustedTime(GetTimeTarget(), key->parent(), time, true); + return GetAdjustedTime(GetTimeTarget(), key->parent(), time, Node::kTransformTowardsInput); } rational KeyframeView::GetAdjustedKeyframeTime(NodeKeyframe *key) { - return GetAdjustedTime(key->parent(), GetTimeTarget(), key->time(), false); + return GetAdjustedTime(key->parent(), GetTimeTarget(), key->time(), Node::kTransformTowardsOutput); } double KeyframeView::GetKeyframeSceneX(NodeKeyframe *key) diff --git a/app/widget/keyframeview/keyframeview.h b/app/widget/keyframeview/keyframeview.h index 0ab17f254..541b81583 100644 --- a/app/widget/keyframeview/keyframeview.h +++ b/app/widget/keyframeview/keyframeview.h @@ -101,7 +101,7 @@ protected: virtual void ScaleChangedEvent(const double& scale) override; - virtual void TimeTargetChangedEvent(Node*) override; + virtual void TimeTargetChangedEvent(ViewerOutput *v) override; virtual void TimebaseChangedEvent(const rational &timebase) override; diff --git a/app/widget/manageddisplay/manageddisplay.cpp b/app/widget/manageddisplay/manageddisplay.cpp index a5446162a..2e26be7c8 100644 --- a/app/widget/manageddisplay/manageddisplay.cpp +++ b/app/widget/manageddisplay/manageddisplay.cpp @@ -38,7 +38,7 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent) : { QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { // Create OpenGL widget @@ -312,11 +312,11 @@ bool ManagedDisplayWidget::eventFilter(QObject *o, QEvent *e) { // 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); + /*QMouseEvent *ev = static_cast(e); if (ev->button() == Qt::RightButton) { emit customContextMenuRequested(ev->pos()); return true; - } + }*/ break; } default: diff --git a/app/widget/manageddisplay/manageddisplay.h b/app/widget/manageddisplay/manageddisplay.h index c26008e77..5dceb3fc1 100644 --- a/app/widget/manageddisplay/manageddisplay.h +++ b/app/widget/manageddisplay/manageddisplay.h @@ -48,6 +48,15 @@ class ManagedDisplayWidgetOpenGL public: ManagedDisplayWidgetOpenGL() = default; + virtual ~ManagedDisplayWidgetOpenGL() override + { + if (context()) { + DestroyListener(); + disconnect(context(), &QOpenGLContext::aboutToBeDestroyed, + this, &ManagedDisplayWidgetOpenGL::DestroyListener); + } + } + signals: // Render signals void OnInit(); diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 1ff70f954..7e6ee594f 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -20,6 +20,8 @@ #include "menushared.h" +#include + #include "core.h" #include "common/timecodefunctions.h" #include "panel/panelmanager.h" diff --git a/app/widget/multicam/multicamwidget.cpp b/app/widget/multicam/multicamwidget.cpp index 5156fd4ed..a11495339 100644 --- a/app/widget/multicam/multicamwidget.cpp +++ b/app/widget/multicam/multicamwidget.cpp @@ -74,7 +74,7 @@ void MulticamWidget::SetMulticamNodeInternal(ViewerOutput *viewer, MultiCamNode void MulticamWidget::SetMulticamNode(ViewerOutput *viewer, MultiCamNode *n, ClipBlock *clip, const rational &time) { - if (time.isNaN() || time == GetTime()) { + if (time.isNaN() || !GetConnectedNode() || time == GetConnectedNode()->GetPlayhead()) { SetMulticamNodeInternal(viewer, n, clip); play_queue_.clear(); } else { @@ -125,13 +125,13 @@ void MulticamWidget::Switch(int source, bool split_clip) BlockSplitPreservingLinksCommand *split = nullptr; - if (clip_ && split_clip && clip_->in() < GetTime() && clip_->out() > GetTime()) { + if (clip_ && split_clip && clip_->in() < GetConnectedNode()->GetPlayhead() && clip_->out() > GetConnectedNode()->GetPlayhead()) { QVector blocks; blocks.append(clip_); blocks.append(clip_->block_links()); - split = new BlockSplitPreservingLinksCommand(blocks, {GetTime()}); + split = new BlockSplitPreservingLinksCommand(blocks, {GetConnectedNode()->GetPlayhead()}); split->redo_now(); command->add_child(split); diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index b0d6f1618..c734af945 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -40,13 +40,12 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : super(true, false, parent), last_scroll_val_(0), focused_node_(nullptr), - time_target_(nullptr), show_all_nodes_(false) { // Create horizontal layout to place scroll area in (and keyframe editing eventually) QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); QSplitter* splitter = new QSplitter(Qt::Horizontal); layout->addWidget(splitter); @@ -113,7 +112,7 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : QWidget* keyframe_area = new QWidget(); QVBoxLayout* keyframe_area_layout = new QVBoxLayout(keyframe_area); keyframe_area_layout->setSpacing(0); - keyframe_area_layout->setMargin(0); + keyframe_area_layout->setContentsMargins(0, 0, 0, 0); // Create ruler object keyframe_area_layout->addWidget(ruler()); @@ -126,15 +125,9 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : keyframe_area_layout->addWidget(keyframe_view_); // Connect ruler and keyframe view together - connect(ruler(), &TimeRuler::TimeChanged, keyframe_view_, &KeyframeView::SetTime); - 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); - splitter->addWidget(keyframe_area); // Set both widgets to 50/50 @@ -148,8 +141,6 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : // TimeBasedWidget's scrollbar has extra functionality that we can take advantage of keyframe_view_->setHorizontalScrollBar(scrollbar()); keyframe_view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - - connect(keyframe_view_->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); } else { keyframe_view_ = nullptr; } @@ -361,19 +352,6 @@ void NodeParamView::TimebaseChangedEvent(const rational &timebase) foreach (NodeParamViewContext* ctx, context_items_) { ctx->SetTimebase(timebase); } - - UpdateItemTime(GetTime()); -} - -void NodeParamView::TimeChangedEvent(const rational &time) -{ - super::TimeChangedEvent(time); - - if (keyframe_view_) { - keyframe_view_->SetTime(time); - } - - UpdateItemTime(time); } void NodeParamView::ConnectedNodeChangeEvent(ViewerOutput *n) @@ -386,13 +364,6 @@ void NodeParamView::ConnectedNodeChangeEvent(ViewerOutput *n) foreach (NodeParamViewContext* item, context_items_) { item->SetTimeTarget(n); } - - time_target_ = n; -} - -Node *NodeParamView::GetTimeTarget() const -{ - return time_target_; } void ReconnectOutputsIfNotDeletingNode(MultiUndoCommand *c, NodeViewDeleteCommand *dc, Node *output, Node *deleting, Node *context) @@ -683,13 +654,6 @@ bool NodeParamView::Paste(QWidget *parent, std::function(co return true; } -void NodeParamView::UpdateItemTime(const rational &time) -{ - foreach (NodeParamViewContext* item, context_items_) { - item->SetTime(time); - } -} - void NodeParamView::QueueKeyframePositionUpdate() { QMetaObject::invokeMethod(this, &NodeParamView::UpdateElementY, Qt::QueuedConnection); @@ -697,13 +661,20 @@ void NodeParamView::QueueKeyframePositionUpdate() void NodeParamView::AddContext(Node *ctx) { + NodeParamViewContext *item = GetContextItemFromContext(ctx); + + // TEMP: Creating many NPV items is EXTREMELY slow so limit to one item per context for now. + // I have a better solution in the works to use one UI for several nodes, but I haven't + // done it yet, and this can severely affect productivity. + if (item->GetContexts().size() == 1) { + return; + } + // Queued so that if any further work is done in connecting this node to the context, it'll be // done before our sorting function is called connect(ctx, &Node::NodeAddedToContext, this, &NodeParamView::NodeAddedToContext, Qt::QueuedConnection); connect(ctx, &Node::NodeRemovedFromContext, this, &NodeParamView::NodeRemovedFromContext, Qt::QueuedConnection); - NodeParamViewContext *item = GetContextItemFromContext(ctx); - item->AddContext(ctx); item->setVisible(true); @@ -735,7 +706,6 @@ void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context) NodeParamViewItem* item = new NodeParamViewItem(n, IsGroupMode() ? kCheckBoxesOnNonConnected : kNoCheckBoxes, context->GetDockArea()); - connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::SetTimeAndSignal); connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::SelectNodeFromConnectedLink); connect(item, &NodeParamViewItem::PinToggled, this, &NodeParamView::PinNode); connect(item, &NodeParamViewItem::InputCheckedChanged, this, &NodeParamView::InputCheckBoxChanged); @@ -743,9 +713,8 @@ void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context) connect(item, &NodeParamViewItem::RequestEditTextInViewer, this, &NodeParamView::RequestEditTextInViewer); item->SetContext(ctx); - item->SetTimeTarget(GetTimeTarget()); + item->SetTimeTarget(GetConnectedNode()); item->SetTimebase(timebase()); - item->SetTime(GetTime()); context->AddNode(item); diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index 1b78055a9..a542fcc56 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -48,8 +48,6 @@ public: void CloseContextsBelongingToProject(Project *p); - Node* GetTimeTarget() const; - void DeleteSelected(); void SelectAll() @@ -95,7 +93,6 @@ protected: virtual void ScaleChangedEvent(const double &) override; virtual void TimebaseChangedEvent(const rational&) override; - virtual void TimeChangedEvent(const rational &time) override; virtual void ConnectedNodeChangeEvent(ViewerOutput* n) override; @@ -115,8 +112,6 @@ protected: } private: - void UpdateItemTime(const rational &time); - void QueueKeyframePositionUpdate(); void AddContext(Node *context); @@ -159,8 +154,6 @@ private: NodeParamViewItem* focused_node_; QVector selected_nodes_; - Node *time_target_; - QVector contexts_; QVector current_contexts_; diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp index 937037219..a4ba1761a 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp @@ -34,10 +34,11 @@ namespace olive { NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, QWidget *parent) : QWidget(parent), input_(input), - connected_node_(nullptr) + connected_node_(nullptr), + viewer_(nullptr) { QVBoxLayout *layout = new QVBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); QSizePolicy p = sizePolicy(); p.setHorizontalStretch(1); @@ -47,7 +48,7 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, // Set up label area QHBoxLayout *label_layout = new QHBoxLayout(); label_layout->setSpacing(QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral(" "))); - label_layout->setMargin(0); + label_layout->setContentsMargins(0, 0, 0, 0); layout->addLayout(label_layout); CollapseButton *collapse_btn = new CollapseButton(this); @@ -85,6 +86,20 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, connect(collapse_btn, &CollapseButton::toggled, this, &NodeParamViewConnectedLabel::SetValueTreeVisible); } +void NodeParamViewConnectedLabel::SetViewerNode(ViewerOutput *viewer) +{ + if (viewer_) { + disconnect(viewer_, &ViewerOutput::PlayheadChanged, this, &NodeParamViewConnectedLabel::UpdateValueTree); + } + + viewer_ = viewer; + + if (viewer_) { + connect(viewer_, &ViewerOutput::PlayheadChanged, this, &NodeParamViewConnectedLabel::UpdateValueTree); + UpdateValueTree(); + } +} + void NodeParamViewConnectedLabel::CreateTree() { // Set up table area @@ -92,15 +107,6 @@ void NodeParamViewConnectedLabel::CreateTree() layout()->addWidget(value_tree_); } -void NodeParamViewConnectedLabel::SetTime(const rational &time) -{ - time_ = time; - - if (value_tree_ && value_tree_->isVisible()) { - UpdateValueTree(); - } -} - void NodeParamViewConnectedLabel::InputConnected(Node *output, const NodeInput& input) { if (input_ != input) { @@ -159,8 +165,8 @@ void NodeParamViewConnectedLabel::UpdateLabel() void NodeParamViewConnectedLabel::UpdateValueTree() { - if (value_tree_) { - value_tree_->SetNode(input_, time_); + if (value_tree_ && viewer_ && value_tree_->isVisible()) { + value_tree_->SetNode(input_, viewer_->GetPlayhead()); } } @@ -173,6 +179,7 @@ void NodeParamViewConnectedLabel::SetValueTreeVisible(bool e) if (e) { if (!value_tree_) { CreateTree(); + value_tree_->setVisible(true); } UpdateValueTree(); diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h index 9a7a81ee7..16a7428af 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h @@ -32,7 +32,7 @@ class NodeParamViewConnectedLabel : public QWidget { public: NodeParamViewConnectedLabel(const NodeInput& input, QWidget* parent = nullptr); - void SetTime(const rational &time); + void SetViewerNode(ViewerOutput *viewer); signals: void RequestSelectNode(Node *n); @@ -61,7 +61,7 @@ private: NodeValueTree *value_tree_; - rational time_; + ViewerOutput *viewer_; private slots: void SetValueTreeVisible(bool e); diff --git a/app/widget/nodeparamview/nodeparamviewcontext.cpp b/app/widget/nodeparamview/nodeparamviewcontext.cpp index 1d3a22253..a2438bddd 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.cpp +++ b/app/widget/nodeparamview/nodeparamviewcontext.cpp @@ -113,20 +113,13 @@ void NodeParamViewContext::SetTimebase(const rational &timebase) } } -void NodeParamViewContext::SetTimeTarget(Node *n) +void NodeParamViewContext::SetTimeTarget(ViewerOutput *n) { foreach (NodeParamViewItem* item, items_) { item->SetTimeTarget(n); } } -void NodeParamViewContext::SetTime(const rational &time) -{ - foreach (NodeParamViewItem* item, items_) { - item->SetTime(time); - } -} - void NodeParamViewContext::SetEffectType(Track::Type type) { type_ = type; diff --git a/app/widget/nodeparamview/nodeparamviewcontext.h b/app/widget/nodeparamview/nodeparamviewcontext.h index 11574e2a5..b89db696a 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.h +++ b/app/widget/nodeparamview/nodeparamviewcontext.h @@ -60,9 +60,7 @@ public: void SetTimebase(const rational &timebase); - void SetTimeTarget(Node *n); - - void SetTime(const rational &time); + void SetTimeTarget(ViewerOutput *n); void SetEffectType(Track::Type type); diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index a27cbe824..7d0f6dc7a 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -90,12 +90,10 @@ void NodeParamViewItem::RecreateBody() body_ = new NodeParamViewItemBody(node_, create_checkboxes_, this); connect(body_, &NodeParamViewItemBody::RequestSelectNode, this, &NodeParamViewItem::RequestSelectNode); - connect(body_, &NodeParamViewItemBody::RequestSetTime, this, &NodeParamViewItem::RequestSetTime); connect(body_, &NodeParamViewItemBody::ArrayExpandedChanged, this, &NodeParamViewItem::ArrayExpandedChanged); connect(body_, &NodeParamViewItemBody::InputCheckedChanged, this, &NodeParamViewItem::InputCheckedChanged); connect(body_, &NodeParamViewItemBody::RequestEditTextInViewer, this, &NodeParamViewItem::RequestEditTextInViewer); body_->Retranslate(); - body_->SetTime(time_); body_->SetTimebase(timebase_); SetBody(body_); } @@ -259,7 +257,6 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const ui_objects.key_control = new NodeParamViewKeyframeControl(this); ui_objects.key_control->SetInput(resolved); layout->addWidget(ui_objects.key_control, row, kKeyControlColumn); - connect(ui_objects.key_control, &NodeParamViewKeyframeControl::RequestSetTime, this, &NodeParamViewItemBody::RequestSetTime); } input_ui_map_.insert(input_ref, ui_objects); @@ -269,31 +266,17 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const } } -void NodeParamViewItemBody::SetTimeTarget(Node *target) +void NodeParamViewItemBody::SetTimeTarget(ViewerOutput *target) { foreach (const InputUI& ui_obj, input_ui_map_) { // Only keyframable inputs have a key control widget if (ui_obj.key_control) { ui_obj.key_control->SetTimeTarget(target); } - - ui_obj.widget_bridge->SetTimeTarget(target); - } -} - -void NodeParamViewItemBody::SetTime(const rational &time) -{ - foreach (const InputUI& ui_obj, input_ui_map_) { - // Only keyframable inputs have a key control widget - if (ui_obj.key_control) { - ui_obj.key_control->SetTime(time); - } - if (ui_obj.connected_label) { - ui_obj.connected_label->SetTime(time); + ui_obj.connected_label->SetViewerNode(target); } - - ui_obj.widget_bridge->SetTime(time); + ui_obj.widget_bridge->SetTimeTarget(target); } } diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 8ec20bb6a..eb52e44d3 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -51,9 +51,7 @@ class NodeParamViewItemBody : public QWidget { public: NodeParamViewItemBody(Node* node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget* parent = nullptr); - void SetTimeTarget(Node* target); - - void SetTime(const rational& time); + void SetTimeTarget(ViewerOutput *target); void Retranslate(); @@ -65,8 +63,6 @@ public: void SetInputChecked(const NodeInput &input, bool e); signals: - void RequestSetTime(const rational& time); - void RequestSelectNode(Node *node); void ArrayExpandedChanged(bool e); @@ -167,18 +163,11 @@ class NodeParamViewItem : public NodeParamViewItemBase public: NodeParamViewItem(Node* node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget* parent = nullptr); - void SetTimeTarget(Node* target) + void SetTimeTarget(ViewerOutput* target) { body_->SetTimeTarget(target); } - void SetTime(const rational& time) - { - time_ = time; - - body_->SetTime(time_); - } - void SetTimebase(const rational& timebase) { timebase_ = timebase; @@ -216,8 +205,6 @@ public: } signals: - void RequestSetTime(const rational& time); - void RequestSelectNode(Node *node); void ArrayExpandedChanged(bool e); @@ -238,7 +225,6 @@ private: Node *ctx_; - rational time_; rational timebase_; KeyframeView::NodeConnections keyframe_connections_; diff --git a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp index c431966a8..cd0c637a5 100644 --- a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp +++ b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp @@ -33,7 +33,7 @@ NodeParamViewKeyframeControl::NodeParamViewKeyframeControl(bool right_align, QWi QWidget(parent) { QHBoxLayout* layout = new QHBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); if (right_align) { @@ -96,10 +96,14 @@ void NodeParamViewKeyframeControl::SetInput(const NodeInput& input) } } -void NodeParamViewKeyframeControl::SetTime(const rational &time) +void NodeParamViewKeyframeControl::TimeTargetDisconnectEvent(ViewerOutput *v) { - time_ = time; + disconnect(v, &ViewerOutput::PlayheadChanged, this, &NodeParamViewKeyframeControl::UpdateState); +} +void NodeParamViewKeyframeControl::TimeTargetConnectEvent(ViewerOutput *v) +{ + connect(v, &ViewerOutput::PlayheadChanged, this, &NodeParamViewKeyframeControl::UpdateState); UpdateState(); } @@ -122,12 +126,12 @@ void NodeParamViewKeyframeControl::SetButtonsEnabled(bool e) rational NodeParamViewKeyframeControl::GetCurrentTimeAsNodeTime() const { - return GetAdjustedTime(GetTimeTarget(), input_.node(), time_, true); + return GetAdjustedTime(GetTimeTarget(), input_.node(), GetTimeTarget()->GetPlayhead(), Node::kTransformTowardsInput); } rational NodeParamViewKeyframeControl::ConvertToViewerTime(const rational &r) const { - return GetAdjustedTime(input_.node(), GetTimeTarget(), r, false); + return GetAdjustedTime(input_.node(), GetTimeTarget(), r, Node::kTransformTowardsOutput); } void NodeParamViewKeyframeControl::ShowButtonsFromKeyframeEnable(bool e) @@ -177,7 +181,7 @@ void NodeParamViewKeyframeControl::ToggleKeyframe(bool e) void NodeParamViewKeyframeControl::UpdateState() { - if (!input_.IsValid() || !input_.IsKeyframing()) { + if (!input_.IsValid() || !input_.IsKeyframing() || !GetTimeTarget()) { return; } @@ -197,10 +201,9 @@ void NodeParamViewKeyframeControl::GoToPreviousKey() NodeKeyframe* previous_key = input_.node()->GetClosestKeyframeBeforeTime(input_, node_time); - if (previous_key) { + if (previous_key && GetTimeTarget()) { rational key_time = ConvertToViewerTime(previous_key->time()); - - emit RequestSetTime(key_time); + GetTimeTarget()->SetPlayhead(key_time); } } @@ -210,10 +213,9 @@ void NodeParamViewKeyframeControl::GoToNextKey() NodeKeyframe* next_key = input_.node()->GetClosestKeyframeAfterTime(input_, node_time); - if (next_key) { + if (next_key && GetTimeTarget()) { rational key_time = ConvertToViewerTime(next_key->time()); - - emit RequestSetTime(key_time); + GetTimeTarget()->SetPlayhead(key_time); } } diff --git a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h index 0f8ffab7d..44b017b56 100644 --- a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h +++ b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h @@ -46,10 +46,9 @@ public: void SetInput(const NodeInput& input); - void SetTime(const rational& time); - -signals: - void RequestSetTime(const rational& time); +protected: + virtual void TimeTargetDisconnectEvent(ViewerOutput *v) override; + virtual void TimeTargetConnectEvent(ViewerOutput *v) override; private: QPushButton* CreateNewToolButton(const QIcon &icon) const; @@ -67,8 +66,6 @@ private: NodeInput input_; - rational time_; - private slots: void ShowButtonsFromKeyframeEnable(bool e); diff --git a/app/widget/nodeparamview/nodeparamviewtextedit.cpp b/app/widget/nodeparamview/nodeparamviewtextedit.cpp index 6e3b291cc..95c533bd5 100644 --- a/app/widget/nodeparamview/nodeparamviewtextedit.cpp +++ b/app/widget/nodeparamview/nodeparamviewtextedit.cpp @@ -31,7 +31,7 @@ NodeParamViewTextEdit::NodeParamViewTextEdit(QWidget *parent) : QWidget(parent) { QHBoxLayout* layout = new QHBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); line_edit_ = new QPlainTextEdit(); line_edit_->setUndoRedoEnabled(true); diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index dd33a7659..693e66bd4 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -57,13 +57,6 @@ NodeParamViewWidgetBridge::NodeParamViewWidgetBridge(NodeInput input, QObject *p CreateWidgets(); } -void NodeParamViewWidgetBridge::SetTime(const rational &time) -{ - time_ = time; - - UpdateWidgetValues(); -} - int GetSliderCount(NodeValue::Type type) { return NodeValue::get_number_of_keyframe_tracks(type); @@ -528,7 +521,11 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() rational NodeParamViewWidgetBridge::GetCurrentTimeAsNodeTime() const { - return GetAdjustedTime(GetTimeTarget(), GetInnerInput().node(), time_, true); + if (GetTimeTarget()) { + return GetAdjustedTime(GetTimeTarget(), GetInnerInput().node(), GetTimeTarget()->GetPlayhead(), Node::kTransformTowardsInput); + } else { + return 0; + } } void NodeParamViewWidgetBridge::SetTimebase(const rational& timebase) @@ -538,11 +535,22 @@ void NodeParamViewWidgetBridge::SetTimebase(const rational& timebase) } } +void NodeParamViewWidgetBridge::TimeTargetDisconnectEvent(ViewerOutput *v) +{ + disconnect(v, &ViewerOutput::PlayheadChanged, this, &NodeParamViewWidgetBridge::UpdateWidgetValues); +} + +void NodeParamViewWidgetBridge::TimeTargetConnectEvent(ViewerOutput *v) +{ + connect(v, &ViewerOutput::PlayheadChanged, this, &NodeParamViewWidgetBridge::UpdateWidgetValues); +} + void NodeParamViewWidgetBridge::InputValueChanged(const NodeInput &input, const TimeRange &range) { - if (GetInnerInput() == input + if (GetTimeTarget() + && GetInnerInput() == input && !dragger_.IsStarted() - && range.in() <= time_ && range.out() >= time_) { + && range.in() <= GetTimeTarget()->GetPlayhead() && range.out() >= GetTimeTarget()->GetPlayhead()) { // We'll need to update the widgets because the values have changed on our current time UpdateWidgetValues(); } @@ -567,7 +575,7 @@ void NodeParamViewWidgetBridge::SetProperty(const QString &key, const QVariant & } } else { // set specific track/widget bool ok; - int element = key.midRef(7).toInt(&ok); + int element = key.mid(7).toInt(&ok); int tracks = NodeValue::get_number_of_keyframe_tracks(data_type); if (ok && element >= 0 && element < tracks) { @@ -686,7 +694,7 @@ void NodeParamViewWidgetBridge::SetProperty(const QString &key, const QVariant & } } else { bool ok; - int element = key.midRef(5).toInt(&ok); + int element = key.mid(5).toInt(&ok); if (ok && element >= 0 && element < tracks) { static_cast(widgets_.at(element))->SetColor(c); } diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h index 772febbde..457c618c6 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h @@ -42,8 +42,6 @@ class NodeParamViewWidgetBridge : public QObject, public TimeTargetObject public: NodeParamViewWidgetBridge(NodeInput input, QObject* parent); - void SetTime(const rational& time); - const QVector& widgets() const { return widgets_; @@ -59,6 +57,10 @@ signals: void RequestEditTextInViewer(); +protected: + virtual void TimeTargetDisconnectEvent(ViewerOutput *v) override; + virtual void TimeTargetConnectEvent(ViewerOutput *v) override; + private: void CreateWidgets(); @@ -102,8 +104,6 @@ private: QVector widgets_; - rational time_; - NodeInputDragger dragger_; NodeParamViewScrollBlocker scroll_filter_; diff --git a/app/widget/nodetableview/nodetablewidget.cpp b/app/widget/nodetableview/nodetablewidget.cpp index 48091357f..2ebb1eac8 100644 --- a/app/widget/nodetableview/nodetablewidget.cpp +++ b/app/widget/nodetableview/nodetablewidget.cpp @@ -29,7 +29,7 @@ NodeTableWidget::NodeTableWidget(QWidget* parent) : { QVBoxLayout* layout = new QVBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); view_ = new NodeTableView(); layout->addWidget(view_); diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index cf1610b02..97a818fe3 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -44,6 +44,7 @@ namespace olive { const double NodeView::kMinimumScale = 0.1; +const int NodeView::kMaximumContexts = 10; NodeView::NodeView(QWidget *parent) : HandMovableView(parent), @@ -102,6 +103,10 @@ void NodeView::SetContexts(const QVector &nodes) // Add contexts that are now in the list foreach (Node *n, nodes) { + if (scene_.context_map().size() >= kMaximumContexts) { + break; + } + if (!contexts_.contains(n)) { AddContext(n); } @@ -1038,7 +1043,7 @@ void NodeView::ProcessMovingAttachedNodes(const QPoint &pos) } } - if (new_drop_edge->input().node()->OutputsTo(attached_node, true)) { + if (attached_node->InputsFrom(new_drop_edge->input().node(), true)) { drop_input_.Reset(); } @@ -1074,7 +1079,7 @@ QVector NodeView::ProcessDroppingAttachedNodes(MultiUndoCommand *command, for (int i=0; iOutputsTo(ai.node, true)) { + if (ai.node->InputsFrom(select_context, true)) { attached.removeAt(i); } else if (select_context->ContextContainsNode(ai.node)) { select_nodes.append(ai.node); @@ -1113,7 +1118,7 @@ QVector NodeView::ProcessDroppingAttachedNodes(MultiUndoCommand *command, Node* dropping_node = nullptr; foreach (const AttachedItem &ai, attached) { - if (ai.item && !select_context->OutputsTo(ai.node, true)) { + if (ai.item && !ai.node->InputsFrom(select_context, true)) { dropping_node = ai.node; break; } @@ -1320,7 +1325,7 @@ void NodeView::PositionNewEdge(const QPoint &pos) // Filter out connecting to a node that connects to us or an item of the same type if (item_at_cursor - && ((create_edge_from_output_ && item_at_cursor->GetNode()->OutputsTo(source_item->GetNode(), true)) + && ((create_edge_from_output_ && source_item->GetNode()->InputsFrom(item_at_cursor->GetNode(), true)) || (!create_edge_from_output_ && item_at_cursor->GetNode()->InputsFrom(source_item->GetNode(), true)) || (create_edge_from_output_ == item_at_cursor->IsOutputItem()))) { item_at_cursor = nullptr; @@ -1407,7 +1412,7 @@ void NodeView::GroupNodes() // Default to the first node we find that doesn't output to a node inside the group output_passthrough = nodes_to_group.first(); foreach (Node *potential_in, nodes_to_group) { - if (potential_in != n && !n->OutputsTo(potential_in, false)) { + if (potential_in != n && !potential_in->InputsFrom(n, false)) { output_passthrough = n; break; } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index bf2d0ee0d..01e1dd487 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -235,6 +235,8 @@ private: static const double kMinimumScale; + static const int kMaximumContexts; + private slots: /** * @brief Receiver for when the scene's selected items change diff --git a/app/widget/nodeview/nodeviewtoolbar.cpp b/app/widget/nodeview/nodeviewtoolbar.cpp index 6ffaa6d35..7f3712a5b 100644 --- a/app/widget/nodeview/nodeviewtoolbar.cpp +++ b/app/widget/nodeview/nodeviewtoolbar.cpp @@ -13,7 +13,7 @@ NodeViewToolBar::NodeViewToolBar(QWidget *parent) : QWidget(parent) { QHBoxLayout *layout = new QHBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); add_node_btn_ = new QPushButton(); connect(add_node_btn_, &QPushButton::clicked, this, &NodeViewToolBar::AddNodeClicked); diff --git a/app/widget/nodeview/nodewidget.cpp b/app/widget/nodeview/nodewidget.cpp index 7ee2d4097..7fe526a8e 100644 --- a/app/widget/nodeview/nodewidget.cpp +++ b/app/widget/nodeview/nodewidget.cpp @@ -28,7 +28,7 @@ NodeWidget::NodeWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *outer_layout = new QVBoxLayout(this); - outer_layout->setMargin(0); + outer_layout->setContentsMargins(0, 0, 0, 0); toolbar_ = new NodeViewToolBar(); outer_layout->addWidget(toolbar_); diff --git a/app/widget/panel/panel.cpp b/app/widget/panel/panel.cpp index f3840fc06..bcc3365e2 100644 --- a/app/widget/panel/panel.cpp +++ b/app/widget/panel/panel.cpp @@ -158,7 +158,7 @@ void PanelWidget::SetWidgetWithPadding(QWidget *widget) { QWidget* wrapper = new QWidget(); QHBoxLayout* layout = new QHBoxLayout(wrapper); - layout->setMargin(layout->margin() / 2); + layout->setContentsMargins(layout->contentsMargins() / 2); layout->addWidget(widget); setWidget(wrapper); } diff --git a/app/widget/path/pathwidget.cpp b/app/widget/path/pathwidget.cpp index 79504273f..3035822ac 100644 --- a/app/widget/path/pathwidget.cpp +++ b/app/widget/path/pathwidget.cpp @@ -32,7 +32,7 @@ PathWidget::PathWidget(const QString &path, QWidget *parent) : QWidget(parent) { QHBoxLayout* layout = new QHBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); path_edit_ = new QLineEdit(); path_edit_->setText(path); diff --git a/app/widget/pixelsampler/pixelsampler.cpp b/app/widget/pixelsampler/pixelsampler.cpp index 81ee86d6c..8c47f7c14 100644 --- a/app/widget/pixelsampler/pixelsampler.cpp +++ b/app/widget/pixelsampler/pixelsampler.cpp @@ -54,21 +54,25 @@ void PixelSamplerWidget::UpdateLabelInternal() box_->SetColor(color_); label_->setText(tr("" - "R: %1
" - "G: %2
" - "B: %3
" - "A: %4" + "R: %1 (%5)
" + "G: %2 (%6)
" + "B: %3 (%7)
" + "A: %4 (%8)" "").arg(QString::number(color_.red()), QString::number(color_.green()), QString::number(color_.blue()), - QString::number(color_.alpha()))); + QString::number(color_.alpha()), + QString::number(int(color_.red()*255.0)), + QString::number(int(color_.green()*255.0)), + QString::number(int(color_.blue()*255.0)), + QString::number(int(color_.alpha()*255.0)))); } ManagedPixelSamplerWidget::ManagedPixelSamplerWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); display_view_ = new PixelSamplerWidget(); display_view_->setTitle(tr("Display")); diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index ea598b734..a87904e9e 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -37,7 +37,7 @@ PlaybackControls::PlaybackControls(QWidget *parent) : // Create lower controls QHBoxLayout* lower_control_layout = new QHBoxLayout(this); lower_control_layout->setSpacing(0); - lower_control_layout->setMargin(0); + lower_control_layout->setContentsMargins(0, 0, 0, 0); QSizePolicy lower_container_size_policy(QSizePolicy::Maximum, QSizePolicy::Expanding); lower_container_size_policy.setHorizontalStretch(1); @@ -51,7 +51,7 @@ PlaybackControls::PlaybackControls(QWidget *parent) : QHBoxLayout* lower_left_layout = new QHBoxLayout(lower_left_container_); lower_left_layout->setSpacing(0); - lower_left_layout->setMargin(0); + lower_left_layout->setContentsMargins(0, 0, 0, 0); cur_tc_lbl_ = new RationalSlider(); cur_tc_lbl_->SetDisplayType(RationalSlider::kTime); @@ -73,7 +73,7 @@ PlaybackControls::PlaybackControls(QWidget *parent) : QHBoxLayout* lower_middle_layout = new QHBoxLayout(lower_middle_container); lower_middle_layout->setSpacing(0); - lower_middle_layout->setMargin(0); + lower_middle_layout->setContentsMargins(0, 0, 0, 0); lower_middle_layout->addStretch(); QSizePolicy btn_sz_policy(QSizePolicy::Maximum, QSizePolicy::Preferred); @@ -124,7 +124,7 @@ PlaybackControls::PlaybackControls(QWidget *parent) : av_btn_widget->setSizePolicy(lower_container_size_policy); QHBoxLayout* av_btn_layout = new QHBoxLayout(av_btn_widget); av_btn_layout->setSpacing(0); - av_btn_layout->setMargin(0); + av_btn_layout->setContentsMargins(0, 0, 0, 0); video_drag_btn_ = new DragButton(); connect(video_drag_btn_, &QPushButton::clicked, this, &PlaybackControls::VideoClicked); connect(video_drag_btn_, &DragButton::MousePressed, this, &PlaybackControls::VideoPressed); @@ -143,7 +143,7 @@ PlaybackControls::PlaybackControls(QWidget *parent) : QHBoxLayout* lower_right_layout = new QHBoxLayout(lower_right_container_); lower_right_layout->setSpacing(0); - lower_right_layout->setMargin(0); + lower_right_layout->setContentsMargins(0, 0, 0, 0); lower_right_layout->addStretch(); end_tc_lbl_ = new QLabel(); diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 48cc8e3aa..a9858f19a 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -39,6 +39,7 @@ #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "widget/nodeparamview/nodeparamviewundo.h" +#include "widget/nodeview/nodeviewundo.h" #include "window/mainwindow/mainwindow.h" #include "window/mainwindow/mainwindowundo.h" #include "widget/nodeview/nodeviewundo.h" @@ -53,7 +54,7 @@ ProjectExplorer::ProjectExplorer(QWidget *parent) : // Create layout QVBoxLayout* layout = new QVBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); // Set up navigation bar nav_bar_ = new ProjectExplorerNavigation(this); @@ -478,8 +479,17 @@ void ProjectExplorer::ReplaceSelectedFootage() 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); + auto p = new MultiUndoCommand(); + + // Change filename parameter + p->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(NodeInput(footage, Footage::kFilenameInput)), file)); + + if (QFileInfo(footage->filename()).fileName() == footage->GetLabel()) { + // Footage label == filename, change label too + p->add_child(new NodeRenameCommand(footage, QFileInfo(file).fileName())); + } + + Core::instance()->undo_stack()->push(p); } } diff --git a/app/widget/projectexplorer/projectexplorernavigation.cpp b/app/widget/projectexplorer/projectexplorernavigation.cpp index ed635f19a..35c5e2210 100644 --- a/app/widget/projectexplorer/projectexplorernavigation.cpp +++ b/app/widget/projectexplorer/projectexplorernavigation.cpp @@ -33,7 +33,7 @@ ProjectExplorerNavigation::ProjectExplorerNavigation(QWidget *parent) : { // Create widget layout QHBoxLayout* layout = new QHBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); // Create "directory up" button dir_up_btn_ = new QPushButton(this); diff --git a/app/widget/projecttoolbar/projecttoolbar.cpp b/app/widget/projecttoolbar/projecttoolbar.cpp index a99a7340c..47c61a69a 100644 --- a/app/widget/projecttoolbar/projecttoolbar.cpp +++ b/app/widget/projecttoolbar/projecttoolbar.cpp @@ -33,7 +33,7 @@ ProjectToolbar::ProjectToolbar(QWidget *parent) : { QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); new_button_ = new QPushButton(); connect(new_button_, &QPushButton::clicked, this, &ProjectToolbar::NewClicked); diff --git a/app/widget/slider/CMakeLists.txt b/app/widget/slider/CMakeLists.txt index d20c999e6..b90c18ab5 100644 --- a/app/widget/slider/CMakeLists.txt +++ b/app/widget/slider/CMakeLists.txt @@ -26,7 +26,5 @@ set(OLIVE_SOURCES widget/slider/rationalslider.cpp widget/slider/stringslider.h widget/slider/stringslider.cpp - widget/slider/timeslider.h - widget/slider/timeslider.cpp PARENT_SCOPE ) diff --git a/app/widget/slider/base/sliderbase.cpp b/app/widget/slider/base/sliderbase.cpp index 1f132e3e6..ef58ce8ed 100644 --- a/app/widget/slider/base/sliderbase.cpp +++ b/app/widget/slider/base/sliderbase.cpp @@ -103,14 +103,26 @@ void SliderBase::changeEvent(QEvent *e) super::changeEvent(e); } +bool SliderBase::GetLabelSubstitution(const QVariant &v, QString *out) const +{ + for (auto it=label_substitutions_.constBegin(); it!=label_substitutions_.constEnd(); it++) { + if (it->first == v) { + *out = it->second; + return true; + } + } + + return false; +} + void SliderBase::UpdateLabel() { QString s; if (tristate_) { s = tr("---"); - } else if (label_substitutions_.contains(GetValueInternal())) { - s = label_substitutions_.value(GetValueInternal()); + } else if (GetLabelSubstitution(GetValueInternal(), &s)) { + // String will already be set, just pass through } else { s = GetFormattedValueToString(); } diff --git a/app/widget/slider/base/sliderbase.h b/app/widget/slider/base/sliderbase.h index 3bf9eb901..9828bb848 100644 --- a/app/widget/slider/base/sliderbase.h +++ b/app/widget/slider/base/sliderbase.h @@ -51,7 +51,7 @@ public: void InsertLabelSubstitution(const QVariant &value, const QString &label) { - label_substitutions_.insert(value, label); + label_substitutions_.append({value, label}); UpdateLabel(); } @@ -90,6 +90,8 @@ protected: virtual void changeEvent(QEvent* e) override; private: + bool GetLabelSubstitution(const QVariant &v, QString *out) const; + SliderLabel* label_; FocusableLineEdit* editor_; @@ -103,7 +105,7 @@ private: bool format_plural_; - QMap label_substitutions_; + QVector > label_substitutions_; private slots: void LineEditConfirmed(); diff --git a/app/widget/slider/base/sliderladder.cpp b/app/widget/slider/base/sliderladder.cpp index 5c927f61a..0ad993039 100644 --- a/app/widget/slider/base/sliderladder.cpp +++ b/app/widget/slider/base/sliderladder.cpp @@ -42,7 +42,7 @@ SliderLadder::SliderLadder(double drag_multiplier, int nb_outer_values, QString QFrame(parent, Qt::Popup) { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); setFrameShape(QFrame::Box); diff --git a/app/widget/slider/rationalslider.cpp b/app/widget/slider/rationalslider.cpp index 3cf9e1e85..8ac41bf0d 100644 --- a/app/widget/slider/rationalslider.cpp +++ b/app/widget/slider/rationalslider.cpp @@ -98,18 +98,24 @@ void RationalSlider::DisableDisplayType(RationalSlider::DisplayType type) QString RationalSlider::ValueToString(const QVariant &v) const { - double val = v.value().toDouble() + GetOffset().value().toDouble(); + rational r = v.value(); - switch (display_type_) { - case kTime: - return Timecode::time_to_timecode(v.value(), timebase_, Core::instance()->GetTimecodeDisplay()); - case kFloat: - return FloatToString(val, GetDecimalPlaces(), GetAutoTrimDecimalPlaces()); - case kRational: - return v.value().toString(); + if (r.isNaN()) { + return tr("NaN"); + } else { + double val = r.toDouble() + GetOffset().value().toDouble(); + + switch (display_type_) { + case kTime: + return Timecode::time_to_timecode(r, timebase_, Core::instance()->GetTimecodeDisplay()); + case kFloat: + return FloatToString(val, GetDecimalPlaces(), GetAutoTrimDecimalPlaces()); + case kRational: + return v.value().toString(); + } + + return v.toString(); } - - return v.toString(); } QVariant RationalSlider::StringToValue(const QString &s, bool *ok) const diff --git a/app/widget/slider/timeslider.cpp b/app/widget/slider/timeslider.cpp deleted file mode 100644 index 57fc557b5..000000000 --- a/app/widget/slider/timeslider.cpp +++ /dev/null @@ -1,63 +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 "timeslider.h" - -#include "common/timecodefunctions.h" -#include "core.h" - -namespace olive { - -#define super IntegerSlider - -TimeSlider::TimeSlider(QWidget *parent) : - super(parent) -{ - SetMinimum(0); - - connect(Core::instance(), &Core::TimecodeDisplayChanged, this, &TimeSlider::UpdateLabel); -} - -void TimeSlider::SetTimebase(const rational &timebase) -{ - timebase_ = timebase; - - // Refresh label since we have a new timebase to generate a timecode with - UpdateLabel(); -} - -QString TimeSlider::ValueToString(const QVariant &v) const -{ - if (timebase_.isNull()) { - // We can't generate a timecode without a timebase, so we just return the number - return super::ValueToString(v); - } - - return Timecode::timestamp_to_timecode(v.toLongLong() + GetOffset().toLongLong(), - timebase_, - Core::instance()->GetTimecodeDisplay()); -} - -QVariant TimeSlider::StringToValue(const QString &s, bool *ok) const -{ - return QVariant::fromValue(Timecode::timecode_to_timestamp(s, timebase_, Core::instance()->GetTimecodeDisplay(), ok) - GetOffset().toLongLong()); -} - -} diff --git a/app/widget/slider/timeslider.h b/app/widget/slider/timeslider.h deleted file mode 100644 index b22bb325c..000000000 --- a/app/widget/slider/timeslider.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 TIMESLIDER_H -#define TIMESLIDER_H - -#include "common/rational.h" -#include "integerslider.h" - -namespace olive { - -class TimeSlider : public IntegerSlider -{ - Q_OBJECT -public: - TimeSlider(QWidget* parent = nullptr); - -public slots: - void SetTimebase(const rational& timebase); - -protected: - virtual QString ValueToString(const QVariant& v) const override; - - virtual QVariant StringToValue(const QString& s, bool* ok) const override; - -private: - rational timebase_; - -}; - -} - -#endif // TIMESLIDER_H diff --git a/app/widget/standardcombos/frameratecombobox.h b/app/widget/standardcombos/frameratecombobox.h index d5ed19c05..ddd12d47a 100644 --- a/app/widget/standardcombos/frameratecombobox.h +++ b/app/widget/standardcombos/frameratecombobox.h @@ -43,7 +43,7 @@ public: QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(inner_); RepopulateList(); diff --git a/app/widget/taskview/elapsedcounterwidget.cpp b/app/widget/taskview/elapsedcounterwidget.cpp index cc191239b..8300be9f4 100644 --- a/app/widget/taskview/elapsedcounterwidget.cpp +++ b/app/widget/taskview/elapsedcounterwidget.cpp @@ -22,6 +22,7 @@ #include #include +#include #include "common/timecodefunctions.h" @@ -34,7 +35,7 @@ ElapsedCounterWidget::ElapsedCounterWidget(QWidget* parent) : { QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(layout->spacing() * 8); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); elapsed_lbl_ = new QLabel(); layout->addWidget(elapsed_lbl_); @@ -80,7 +81,7 @@ void ElapsedCounterWidget::UpdateTimers() double ms_per_progress_unit = elapsed_ms / last_progress_; double remaining_progress = 1.0 - last_progress_; - remaining_ms = qRound64(ms_per_progress_unit * remaining_progress); + remaining_ms = std::ceil(ms_per_progress_unit * remaining_progress); } else { elapsed_ms = 0; remaining_ms = 0; diff --git a/app/widget/taskview/elapsedcounterwidget.h b/app/widget/taskview/elapsedcounterwidget.h index 809853fd0..ad1901c60 100644 --- a/app/widget/taskview/elapsedcounterwidget.h +++ b/app/widget/taskview/elapsedcounterwidget.h @@ -37,8 +37,9 @@ public: void SetProgress(double d); - void Start(); +public slots: void Start(qint64 start_time); + void Start(); public slots: void Stop(); diff --git a/app/widget/taskview/taskview.cpp b/app/widget/taskview/taskview.cpp index ff0cd0e54..92a414191 100644 --- a/app/widget/taskview/taskview.cpp +++ b/app/widget/taskview/taskview.cpp @@ -37,7 +37,7 @@ TaskView::TaskView(QWidget* parent) : // Create layout for central widget layout_ = new QVBoxLayout(central_widget_); layout_->setSpacing(0); - layout_->setMargin(0); + layout_->setContentsMargins(0, 0, 0, 0); // Add a "stretch" so that TaskViewItems don't try to expand all the way to the bottom layout_->addStretch(); diff --git a/app/widget/taskview/taskviewitem.cpp b/app/widget/taskview/taskviewitem.cpp index d63e8f42b..eaa7f04e0 100644 --- a/app/widget/taskview/taskviewitem.cpp +++ b/app/widget/taskview/taskviewitem.cpp @@ -72,9 +72,9 @@ TaskViewItem::TaskViewItem(Task* task, QWidget *parent) : // Set up elapsed timer status_stack_->setCurrentWidget(elapsed_timer_lbl_); - elapsed_timer_lbl_->Start(task_->GetStartTime()); // Connect to the task + connect(task_, &Task::Started, elapsed_timer_lbl_, qOverload(&ElapsedCounterWidget::Start)); connect(task_, &Task::ProgressChanged, this, &TaskViewItem::UpdateProgress); connect(cancel_btn_, &QPushButton::clicked, this, [this] { emit TaskCancelled(task_); }); } diff --git a/app/widget/timebased/timebasedview.cpp b/app/widget/timebased/timebasedview.cpp index 310df8866..869bc6d78 100644 --- a/app/widget/timebased/timebasedview.cpp +++ b/app/widget/timebased/timebasedview.cpp @@ -38,7 +38,8 @@ TimeBasedView::TimeBasedView(QWidget *parent) : snapped_(false), snap_service_(nullptr), y_axis_enabled_(false), - y_scale_(1.0) + y_scale_(1.0), + viewer_(nullptr) { // Sets scene to our scene setScene(&scene_); @@ -142,12 +143,17 @@ void TimeBasedView::SetYScale(const double &y_scale) } } -void TimeBasedView::SetTime(const rational &time) +void TimeBasedView::SetViewerNode(ViewerOutput *v) { - playhead_ = time; + if (viewer_) { + disconnect(viewer_, &ViewerOutput::PlayheadChanged, viewport(), static_cast(&TimeBasedView::update)); + } - // Force redraw for playhead - viewport()->update(); + viewer_ = v; + + if (viewer_) { + connect(viewer_, &ViewerOutput::PlayheadChanged, viewport(), static_cast(&TimeBasedView::update)); + } } void TimeBasedView::drawForeground(QPainter *painter, const QRectF &rect) @@ -203,20 +209,21 @@ bool TimeBasedView::PlayheadMove(QMouseEvent *event) return false; } - QPointF scene_pos = mapToScene(event->pos()); - rational mouse_time = qMax(rational(0), SceneToTime(scene_pos.x())); + if (viewer_) { + QPointF scene_pos = mapToScene(event->pos()); + rational mouse_time = qMax(rational(0), SceneToTime(scene_pos.x())); - if (Core::instance()->snapping() && snap_service_) { - rational movement; + if (Core::instance()->snapping() && snap_service_) { + rational movement; - snap_service_->SnapPoint({mouse_time}, &movement, TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToPlayhead); + snap_service_->SnapPoint({mouse_time}, &movement, TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToPlayhead); - mouse_time += movement; + mouse_time += movement; + } + + viewer_->SetPlayhead(mouse_time); } - SetTime(mouse_time); - emit TimeChanged(mouse_time); - return true; } @@ -237,7 +244,11 @@ bool TimeBasedView::PlayheadRelease(QMouseEvent*) qreal TimeBasedView::GetPlayheadX() { - return TimeToScene(playhead_); + if (viewer_) { + return TimeToScene(viewer_->GetPlayhead()); + } else { + return 0; + } } void TimeBasedView::SetEndTime(const rational &length) diff --git a/app/widget/timebased/timebasedview.h b/app/widget/timebased/timebasedview.h index 9f708553a..cb149b445 100644 --- a/app/widget/timebased/timebasedview.h +++ b/app/widget/timebased/timebasedview.h @@ -45,15 +45,13 @@ public: return snapped_; } - const rational &GetTime() const { return playhead_; } - TimeBasedWidget *GetSnapService() const { return snap_service_; } void SetSnapService(TimeBasedWidget* service) { snap_service_ = service; } const double& GetYScale() const; void SetYScale(const double& y_scale); - bool IsDraggingPlayhead() const + virtual bool IsDraggingPlayhead() const { return dragging_playhead_; } @@ -62,9 +60,11 @@ public: virtual void SelectionManagerSelectEvent(void *obj){} virtual void SelectionManagerDeselectEvent(void *obj){} -public slots: - void SetTime(const rational &time); + ViewerOutput *GetViewerNode() const { return viewer_; } + void SetViewerNode(ViewerOutput *v); + +public slots: void SetEndTime(const rational& length); /** @@ -73,8 +73,6 @@ public slots: void UpdateSceneRect(); signals: - void TimeChanged(const rational& time); - void ScaleChanged(double scale); protected: @@ -109,8 +107,6 @@ protected: private: qreal GetPlayheadX(); - rational playhead_; - double playhead_scene_left_; double playhead_scene_right_; @@ -129,6 +125,8 @@ private: double y_scale_; + ViewerOutput *viewer_; + }; } diff --git a/app/widget/timebased/timebasedviewselectionmanager.h b/app/widget/timebased/timebasedviewselectionmanager.h index 6d9e6962f..f9ac6faf9 100644 --- a/app/widget/timebased/timebasedviewselectionmanager.h +++ b/app/widget/timebased/timebasedviewselectionmanager.h @@ -216,7 +216,7 @@ public: if (time_target_) { for (size_t i=0; iGetAdjustedTime(parent, time_target_->GetTimeTarget(), copy[i], false); + copy[i] = time_target_->GetAdjustedTime(parent, time_target_->GetTimeTarget(), copy[i], Node::kTransformTowardsOutput); } } } diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index a4beb3bfa..a87b6dbbb 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -43,16 +43,14 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_statu 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_); + ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this); + ConnectTimelineView(ruler_); + ruler()->SetSnapService(this); + connect(ruler(), &TimeRuler::DragReleased, this, static_cast(&TimeBasedWidget::StopCatchUpScrollTimer)); catchup_scroll_timer_ = new QTimer(this); catchup_scroll_timer_->setInterval(250); // Hardcoded 1/4 scroll limit value @@ -68,11 +66,6 @@ void TimeBasedWidget::SetScaleAndCenterOnPlayhead(const double &scale) QTimer::singleShot(0, this, &TimeBasedWidget::CenterScrollOnPlayhead); } -const rational &TimeBasedWidget::GetTime() const -{ - return ruler_->GetTime(); -} - ViewerOutput *TimeBasedWidget::GetConnectedNode() const { return viewer_node_; @@ -96,6 +89,7 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) // Disconnect length changed signal disconnect(old, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); disconnect(old, &ViewerOutput::RemovedFromGraph, this, &TimeBasedWidget::ConnectedNodeRemovedFromGraph); + disconnect(old, &ViewerOutput::PlayheadChanged, this, &TimeBasedWidget::PlayheadTimeChanged); // Disconnect rate change signals if they were connected disconnect(old, &ViewerOutput::FrameRateChanged, this, &TimeBasedWidget::AutoUpdateTimebase); @@ -110,12 +104,16 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) } // Call derivatives + for (TimeBasedView *view : timeline_views_) { + view->SetViewerNode(viewer_node_); + } ConnectedNodeChangeEvent(viewer_node_); if (viewer_node_) { // Connect length changed signal connect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); connect(viewer_node_, &ViewerOutput::RemovedFromGraph, this, &TimeBasedWidget::ConnectedNodeRemovedFromGraph); + connect(viewer_node_, &ViewerOutput::PlayheadChanged, this, &TimeBasedWidget::PlayheadTimeChanged); // Connect ruler and scrollbar to timeline points ConnectWorkArea(viewer_node_->GetWorkArea()); @@ -209,12 +207,16 @@ void TimeBasedWidget::ScrollBarResizeMoved(int movement) void TimeBasedWidget::PageScrollToPlayhead() { - PageScrollInternal(qRound(TimeToScene(GetTime())), true); + if (GetConnectedNode()) { + PageScrollInternal(qRound(TimeToScene(GetConnectedNode()->GetPlayhead())), true); + } } void TimeBasedWidget::CatchUpScrollToPlayhead() { - CatchUpScrollToPoint(qRound(TimeToScene(GetTime()))); + if (GetConnectedNode()) { + CatchUpScrollToPoint(qRound(TimeToScene(GetConnectedNode()->GetPlayhead()))); + } } void TimeBasedWidget::CatchUpScrollToPoint(int point) @@ -300,21 +302,24 @@ void TimeBasedWidget::resizeEvent(QResizeEvent *event) UpdateMaximumScroll(); } -void TimeBasedWidget::ConnectTimelineView(TimeBasedView *base, bool connect_time_change_event) +void TimeBasedWidget::ConnectTimelineView(TimeBasedView *base) { - if (connect_time_change_event) { - connect(base, &TimeBasedView::TimeChanged, this, &TimeBasedWidget::SetTimeAndSignal); + // Connect scale + connect(base, &TimeBasedView::ScaleChanged, this, &TimeBasedWidget::SetScale); + + // Main scrollbar to view scrollbar and vice versa + connect(scrollbar(), &QScrollBar::valueChanged, base->horizontalScrollBar(), &QScrollBar::setValue); + connect(base->horizontalScrollBar(), &QScrollBar::valueChanged, scrollbar(), &QScrollBar::setValue); + + // Connect scrollbar to other scrollbars + for (TimeBasedView *other : qAsConst(timeline_views_)) { + connect(other->horizontalScrollBar(), &QScrollBar::valueChanged, base->horizontalScrollBar(), &QScrollBar::setValue); + connect(base->horizontalScrollBar(), &QScrollBar::valueChanged, other->horizontalScrollBar(), &QScrollBar::setValue); } timeline_views_.append(base); } -void TimeBasedWidget::PassWheelEventsToScrollBar(QObject *object) -{ - wheel_passthrough_objects_.append(object); - object->installEventFilter(this); -} - void TimeBasedWidget::SetCatchUpScrollValue(QScrollBar *b, int v, int maximum) { CatchUpScrollData &cudata = catchup_scroll_values_[b]; @@ -345,7 +350,7 @@ void TimeBasedWidget::StopCatchUpScrollTimer(QScrollBar *b) } } -void TimeBasedWidget::SetTime(const rational &time) +void TimeBasedWidget::PlayheadTimeChanged(const rational &time) { if (UserIsDraggingPlayhead()) { // If the user is dragging the playhead, we will simply nudge over and not use autoscroll rules. @@ -365,8 +370,6 @@ void TimeBasedWidget::SetTime(const rational &time) } } - ruler_->SetTime(time); - TimeChangedEvent(time); } @@ -400,7 +403,7 @@ void TimeBasedWidget::GoToPrevCut() return; } - if (GetTime().isNull()) { + if (GetConnectedNode()->GetPlayhead().isNull()) { return; } @@ -410,7 +413,7 @@ void TimeBasedWidget::GoToPrevCut() rational this_track_closest_cut = 0; for (Block* block : track->Blocks()) { - if (block->out() < GetTime()) { + if (block->out() < GetConnectedNode()->GetPlayhead()) { this_track_closest_cut = block->out(); } else { break; @@ -420,7 +423,7 @@ void TimeBasedWidget::GoToPrevCut() closest_cut = qMax(closest_cut, this_track_closest_cut); } - SetTimeAndSignal(closest_cut); + GetConnectedNode()->SetPlayhead(closest_cut); } void TimeBasedWidget::GoToNextCut() @@ -437,12 +440,12 @@ void TimeBasedWidget::GoToNextCut() for (Track* track : sequence->GetTracks()) { rational this_track_closest_cut = track->track_length(); - if (this_track_closest_cut <= GetTime()) { + if (this_track_closest_cut <= GetConnectedNode()->GetPlayhead()) { this_track_closest_cut = RATIONAL_MAX; } for (Block* block : track->Blocks()) { - if (block->in() > GetTime()) { + if (block->in() > GetConnectedNode()->GetPlayhead()) { this_track_closest_cut = block->in(); break; } @@ -452,57 +455,51 @@ void TimeBasedWidget::GoToNextCut() } if (closest_cut < RATIONAL_MAX) { - SetTimeAndSignal(closest_cut); + GetConnectedNode()->SetPlayhead(closest_cut); } } void TimeBasedWidget::GoToStart() { if (viewer_node_) { - SetTimeAndSignal(0); + viewer_node_->SetPlayhead(0); } } void TimeBasedWidget::PrevFrame() { if (viewer_node_) { - rational proposed_time = Timecode::snap_time_to_timebase(GetTime() - timebase(), timebase(), Timecode::kCeil); - if (proposed_time == GetTime()) { + rational proposed_time = Timecode::snap_time_to_timebase(GetConnectedNode()->GetPlayhead() - timebase(), timebase(), Timecode::kCeil); + if (proposed_time == GetConnectedNode()->GetPlayhead()) { // Catch rounding error, assume this time is snapped and just subtract a timebase proposed_time -= timebase(); } - SetTimeAndSignal(qMax(rational(0), proposed_time)); + viewer_node_->SetPlayhead(qMax(rational(0), proposed_time)); } } void TimeBasedWidget::NextFrame() { if (viewer_node_) { - rational proposed_time = Timecode::snap_time_to_timebase(GetTime() + timebase(), timebase(), Timecode::kFloor); - if (proposed_time == GetTime()) { + rational proposed_time = Timecode::snap_time_to_timebase(GetConnectedNode()->GetPlayhead() + timebase(), timebase(), Timecode::kFloor); + if (proposed_time == GetConnectedNode()->GetPlayhead()) { // Catch rounding error, assume this time is snapped and just add a timebase proposed_time += timebase(); } - SetTimeAndSignal(proposed_time); + viewer_node_->SetPlayhead(proposed_time); } } void TimeBasedWidget::GoToEnd() { if (viewer_node_) { - SetTimeAndSignal(viewer_node_->GetLength()); + viewer_node_->SetPlayhead(viewer_node_->GetLength()); } } -void TimeBasedWidget::SetTimeAndSignal(const rational &t) -{ - SetTime(t); - emit TimeChanged(t); -} - void TimeBasedWidget::CenterScrollOnPlayhead() { - scrollbar_->setValue(qRound(TimeToScene(ruler_->GetTime())) - scrollbar_->width()/2); + scrollbar_->setValue(qRound(TimeToScene(GetConnectedNode()->GetPlayhead())) - scrollbar_->width()/2); } void TimeBasedWidget::SetAutoSetTimebase(bool e) @@ -603,10 +600,6 @@ void TimeBasedWidget::PageScrollInternal(int screen_position, bool whole_page_sc bool TimeBasedWidget::UserIsDraggingPlayhead() const { - if (ruler_->IsDraggingPlayhead()) { - return true; - } - foreach (TimeBasedView* view, timeline_views_) { if (view->IsDraggingPlayhead()) { return true; @@ -618,12 +611,12 @@ bool TimeBasedWidget::UserIsDraggingPlayhead() const void TimeBasedWidget::SetInAtPlayhead() { - SetPoint(Timeline::kTrimIn, GetTime()); + SetPoint(Timeline::kTrimIn, GetConnectedNode()->GetPlayhead()); } void TimeBasedWidget::SetOutAtPlayhead() { - SetPoint(Timeline::kTrimOut, GetTime()); + SetPoint(Timeline::kTrimOut, GetConnectedNode()->GetPlayhead()); } void TimeBasedWidget::ResetIn() @@ -653,14 +646,14 @@ void TimeBasedWidget::SetMarker() TimelineMarkerList *markers = GetConnectedNode()->GetMarkers(); - if (TimelineMarker *existing = markers->GetMarkerAtTime(GetTime())) { + if (TimelineMarker *existing = markers->GetMarkerAtTime(GetConnectedNode()->GetPlayhead())) { // We already have a marker here, so pop open the edit dialog MarkerPropertiesDialog mpd({existing}, timebase(), this); mpd.exec(); } else { // Create a new marker and place it here int color; - if (TimelineMarker *closest = markers->GetClosestMarkerToTime(GetTime())) { + if (TimelineMarker *closest = markers->GetClosestMarkerToTime(GetConnectedNode()->GetPlayhead())) { // Copy color of closest marker to this time color = closest->color(); } else { @@ -668,7 +661,7 @@ void TimeBasedWidget::SetMarker() color = OLIVE_CONFIG("MarkerColor").toInt(); } - TimelineMarker *marker = new TimelineMarker(color, TimeRange(GetTime(), GetTime())); + TimelineMarker *marker = new TimelineMarker(color, TimeRange(GetConnectedNode()->GetPlayhead(), GetConnectedNode()->GetPlayhead())); if (OLIVE_CONFIG("SetNameWithMarker").toBool()) { MarkerPropertiesDialog mpd({marker}, timebase(), this); @@ -704,8 +697,6 @@ void TimeBasedWidget::ToggleShowAll() w = timeline_views_.first()->width(); } - - toggle_show_all_old_scale_ = GetScale(); toggle_show_all_old_scroll_ = scrollbar_->value(); @@ -721,7 +712,7 @@ void TimeBasedWidget::GoToIn() { if (GetConnectedNode()) { if (GetConnectedNode()->GetWorkArea()->enabled()) { - SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->in()); + GetConnectedNode()->SetPlayhead(GetConnectedNode()->GetWorkArea()->in()); } else { GoToStart(); } @@ -732,7 +723,7 @@ void TimeBasedWidget::GoToOut() { if (GetConnectedNode()) { if (GetConnectedNode()->GetWorkArea()->enabled()) { - SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->out()); + GetConnectedNode()->SetPlayhead(GetConnectedNode()->GetWorkArea()->out()); } else { GoToEnd(); } @@ -746,15 +737,6 @@ void TimeBasedWidget::DeleteSelected() } } -bool TimeBasedWidget::eventFilter(QObject *object, QEvent *event) -{ - if (wheel_passthrough_objects_.contains(object) && event->type() == QEvent::Wheel) { - QCoreApplication::sendEvent(scrollbar(), event); - } - - return false; -} - struct SnapData { rational time; rational movement; @@ -787,7 +769,7 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration std::vector potential_snaps; if (snap_points & kSnapToPlayhead) { - rational playhead_abs_time = GetTime(); + rational playhead_abs_time = GetConnectedNode()->GetPlayhead(); qreal playhead_pos = TimeToScene(playhead_abs_time); AttemptSnap(potential_snaps, screen_pt, playhead_pos, start_times, playhead_abs_time); } @@ -846,7 +828,7 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration } } - if ((snap_points & kSnapToWorkarea) && ruler()->GetWorkArea()) { + if ((snap_points & kSnapToWorkarea) && ruler()->GetWorkArea() && ruler()->GetWorkArea()->enabled()) { const rational &workarea_in = ruler()->GetWorkArea()->in(); const rational &workarea_out = ruler()->GetWorkArea()->out(); @@ -868,7 +850,7 @@ bool TimeBasedWidget::SnapPoint(const std::vector &start_times, ration rational time = key->time(); if (const TimeTargetObject *target = GetKeyframeTimeTarget()) { if (Node *parent = key->parent()) { - time = target->GetAdjustedTime(parent, target->GetTimeTarget(), time, false); + time = target->GetAdjustedTime(parent, target->GetTimeTarget(), time, Node::kTransformTowardsOutput); } } diff --git a/app/widget/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index 347c079c7..51e35dd77 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -41,8 +41,6 @@ class TimeBasedWidget : public TimelineScaledWidget public: TimeBasedWidget(bool ruler_text_visible = true, bool ruler_cache_status_visible = false, QWidget* parent = nullptr); - const rational &GetTime() const; - void ZoomIn(); void ZoomOut(); @@ -60,8 +58,6 @@ public: TimeRuler* ruler() const; - virtual bool eventFilter(QObject* object, QEvent* event) override; - using SnapMask = uint32_t; enum SnapPoints { kSnapToClips = 0x1, @@ -84,8 +80,6 @@ public: virtual bool Paste(); public slots: - void SetTime(const rational &time); - void SetTimebase(const rational& timebase); void SetScale(const double& scale); @@ -144,9 +138,7 @@ protected: virtual void resizeEvent(QResizeEvent *event) override; - void ConnectTimelineView(TimeBasedView* base, bool connect_time_change_event = true); - - void PassWheelEventsToScrollBar(QObject* object); + void ConnectTimelineView(TimeBasedView* base); void SetCatchUpScrollValue(QScrollBar *b, int v, int maximum); void SetCatchUpScrollValue(int v); @@ -172,16 +164,12 @@ 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&); - void TimebaseChanged(const rational&); void ConnectedNodeChanged(ViewerOutput* old, ViewerOutput* now); @@ -228,8 +216,6 @@ private: bool auto_set_timebase_; - QVector wheel_passthrough_objects_; - int scrollbar_start_width_; double scrollbar_start_value_; double scrollbar_start_scale_; @@ -271,6 +257,8 @@ private slots: void ConnectedNodeRemovedFromGraph(); + void PlayheadTimeChanged(const rational &time); + }; } diff --git a/app/widget/timelinewidget/timelineandtrackview.cpp b/app/widget/timelinewidget/timelineandtrackview.cpp index a9ce5bbcd..d5fb5cbb8 100644 --- a/app/widget/timelinewidget/timelineandtrackview.cpp +++ b/app/widget/timelinewidget/timelineandtrackview.cpp @@ -30,7 +30,7 @@ TimelineAndTrackView::TimelineAndTrackView(Qt::Alignment vertical_alignment, QWi { QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); splitter_ = new QSplitter(Qt::Horizontal); splitter_->setChildrenCollapsible(false); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index b68881868..1ca6e3569 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -73,7 +73,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) : { QVBoxLayout* vert_layout = new QVBoxLayout(this); vert_layout->setSpacing(0); - vert_layout->setMargin(0); + vert_layout->setContentsMargins(0, 0, 0, 0); QHBoxLayout* ruler_and_time_layout = new QHBoxLayout(); vert_layout->addLayout(ruler_and_time_layout); @@ -83,7 +83,6 @@ TimelineWidget::TimelineWidget(QWidget *parent) : timecode_label_->SetDisplayType(RationalSlider::kTime); timecode_label_->setVisible(false); timecode_label_->SetMinimum(0); - connect(timecode_label_, &RationalSlider::ValueChanged, this, &TimelineWidget::SetTimeAndSignal); ruler_and_time_layout->addWidget(timecode_label_); ruler_and_time_layout->addWidget(ruler()); @@ -129,7 +128,6 @@ TimelineWidget::TimelineWidget(QWidget *parent) : tools_.append(import_tool_); // Global scrollbar - connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); connect(views_.first()->view()->horizontalScrollBar(), &QScrollBar::rangeChanged, scrollbar(), &QScrollBar::setRange); vert_layout->addWidget(scrollbar()); @@ -144,14 +142,9 @@ TimelineWidget::TimelineWidget(QWidget *parent) : view_splitter_->addWidget(tview); - ConnectTimelineView(view, false); + ConnectTimelineView(view); - connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); - connect(view, &TimelineView::ScaleChanged, this, &TimelineWidget::SetScale); - connect(view, &TimelineView::TimeChanged, this, &TimelineWidget::SetTimeAndSignal); connect(view, &TimelineView::customContextMenuRequested, this, &TimelineWidget::ShowContextMenu); - connect(scrollbar(), &QScrollBar::valueChanged, view->horizontalScrollBar(), &QScrollBar::setValue); - connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, scrollbar(), &QScrollBar::setValue); connect(view, &TimelineView::MousePressed, this, &TimelineWidget::ViewMousePressed); connect(view, &TimelineView::MouseMoved, this, &TimelineWidget::ViewMouseMoved); @@ -163,15 +156,6 @@ TimelineWidget::TimelineWidget(QWidget *parent) : connect(view, &TimelineView::DragDropped, this, &TimelineWidget::ViewDragDropped); connect(tview->splitter(), &QSplitter::splitterMoved, this, &TimelineWidget::UpdateHorizontalSplitters); - - // Connect each view's scroll to each other - foreach (TimelineAndTrackView* other_tview, views_) { - TimelineView* other_view = other_tview->view(); - - if (view != other_view) { - connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, other_view->horizontalScrollBar(), &QScrollBar::setValue); - } - } } // Split viewer 50/50 @@ -200,6 +184,17 @@ TimelineWidget::TimelineWidget(QWidget *parent) : signal_block_change_timer_->setSingleShot(true); connect(signal_block_change_timer_, &QTimer::timeout, this, [this]{ signal_block_change_timer_->stop(); + + if (OLIVE_CONFIG("SelectAlsoSeeks").toBool()) { + rational start = RATIONAL_MAX; + for (Block *b : selected_blocks_) { + start = std::min(start, b->in()); + } + if (start != RATIONAL_MAX) { + GetConnectedNode()->SetPlayhead(start); + } + } + emit BlockSelectionChanged(selected_blocks_); }); } @@ -244,13 +239,34 @@ void TimelineWidget::resizeEvent(QResizeEvent *event) UpdateTimecodeWidthFromSplitters(views_.first()->splitter()); } -void TimelineWidget::TimeChangedEvent(const rational &time) +void TimelineWidget::TimeChangedEvent(const rational &t) { - super::TimeChangedEvent(time); + if (OLIVE_CONFIG("SeekAlsoSelects").toBool()) { + TimelineWidgetSelections sels; - SetViewTime(time); + QVector new_blocks; - timecode_label_->SetValue(time); + for (auto it=sequence()->GetTracks().cbegin(); it!=sequence()->GetTracks().cend(); it++) { + Track *track = *it; + if (track->IsLocked()) { + continue; + } + + Block *b = track->VisibleBlockAtTime(sequence()->GetPlayhead()); + if (!b || dynamic_cast(b)) { + continue; + } + + new_blocks.push_back(b); + sels[track->ToReference()].insert(b->range()); + } + + if (selected_blocks_ != new_blocks) { + selected_blocks_ = new_blocks; + SetSelections(sels, false); + SignalBlockSelectionChange(); + } + } } void TimelineWidget::ScaleChangedEvent(const double &scale) @@ -271,6 +287,10 @@ void TimelineWidget::ConnectNodeEvent(ViewerOutput *n) connect(s, &Sequence::FrameRateChanged, this, &TimelineWidget::FrameRateChanged); connect(s, &Sequence::SampleRateChanged, this, &TimelineWidget::SampleRateChanged); + connect(timecode_label_, &RationalSlider::ValueChanged, s, &Sequence::SetPlayhead); + connect(s, &Sequence::PlayheadChanged, timecode_label_, &RationalSlider::SetValue); + timecode_label_->SetValue(s->GetPlayhead()); + ruler()->SetPlaybackCache(n->video_frame_cache()); SetTimebase(n->GetVideoParams().frame_rate_as_time_base()); @@ -301,6 +321,8 @@ void TimelineWidget::DisconnectNodeEvent(ViewerOutput *n) disconnect(s, &Sequence::FrameRateChanged, this, &TimelineWidget::FrameRateChanged); disconnect(s, &Sequence::SampleRateChanged, this, &TimelineWidget::SampleRateChanged); + disconnect(timecode_label_, &RationalSlider::ValueChanged, s, &Sequence::SetPlayhead); + DeselectAll(); foreach (Track* track, s->GetTracks()) { @@ -371,7 +393,7 @@ void TimelineWidget::SplitAtPlayhead() return; } - const rational &playhead_time = GetTime(); + const rational &playhead_time = GetConnectedNode()->GetPlayhead(); QVector selected_blocks = GetSelectedBlocks(); @@ -383,6 +405,10 @@ void TimelineWidget::SplitAtPlayhead() // Get all blocks at the playhead foreach (Track* track, sequence()->GetTracks()) { + if (track->IsLocked()) { + continue; + } + Block* b = track->BlockContainingTime(playhead_time); if (dynamic_cast(b)) { @@ -448,21 +474,22 @@ void TimelineWidget::DeleteSelected(bool ripple) } QVector selected_list = GetSelectedBlocks(); - QVector blocks_to_delete; - - foreach (Block* b, selected_list) { - blocks_to_delete.append(b); - } // No-op if nothing is selected - if (blocks_to_delete.isEmpty()) { + if (selected_list.isEmpty()) { return; } QVector clips_to_delete; QVector transitions_to_delete; - foreach (Block* b, blocks_to_delete) { + bool all_gaps = true; + + foreach (Block* b, selected_list) { + if (!dynamic_cast(b)) { + all_gaps = false; + } + if (dynamic_cast(b)) { clips_to_delete.append(b); } else if (dynamic_cast(b)) { @@ -470,6 +497,10 @@ void TimelineWidget::DeleteSelected(bool ripple) } } + if (all_gaps) { + ripple = true; + } + MultiUndoCommand* command = new MultiUndoCommand(); // Remove all selections @@ -494,7 +525,7 @@ void TimelineWidget::DeleteSelected(bool ripple) if (ripple) { TimelineRippleDeleteGapsAtRegionsCommand::RangeList range_list; - foreach (Block* b, blocks_to_delete) { + foreach (Block* b, selected_list) { range_list.append({b->track(), b->range()}); new_playhead = qMin(new_playhead, b->in()); } @@ -509,7 +540,7 @@ void TimelineWidget::DeleteSelected(bool ripple) ClearGhosts(); if (ripple_command && ripple_command->HasCommands() && new_playhead != RATIONAL_MAX) { - SetTimeAndSignal(new_playhead); + GetConnectedNode()->SetPlayhead(new_playhead); } } @@ -540,14 +571,14 @@ void TimelineWidget::DecreaseTrackHeight() void TimelineWidget::InsertFootageAtPlayhead(const QVector& footage) { auto command = new MultiUndoCommand(); - import_tool_->PlaceAt(footage, GetTime(), true, command); + import_tool_->PlaceAt(footage, GetConnectedNode()->GetPlayhead(), true, command); Core::instance()->undo_stack()->push(command); } void TimelineWidget::OverwriteFootageAtPlayhead(const QVector &footage) { auto command = new MultiUndoCommand(); - import_tool_->PlaceAt(footage, GetTime(), false, command); + import_tool_->PlaceAt(footage, GetConnectedNode()->GetPlayhead(), false, command); Core::instance()->undo_stack()->push(command); } @@ -710,7 +741,7 @@ void TimelineWidget::DeleteInToOut(bool ripple) false)); if (ripple) { - SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->in()); + GetConnectedNode()->SetPlayhead(GetConnectedNode()->GetWorkArea()->in()); } Core::instance()->undo_stack()->push(command); @@ -791,15 +822,20 @@ void TimelineWidget::RecordingCallback(const QString &filename, const TimeRange ProjectImportTask task(GetConnectedNode()->project()->root(), {filename}); task.Start(); - MultiUndoCommand *import_command = task.GetCommand(); + auto subimport_command = task.GetCommand(); if (task.GetImportedFootage().empty()) { qCritical() << "Failed to import recorded audio file" << filename; + delete subimport_command; } else { - import_tool_->PlaceAt({task.GetImportedFootage().front()}, time.in(), false, import_command, track.index()); - } + subimport_command->redo_now(); - Core::instance()->undo_stack()->pushIfHasChildren(import_command); + auto import_command = new MultiUndoCommand(); + import_command->add_child(subimport_command); + + import_tool_->PlaceAt({task.GetImportedFootage().front()}, time.in(), false, import_command, track.index()); + Core::instance()->undo_stack()->pushIfHasChildren(import_command); + } } void TimelineWidget::EnableRecordingOverlay(const TimelineCoordinate &coord) @@ -1135,7 +1171,7 @@ void TimelineWidget::AddTrack(Track *track) connect(track, &Track::IndexChanged, this, &TimelineWidget::TrackUpdated); connect(track, &Track::IndexChanged, this, &TimelineWidget::TrackIndexChanged); connect(track, &Track::BlocksRefreshed, this, &TimelineWidget::TrackUpdated); - connect(track, &Track::TrackHeightChangedInPixels, this, &TimelineWidget::TrackUpdated); + connect(track, &Track::TrackHeightChanged, this, &TimelineWidget::TrackUpdated); connect(track, &Track::BlockAdded, this, &TimelineWidget::AddBlock); connect(track, &Track::BlockRemoved, this, &TimelineWidget::RemoveBlock); } @@ -1145,7 +1181,7 @@ void TimelineWidget::RemoveTrack(Track *track) disconnect(track, &Track::IndexChanged, this, &TimelineWidget::TrackUpdated); disconnect(track, &Track::IndexChanged, this, &TimelineWidget::TrackIndexChanged); disconnect(track, &Track::BlocksRefreshed, this, &TimelineWidget::TrackUpdated); - disconnect(track, &Track::TrackHeightChangedInPixels, this, &TimelineWidget::TrackUpdated); + disconnect(track, &Track::TrackHeightChanged, this, &TimelineWidget::TrackUpdated); disconnect(track, &Track::BlockAdded, this, &TimelineWidget::AddBlock); disconnect(track, &Track::BlockRemoved, this, &TimelineWidget::RemoveBlock); @@ -1316,14 +1352,6 @@ void TimelineWidget::SetUseAudioTimeUnits(bool use) UpdateViewTimebases(); } -void TimelineWidget::SetViewTime(const rational &time) -{ - for (int i=0;iview()->SetTime(time); - } -} - void TimelineWidget::ToolChanged() { HideSnaps(); @@ -1461,7 +1489,7 @@ void TimelineWidget::CacheClipsInOut() for (Block *b : qAsConst(selected_blocks_)) { if (ClipBlock *clip = dynamic_cast(b)) { if (Node *connected = clip->GetConnectedOutput(clip->kBufferIn)) { - TimeRange adjusted = tto.GetAdjustedTime(this->sequence(), connected, r, true); + TimeRange adjusted = tto.GetAdjustedTime(this->sequence(), connected, r, Node::kTransformTowardsInput); clip->RequestInvalidatedFromConnected(true, adjusted); } } @@ -1609,7 +1637,7 @@ void TimelineWidget::MoveToPlayheadInternal(bool out) } foreach (Block *b, selected_blocks_) { - rational shift_amt = GetTime() - earliest_pts.value(b->track()); + rational shift_amt = GetConnectedNode()->GetPlayhead() - earliest_pts.value(b->track()); rational new_in = b->in() + shift_amt; bool can_shift = true; @@ -1632,7 +1660,7 @@ void TimelineWidget::MoveToPlayheadInternal(bool out) // Shift selections TimelineWidgetSelections new_sel = GetSelections(); for (auto it=new_sel.begin(); it!=new_sel.end(); it++) { - rational track_adj = GetTime() - earliest_pts.value(GetTrackFromReference(it.key()), GetTime()); + rational track_adj = GetConnectedNode()->GetPlayhead() - earliest_pts.value(GetTrackFromReference(it.key()), GetConnectedNode()->GetPlayhead()); if (!track_adj.isNull()) { it.value().shift(track_adj); } @@ -1785,7 +1813,7 @@ void TimelineWidget::RippleTo(Timeline::MovementMode mode) return; } - rational playhead_time = GetTime(); + rational playhead_time = GetConnectedNode()->GetPlayhead(); QVector tracks = GetEditToInfo(playhead_time, mode); @@ -1833,15 +1861,15 @@ void TimelineWidget::RippleTo(Timeline::MovementMode mode) // If we rippled, ump to where new cut is if applicable if (mode == Timeline::kTrimIn) { - SetTimeAndSignal(closest_point_to_playhead); - } else if (mode == Timeline::kTrimOut && closest_point_to_playhead == GetTime()) { - SetTimeAndSignal(playhead_time); + GetConnectedNode()->SetPlayhead(closest_point_to_playhead); + } else if (mode == Timeline::kTrimOut && closest_point_to_playhead == GetConnectedNode()->GetPlayhead()) { + GetConnectedNode()->SetPlayhead(playhead_time); } } void TimelineWidget::EditTo(Timeline::MovementMode mode) { - const rational playhead_time = GetTime(); + const rational playhead_time = GetConnectedNode()->GetPlayhead(); // Get list of unlocked tracks QVector tracks = GetEditToInfo(playhead_time, mode); @@ -1943,10 +1971,10 @@ bool TimelineWidget::PasteInternal(bool insert) command->add_child(new NodeAddCommand(GetConnectedNode()->project(), n)); } - rational paste_start = GetTime(); + rational paste_start = GetConnectedNode()->GetPlayhead(); if (insert) { - rational paste_end = GetTime(); + rational paste_end = paste_start; for (auto it=res.GetLoadData().properties.cbegin(); it!=res.GetLoadData().properties.cend(); it++) { rational length = static_cast(it.key())->length(); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 5161f6d55..4a49bcfb7 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -290,8 +290,8 @@ signals: protected: virtual void resizeEvent(QResizeEvent *event) override; + virtual void TimeChangedEvent(const rational &) override; virtual void TimebaseChangedEvent(const rational &) override; - virtual void TimeChangedEvent(const rational &time) override; virtual void ScaleChangedEvent(const double &) override; virtual void ConnectNodeEvent(ViewerOutput* n) override; @@ -417,8 +417,6 @@ private slots: void SetUseAudioTimeUnits(bool use); - void SetViewTime(const rational &time); - void ToolChanged(); void AddableObjectChanged(); diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index caa0037ed..214056f2c 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -221,7 +221,7 @@ void ImportTool::FootageToGhosts(rational ghost_start, const DraggedFootageData for (auto it=sorted.cbegin(); it!=sorted.cend(); it++) { ViewerOutput* footage = it->first; - if (footage == sequence() || (sequence() && sequence()->OutputsTo(footage, true))) { + if (footage == sequence() || (sequence() && footage->InputsFrom(sequence(), true))) { // Prevent cyclical dependency continue; } diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index 3146142a8..73a89d02c 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -194,12 +194,16 @@ bool TransitionTool::GetBlocksAtCoord(const TimelineCoordinate &coord, ClipBlock return false; } + ClipBlock *adjacent = dynamic_cast(block_at_time->previous()); + if (adjacent) { + tenth_point = std::min(tenth_point, adjacent->length()/10); + } + transition_start_point = block_at_time->in(); trim_mode = Timeline::kTrimIn; - if (cursor_frame < (block_at_time->in() + tenth_point) - && dynamic_cast(block_at_time->previous())) { - other_block = block_at_time->previous(); + if (cursor_frame < (block_at_time->in() + tenth_point) && adjacent) { + other_block = adjacent; } } else { if (static_cast(block_at_time)->out_transition()) { @@ -207,11 +211,15 @@ bool TransitionTool::GetBlocksAtCoord(const TimelineCoordinate &coord, ClipBlock return false; } + ClipBlock *adjacent = dynamic_cast(block_at_time->next()); + if (adjacent) { + tenth_point = std::min(tenth_point, adjacent->length()/10); + } + transition_start_point = block_at_time->out(); trim_mode = Timeline::kTrimOut; - if (cursor_frame > block_at_time->out() - tenth_point - && dynamic_cast(block_at_time->next())) { + if (cursor_frame > block_at_time->out() - tenth_point && adjacent) { other_block = block_at_time->next(); } } diff --git a/app/widget/timelinewidget/trackview/trackview.cpp b/app/widget/timelinewidget/trackview/trackview.cpp index 562c68d55..65bf429ec 100644 --- a/app/widget/timelinewidget/trackview/trackview.cpp +++ b/app/widget/timelinewidget/trackview/trackview.cpp @@ -42,7 +42,7 @@ TrackView::TrackView(Qt::Alignment vertical_alignment, QWidget *parent) : setWidgetResizable(true); QVBoxLayout* layout = new QVBoxLayout(central); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); if (alignment_ == Qt::AlignBottom) { diff --git a/app/widget/timelinewidget/trackview/trackviewitem.cpp b/app/widget/timelinewidget/trackview/trackviewitem.cpp index 6a5ba698e..e0b1eab5d 100644 --- a/app/widget/timelinewidget/trackview/trackviewitem.cpp +++ b/app/widget/timelinewidget/trackview/trackviewitem.cpp @@ -40,7 +40,7 @@ TrackViewItem::TrackViewItem(Track* track, QWidget *parent) : { QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); stack_ = new QStackedWidget(); layout->addWidget(stack_); diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.cpp b/app/widget/timelinewidget/undo/timelineundogeneral.cpp index 634188e65..d46da4fb8 100644 --- a/app/widget/timelinewidget/undo/timelineundogeneral.cpp +++ b/app/widget/timelinewidget/undo/timelineundogeneral.cpp @@ -126,6 +126,9 @@ void TimelineAddTrackCommand::redo() // Add track to sequence track_->setParent(timeline_->GetParentGraph()); + if (timeline_->GetTrackCount() > 0) { + track_->SetTrackHeight(timeline_->GetTrackAt(timeline_->GetTrackCount()-1)->GetTrackHeight()); + } timeline_->ArrayAppend(); Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index 5bad687f6..5554a3677 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -56,6 +56,8 @@ TimelineView::TimelineView(Qt::Alignment vertical_alignment, QWidget *parent) : setBackgroundRole(QPalette::Window); setContextMenuPolicy(Qt::CustomContextMenu); viewport()->setMouseTracking(true); + + SetIsTimelineAxes(true); } void TimelineView::mousePressEvent(QMouseEvent *event) @@ -64,15 +66,8 @@ void TimelineView::mousePressEvent(QMouseEvent *event) QPointF scene_pos = mapToScene(event->pos()); for (auto it=clip_marker_rects_.cbegin(); it!=clip_marker_rects_.cend(); it++) { if (it.value().contains(scene_pos)) { - QObject *p = this->parent(); - while (p) { - if (TimelineWidget *timeline = dynamic_cast(p)) { - timeline->SetTime(it.key()->time().in()); - break; - } - - p = p->parent(); - } + GetViewerNode()->SetPlayhead(it.key()->time().in()); + break; } } @@ -147,61 +142,6 @@ void TimelineView::mouseDoubleClickEvent(QMouseEvent *event) emit MouseDoubleClicked(&timeline_event); } -void TimelineView::wheelEvent(QWheelEvent *event) -{ - if (WheelEventIsAZoomEvent(event)) { - super::wheelEvent(event); - } else { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) - - QPoint angle_delta = event->angleDelta(); - - if (OLIVE_CONFIG("InvertTimelineScrollAxes").toBool() // Check if config is set to invert timeline axes - && event->source() != Qt::MouseEventSynthesizedBySystem) { // Never flip axes on Apple trackpads though - angle_delta = QPoint(angle_delta.y(), angle_delta.x()); - } - - QWheelEvent e( - #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - event->position(), - event->globalPosition(), - #else - event->pos(), - event->globalPos(), - #endif - event->pixelDelta(), - angle_delta, - event->buttons(), - event->modifiers(), - event->phase(), - event->inverted(), - event->source() - ); - -#else - - Qt::Orientation orientation = event->orientation(); - - if (OLIVE_CONFIG("InvertTimelineScrollAxes").toBool()) { - orientation = (orientation == Qt::Horizontal) ? Qt::Vertical : Qt::Horizontal; - } - - QWheelEvent e( - event->pos(), - event->globalPos(), - event->pixelDelta(), - event->angleDelta(), - event->delta(), - orientation, - event->buttons(), - event->modifiers() - ); -#endif - - super::wheelEvent(&e); - } -} - void TimelineView::dragEnterEvent(QDragEnterEvent *event) { TimelineViewMouseEvent timeline_event = CreateMouseEvent(event->pos(), Qt::NoButton, event->keyboardModifiers()); @@ -353,7 +293,7 @@ void TimelineView::drawForeground(QPainter *painter, const QRectF &rect) int x = TimeToScene(recording_coord_.GetFrame()); painter->drawRect(x, GetTrackY(recording_coord_.GetTrack().index()), - TimeToScene(GetTime()) - x, GetTrackHeight(recording_coord_.GetTrack().index())); + TimeToScene(GetViewerNode()->GetPlayhead()) - x, GetTrackHeight(recording_coord_.GetTrack().index())); } // Draw standard TimelineViewBase things (such as playhead) @@ -478,8 +418,17 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q QColor shadow_color = block->is_enabled() ? block->color().toQColor().darker() : QColor(Qt::darkGray).darker(); - if (r.width() <= 3) { - painter->fillRect(r, shadow_color); + const qreal MINIMUM_RECT_WIDTH = 2; + const qreal MINIMUM_DETAIL_WIDTH = 8; + + if (r.width() <= MINIMUM_RECT_WIDTH) { + if (!foreground) { + // Just draw a green background + // Width is likely fractional, so we ceil it and add 1 to ensure the entire width of the + // rect is painted + r.setWidth(std::ceil(r.width())+1); + painter->fillRect(r, shadow_color); + } } else { QFontMetrics fm = fontMetrics(); int text_height = fm.height(); @@ -489,19 +438,21 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q if (foreground) { painter->setBrush(Qt::NoBrush); - QString using_label = block->GetLabelOrName(); + if (r.width() > MINIMUM_DETAIL_WIDTH) { + 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); - painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignTop, using_label); + QRectF text_rect = r.adjusted(text_padding, text_padding, -text_padding, -text_padding); + painter->setPen(block->is_enabled() ? ColorCoding::GetUISelectorColor(block->color()) : Qt::lightGray); + painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignTop, using_label); - if (block->HasLinks()) { - int text_width = qMin(qRound(text_rect.width()), - QtUtils::QFontMetricsWidth(fm, using_label)); + if (block->HasLinks()) { + int text_width = qMin(qRound(text_rect.width()), + QtUtils::QFontMetricsWidth(fm, using_label)); - int underline_y = text_rect.y() + text_height; + int underline_y = text_rect.y() + text_height; - painter->drawLine(text_rect.x(), underline_y, text_width + text_rect.x(), underline_y); + painter->drawLine(text_rect.x(), underline_y, text_width + text_rect.x(), underline_y); + } } qreal line_bottom = block_top+block_height-1; @@ -518,177 +469,179 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q painter->setBrush(block->is_enabled() ? block->brush(block_top, block_top + block_height) : Qt::gray); painter->drawRect(r); - if (ClipBlock *clip = dynamic_cast(block)) { - QRect preview_rect = r.toRect(); + if (r.width() > MINIMUM_DETAIL_WIDTH) { + if (ClipBlock *clip = dynamic_cast(block)) { + QRect preview_rect = r.toRect(); - // Draw clip thumbnails - if (clip->GetTrackType() == Track::kVideo - && OLIVE_CONFIG("TimelineThumbnailMode").toInt() != Timeline::kThumbnailOff - && preview_rect.height() > r.height()/3) { - if (const FrameHashCache *thumbs = clip->thumbnails()) { - // Start thumbnails underneath clip name - preview_rect.adjust(0, text_total_height, 0, 0); + // Draw clip thumbnails + if (clip->GetTrackType() == Track::kVideo + && OLIVE_CONFIG("TimelineThumbnailMode").toInt() != Timeline::kThumbnailOff + && preview_rect.height() > r.height()/3) { + if (const FrameHashCache *thumbs = clip->thumbnails()) { + // Start thumbnails underneath clip name + preview_rect.adjust(0, text_total_height, 0, 0); - QRect thumb_rect; - painter->setRenderHint(QPainter::SmoothPixmapTransform); - painter->setClipRect(preview_rect); + QRect thumb_rect; + painter->setRenderHint(QPainter::SmoothPixmapTransform); + painter->setClipRect(preview_rect); - if (OLIVE_CONFIG("TimelineThumbnailMode") == Timeline::kThumbnailOn) { + if (OLIVE_CONFIG("TimelineThumbnailMode") == Timeline::kThumbnailOn) { + + Sequence *s = clip->track()->sequence(); + int width = s->GetVideoParams().width(); + int height = s->GetVideoParams().height(); + int start; + if (height > 0) { // Prevent divide by zero/invalid params + double scale = double(preview_rect.height())/double(height); + thumb_rect.setWidth(width * scale); + start = (((preview_rect.left() - int(qFloor(block_in))) / thumb_rect.width()) * thumb_rect.width()) + qFloor(block_in); + } else { + start = preview_rect.left(); + } + + for (int i=start; iparent()->GetVideoParams().frame_rate_as_time_base()) + media_in; + DrawThumbnail(painter, thumbs, time_here, i, preview_rect, &thumb_rect); + } - Sequence *s = clip->track()->sequence(); - int width = s->GetVideoParams().width(); - int height = s->GetVideoParams().height(); - int start; - if (height > 0) { // Prevent divide by zero/invalid params - double scale = double(preview_rect.height())/double(height); - thumb_rect.setWidth(width * scale); - start = (((preview_rect.left() - int(qFloor(block_in))) / thumb_rect.width()) * thumb_rect.width()) + qFloor(block_in); } else { - start = preview_rect.left(); + + rational time = clip->media_range().in(); + time = Timecode::snap_time_to_timebase(time, thumbs->GetTimebase(), Timecode::kFloor); + DrawThumbnail(painter, thumbs, time, block_left, preview_rect, &thumb_rect); + } - for (int i=start; iparent()->GetVideoParams().frame_rate_as_time_base()) + media_in; - DrawThumbnail(painter, thumbs, time_here, i, preview_rect, &thumb_rect); - } - - } else { - - rational time = clip->media_range().in(); - time = Timecode::snap_time_to_timebase(time, thumbs->GetTimebase(), Timecode::kFloor); - DrawThumbnail(painter, thumbs, time, block_left, preview_rect, &thumb_rect); + painter->setClipping(false); } - - painter->setClipping(false); - } - } - // Draw waveform - if (clip->GetTrackType() == Track::kAudio - && OLIVE_CONFIG("TimelineWaveformMode").toInt() == Timeline::kWaveformsEnabled) { - if (const AudioWaveformCache *wave = clip->waveform()) { - rational waveform_start = SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base()) + media_in; - painter->setPen(shadow_color); + // Draw waveform + if (clip->GetTrackType() == Track::kAudio + && OLIVE_CONFIG("TimelineWaveformMode").toInt() == Timeline::kWaveformsEnabled) { + if (const AudioWaveformCache *wave = clip->waveform()) { + rational waveform_start = SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base()) + media_in; + painter->setPen(shadow_color); - wave->Draw(painter, preview_rect, this->GetScale(), waveform_start); + wave->Draw(painter, preview_rect, this->GetScale(), waveform_start); + } } - } - // Draw zebra stripes and markers - if (clip->connected_viewer()) { - if (!clip->connected_viewer()->GetLength().isNull()) { - painter->setPen(shadow_color); + // Draw zebra stripes and markers + if (clip->connected_viewer()) { + if (!clip->connected_viewer()->GetLength().isNull()) { + painter->setPen(shadow_color); - if (clip->media_in() < 0) { - qreal zebra_right = TimeToScene(clip->in() - clip->media_in()); + if (clip->media_in() < 0) { + 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)); + switch (clip->loop_mode()) { + case LoopMode::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 LoopMode::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 LoopMode::kLoopModeClamp: + painter->drawLine(zebra_right, block_top, zebra_right, block_top + block_height); + break; } - 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); + } + + if (clip->length() + clip->media_in() > clip->connected_viewer()->GetLength()) { + qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength())); + switch (clip->loop_mode()) { + case LoopMode::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 LoopMode::kLoopModeLoop: + for (qreal i=zebra_left; iconnected_viewer()->GetLength())) { + painter->drawLine(i, block_top, i, block_top + block_height); + } + break; + case LoopMode::kLoopModeClamp: + painter->drawLine(zebra_left, block_top, zebra_left, block_top + block_height); + break; } - 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()) { - qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength())); - 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)); + TimelineMarkerList *marker_list = clip->connected_viewer()->GetMarkers(); + if (!marker_list->empty()) { + + clip_marker_rects_.clear(); + + for (auto it=marker_list->cbegin(); it!=marker_list->cend(); it++) { + TimelineMarker *marker = *it; + // Make sure marker is within In/Out points of the clip + 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, -1, GetScale(), false); + clip_marker_rects_.insert(marker, marker_rect); + painter->setClipping(false); } - 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()->GetMarkers(); - if (!marker_list->empty()) { - - clip_marker_rects_.clear(); - - for (auto it=marker_list->cbegin(); it!=marker_list->cend(); it++) { - TimelineMarker *marker = *it; - // Make sure marker is within In/Out points of the clip - 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, -1, GetScale(), false); - clip_marker_rects_.insert(marker, marker_rect); - painter->setClipping(false); - } + if (const FrameHashCache *cache = clip->connected_video_cache()) { + if (cache->HasValidatedRanges()) { + QRect cache_rect = r.adjusted(0, r.height() - PlaybackCache::GetCacheIndicatorHeight(), 0, 0).toRect(); + cache->Draw(painter, clip->media_in(), GetScale(), cache_rect); } } } - if (const FrameHashCache *cache = clip->connected_video_cache()) { - if (cache->HasValidatedRanges()) { - QRect cache_rect = r.adjusted(0, r.height() - PlaybackCache::GetCacheIndicatorHeight(), 0, 0).toRect(); - cache->Draw(painter, clip->media_in(), GetScale(), cache_rect); + // For transitions, show lines representing a transition + if (TransitionBlock* transition = dynamic_cast(block)) { + QVector lines; + + if (transition->connected_in_block()) { + lines.append(QLineF(r.bottomLeft(), r.topRight())); } - } - } - // For transitions, show lines representing a transition - if (TransitionBlock* transition = dynamic_cast(block)) { - QVector lines; + if (transition->connected_out_block()) { + lines.append(QLineF(r.topLeft(), r.bottomRight())); + } - if (transition->connected_in_block()) { - lines.append(QLineF(r.bottomLeft(), r.topRight())); + painter->setPen(shadow_color); + painter->drawLines(lines); } - if (transition->connected_out_block()) { - lines.append(QLineF(r.topLeft(), r.bottomRight())); + if (transition_overlay_out_ == block || transition_overlay_in_ == block) { + QRectF transition_overlay_rect = r; + + qreal transition_overlay_width = TimeToScene(block->length()) * 0.5; + if (transition_overlay_out_ && transition_overlay_in_) { + // This is a dual transition, use the smallest width + Block *other_block = (transition_overlay_out_ == block) ? transition_overlay_in_ : transition_overlay_out_; + + qreal other_width = TimeToScene(other_block->length()) * 0.5; + + transition_overlay_width = qMin(transition_overlay_width, other_width); + } + + if (transition_overlay_out_ == block) { + transition_overlay_rect.setLeft(transition_overlay_rect.right() - transition_overlay_width); + } else { + transition_overlay_rect.setRight(transition_overlay_rect.left() + transition_overlay_width); + } + + painter->setPen(Qt::NoPen); + painter->setBrush(QColor(0, 0, 0, 64)); + + painter->drawRect(transition_overlay_rect); } - - painter->setPen(shadow_color); - painter->drawLines(lines); - } - - if (transition_overlay_out_ == block || transition_overlay_in_ == block) { - QRectF transition_overlay_rect = r; - - qreal transition_overlay_width = TimeToScene(block->length()) * 0.5; - if (transition_overlay_out_ && transition_overlay_in_) { - // This is a dual transition, use the smallest width - Block *other_block = (transition_overlay_out_ == block) ? transition_overlay_in_ : transition_overlay_out_; - - qreal other_width = TimeToScene(other_block->length()) * 0.5; - - transition_overlay_width = qMin(transition_overlay_width, other_width); - } - - if (transition_overlay_out_ == block) { - transition_overlay_rect.setLeft(transition_overlay_rect.right() - transition_overlay_width); - } else { - transition_overlay_rect.setRight(transition_overlay_rect.left() + transition_overlay_width); - } - - painter->setPen(Qt::NoPen); - painter->setBrush(QColor(0, 0, 0, 64)); - - painter->drawRect(transition_overlay_rect); } } } @@ -782,10 +735,22 @@ int TimelineView::GetTrackY(int track_index) const int TimelineView::GetTrackHeight(int track_index) const { - if (!connected_track_list_ || track_index >= connected_track_list_->GetTrackCount()) { + if (!connected_track_list_ || connected_track_list_->GetTrackCount() == 0) { + // Handle null or empty track list return Track::GetDefaultTrackHeightInPixels(); } + if (track_index >= connected_track_list_->GetTrackCount()) { + // Handle new track at the end of the list + return connected_track_list_->GetTrackAt(connected_track_list_->GetTrackCount()-1)->GetTrackHeightInPixels(); + } + + if (track_index < 0) { + // Handle new track at the beginning of the list + return connected_track_list_->GetTrackAt(0)->GetTrackHeightInPixels(); + } + + // Track definitely exists, return its actual height return connected_track_list_->GetTrackAt(track_index)->GetTrackHeightInPixels(); } diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index 6255af2da..a1e1698a9 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -90,8 +90,6 @@ protected: virtual void mouseReleaseEvent(QMouseEvent *event) override; virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void wheelEvent(QWheelEvent* event) override; - virtual void dragEnterEvent(QDragEnterEvent *event) override; virtual void dragMoveEvent(QDragMoveEvent *event) override; virtual void dragLeaveEvent(QDragLeaveEvent *event) override; diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index 8dc21dace..d40015680 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -63,6 +63,8 @@ SeekableWidget::SeekableWidget(QWidget* parent) : setMouseTracking(true); selection_manager_.SetSnapMask(TimeBasedWidget::kSnapAll); + + SetIsTimelineAxes(true); } void SeekableWidget::SetMarkers(TimelineMarkerList *markers) @@ -149,7 +151,7 @@ bool SeekableWidget::PasteMarkers() for (auto it=markers.cbegin(); it!=markers.cend(); it++) { min = std::min(min, (*it)->time().in()); } - min -= GetTime(); + min -= GetViewerNode()->GetPlayhead(); for (auto it=markers.cbegin(); it!=markers.cend(); it++) { TimelineMarker *m = *it; @@ -177,6 +179,10 @@ void SeekableWidget::mousePressEvent(QMouseEvent *event) if (HandPress(event)) { return; + } else if (event->modifiers() & Qt::ControlModifier) { + selection_manager_.RubberBandStart(event); + } else if (marker_editing_enabled_ && (initial = selection_manager_.MousePress(event))) { + selection_manager_.DragStart(initial, event); } else if (resize_item_) { // Handle selection, even though we won't be using it for dragging if (!(event->modifiers() & Qt::ShiftModifier)) { @@ -187,8 +193,6 @@ void SeekableWidget::mousePressEvent(QMouseEvent *event) } dragging_ = true; resize_start_ = mapToScene(event->pos()); - } 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()); dragging_ = true; @@ -201,6 +205,9 @@ void SeekableWidget::mouseMoveEvent(QMouseEvent *event) { if (HandMove(event)) { return; + } else if (selection_manager_.IsRubberBanding()) { + selection_manager_.RubberBandMove(event); + viewport()->update(); } else if (selection_manager_.IsDragging()) { selection_manager_.DragMove(event); } else if (dragging_) { @@ -212,10 +219,13 @@ void SeekableWidget::mouseMoveEvent(QMouseEvent *event) } } else { // Look for resize points - if (FindResizeHandle(event)) { + if (!last_playhead_shape_.containsPoint(event->pos(), Qt::OddEvenFill) + && !selection_manager_.GetObjectAtPoint(event->pos()) + && FindResizeHandle(event)) { setCursor(Qt::SizeHorCursor); } else { unsetCursor(); + ClearResizeHandle(); } } } @@ -226,6 +236,11 @@ void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) return; } + if (selection_manager_.IsRubberBanding()) { + selection_manager_.RubberBandStop(); + return; + } + if (selection_manager_.IsDragging()) { MultiUndoCommand *command = new MultiUndoCommand(); selection_manager_.DragStop(command); @@ -379,10 +394,8 @@ void SeekableWidget::SeekToScenePoint(qreal scene) playhead_time += movement; } - if (playhead_time != GetTime()) { - SetTime(playhead_time); - - emit TimeChanged(playhead_time); + if (playhead_time != GetViewerNode()->GetPlayhead()) { + GetViewerNode()->SetPlayhead(playhead_time); } } @@ -415,16 +428,16 @@ void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) int half_text_height = text_height() / 3; - QPoint points[] = { + last_playhead_shape_ = QPolygon({ QPoint(x, y), QPoint(x - half_width, y - half_text_height), QPoint(x - half_width, y - text_height()), QPoint(x + 1 + half_width, y - text_height()), QPoint(x + 1 + half_width, y - half_text_height), QPoint(x + 1, y), - }; + }); - p->drawPolygon(points, 6); + p->drawPolygon(last_playhead_shape_); p->setRenderHint(QPainter::Antialiasing, false); } @@ -472,8 +485,7 @@ bool SeekableWidget::FindResizeHandle(QMouseEvent *event) return false; } - resize_item_ = nullptr; - resize_mode_ = kResizeNone; + ClearResizeHandle(); QPointF scene = mapToScene(event->pos()); const int border = 10; @@ -481,7 +493,7 @@ bool SeekableWidget::FindResizeHandle(QMouseEvent *event) rational max = SceneToTimeNoGrid(scene.x() + border); // Test for workarea - if (workarea_) { + if (workarea_ && workarea_->enabled()) { if (workarea_->in() >= min && workarea_->in() < max) { resize_mode_ = kResizeIn; } else if (workarea_->out() >= min && workarea_->out() < max) { @@ -521,6 +533,12 @@ bool SeekableWidget::FindResizeHandle(QMouseEvent *event) return resize_item_; } +void SeekableWidget::ClearResizeHandle() +{ + resize_item_ = nullptr; + resize_mode_ = kResizeNone; +} + void SeekableWidget::DragResizeHandle(const QPointF &scene) { qreal diff = scene.x() - resize_start_.x(); diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h index ef90f872e..47e730542 100644 --- a/app/widget/timeruler/seekablewidget.h +++ b/app/widget/timeruler/seekablewidget.h @@ -47,7 +47,7 @@ public: void SetMarkers(TimelineMarkerList *markers); void SetWorkArea(TimelineWorkArea *workarea); - bool IsDraggingPlayhead() const + virtual bool IsDraggingPlayhead() const override { return dragging_; } @@ -125,6 +125,8 @@ private: bool FindResizeHandle(QMouseEvent *event); + void ClearResizeHandle(); + void DragResizeHandle(const QPointF &scene_pos); void CommitResizeHandle(); @@ -153,6 +155,8 @@ private: bool marker_editing_enabled_; + QPolygon last_playhead_shape_; + private slots: void SetMarkerColor(int c); diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index fab83276a..3d3a00808 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -102,8 +102,8 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) // Draw timeline points if connected int marker_height = TimelineMarker::GetMarkerHeight(p->fontMetrics()); - DrawMarkers(p, marker_height); DrawWorkArea(p); + DrawMarkers(p, marker_height); double width_of_frame = timebase_dbl() * GetScale(); double width_of_second = 0; @@ -267,7 +267,7 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) } // Draw the playhead if it's on screen at the moment - int playhead_pos = TimeToScene(GetTime()); + int playhead_pos = TimeToScene(GetViewerNode()->GetPlayhead()); p->setPen(Qt::NoPen); p->setBrush(PLAYHEAD_COLOR); DrawPlayhead(p, playhead_pos, line_bottom); diff --git a/app/widget/timetarget/timetarget.cpp b/app/widget/timetarget/timetarget.cpp index 8662999e5..3262b7a0a 100644 --- a/app/widget/timetarget/timetarget.cpp +++ b/app/widget/timetarget/timetarget.cpp @@ -28,16 +28,23 @@ TimeTargetObject::TimeTargetObject() : { } -Node *TimeTargetObject::GetTimeTarget() const +ViewerOutput *TimeTargetObject::GetTimeTarget() const { return time_target_; } -void TimeTargetObject::SetTimeTarget(Node *target) +void TimeTargetObject::SetTimeTarget(ViewerOutput *target) { - time_target_ = target; + if (time_target_) { + TimeTargetDisconnectEvent(time_target_); + } + time_target_ = target; TimeTargetChangedEvent(time_target_); + + if (time_target_) { + TimeTargetConnectEvent(time_target_); + } } void TimeTargetObject::SetPathIndex(int index) @@ -45,28 +52,22 @@ void TimeTargetObject::SetPathIndex(int index) path_index_ = index; } -rational TimeTargetObject::GetAdjustedTime(Node* from, Node* to, const rational &r, bool input_direction) const +rational TimeTargetObject::GetAdjustedTime(Node* from, Node* to, const rational &r, Node::TransformTimeDirection dir) const { if (!from || !to) { return r; } - return GetAdjustedTime(from, to, TimeRange(r, r), input_direction).in(); + return GetAdjustedTime(from, to, TimeRange(r, r), dir).in(); } -TimeRange TimeTargetObject::GetAdjustedTime(Node* from, Node* to, const TimeRange &r, bool input_direction) const +TimeRange TimeTargetObject::GetAdjustedTime(Node* from, Node* to, const TimeRange &r, Node::TransformTimeDirection dir) const { if (!from || !to) { return r; } - QVector adjusted = from->TransformTimeTo(r, to, input_direction); - - if (adjusted.isEmpty()) { - return r; - } - - return adjusted.at(path_index_); + return from->TransformTimeTo(r, to, dir, path_index_); } /*int TimeTargetObject::GetNumberOfPathAdjustments(Node* from, NodeParam::Type direction) const diff --git a/app/widget/timetarget/timetarget.h b/app/widget/timetarget/timetarget.h index f1b2aa285..5f11fc0f1 100644 --- a/app/widget/timetarget/timetarget.h +++ b/app/widget/timetarget/timetarget.h @@ -21,7 +21,7 @@ #ifndef TIMETARGETOBJECT_H #define TIMETARGETOBJECT_H -#include "node/node.h" +#include "node/output/viewer/viewer.h" namespace olive { @@ -30,21 +30,23 @@ class TimeTargetObject public: TimeTargetObject(); - Node* GetTimeTarget() const; - void SetTimeTarget(Node* target); + ViewerOutput* GetTimeTarget() const; + void SetTimeTarget(ViewerOutput* target); void SetPathIndex(int index); - rational GetAdjustedTime(Node* from, Node* to, const rational& r, bool input_direction) const; - TimeRange GetAdjustedTime(Node* from, Node* to, const TimeRange& r, bool input_direction) const; + rational GetAdjustedTime(Node* from, Node* to, const rational& r, Node::TransformTimeDirection dir) const; + TimeRange GetAdjustedTime(Node* from, Node* to, const TimeRange& r, Node::TransformTimeDirection dir) const; //int GetNumberOfPathAdjustments(Node* from, NodeParam::Type direction) const; protected: - virtual void TimeTargetChangedEvent(Node* ){} + virtual void TimeTargetDisconnectEvent(ViewerOutput *){} + virtual void TimeTargetChangedEvent(ViewerOutput *){} + virtual void TimeTargetConnectEvent(ViewerOutput *){} private: - Node* time_target_; + ViewerOutput* time_target_; int path_index_; diff --git a/app/widget/toolbar/toolbar.cpp b/app/widget/toolbar/toolbar.cpp index 900e6532b..50989d68e 100644 --- a/app/widget/toolbar/toolbar.cpp +++ b/app/widget/toolbar/toolbar.cpp @@ -38,7 +38,7 @@ Toolbar::Toolbar(QWidget *parent) : super(parent) { layout_ = new FlowLayout(this); - layout_->setMargin(0); + layout_->setContentsMargins(0, 0, 0, 0); // Create standard tool buttons btn_pointer_tool_ = CreateToolButton(Tool::kPointer); diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 10a4c85eb..8ad081e6f 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -96,7 +96,7 @@ void AudioWaveformView::drawForeground(QPainter *p, const QRectF &rect) // Draw playhead p->setPen(PLAYHEAD_COLOR); - int playhead_x = TimeToScene(GetTime()); + int playhead_x = TimeToScene(GetViewerNode()->GetPlayhead()); p->drawLine(playhead_x, 0, playhead_x, height()); } diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index b643f6740..05373b0f2 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -56,25 +56,6 @@ void FootageViewerWidget::ResetWorkArea() } } -void FootageViewerWidget::ConnectNodeEvent(ViewerOutput *n) -{ - super::ConnectNodeEvent(n); - - IgnoreNextScrubEvent(); - SetTime(cached_timestamps_.value(n, 0)); -} - -void FootageViewerWidget::DisconnectNodeEvent(ViewerOutput *n) -{ - // Cache timestamp in case this footage is opened again later - cached_timestamps_.insert(n, GetTime()); - - super::DisconnectNodeEvent(n); - - IgnoreNextScrubEvent(); - SetTime(0); -} - void FootageViewerWidget::StartFootageDragInternal(bool enable_video, bool enable_audio) { if (!GetConnectedNode()) { diff --git a/app/widget/viewer/footageviewer.h b/app/widget/viewer/footageviewer.h index de98866c8..802e6e4d8 100644 --- a/app/widget/viewer/footageviewer.h +++ b/app/widget/viewer/footageviewer.h @@ -35,16 +35,9 @@ public: void OverrideWorkArea(const TimeRange &r); void ResetWorkArea(); -protected: - virtual void ConnectNodeEvent(ViewerOutput *) override; - - virtual void DisconnectNodeEvent(ViewerOutput *) override; - private: void StartFootageDragInternal(bool enable_video, bool enable_audio); - QHash cached_timestamps_; - TimelineWorkArea *override_workarea_; private slots: diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 3beb3f44e..c461743b9 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -82,7 +82,7 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) : { // Set up main layout QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); // Create main OpenGL-based view and sizer sizer_ = new ViewerSizer(); @@ -113,8 +113,7 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) : // Create waveform view when audio is connected and video isn't waveform_view_ = new AudioWaveformView(); - ConnectTimelineView(waveform_view_, true); - PassWheelEventsToScrollBar(waveform_view_); + ConnectTimelineView(waveform_view_); layout->addWidget(waveform_view_); // Create time ruler @@ -122,8 +121,6 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) : // Create scrollbar layout->addWidget(scrollbar()); - connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); - connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &AudioWaveformView::SetScroll); // Create lower controls controls_ = new PlaybackControls(); @@ -135,7 +132,6 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) : connect(controls_, &PlaybackControls::NextFrameClicked, this, &ViewerWidget::NextFrame); connect(controls_, &PlaybackControls::BeginClicked, this, &ViewerWidget::GoToStart); connect(controls_, &PlaybackControls::EndClicked, this, &ViewerWidget::GoToEnd); - connect(controls_, &PlaybackControls::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); layout->addWidget(controls_); // FIXME: Magic number @@ -169,6 +165,9 @@ ViewerWidget::~ViewerWidget() foreach (ViewerWindow* window, windows) { delete window; } + + delete display_widget_; + display_widget_ = nullptr; } void ViewerWidget::TimeChangedEvent(const rational &time) @@ -182,7 +181,6 @@ void ViewerWidget::TimeChangedEvent(const rational &time) } controls_->SetTime(time); - waveform_view_->SetTime(time); if (GetConnectedNode() && last_time_ != time) { if (!IsPlaying()) { @@ -216,6 +214,8 @@ void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); connect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode); + connect(controls_, &PlaybackControls::TimeChanged, n, &ViewerOutput::SetPlayhead); + VideoParams vp = n->GetVideoParams(); InterlacingChangedSlot(vp.interlacing()); @@ -260,6 +260,8 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange); disconnect(n, &ViewerOutput::TextureInputChanged, this, &ViewerWidget::UpdateWaveformViewFromMode); + disconnect(controls_, &PlaybackControls::TimeChanged, n, &ViewerOutput::SetPlayhead); + timeline_selected_blocks_.clear(); node_view_selected_.clear(); if (multicam_panel_) { @@ -386,7 +388,7 @@ void ViewerWidget::SetFullScreen(QScreen *screen) (*vw->display_widget()->queue()) = *playback_devices_.first()->queue(); if (IsPlaying()) { - vw->display_widget()->Play(GetTimestamp(), playback_speed_, timebase()); + vw->display_widget()->Play(GetTimestamp(), playback_speed_, timebase(), true); } windows_.insert(screen, vw); @@ -417,7 +419,7 @@ void ViewerWidget::SetGizmos(Node *node) void ViewerWidget::StartCapture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track) { - SetTimeAndSignal(time.in()); + GetConnectedNode()->SetPlayhead(time.in()); ArmForRecording(); recording_callback_ = source; @@ -480,7 +482,7 @@ void ViewerWidget::SetEmptyImage() void ViewerWidget::UpdateAutoCacher() { - auto_cacher_->SetPlayhead(GetTime()); + auto_cacher_->SetPlayhead(GetConnectedNode()->GetPlayhead()); } void ViewerWidget::DecrementPrequeuedAudio() @@ -523,7 +525,7 @@ void ViewerWidget::CreateAddableAt(const QRectF &f) Track::Type type = Track::kVideo; int track_index = -1; TrackList *list = s->track_list(type); - const rational &in = GetTime(); + const rational &in = GetConnectedNode()->GetPlayhead(); rational length = OLIVE_CONFIG("DefaultStillLength").value(); rational out = in + length; @@ -597,7 +599,7 @@ 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) { + if (next_time > GetConnectedNode()->GetPlayhead() + RenderManager::kDryRunInterval) { QTimer::singleShot(timebase().toDouble() / playback_speed_, this, &ViewerWidget::RequestNextDryRun); } else { RenderTicketWatcher *watcher = new RenderTicketWatcher(this); @@ -612,12 +614,14 @@ void ViewerWidget::RequestNextDryRun() void ViewerWidget::SaveFrameAsImage() { - Core::instance()->OpenExportDialogForViewer(GetConnectedNode(), GetTime(), true); + Core::instance()->OpenExportDialogForViewer(GetConnectedNode(), true); } void ViewerWidget::DetectMulticamNodeNow() { - DetectMulticamNode(GetTime()); + if (GetConnectedNode()) { + DetectMulticamNode(GetConnectedNode()->GetPlayhead()); + } } void ViewerWidget::CloseAudioProcessor() @@ -700,6 +704,12 @@ void ViewerWidget::DetectMulticamNode(const rational &time) } } +bool ViewerWidget::IsVideoVisible() const +{ + return GetConnectedNode()->GetVideoParams().video_type() != VideoParams::kVideoTypeStill + && (display_widget_->isVisible() || !windows_.isEmpty()); +} + void ViewerWidget::UpdateWaveformViewFromMode() { bool prefer_waveform = ShouldForceWaveform(); @@ -828,7 +838,7 @@ void ViewerWidget::QueueStarved() queue_starved_start_ = now; } else if (now > queue_starved_start_ + kMaximumWaitTimeMs) { if (first_requeue_watcher_) { - if (GetTime() + kMaximumWaitTime < first_requeue_watcher_->property("time").value()) { + if (GetConnectedNode()->GetPlayhead() + kMaximumWaitTime < first_requeue_watcher_->property("time").value()) { // We still have time return; } @@ -874,7 +884,7 @@ void ViewerWidget::UpdateTextureFromNode() return; } - rational time = GetTime(); + rational time = GetConnectedNode()->GetPlayhead(); bool frame_exists_at_time = FrameExistsAtTime(time); bool frame_might_be_still = ViewerMightBeAStill(); @@ -933,11 +943,11 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) // If the playhead is beyond the end, restart at 0 if (!recording_) { rational last_frame = GetConnectedNode()->GetLength() - timebase(); - if (!in_to_out_only && GetTime() >= last_frame) { + if (!in_to_out_only && GetConnectedNode()->GetPlayhead() >= last_frame) { if (speed > 0) { - SetTimeAndSignal(0); + GetConnectedNode()->SetPlayhead(0); } else { - SetTimeAndSignal(last_frame); + GetConnectedNode()->SetPlayhead(last_frame); } } } @@ -952,7 +962,7 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) queue_starved_start_ = 0; // Attempt to fill playback queue - if (display_widget_->isVisible() || !windows_.isEmpty()) { + if (IsVideoVisible()) { prequeue_length_ = DeterminePlaybackQueueSize(); if (prequeue_length_ > 0) { @@ -977,7 +987,7 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) static const int prequeue_count = 2; prequeuing_audio_ = prequeue_count; // Queue two buffers ahead of time - audio_playback_queue_time_ = GetTime(); + audio_playback_queue_time_ = GetConnectedNode()->GetPlayhead(); for (int i=0; iSetTicket(auto_cacher_->GetRangeOfAudio(TimeRange(GetTime(), GetTime() + interval))); + watcher->SetTicket(auto_cacher_->GetRangeOfAudio(TimeRange(GetConnectedNode()->GetPlayhead(), GetConnectedNode()->GetPlayhead() + interval))); } } } @@ -1149,7 +1159,7 @@ RenderTicketPtr ViewerWidget::GetFrame(const rational &t) // Frame has been cached, grab the frame RenderTicketPtr ticket = std::make_shared(); ticket->setProperty("time", QVariant::fromValue(t)); - QtConcurrent::run(ViewerWidget::DecodeCachedImage, ticket, GetConnectedNode()->video_frame_cache()->GetCacheDirectory(), GetConnectedNode()->video_frame_cache()->GetUuid(), Timecode::time_to_timestamp(t, timebase(), Timecode::kFloor)); + QtConcurrent::run(static_cast(ViewerWidget::DecodeCachedImage), ticket, GetConnectedNode()->video_frame_cache()->GetCacheDirectory(), GetConnectedNode()->video_frame_cache()->GetUuid(), Timecode::time_to_timestamp(t, timebase(), Timecode::kFloor)); return ticket; } } @@ -1173,13 +1183,13 @@ void ViewerWidget::FinishPlayPreprocess() prequeued_audio_.clear(); AudioMonitor::StartWaveformOnAll(GetConnectedNode()->GetConnectedWaveform(), - GetTime(), playback_speed_); + GetConnectedNode()->GetPlayhead(), playback_speed_); } display_widget_->ResetFPSTimer(); foreach (ViewerDisplayWidget *dw, playback_devices_) { - dw->Play(playback_start_time, playback_speed_, timebase()); + dw->Play(playback_start_time, playback_speed_, timebase(), IsVideoVisible()); } // This is our timer for loading the queue and setting the time @@ -1363,10 +1373,10 @@ void ViewerWidget::ShowContextMenu(const QPoint &pos) Menu* zoom_menu = new Menu(tr("Zoom"), &menu); menu.addMenu(zoom_menu); - int zoom_levels[] = {10, 25, 50, 75, 100, 150, 200, 400}; zoom_menu->addAction(tr("Fit"))->setData(0); - for (int i=0;i<8;i++) { - zoom_menu->addAction(tr("%1%").arg(zoom_levels[i]))->setData(zoom_levels[i]); + for (int i=0;iaddAction(tr("%1%").arg(z))->setData(z); } connect(zoom_menu, &QMenu::triggered, this, &ViewerWidget::SetZoomFromMenu); @@ -1522,7 +1532,7 @@ void ViewerWidget::Play(bool in_to_out_only) if (GetConnectedNode() && GetConnectedNode()->GetWorkArea()->enabled()) { // Jump to in point - SetTimeAndSignal(GetConnectedNode()->GetWorkArea()->in()); + GetConnectedNode()->SetPlayhead(GetConnectedNode()->GetWorkArea()->in()); } else { in_to_out_only = false; } @@ -1632,7 +1642,7 @@ void ViewerWidget::TimebaseChangedEvent(const rational &timebase) controls_->SetTimebase(timebase); - controls_->SetTime(ruler()->GetTime()); + controls_->SetTime(GetConnectedNode() ? GetConnectedNode()->GetPlayhead() : 0); LengthChangedSlot(GetConnectedNode() ? GetConnectedNode()->GetLength() : 0); } @@ -1717,7 +1727,7 @@ void ViewerWidget::PlaybackTimerUpdate() // pausing. Even if we pause it later with `end_of_line`, we prefer pausing after setting the time // so that an audio scrub event, etc. isn't sent. time_changed_from_timer_ = true; - SetTimeAndSignal(time_to_set); + GetConnectedNode()->SetPlayhead(time_to_set); time_changed_from_timer_ = false; if (end_of_line) { // Cache the current speed @@ -1729,7 +1739,7 @@ void ViewerWidget::PlaybackTimerUpdate() } } - if (IsPlaying()) { + if (IsPlaying() && IsVideoVisible()) { while ((int(display_widget_->queue()->size()) + queue_watchers_.size()) < DeterminePlaybackQueueSize()) { if (!RequestNextFrameForQueue()) { // Prevent infinite loop @@ -1767,7 +1777,7 @@ void ViewerWidget::LengthChangedSlot(const rational &length) controls_->SetEndTime(length); UpdateMinimumScale(); - if (length < last_length_ && GetTime() >= length) { + if (GetConnectedNode() && length < last_length_ && GetConnectedNode()->GetPlayhead() >= length) { UpdateTextureFromNode(); } @@ -1813,7 +1823,7 @@ void ViewerWidget::SetZoomFromMenu(QAction *action) void ViewerWidget::ViewerInvalidatedVideoRange(const TimeRange &range) { // If our current frame is within this range, we need to update - if (!IsPlaying() && GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) { + if (!IsPlaying() && GetConnectedNode()->GetPlayhead() >= range.in() && (GetConnectedNode()->GetPlayhead() < range.out() || range.in() == range.out())) { QMetaObject::invokeMethod(this, &ViewerWidget::UpdateTextureFromNode, Qt::QueuedConnection); } } diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index e4f0d7289..203a03801 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -229,7 +229,7 @@ protected: private: int64_t GetTimestamp() const { - return Timecode::time_to_timestamp(GetTime(), timebase(), Timecode::kFloor); + return Timecode::time_to_timestamp(GetConnectedNode()->GetPlayhead(), timebase(), Timecode::kFloor); } void UpdateTimeInternal(int64_t i); @@ -282,6 +282,8 @@ private: void DetectMulticamNode(const rational &time); + bool IsVideoVisible() const; + ViewerSizer* sizer_; int playback_speed_; diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index e7ec99fb5..0fcb70e40 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -81,6 +81,13 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) : inner_widget()->setAcceptDrops(true); } +ViewerDisplayWidget::~ViewerDisplayWidget() +{ + delete text_edit_; + + MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER; +} + void ViewerDisplayWidget::SetMatrixTranslate(const QMatrix4x4 &mat) { translate_matrix_ = mat; @@ -373,15 +380,7 @@ void ViewerDisplayWidget::OnPaint() VideoParams device_params = GetViewportParams(); if (push_mode_ == kPushBlank) { - if (blank_shader_.isNull()) { - blank_shader_ = renderer()->CreateNativeShader(ShaderCode()); - } - - ShaderJob job; - job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, combined_matrix_flipped_)); - job.Insert(QStringLiteral("ove_cropmatrix"), NodeValue(NodeValue::kMatrix, crop_matrix_)); - - renderer()->Blit(blank_shader_, job, device_params, false); + DrawBlank(device_params); } else if (color_service()) { if (FramePtr frame = load_frame_.value()) { // This is a CPU frame, upload it now @@ -408,35 +407,39 @@ void ViewerDisplayWidget::OnPaint() TexturePtr texture_to_draw = texture_; - if (deinterlace_) { - if (deinterlace_shader_.isNull()) { - deinterlace_shader_ = renderer()->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/deinterlace.frag")))); + if (!texture_to_draw || texture_to_draw->IsDummy()) { + DrawBlank(device_params); + } else { + if (deinterlace_) { + if (deinterlace_shader_.isNull()) { + deinterlace_shader_ = renderer()->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/deinterlace.frag")))); + } + + if (!deinterlace_texture_ + || deinterlace_texture_->params() != texture_to_draw->params()) { + // (Re)create texture + deinterlace_texture_ = renderer()->CreateTexture(texture_to_draw->params()); + } + + ShaderJob job; + job.Insert(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, QVector2D(texture_to_draw->width(), texture_to_draw->height()))); + job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture_to_draw))); + + renderer()->BlitToTexture(deinterlace_shader_, job, deinterlace_texture_.get()); + + texture_to_draw = deinterlace_texture_; } - if (!deinterlace_texture_ - || deinterlace_texture_->params() != texture_to_draw->params()) { - // (Re)create texture - deinterlace_texture_ = renderer()->CreateTexture(texture_to_draw->params()); - } + ColorTransformJob ctj; + ctj.SetColorProcessor(color_service()); + ctj.SetInputTexture(texture_to_draw); + ctj.SetInputAlphaAssociation(OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated : kAlphaNone); + ctj.SetClearDestinationEnabled(false); + ctj.SetTransformMatrix(combined_matrix_flipped_); + ctj.SetCropMatrix(crop_matrix_); - ShaderJob job; - job.Insert(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, QVector2D(texture_to_draw->width(), texture_to_draw->height()))); - job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture_to_draw))); - - renderer()->BlitToTexture(deinterlace_shader_, job, deinterlace_texture_.get()); - - texture_to_draw = deinterlace_texture_; + renderer()->BlitColorManaged(ctj, device_params); } - - ColorTransformJob ctj; - ctj.SetColorProcessor(color_service()); - ctj.SetInputTexture(texture_to_draw); - ctj.SetInputAlphaAssociation(OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated : kAlphaNone); - ctj.SetClearDestinationEnabled(false); - ctj.SetTransformMatrix(combined_matrix_flipped_); - ctj.SetCropMatrix(crop_matrix_); - - renderer()->BlitColorManaged(ctj, device_params); } } @@ -448,7 +451,7 @@ void ViewerDisplayWidget::OnPaint() p.setWorldTransform(gizmo_last_draw_transform_); - gizmos_->UpdateGizmoPositions(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, gizmo_audio_params_, gizmo_draw_time_)); + gizmos_->UpdateGizmoPositions(gizmo_db_, NodeGlobals(gizmo_params_, gizmo_audio_params_, gizmo_draw_time_, LoopMode::kLoopModeOff)); foreach (NodeGizmo *gizmo, gizmos_->GetGizmos()) { if (gizmo->IsVisible()) { gizmo->Draw(&p); @@ -596,7 +599,7 @@ void ViewerDisplayWidget::DrawTextWithCrudeShadow(QPainter *painter, const QRect rational ViewerDisplayWidget::GetGizmoTime() { - return GetAdjustedTime(GetTimeTarget(), gizmos_, time_, true); + return GetAdjustedTime(GetTimeTarget(), gizmos_, time_, Node::kTransformTowardsInput); } bool ViewerDisplayWidget::IsHandDrag(QMouseEvent *event) const @@ -742,7 +745,7 @@ void ViewerDisplayWidget::OpenTextGizmo(TextGizmo *text, QMouseEvent *event) // Create toolbar text_toolbar_ = new ViewerTextEditorToolBar(text_edit_); - text_toolbar_->setWindowFlags(Qt::Dialog| Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint); + text_toolbar_->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); connect(text_toolbar_, &ViewerTextEditorToolBar::VerticalAlignmentChanged, text, &TextGizmo::SetVerticalAlignment); connect(text, &TextGizmo::VerticalAlignmentChanged, text_toolbar_, &ViewerTextEditorToolBar::SetVerticalAlignment); text_toolbar_->SetVerticalAlignment(text->GetVerticalAlignment()); @@ -824,7 +827,7 @@ bool ViewerDisplayWidget::OnMousePress(QMouseEvent *event) // Handle gizmo click gizmo_start_drag_ = event->pos(); gizmo_last_drag_ = gizmo_start_drag_; - current_gizmo_->SetGlobals(NodeTraverser::GenerateGlobals(gizmo_params_, gizmo_audio_params_, GenerateGizmoTime())); + current_gizmo_->SetGlobals(NodeGlobals(gizmo_params_, gizmo_audio_params_, GenerateGizmoTime(), LoopMode::kLoopModeOff)); } else { @@ -1051,7 +1054,7 @@ void ViewerDisplayWidget::DrawSubtitleTracks() f.setFamily(family); } - f.setWeight(OLIVE_CONFIG("DefaultSubtitleWeight").toInt()); + f.setWeight(static_cast(OLIVE_CONFIG("DefaultSubtitleWeight").toInt())); bounding_box.adjust(bounding_box.width()/10, bounding_box.height()/10, -bounding_box.width()/10, -bounding_box.height()/10); @@ -1122,7 +1125,8 @@ void ViewerDisplayWidget::ForwardDragEventToTextEdit(T *e) if constexpr (std::is_same_v) { text_edit_->dragLeaveEvent(e); } else { - T relay(AdjustPosByVAlign(GetVirtualPosForTextEdit(e->posF())).toPoint(), + + T relay(AdjustPosByVAlign(GetVirtualPosForTextEdit(e->pos())).toPoint(), e->possibleActions(), e->mimeData(), e->mouseButtons(), @@ -1145,7 +1149,7 @@ void ViewerDisplayWidget::ForwardDragEventToTextEdit(T *e) bool ViewerDisplayWidget::ForwardMouseEventToTextEdit(QMouseEvent *event, bool check_if_outside) { // Transform screen mouse coords to world mouse coords - QPointF local_pos = GetVirtualPosForTextEdit(event->localPos()); + QPointF local_pos = GetVirtualPosForTextEdit(event->pos()); 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()) { @@ -1156,8 +1160,8 @@ bool ViewerDisplayWidget::ForwardMouseEventToTextEdit(QMouseEvent *event, bool c local_pos = AdjustPosByVAlign(local_pos); - event->setLocalPos(local_pos); - return ForwardEventToTextEdit(event); + QMouseEvent derived(event->type(), local_pos, event->windowPos(), event->screenPos(), event->button(), event->buttons(), event->modifiers(), event->source()); + return ForwardEventToTextEdit(&derived); } bool ViewerDisplayWidget::ForwardEventToTextEdit(QEvent *event) @@ -1205,6 +1209,19 @@ void ViewerDisplayWidget::GenerateGizmoTransforms() gizmo_last_draw_transform_inverted_ = gizmo_last_draw_transform_.inverted(); } +void ViewerDisplayWidget::DrawBlank(const VideoParams &device_params) +{ + if (blank_shader_.isNull()) { + blank_shader_ = renderer()->CreateNativeShader(ShaderCode()); + } + + ShaderJob job; + job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, combined_matrix_flipped_)); + job.Insert(QStringLiteral("ove_cropmatrix"), NodeValue(NodeValue::kMatrix, crop_matrix_)); + + renderer()->Blit(blank_shader_, job, device_params, false); +} + void ViewerDisplayWidget::SetShowFPS(bool e) { show_fps_ = e; @@ -1224,16 +1241,18 @@ void ViewerDisplayWidget::RequestStartEditingText() } } -void ViewerDisplayWidget::Play(const int64_t &start_timestamp, const int &playback_speed, const rational &timebase) +void ViewerDisplayWidget::Play(const int64_t &start_timestamp, const int &playback_speed, const rational &timebase, bool start_updating) { playback_timebase_ = timebase; playback_speed_ = playback_speed; timer_.Start(start_timestamp, playback_speed, timebase.toDouble()); - connect(this, &ViewerDisplayWidget::frameSwapped, this, &ViewerDisplayWidget::UpdateFromQueue); + if (start_updating) { + connect(this, &ViewerDisplayWidget::frameSwapped, this, &ViewerDisplayWidget::UpdateFromQueue); - update(); + update(); + } } void ViewerDisplayWidget::Pause() diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index f2e89102f..37567dabc 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -68,7 +68,7 @@ public: */ ViewerDisplayWidget(QWidget* parent = nullptr); - MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR(ViewerDisplayWidget) + virtual ~ViewerDisplayWidget() override; const ViewerSafeMarginInfo& GetSafeMargin() const; void SetSafeMargins(const ViewerSafeMarginInfo& safe_margin); @@ -123,7 +123,7 @@ public: return texture_; } - void Play(const int64_t &start_timestamp, const int &playback_speed, const rational &timebase); + void Play(const int64_t &start_timestamp, const int &playback_speed, const rational &timebase, bool start_updating); void Pause(); @@ -306,6 +306,8 @@ private: void GenerateGizmoTransforms(); + void DrawBlank(const VideoParams &device_params); + /** * @brief Internal reference to the OpenGL texture to draw. Set in SetTexture() and used in paintGL(). */ diff --git a/app/widget/viewer/viewerqueue.h b/app/widget/viewer/viewerqueue.h index 053d8f013..1f529bf55 100644 --- a/app/widget/viewer/viewerqueue.h +++ b/app/widget/viewer/viewerqueue.h @@ -21,8 +21,6 @@ #ifndef VIEWERQUEUE_H #define VIEWERQUEUE_H -#include - #include "codec/frame.h" namespace olive { diff --git a/app/widget/viewer/viewersizer.cpp b/app/widget/viewer/viewersizer.cpp index d8ae52b84..505cb4eb9 100644 --- a/app/widget/viewer/viewersizer.cpp +++ b/app/widget/viewer/viewersizer.cpp @@ -20,7 +20,12 @@ #include "viewersizer.h" +#include +#include #include +#include + +#include "widget/handmovableview/handmovableview.h" namespace olive { @@ -30,7 +35,8 @@ ViewerSizer::ViewerSizer(QWidget *parent) : width_(0), height_(0), pixel_aspect_(1), - zoom_(0) + zoom_(0), + current_widget_scale_(0) { horiz_scrollbar_ = new QScrollBar(Qt::Horizontal, this); horiz_scrollbar_->setVisible(false); @@ -52,6 +58,7 @@ void ViewerSizer::SetWidget(QWidget *widget) if (widget_ != nullptr) { widget_->setParent(this); + widget_->installEventFilter(this); UpdateSize(); } @@ -90,6 +97,51 @@ void ViewerSizer::HandDragMove(int x, int y) } } +bool ViewerSizer::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == widget_) { + if (event->type() == QEvent::Wheel) { + QWheelEvent *w = static_cast(event); + + if (HandMovableView::WheelEventIsAZoomEvent(w)) { + int x = w->angleDelta().x() + w->angleDelta().y(); + + int current_percent = zoom_; + if (current_percent == 0) { + // Currently set to "fit" + current_percent = current_widget_scale_; + } + + if (x > 0) { + // Zoom in + for (int i=kZoomLevelCount-2; i>=0; i--) { + if (current_percent >= kZoomLevels[i]) { + SetZoom(kZoomLevels[i+1]); + break; + } + } + } else if (x < 0) { + // Zoom out + for (int i=1; ipixelDelta(); + horiz_scrollbar_->setValue(horiz_scrollbar_->value() - p.x()); + vert_scrollbar_->setValue(vert_scrollbar_->value() - p.y()); + } + return true; + } + } + + return QWidget::eventFilter(watched, event); +} + void ViewerSizer::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); @@ -169,17 +221,14 @@ void ViewerSizer::UpdateSize() } + current_widget_scale_ = current_scale * 100; + if (zoom_ > 0) { // Scale to get to the requested zoom double zoom_diff = (zoom_ * 0.01) / current_scale; child_matrix.scale(zoom_diff, zoom_diff, 1.0); - } else { - - // Fit - add a small amount of padding - child_matrix.scale(0.95f, 0.95f); - } emit RequestScale(child_matrix); diff --git a/app/widget/viewer/viewersizer.h b/app/widget/viewer/viewersizer.h index b4afe5015..21cb532f1 100644 --- a/app/widget/viewer/viewersizer.h +++ b/app/widget/viewer/viewersizer.h @@ -51,6 +51,9 @@ public: */ void SetWidget(QWidget* widget); + static constexpr int kZoomLevelCount = 8; + static constexpr int kZoomLevels[kZoomLevelCount] = {10, 25, 50, 75, 100, 150, 200, 400}; + public slots: /** * @brief Set resolution to use @@ -73,6 +76,8 @@ public slots: void HandDragMove(int x, int y); + virtual bool eventFilter(QObject *watched, QEvent *event) override; + signals: void RequestScale(const QMatrix4x4& matrix); @@ -111,6 +116,7 @@ private: * @brief Internal zoom value */ int zoom_; + int current_widget_scale_; QScrollBar* horiz_scrollbar_; QScrollBar* vert_scrollbar_; diff --git a/app/widget/viewer/viewertexteditor.cpp b/app/widget/viewer/viewertexteditor.cpp index 99da81967..7c8c27fad 100644 --- a/app/widget/viewer/viewertexteditor.cpp +++ b/app/widget/viewer/viewertexteditor.cpp @@ -40,7 +40,6 @@ ViewerTextEditor::ViewerTextEditor(double scale, QWidget *parent) : super(parent), transparent_clone_(nullptr), block_update_toolbar_signal_(false), - listen_to_focus_events_(false), forced_default_(false) { // Ensure default text color is white @@ -152,15 +151,16 @@ void ViewerTextEditor::paintEvent(QPaintEvent *e) void ViewerTextEditor::UpdateToolBar(ViewerTextEditorToolBar *toolbar, const QTextCharFormat &f, const QTextBlockFormat &b, Qt::Alignment alignment) { - QFontDatabase fd; - - QString family = f.fontFamily(); - if (family.isEmpty()) { + QStringList families = f.fontFamilies().toStringList(); + QString family; + if (families.isEmpty()) { family = qApp->font().family(); + } else { + family = families.first(); } QString style = f.fontStyleName().toString(); - QStringList styles = fd.styles(family); + QStringList styles = QFontDatabase().styles(family); if (!styles.isEmpty() && (style.isEmpty() || !styles.contains(style))) { // There seems to be no better way to find the "regular" style outside of this heuristic. // Feel free to add more if a font isn't working right. @@ -207,10 +207,7 @@ void ViewerTextEditor::SetFamily(const QString &s) ViewerTextEditorToolBar *toolbar = static_cast(sender()); QTextCharFormat f; - f.setFontFamily(s); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) f.setFontFamilies({s}); -#endif ApplyStyle(&f, s, toolbar->GetFontStyleName()); @@ -270,9 +267,8 @@ void ViewerTextEditor::ApplyStyle(QTextCharFormat *format, const QString &family { // NOTE: Windows appears to require setting weight and italic manually, while macOS and Linux are // perfectly fine with just the style name - QFontDatabase fd; - format->setFontWeight(fd.weight(family, style)); - format->setFontItalic(fd.italic(family, style)); + format->setFontWeight(QFontDatabase().weight(family, style)); + format->setFontItalic(QFontDatabase().italic(family, style)); format->setFontStyleName(style); } diff --git a/app/widget/viewer/viewertexteditor.h b/app/widget/viewer/viewertexteditor.h index 8bef901b9..b58db6565 100644 --- a/app/widget/viewer/viewertexteditor.h +++ b/app/widget/viewer/viewertexteditor.h @@ -151,8 +151,6 @@ public: void ConnectToolBar(ViewerTextEditorToolBar *toolbar); - void SetListenToFocusEvents(bool e) { listen_to_focus_events_ = e; } - void Paint(QPainter *p, Qt::Alignment valign); virtual void dragEnterEvent(QDragEnterEvent *e) override { return QTextEdit::dragEnterEvent(e); } @@ -178,8 +176,6 @@ private: bool block_update_toolbar_signal_; - bool listen_to_focus_events_; - bool forced_default_; QTextCharFormat default_fmt_; diff --git a/app/widget/viewer/viewerwindow.cpp b/app/widget/viewer/viewerwindow.cpp index be582b139..41e43462f 100644 --- a/app/widget/viewer/viewerwindow.cpp +++ b/app/widget/viewer/viewerwindow.cpp @@ -32,7 +32,7 @@ ViewerWindow::ViewerWindow(QWidget *parent) : pixel_aspect_(1) { QVBoxLayout* layout = new QVBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); display_widget_ = new ViewerDisplayWidget(); diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 22ae0e70d..6d5daa527 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -20,6 +20,7 @@ #include "mainmenu.h" +#include #include #include #include @@ -281,6 +282,11 @@ MainMenu::MainMenu(MainWindow *parent) : tools_preferences_item_ = tools_menu_->AddItem("prefs", Core::instance(), &Core::DialogPreferencesShow, tr("Ctrl+,")); +#ifndef NDEBUG + tools_magic_item_ = tools_menu_->AddItem("magic", Core::instance(), &Core::SetMagic); + tools_magic_item_->setCheckable(true); +#endif + // // HELP MENU // @@ -786,6 +792,9 @@ void MainMenu::Retranslate() tools_record_item_->setText(tr("Record Tool")); tools_snapping_item_->setText(tr("Enable Snapping")); tools_preferences_item_->setText(tr("Preferences")); +#ifndef NDEBUG + tools_magic_item_->setText("Magic"); +#endif // Help menu help_menu_->setTitle(tr("&Help")); diff --git a/app/window/mainwindow/mainmenu.h b/app/window/mainwindow/mainmenu.h index a4dc36b81..8b88927e0 100644 --- a/app/window/mainwindow/mainmenu.h +++ b/app/window/mainwindow/mainmenu.h @@ -287,6 +287,10 @@ private: QAction* tools_snapping_item_; QAction* tools_preferences_item_; +#ifndef NDEBUG + QAction* tools_magic_item_; +#endif + Menu* help_menu_; QAction* help_action_search_item_; QAction* help_feedback_item_; diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 20adf7800..f98b9ccf6 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -107,12 +106,7 @@ MainWindow::MainWindow(QWidget *parent) : connect(node_panel_, &NodePanel::NodeSelectionChanged, sequence_viewer_panel_, &ViewerPanel::SetNodeViewSelections); - // Connect time signals together - AddMainTimePanel(multicam_panel_); - AddMainTimePanel(curve_panel_); - AddMainTimePanel(param_panel_); - AddMainTimePanel(sequence_viewer_panel_); - + // Route play/pause/shuttle commands from these panels to the sequence viewer sequence_viewer_panel_->ConnectTimeBasedPanel(param_panel_); sequence_viewer_panel_->ConnectTimeBasedPanel(curve_panel_); sequence_viewer_panel_->ConnectTimeBasedPanel(multicam_panel_); @@ -526,7 +520,7 @@ void MainWindow::RevealViewerInFootageViewer(ViewerOutput *r, const TimeRange &r command->add_child(new WorkareaSetRangeCommand(r->GetWorkArea(), range)); Core::instance()->undo_stack()->push(command); - footage_viewer_panel_->SetTime(range.in()); + r->SetPlayhead(range.in()); } #ifdef Q_OS_LINUX @@ -557,7 +551,6 @@ void MainWindow::TimelineCloseRequested() { TimelinePanel *t = static_cast(sender()); RemoveTimelinePanel(t); - main_time_panels_.removeOne(t); } void MainWindow::ProjectCloseRequested() @@ -599,21 +592,6 @@ void MainWindow::FloatingPanelCloseRequested() panel->deleteLater(); } -void MainWindow::AddMainTimePanel(TimeBasedPanel *p) -{ - main_time_panels_.append(p); - connect(p, &TimeBasedPanel::TimeChanged, this, &MainWindow::UpdateMainTimePanels); -} - -void MainWindow::UpdateMainTimePanels(const rational &r) -{ - for (TimeBasedPanel *p : main_time_panels_) { - if (p != sender()) { - p->SetTime(r); - } - } -} - TimelinePanel* MainWindow::AppendTimelinePanel() { TimelinePanel* panel = AppendPanelInternal(timeline_panels_); @@ -624,8 +602,6 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &TimelinePanel::RevealViewerInProject, this, &MainWindow::RevealViewerInProject); connect(panel, &TimelinePanel::RevealViewerInFootageViewer, this, &MainWindow::RevealViewerInFootageViewer); - AddMainTimePanel(panel); - sequence_viewer_panel_->ConnectTimeBasedPanel(panel); return panel; diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 7f319e4b1..892d2ca54 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -147,8 +147,6 @@ private: void SelectFootageForProjectPanel(const QVector &e, ProjectPanel *p); - void AddMainTimePanel(TimeBasedPanel *p); - QByteArray premaximized_state_; // Standard panels @@ -176,8 +174,6 @@ private: bool first_show_; - QVector main_time_panels_; - private slots: void FocusedPanelChanged(PanelWidget* panel); @@ -208,8 +204,6 @@ private slots: void RevealViewerInProject(ViewerOutput *r); void RevealViewerInFootageViewer(ViewerOutput *r, const TimeRange &range); - void UpdateMainTimePanels(const rational &r); - }; }