From 6ebe0084a3f0f27c7525212af3a993828d384709 Mon Sep 17 00:00:00 2001 From: Troy James Sobotka Date: Mon, 11 May 2020 16:07:15 -0700 Subject: [PATCH 001/138] Implement Histogram Basic pass at histogram. Uses a power based scale compared against the total pixel count. Probably needs a UI toggle to flip lines on and off as the text and lines in OpenGL are a large cycles sucker. Could be likely much faster when migrated to a geometry shader approach. Uses the scaled viewport resolution to increase performance as much as possible. --- app/shaders/rgbhistogram.frag | 37 +++++ app/shaders/rgbhistogram.vert | 29 ++++ app/shaders/rgbhistogram_secondary.frag | 35 +++++ app/shaders/rgbwaveform.frag | 60 +++----- app/widget/scope/histogram/histogram.cpp | 171 ++++++++++++++++++++++- app/widget/scope/histogram/histogram.h | 17 ++- app/widget/scope/scopebase/scopebase.h | 2 +- app/widget/scope/waveform/waveform.cpp | 11 +- 8 files changed, 314 insertions(+), 48 deletions(-) create mode 100644 app/shaders/rgbhistogram.frag create mode 100644 app/shaders/rgbhistogram.vert create mode 100644 app/shaders/rgbhistogram_secondary.frag diff --git a/app/shaders/rgbhistogram.frag b/app/shaders/rgbhistogram.frag new file mode 100644 index 000000000..435949138 --- /dev/null +++ b/app/shaders/rgbhistogram.frag @@ -0,0 +1,37 @@ +#version 150 + +uniform sampler2D ove_maintex; +uniform vec2 ove_resolution; +uniform vec2 ove_viewport; + +uniform float histogram_scale; + +in vec2 ove_texcoord; + +out vec4 fragColor; + +void main(void) { + float histogram_width = ceil(histogram_scale * ove_viewport.y); + float quantisation = 1.0 / (histogram_width - 1.0); + vec3 cur_col = vec3(0.0); + vec3 sum = vec3(0.0); + float ratio = 0.0; + + for (int i = 0; i < histogram_width; i++) { + ratio = float(i) / float(histogram_width - 1); + cur_col = texture( + ove_maintex, + vec2(ove_texcoord.y, ratio) + ).rgb; + + sum += step(vec3(ove_texcoord.x - quantisation), cur_col) * + step(cur_col, vec3(ove_texcoord.x + quantisation)) + + ( + // Account for values beyond the upper x limit. + step(1.0 - quantisation, ove_texcoord.x) * + step(vec3(1.0 - quantisation), cur_col) + ); + } + + fragColor = vec4(sum, 1.0); +} diff --git a/app/shaders/rgbhistogram.vert b/app/shaders/rgbhistogram.vert new file mode 100644 index 000000000..92536144c --- /dev/null +++ b/app/shaders/rgbhistogram.vert @@ -0,0 +1,29 @@ +#version 150 + +uniform float histogram_scale; +uniform vec2 ove_resolution; + +in vec4 a_position; +in vec2 a_texcoord; + +out vec2 ove_texcoord; + +mat4 scale_mat4(vec3 scale) { + return mat4( + scale.x, 0.0, 0.0, 0.0, + 0.0, scale.y, 0.0, 0.0, + 0.0, 0.0, scale.z, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} + +void main() { + // Create identity matrix + mat4 transform = mat4(1.0); + + // Scale the scope + transform *= scale_mat4(vec3(histogram_scale, histogram_scale, 1.0)); + + gl_Position = transform * a_position; + ove_texcoord = a_texcoord; +} \ No newline at end of file diff --git a/app/shaders/rgbhistogram_secondary.frag b/app/shaders/rgbhistogram_secondary.frag new file mode 100644 index 000000000..474db6b74 --- /dev/null +++ b/app/shaders/rgbhistogram_secondary.frag @@ -0,0 +1,35 @@ +#version 150 + +uniform sampler2D ove_maintex; +uniform vec2 ove_resolution; +uniform vec2 ove_viewport; + +uniform float histogram_scale; +uniform float histogram_power; + +in vec2 ove_texcoord; + +out vec4 fragColor; + +void main(void) { + vec3 col = vec3(0.0); + float histogram_height = ceil(ove_viewport.y * histogram_scale); + vec3 histogram_ratio = vec3(0.0); + vec3 sum = vec3(0.0); + float ratio = 0.0; + vec3 total_pixels = vec3(ceil(ove_viewport.x * ove_resolution.y * + histogram_scale)); + + for (int i = 0; i < histogram_height; i++) { + ratio = float(i) / float(histogram_height - 1.0); + sum += texture( + ove_maintex, + vec2(ove_texcoord.x, ratio) + ).rgb; + } + + histogram_ratio = pow(sum / total_pixels, vec3(histogram_power)); + col = step(vec3(ove_texcoord.y), histogram_ratio); + + fragColor = vec4(col, 1.0); +} diff --git a/app/shaders/rgbwaveform.frag b/app/shaders/rgbwaveform.frag index 4e6b13f08..ab677523f 100644 --- a/app/shaders/rgbwaveform.frag +++ b/app/shaders/rgbwaveform.frag @@ -1,6 +1,3 @@ -// Adapted from "RGB Waveform" by lebek -// https://www.shadertoy.com/view/4dK3Wc - #version 150 uniform sampler2D ove_maintex; @@ -11,33 +8,14 @@ uniform vec3 luma_coeffs; uniform float waveform_scale; uniform vec2 waveform_dims; uniform vec4 waveform_region; -uniform vec4 waveform_uv; +uniform vec4 waveform_region_uv; in vec2 ove_texcoord; out vec4 fragColor; void main(void) { - vec3 col = vec3(0.0); - // Set an increment default to 10 bit encodings. This would likely be - // better served as a UI control, as waveforms will change their combing - // based on how granular the increment is set. For example, it can be - // challenging to spot 8 bit combing with an increment of 1. / 2.^8 - 1. - float increment = 1.0 / (pow(2, 10) - 1.0); - float maxb = waveform_dims.y + increment; - float minb = waveform_dims.y - increment; - - // Intensity would make sense to also expose via the UI, as a density - // slider allows you to peek past certain values or reveal very low - // values. Hard coding it for now, as there isn't a clear way to have - // the various bit depth / code values always display at a consistent - // emission output strength. - float intensity = 0.10; - - int y_lim = int(waveform_dims.y); - - vec3 cur_col = vec3(0.0); - vec3 cur_lum = vec3(0.0); + vec4 col = vec4(0.0); if ( (gl_FragCoord.x >= waveform_region.x) && @@ -45,28 +23,34 @@ void main(void) { (gl_FragCoord.x < waveform_region.z) && (gl_FragCoord.y < waveform_region.w) ) { - // col = vec3(0.5, 0.5, 0.0); - // int start = int(waveform_region.y); - int stop = int(waveform_dims.y); + float increment = 0.5; + float uv_increment = increment / (ove_viewport.y - 1.0); + float intensity = 0.10; + vec4 cur_col = vec4(0.0); float ratio = 0.0; - float waveform_x = (ove_texcoord.x - waveform_uv.x) / waveform_scale; - float waveform_y = (ove_texcoord.y - waveform_uv.y) / waveform_scale; + vec2 waveform_current_uv = vec2( + (ove_texcoord.x - waveform_region_uv.x) / waveform_scale, + (ove_texcoord.y - waveform_region_uv.y) / waveform_scale + ); + for (int i = 0; i < waveform_dims.y; i++) { ratio = float(i) / float(waveform_dims.y - 1); - cur_col = texture( + cur_col.rgb = texture( ove_maintex, - vec2(waveform_x, ratio) + vec2(waveform_current_uv.x, ratio) ).rgb; - col += step(vec3(waveform_y - increment), cur_col) * - step(cur_col, vec3(waveform_y + increment)) * intensity; + cur_col.w = dot(cur_col.rgb, luma_coeffs); - cur_lum = vec3(dot(cur_col, luma_coeffs)); - - col += step(vec3(waveform_y - increment), cur_lum) * - step(cur_lum, vec3(waveform_y + increment)) * intensity; + col += ( + step(vec4(waveform_current_uv.y - uv_increment), cur_col) * + step(cur_col, vec4(waveform_current_uv.y + uv_increment)) * + intensity) + + (step(1.0 - uv_increment, waveform_current_uv.y) * + step(vec4(1.0 - uv_increment), cur_col) * intensity); } } - fragColor = vec4(col, 1.0); + col.rgb += vec3(col.w); + fragColor = vec4(col.rgb, 1.0); } diff --git a/app/widget/scope/histogram/histogram.cpp b/app/widget/scope/histogram/histogram.cpp index 9665e32de..be96bb2bd 100644 --- a/app/widget/scope/histogram/histogram.cpp +++ b/app/widget/scope/histogram/histogram.cpp @@ -23,8 +23,9 @@ #include #include -#include "common/clamp.h" -#include "common/functiontimer.h" +#include "common/qtutils.h" +#include "node/node.h" +#include "render/backend/opengl/openglrenderfunctions.h" OLIVE_NAMESPACE_ENTER @@ -33,4 +34,170 @@ HistogramScope::HistogramScope(QWidget* parent) : { } +HistogramScope::~HistogramScope() +{ + CleanUp(); + + if (context()) { + disconnect(context(), &QOpenGLContext::aboutToBeDestroyed, this, + &HistogramScope::CleanUp); + } +} + +void HistogramScope::initializeGL() +{ + ScopeBase::initializeGL(); + + pipeline_secondary_ = CreateSecondaryShader(); + + connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, + &HistogramScope::CleanUp, Qt::DirectConnection); +} + +void HistogramScope::AssertAdditionalTextures() +{ + if (!texture_row_sums_.IsCreated() + || texture_row_sums_.width() != width() + || texture_row_sums_.height() != height()) { + texture_row_sums_.Destroy(); + texture_row_sums_.Create(context(), VideoRenderingParams(width(), + height(), managed_tex_.format())); + } +} + +void HistogramScope::CleanUp() +{ + makeCurrent(); + + pipeline_secondary_ = nullptr; + texture_row_sums_.Destroy(); + + doneCurrent(); +} + +OpenGLShaderPtr HistogramScope::CreateShader() +{ + OpenGLShaderPtr pipeline = OpenGLShader::Create(); + + pipeline->create(); + pipeline->addShaderFromSourceCode(QOpenGLShader::Vertex, + OpenGLShader::CodeDefaultVertex()); + pipeline->addShaderFromSourceCode(QOpenGLShader::Fragment, + Node::ReadFileAsString(":/shaders/rgbhistogram.frag")); + pipeline->link(); + + return pipeline; +} + +OpenGLShaderPtr HistogramScope::CreateSecondaryShader() +{ + OpenGLShaderPtr pipeline_secondary_ = OpenGLShader::Create(); + + pipeline_secondary_->create(); + pipeline_secondary_->addShaderFromSourceCode(QOpenGLShader::Vertex, + Node::ReadFileAsString(":/shaders/rgbhistogram.vert")); + pipeline_secondary_->addShaderFromSourceCode(QOpenGLShader::Fragment, + Node::ReadFileAsString(":/shaders/rgbhistogram_secondary.frag")); + pipeline_secondary_->link(); + + return pipeline_secondary_; +} + +void HistogramScope::DrawScope() +{ + float histogram_scale = 0.80f; + // This value is eyeballed for usefulness. Until we have a geometry + // shader approach, it is impossible to normalize against a peak + // sum of image values. + float histogram_base = 2.5f; + float histogram_power = 1.0f / histogram_base; + + pipeline()->bind(); + pipeline()->setUniformValue("ove_resolution", managed_tex().width(), + managed_tex().height()); + pipeline()->setUniformValue("ove_viewport", width(), height()); + pipeline()->setUniformValue("histogram_scale", histogram_scale); + pipeline()->release(); + + AssertAdditionalTextures(); + + framebuffer_.Attach(&texture_row_sums_, true); + framebuffer_.Bind(); + + managed_tex().Bind(); + + OpenGLRenderFunctions::Blit(pipeline()); + + managed_tex().Release(); + + framebuffer_.Release(); + framebuffer_.Detach(); + + pipeline_secondary_->bind(); + pipeline_secondary_->setUniformValue("ove_resolution", + texture_row_sums_.width(), texture_row_sums_.height()); + pipeline_secondary_->setUniformValue("ove_viewport", width(), height()); + pipeline_secondary_->setUniformValue("histogram_scale", histogram_scale); + pipeline_secondary_->setUniformValue("histogram_power", histogram_power); + pipeline_secondary_->release(); + + texture_row_sums_.Bind(); + + OpenGLRenderFunctions::Blit(pipeline_secondary_); + + texture_row_sums_.Release(); + + // Draw line overlays + QPainter p(this); + QFont font = p.font(); + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); + QString label; + std::vector histogram_increments = { + 0.00, + 0.25, + 0.50, + 1.0 + }; + + int histogram_steps = histogram_increments.size(); + QVector histogram_lines(histogram_steps + 1); + int font_x_offset = 0; + int font_y_offset = font_metrics.capHeight() / 2.0f; + + p.setCompositionMode(QPainter::CompositionMode_Plus); + + p.setPen(QColor(0.0, 0.6 * 255.0, 0.0)); + p.setFont(font); + + float histogram_dim_x = ceil((width() - 1.0) * histogram_scale); + float histogram_dim_y = ceil((height() - 1.0) * histogram_scale); + float histogram_start_dim_x = + ((width() - 1.0) - histogram_dim_x) / 2.0f; + float histogram_start_dim_y = + ((height() - 1.0) - histogram_dim_y) / 2.0f; + float histogram_end_dim_x = (width() - 1.0) - histogram_start_dim_x; + + // for (int i=0; i <= histogram_steps; i++) { + for(std::vector::iterator it = histogram_increments.begin(); + it != histogram_increments.end(); it++) { + histogram_lines[it - histogram_increments.begin()].setLine( + histogram_start_dim_x, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y, + histogram_end_dim_x, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y); + label = QString::number( + *it * 100, 'f', 1) + "%"; + font_x_offset = QFontMetricsWidth(font_metrics, label) + 4; + + p.drawText( + histogram_start_dim_x - font_x_offset, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y + font_y_offset, label); + } + p.drawLines(histogram_lines); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/scope/histogram/histogram.h b/app/widget/scope/histogram/histogram.h index 625074a89..70751355f 100644 --- a/app/widget/scope/histogram/histogram.h +++ b/app/widget/scope/histogram/histogram.h @@ -31,11 +31,24 @@ class HistogramScope : public ScopeBase public: HistogramScope(QWidget* parent = nullptr); + virtual ~HistogramScope() override; + protected: - //virtual OpenGLShaderPtr CreateShader() override; + virtual void initializeGL() override; - //virtual void DrawScope() override; + virtual OpenGLShaderPtr CreateShader() override; + OpenGLShaderPtr CreateSecondaryShader(); + void AssertAdditionalTextures(); + + virtual void DrawScope() override; + +private: + OpenGLShaderPtr pipeline_secondary_; + OpenGLTexture texture_row_sums_; + +private slots: + void CleanUp(); }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/scope/scopebase/scopebase.h b/app/widget/scope/scopebase/scopebase.h index 3098af212..cec2256b1 100644 --- a/app/widget/scope/scopebase/scopebase.h +++ b/app/widget/scope/scopebase/scopebase.h @@ -55,7 +55,7 @@ protected: OpenGLTexture& managed_tex(); -private: +protected: void UploadTextureFromBuffer(); OpenGLShaderPtr pipeline_; diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index daefe84e7..78e054836 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -76,12 +76,13 @@ void WaveformScope::DrawScope() waveform_start_dim_x, waveform_start_dim_y, waveform_end_dim_x, waveform_end_dim_y); - float waveform_start_uv_x = waveform_start_dim_x / width(); - float waveform_start_uv_y = waveform_start_dim_y / height(); - float waveform_end_uv_x = waveform_end_dim_x / width(); - float waveform_end_uv_y = waveform_end_dim_y / height(); + float waveform_start_uv_x = (waveform_start_dim_x - 1.0) / (width() - 1.0); + float waveform_start_uv_y = (waveform_start_dim_y - 1.0) / (height() - 1.0); + float waveform_end_uv_x = (waveform_end_dim_x - 1.0) / (width() - 1.0); + float waveform_end_uv_y = (waveform_end_dim_y - 1.0) / (height() - 1.0); + pipeline()->setUniformValue( - "waveform_uv", + "waveform_region_uv", waveform_start_uv_x, waveform_start_uv_y, waveform_end_uv_x, waveform_end_uv_y); From 80badaa5a81ab9f99302e6b89a479e0860df8988 Mon Sep 17 00:00:00 2001 From: Troy James Sobotka Date: Mon, 11 May 2020 16:07:15 -0700 Subject: [PATCH 002/138] Waveform Fixes and Modifications Fixes: * Values exceeding the encoded range. Future may account for a certain range above and below current values. * Minor calculation problem that prevented the above fix from working properly. * Streamlined the shader code. * Add a vertex shader to handle the scaling. * Use the viewport resolution as the scaled version to save performance. --- app/shaders/rgbwaveform.frag | 71 ++++++++------------------ app/shaders/rgbwaveform.vert | 29 +++++++++++ app/widget/scope/waveform/waveform.cpp | 42 ++++++--------- 3 files changed, 65 insertions(+), 77 deletions(-) create mode 100644 app/shaders/rgbwaveform.vert diff --git a/app/shaders/rgbwaveform.frag b/app/shaders/rgbwaveform.frag index 4e6b13f08..e568b37cd 100644 --- a/app/shaders/rgbwaveform.frag +++ b/app/shaders/rgbwaveform.frag @@ -1,6 +1,3 @@ -// Adapted from "RGB Waveform" by lebek -// https://www.shadertoy.com/view/4dK3Wc - #version 150 uniform sampler2D ove_maintex; @@ -9,64 +6,36 @@ uniform vec2 ove_viewport; uniform vec3 luma_coeffs; uniform float waveform_scale; -uniform vec2 waveform_dims; -uniform vec4 waveform_region; -uniform vec4 waveform_uv; in vec2 ove_texcoord; out vec4 fragColor; void main(void) { - vec3 col = vec3(0.0); - // Set an increment default to 10 bit encodings. This would likely be - // better served as a UI control, as waveforms will change their combing - // based on how granular the increment is set. For example, it can be - // challenging to spot 8 bit combing with an increment of 1. / 2.^8 - 1. - float increment = 1.0 / (pow(2, 10) - 1.0); - float maxb = waveform_dims.y + increment; - float minb = waveform_dims.y - increment; - - // Intensity would make sense to also expose via the UI, as a density - // slider allows you to peek past certain values or reveal very low - // values. Hard coding it for now, as there isn't a clear way to have - // the various bit depth / code values always display at a consistent - // emission output strength. + float waveform_height = ceil(waveform_scale * ove_viewport.y); + float quantisation = 1.0 / (waveform_height - 1.0); float intensity = 0.10; + vec4 col = vec4(0.0); + vec4 cur_col = vec4(0.0); + float ratio = 0.0; - int y_lim = int(waveform_dims.y); + for (int i = 0; i < waveform_height; i++) { + ratio = float(i) / float(waveform_height - 1.0); + cur_col.rgb = texture( + ove_maintex, + vec2(ove_texcoord.x, ratio) + ).rgb; - vec3 cur_col = vec3(0.0); - vec3 cur_lum = vec3(0.0); + cur_col.w = dot(cur_col.rgb, luma_coeffs); - if ( - (gl_FragCoord.x >= waveform_region.x) && - (gl_FragCoord.y >= waveform_region.y) && - (gl_FragCoord.x < waveform_region.z) && - (gl_FragCoord.y < waveform_region.w) - ) { - // col = vec3(0.5, 0.5, 0.0); - // int start = int(waveform_region.y); - int stop = int(waveform_dims.y); - float ratio = 0.0; - float waveform_x = (ove_texcoord.x - waveform_uv.x) / waveform_scale; - float waveform_y = (ove_texcoord.y - waveform_uv.y) / waveform_scale; - for (int i = 0; i < waveform_dims.y; i++) { - ratio = float(i) / float(waveform_dims.y - 1); - cur_col = texture( - ove_maintex, - vec2(waveform_x, ratio) - ).rgb; - - col += step(vec3(waveform_y - increment), cur_col) * - step(cur_col, vec3(waveform_y + increment)) * intensity; - - cur_lum = vec3(dot(cur_col, luma_coeffs)); - - col += step(vec3(waveform_y - increment), cur_lum) * - step(cur_lum, vec3(waveform_y + increment)) * intensity; - } + col += ( + step(vec4(ove_texcoord.y - quantisation), cur_col) * + step(cur_col, vec4(ove_texcoord.y + quantisation)) * + intensity) + + (step(1.0 - quantisation, ove_texcoord.y) * + step(vec4(1.0 - quantisation), cur_col) * intensity); } - fragColor = vec4(col, 1.0); + col.rgb += vec3(col.w); + fragColor = vec4(col.rgb, 1.0); } diff --git a/app/shaders/rgbwaveform.vert b/app/shaders/rgbwaveform.vert new file mode 100644 index 000000000..88fde9934 --- /dev/null +++ b/app/shaders/rgbwaveform.vert @@ -0,0 +1,29 @@ +#version 150 + +uniform float waveform_scale; +uniform vec2 ove_resolution; + +in vec4 a_position; +in vec2 a_texcoord; + +out vec2 ove_texcoord; + +mat4 scale_mat4(vec3 scale) { + return mat4( + scale.x, 0.0, 0.0, 0.0, + 0.0, scale.y, 0.0, 0.0, + 0.0, 0.0, scale.z, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); +} + +void main() { + // Create identity matrix + mat4 transform = mat4(1.0); + + // Scale the scope + transform *= scale_mat4(vec3(waveform_scale, waveform_scale, 1.0)); + + gl_Position = transform * a_position; + ove_texcoord = a_texcoord; +} \ No newline at end of file diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index daefe84e7..ff4f1296b 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -41,8 +41,10 @@ OpenGLShaderPtr WaveformScope::CreateShader() OpenGLShaderPtr pipeline = OpenGLShader::Create(); pipeline->create(); - pipeline->addShaderFromSourceCode(QOpenGLShader::Vertex, OpenGLShader::CodeDefaultVertex()); - pipeline->addShaderFromSourceCode(QOpenGLShader::Fragment, Node::ReadFileAsString(":/shaders/rgbwaveform.frag")); + pipeline->addShaderFromSourceCode(QOpenGLShader::Vertex, + Node::ReadFileAsString(":/shaders/rgbwaveform.vert")); + pipeline->addShaderFromSourceCode(QOpenGLShader::Fragment, + Node::ReadFileAsString(":/shaders/rgbwaveform.frag")); pipeline->link(); return pipeline; @@ -51,12 +53,6 @@ OpenGLShaderPtr WaveformScope::CreateShader() void WaveformScope::DrawScope() { float waveform_scale = 0.80f; - float waveform_dim_x = width() * waveform_scale; - float waveform_dim_y = height() * waveform_scale; - float waveform_start_dim_x = (width() - waveform_dim_x) / 2.0f; - float waveform_start_dim_y = (height() - waveform_dim_y) / 2.0f; - float waveform_end_dim_x = width() - waveform_start_dim_x; - float waveform_end_dim_y = height() - waveform_start_dim_y; // Draw waveform through shader pipeline()->bind(); @@ -68,22 +64,6 @@ void WaveformScope::DrawScope() // Scale of the waveform relative to the viewport surface. pipeline()->setUniformValue("waveform_scale", waveform_scale); - pipeline()->setUniformValue( - "waveform_dims", waveform_dim_x, waveform_dim_y); - - pipeline()->setUniformValue( - "waveform_region", - waveform_start_dim_x, waveform_start_dim_y, - waveform_end_dim_x, waveform_end_dim_y); - - float waveform_start_uv_x = waveform_start_dim_x / width(); - float waveform_start_uv_y = waveform_start_dim_y / height(); - float waveform_end_uv_x = waveform_end_dim_x / width(); - float waveform_end_uv_y = waveform_end_dim_y / height(); - pipeline()->setUniformValue( - "waveform_uv", - waveform_start_uv_x, waveform_start_uv_y, - waveform_end_uv_x, waveform_end_uv_y); pipeline()->release(); @@ -93,9 +73,19 @@ void WaveformScope::DrawScope() managed_tex().Release(); + float waveform_dim_x = ceil((width() - 1.0) * waveform_scale); + float waveform_dim_y = ceil((height() - 1.0) * waveform_scale); + float waveform_start_dim_x = + ((width() - 1.0) - waveform_dim_x) / 2.0f; + float waveform_start_dim_y = + ((height() - 1.0) - waveform_dim_y) / 2.0f; + float waveform_end_dim_x = (width() - 1.0) - waveform_start_dim_x; + // Draw line overlays QPainter p(this); - QFontMetrics font_metrics = QFontMetrics(QFont()); + QFont font; + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); QString label; float ire_increment = 0.1f; int ire_steps = qRound(1.0 / ire_increment); @@ -106,7 +96,7 @@ void WaveformScope::DrawScope() p.setCompositionMode(QPainter::CompositionMode_Plus); p.setPen(QColor(0.0, 0.6 * 255.0, 0.0)); - p.setFont(QFont()); + p.setFont(font); for (int i=0; i <= ire_steps; i++) { ire_lines[i].setLine( From 67ee78ac9ac76a1d5f3545c7ad417f53916c330e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 16 Jun 2020 17:48:10 +1000 Subject: [PATCH 003/138] nodeparamview: allow disconnecting nodes by right clicking the "connected to" label --- app/widget/clickablelabel/clickablelabel.cpp | 12 ++++++++---- .../nodeparamviewconnectedlabel.cpp | 17 +++++++++++++++++ .../nodeparamview/nodeparamviewconnectedlabel.h | 2 ++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/widget/clickablelabel/clickablelabel.cpp b/app/widget/clickablelabel/clickablelabel.cpp index 0ab4dd631..a267a1019 100644 --- a/app/widget/clickablelabel/clickablelabel.cpp +++ b/app/widget/clickablelabel/clickablelabel.cpp @@ -20,6 +20,8 @@ #include "clickablelabel.h" +#include + OLIVE_NAMESPACE_ENTER ClickableLabel::ClickableLabel(const QString &text, QWidget *parent) : @@ -32,16 +34,18 @@ ClickableLabel::ClickableLabel(QWidget *parent) : { } -void ClickableLabel::mouseReleaseEvent(QMouseEvent *) +void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) { - if (underMouse()) { + if (event->button() == Qt::LeftButton && underMouse()) { emit MouseClicked(); } } -void ClickableLabel::mouseDoubleClickEvent(QMouseEvent *) +void ClickableLabel::mouseDoubleClickEvent(QMouseEvent *event) { - emit MouseDoubleClicked(); + if (event->button() == Qt::LeftButton) { + emit MouseDoubleClicked(); + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp index 612b14f81..43507cac6 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp @@ -23,7 +23,10 @@ #include #include "common/qtutils.h" +#include "core.h" #include "node/node.h" +#include "widget/menu/menu.h" +#include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER @@ -39,7 +42,9 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(NodeInput *input, QWidg connected_to_lbl_ = new ClickableLabel(); connected_to_lbl_->setCursor(Qt::PointingHandCursor); + connected_to_lbl_->setContextMenuPolicy(Qt::CustomContextMenu); connect(connected_to_lbl_, &ClickableLabel::MouseClicked, this, &NodeParamViewConnectedLabel::ConnectionClicked); + connect(connected_to_lbl_, &ClickableLabel::customContextMenuRequested, this, &NodeParamViewConnectedLabel::ShowLabelContextMenu); layout->addWidget(connected_to_lbl_); layout->addStretch(); @@ -69,4 +74,16 @@ void NodeParamViewConnectedLabel::UpdateConnected() connected_to_lbl_->setText(connection_str); } +void NodeParamViewConnectedLabel::ShowLabelContextMenu() +{ + Menu m(this); + + QAction* disconnect_action = m.addAction(tr("Disconnect")); + connect(disconnect_action, &QAction::triggered, this, [this](){ + Core::instance()->undo_stack()->push(new NodeEdgeRemoveCommand(input_->get_connected_output(), input_)); + }); + + m.exec(QCursor::pos()); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h index 161bf2a0d..04103cae1 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h @@ -37,6 +37,8 @@ signals: private slots: void UpdateConnected(); + void ShowLabelContextMenu(); + private: ClickableLabel* connected_to_lbl_; From 29d56a15830ec31762b799a1bb0315d3d86da34d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 16 Jun 2020 18:30:00 +1000 Subject: [PATCH 004/138] timeline: implemented UI indicator for tools like razor and edit --- app/widget/timelinewidget/timelinewidget.cpp | 7 ++ app/widget/timelinewidget/timelinewidget.h | 15 +++- app/widget/timelinewidget/tool/CMakeLists.txt | 1 + app/widget/timelinewidget/tool/beam.cpp | 35 ++++++++ app/widget/timelinewidget/tool/edit.cpp | 2 +- app/widget/timelinewidget/tool/razor.cpp | 2 +- .../timelinewidget/view/timelineview.cpp | 88 ++++++++++++++----- app/widget/timelinewidget/view/timelineview.h | 7 ++ 8 files changed, 129 insertions(+), 28 deletions(-) create mode 100644 app/widget/timelinewidget/tool/beam.cpp diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index c011fa558..97f186cca 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1241,6 +1241,13 @@ void TimelineWidget::UpdateViewTimebases() } } +void TimelineWidget::SetViewBeamCursor(const TimelineCoordinate &coord) +{ + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->SetBeamCursor(coord); + } +} + void TimelineWidget::SetBlockLinksSelected(Block* block, bool selected) { TimelineViewBlockItem* link_item; diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index d8a6c0dc5..fad1da588 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -209,6 +209,15 @@ private: }; + class BeamTool : public Tool + { + public: + BeamTool(TimelineWidget *parent); + + virtual void HoverMove(TimelineViewMouseEvent *event) override; + + }; + class PointerTool : public Tool { public: @@ -327,7 +336,7 @@ private: }; - class EditTool : public Tool + class EditTool : public BeamTool { public: EditTool(TimelineWidget* parent); @@ -337,7 +346,7 @@ private: virtual void MouseRelease(TimelineViewMouseEvent *event) override; }; - class RazorTool : public Tool + class RazorTool : public BeamTool { public: RazorTool(TimelineWidget* parent); @@ -498,6 +507,8 @@ private: void UpdateViewTimebases(); + void SetViewBeamCursor(const TimelineCoordinate& coord); + private slots: void ViewMousePressed(TimelineViewMouseEvent* event); void ViewMouseMoved(TimelineViewMouseEvent* event); diff --git a/app/widget/timelinewidget/tool/CMakeLists.txt b/app/widget/timelinewidget/tool/CMakeLists.txt index 53b862a84..f14d4a8de 100644 --- a/app/widget/timelinewidget/tool/CMakeLists.txt +++ b/app/widget/timelinewidget/tool/CMakeLists.txt @@ -17,6 +17,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/timelinewidget/tool/add.cpp + widget/timelinewidget/tool/beam.cpp widget/timelinewidget/tool/edit.cpp widget/timelinewidget/tool/import.cpp widget/timelinewidget/tool/pointer.cpp diff --git a/app/widget/timelinewidget/tool/beam.cpp b/app/widget/timelinewidget/tool/beam.cpp new file mode 100644 index 000000000..a04d14b9e --- /dev/null +++ b/app/widget/timelinewidget/tool/beam.cpp @@ -0,0 +1,35 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "widget/timelinewidget/timelinewidget.h" + +OLIVE_NAMESPACE_ENTER + +TimelineWidget::BeamTool::BeamTool(TimelineWidget *parent) : + Tool(parent) +{ +} + +void TimelineWidget::BeamTool::HoverMove(TimelineViewMouseEvent *event) +{ + parent()->SetViewBeamCursor(event->GetCoordinates(true)); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/edit.cpp b/app/widget/timelinewidget/tool/edit.cpp index d7940f0c4..cb804bebc 100644 --- a/app/widget/timelinewidget/tool/edit.cpp +++ b/app/widget/timelinewidget/tool/edit.cpp @@ -23,7 +23,7 @@ OLIVE_NAMESPACE_ENTER TimelineWidget::EditTool::EditTool(TimelineWidget* parent) : - Tool(parent) + BeamTool(parent) { } diff --git a/app/widget/timelinewidget/tool/razor.cpp b/app/widget/timelinewidget/tool/razor.cpp index d89e9125d..298a36b3b 100644 --- a/app/widget/timelinewidget/tool/razor.cpp +++ b/app/widget/timelinewidget/tool/razor.cpp @@ -23,7 +23,7 @@ OLIVE_NAMESPACE_ENTER TimelineWidget::RazorTool::RazorTool(TimelineWidget* parent) : - Tool(parent) + BeamTool(parent) { } diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index 91409bf94..2d3cc4511 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -141,21 +141,21 @@ void TimelineView::wheelEvent(QWheelEvent *event) } 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() - ); + #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 @@ -166,15 +166,15 @@ void TimelineView::wheelEvent(QWheelEvent *event) } QWheelEvent e( - event->pos(), - event->globalPos(), - event->pixelDelta(), - event->angleDelta(), - event->delta(), - orientation, - event->buttons(), - event->modifiers() - ); + event->pos(), + event->globalPos(), + event->pixelDelta(), + event->angleDelta(), + event->delta(), + orientation, + event->buttons(), + event->modifiers() + ); #endif QGraphicsView::wheelEvent(&e); @@ -244,6 +244,26 @@ void TimelineView::drawBackground(QPainter *painter, const QRectF &rect) } } +void TimelineView::drawForeground(QPainter *painter, const QRectF &rect) +{ + TimelineViewBase::drawForeground(painter, rect); + + if (show_beam_cursor_ + && connected_track_list_ + && cursor_coord_.GetTrack().type() == connected_track_list_->type() + && cursor_coord_.GetTrack().index() < connected_track_list_->GetTrackCount()) { + painter->setPen(Qt::gray); + + double cursor_x = TimeToScene(cursor_coord_.GetFrame()); + int track_index = cursor_coord_.GetTrack().index(); + + painter->drawLine(cursor_x, + GetTrackY(track_index), + cursor_x, + GetTrackHeight(track_index)); + } +} + void TimelineView::ToolChangedEvent(Tool::Item tool) { switch (tool) { @@ -261,6 +281,12 @@ void TimelineView::ToolChangedEvent(Tool::Item tool) default: unsetCursor(); } + + // Hide/show cursor if necessary + if (show_beam_cursor_) { + show_beam_cursor_ = false; + viewport()->update(); + } } void TimelineView::SceneRectUpdateEvent(QRectF &rect) @@ -399,6 +425,20 @@ void TimelineView::ConnectTrackList(TrackList *list) } } +void TimelineView::SetBeamCursor(const TimelineCoordinate &coord) +{ + bool update_required = true;/*(coord.GetTrack().type() == connected_track_list_->type() + || cursor_coord_.GetTrack().type() == connected_track_list_->type() + || !show_beam_cursor_);*/ + + show_beam_cursor_ = true; + cursor_coord_ = coord; + + if (update_required) { + viewport()->update(); + } +} + int TimelineView::SceneToTrack(double y) { int track = -1; diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index 53daceca0..4bacad401 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -61,6 +61,8 @@ public: void ConnectTrackList(TrackList* list); + void SetBeamCursor(const TimelineCoordinate& coord); + signals: void MousePressed(TimelineViewMouseEvent* event); void MouseMoved(TimelineViewMouseEvent* event); @@ -88,6 +90,7 @@ protected: virtual void dropEvent(QDropEvent *event) override; virtual void drawBackground(QPainter *painter, const QRectF &rect) override; + virtual void drawForeground(QPainter *painter, const QRectF &rect) override; virtual void ToolChangedEvent(Tool::Item tool) override; @@ -111,6 +114,10 @@ private: void UpdatePlayheadRect(); + bool show_beam_cursor_; + + TimelineCoordinate cursor_coord_; + TrackList* connected_track_list_; }; From 1b6e9c1c79f72865da0afbc942fbf1e061a9b809 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 03:00:00 +1000 Subject: [PATCH 005/138] viewer: moved nouveau to application launch rather than viewer init Viewer init was kind of too late since in many cases the sequence would start caching and crash the app before the user could even read the message. Moving to the startup makes it clearer from the beginning. --- app/widget/viewer/viewerdisplay.cpp | 31 ---------------------------- app/widget/viewer/viewerdisplay.h | 11 ---------- app/window/mainwindow/mainwindow.cpp | 30 +++++++++++++++++++++++++++ app/window/mainwindow/mainwindow.h | 6 ++++++ 4 files changed, 36 insertions(+), 42 deletions(-) diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 420eaf423..943314ef7 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -38,10 +38,6 @@ OLIVE_NAMESPACE_ENTER -#ifdef Q_OS_LINUX -bool ViewerDisplayWidget::nouveau_check_done_ = false; -#endif - ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) : ManagedDisplayWidget(parent), signal_cursor_color_(false), @@ -199,21 +195,6 @@ void ViewerDisplayWidget::initializeGL() ManagedDisplayWidget::initializeGL(); connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &ViewerDisplayWidget::ContextCleanup, Qt::DirectConnection); - -#ifdef Q_OS_LINUX - if (!nouveau_check_done_) { - const char* vendor = reinterpret_cast(context()->functions()->glGetString(GL_VENDOR)); - - if (!strcmp(vendor, "nouveau")) { - // Working with Qt widgets in this function segfaults, so we queue the messagebox for later - QMetaObject::invokeMethod(this, - "ShowNouveauWarning", - Qt::QueuedConnection); - } - - nouveau_check_done_ = true; - } -#endif } void ViewerDisplayWidget::paintGL() @@ -305,18 +286,6 @@ rational ViewerDisplayWidget::GetGizmoTime() return GetAdjustedTime(GetTimeTarget(), gizmos_, time_, NodeParam::kInput); } -#ifdef Q_OS_LINUX -void ViewerDisplayWidget::ShowNouveauWarning() -{ - QMessageBox::warning(this, - tr("Driver Warning"), - tr("Olive has detected your system is using the Nouveau graphics driver.\n\nThis driver is " - "known to have stability and performance issues with Olive. It is highly recommended " - "you install the proprietary NVIDIA driver before continuing to use Olive."), - QMessageBox::Ok); -} -#endif - void ViewerDisplayWidget::ContextCleanup() { makeCurrent(); diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index 5f18cc303..f8db608ef 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -160,10 +160,6 @@ private: */ QMatrix4x4 matrix_; -#ifdef Q_OS_LINUX - static bool nouveau_check_done_; -#endif - bool signal_cursor_color_; ViewerSafeMarginInfo safe_margin_; @@ -184,13 +180,6 @@ private slots: */ void ContextCleanup(); -#ifdef Q_OS_LINUX - /** - * @brief Shows warning messagebox if Nouveau is detected - */ - void ShowNouveauWarning(); -#endif - }; OLIVE_NAMESPACE_EXIT diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 31239436d..5e61cd1d6 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -415,6 +415,18 @@ void MainWindow::StatusBarDoubleClicked() task_man_panel_->raise(); } +#ifdef Q_OS_LINUX +void MainWindow::ShowNouveauWarning() +{ + QMessageBox::warning(this, + tr("Driver Warning"), + tr("Olive has detected your system is using the Nouveau graphics driver.\n\nThis driver is " + "known to have stability and performance issues with Olive. It is highly recommended " + "you install the proprietary NVIDIA driver before continuing to use Olive."), + QMessageBox::Ok); +} +#endif + void MainWindow::UpdateTitle() { if (Core::instance()->GetActiveProject()) { @@ -626,6 +638,24 @@ void MainWindow::SetDefaultLayout() Qt::Vertical); } +void MainWindow::showEvent(QShowEvent *e) +{ + QMainWindow::showEvent(e); + +#ifdef Q_OS_LINUX + // Check for nouveau since that driver really doesn't work with Olive + QOffscreenSurface surface; + surface.create(); + QOpenGLContext context; + context.create(); + context.makeCurrent(&surface); + const char* vendor = reinterpret_cast(context.functions()->glGetString(GL_VENDOR)); + if (!strcmp(vendor, "nouveau")) { + QMetaObject::invokeMethod(this, "ShowNouveauWarning", Qt::QueuedConnection); + } +#endif +} + template T *MainWindow::AppendPanelInternal(QList& list) { diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 21a6d561c..f01f0524a 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -103,6 +103,8 @@ public slots: void SetDefaultLayout(); protected: + virtual void showEvent(QShowEvent* e) override; + virtual void closeEvent(QCloseEvent* e) override; #ifdef Q_OS_WINDOWS @@ -167,6 +169,10 @@ private slots: void StatusBarDoubleClicked(); +#ifdef Q_OS_LINUX + void ShowNouveauWarning(); +#endif + }; OLIVE_NAMESPACE_EXIT From a871c5081bdc947ec4c72997ec54b0cf8e0c12c0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 17:48:07 +1000 Subject: [PATCH 006/138] nodeparamtable: bare bones table view for node inputs --- app/codec/ffmpeg/ffmpegdecoder.cpp | 1 + app/codec/oiio/oiiodecoder.cpp | 31 +++-- app/codec/oiio/oiiodecoder.h | 2 + app/node/param.cpp | 85 ++++++++----- app/node/param.h | 7 +- app/node/value.cpp | 15 --- app/node/value.h | 32 ++++- app/panel/CMakeLists.txt | 1 + app/panel/table/CMakeLists.txt | 22 ++++ app/panel/table/table.cpp | 44 +++++++ app/panel/table/table.h | 45 +++++++ app/project/item/footage/imagestream.cpp | 20 ---- app/project/item/footage/imagestream.h | 35 +++++- app/render/videoparams.h | 2 + app/widget/CMakeLists.txt | 1 + app/widget/nodetableview/CMakeLists.txt | 26 ++++ .../nodetableview/nodetabletraverser.cpp | 44 +++++++ app/widget/nodetableview/nodetabletraverser.h | 42 +++++++ app/widget/nodetableview/nodetableview.cpp | 113 ++++++++++++++++++ app/widget/nodetableview/nodetableview.h | 43 +++++++ app/widget/nodetableview/nodetablewidget.cpp | 49 ++++++++ app/widget/nodetableview/nodetablewidget.h | 43 +++++++ app/window/mainwindow/mainwindow.cpp | 6 + app/window/mainwindow/mainwindow.h | 2 + 24 files changed, 620 insertions(+), 91 deletions(-) create mode 100644 app/panel/table/CMakeLists.txt create mode 100644 app/panel/table/table.cpp create mode 100644 app/panel/table/table.h create mode 100644 app/widget/nodetableview/CMakeLists.txt create mode 100644 app/widget/nodetableview/nodetabletraverser.cpp create mode 100644 app/widget/nodetableview/nodetabletraverser.h create mode 100644 app/widget/nodetableview/nodetableview.cpp create mode 100644 app/widget/nodetableview/nodetableview.h create mode 100644 app/widget/nodetableview/nodetablewidget.cpp create mode 100644 app/widget/nodetableview/nodetablewidget.h diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 4d2430e30..3b54fcdb2 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -458,6 +458,7 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) video_stream->set_width(avstream->codecpar->width); video_stream->set_height(avstream->codecpar->height); + video_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast(avstream->codecpar->format)))); video_stream->set_frame_rate(av_guess_frame_rate(fmt_ctx, avstream, nullptr)); video_stream->set_start_time(avstream->start_time); diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index e5f7b74c8..6d0804267 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -117,6 +117,7 @@ bool OIIODecoder::Probe(Footage *f, const QAtomicInt *cancelled) image_stream->set_width(in->spec().width); image_stream->set_height(in->spec().height); + image_stream->set_format(GetFormatFromOIIOBasetype(in->spec())); // Images will always have just one stream image_stream->set_index(0); @@ -278,6 +279,23 @@ void OIIODecoder::BufferToFrame(OIIO::ImageBuf *buf, FramePtr frame) #endif } +PixelFormat::Format OIIODecoder::GetFormatFromOIIOBasetype(const OIIO::ImageSpec& spec) +{ + bool has_alpha = (spec.nchannels == kRGBAChannels); + + if (spec.format == OIIO::TypeDesc::UINT8) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA8 : PixelFormat::PIX_FMT_RGB8; + } else if (spec.format == OIIO::TypeDesc::UINT16) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA16U : PixelFormat::PIX_FMT_RGB16U; + } else if (spec.format == OIIO::TypeDesc::HALF) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA16F : PixelFormat::PIX_FMT_RGB16F; + } else if (spec.format == OIIO::TypeDesc::FLOAT) { + return has_alpha ? PixelFormat::PIX_FMT_RGBA32F : PixelFormat::PIX_FMT_RGB32F; + } else { + return PixelFormat::PIX_FMT_INVALID; + } +} + bool OIIODecoder::FileTypeIsSupported(const QString& fn) { // We prioritize OIIO over FFmpeg to pick up still images more effectively, but some OIIO decoders (notably OpenJPEG) @@ -361,16 +379,9 @@ bool OIIODecoder::OpenImageHandler(const QString &fn) is_rgba_ = (spec.nchannels == kRGBAChannels); - // Weirdly, switch statement doesn't work correctly here - if (spec.format == OIIO::TypeDesc::UINT8) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA8 : PixelFormat::PIX_FMT_RGB8; - } else if (spec.format == OIIO::TypeDesc::UINT16) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA16U : PixelFormat::PIX_FMT_RGB16U; - } else if (spec.format == OIIO::TypeDesc::HALF) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA16F : PixelFormat::PIX_FMT_RGB16F; - } else if (spec.format == OIIO::TypeDesc::FLOAT) { - pix_fmt_ = is_rgba_ ? PixelFormat::PIX_FMT_RGBA32F : PixelFormat::PIX_FMT_RGB32F; - } else { + pix_fmt_ = GetFormatFromOIIOBasetype(spec.format); + + if (pix_fmt_ == PixelFormat::PIX_FMT_INVALID) { qWarning() << "Failed to convert OIIO::ImageDesc to native pixel format"; return false; } diff --git a/app/codec/oiio/oiiodecoder.h b/app/codec/oiio/oiiodecoder.h index f481481b1..990fc3aff 100644 --- a/app/codec/oiio/oiiodecoder.h +++ b/app/codec/oiio/oiiodecoder.h @@ -57,6 +57,8 @@ public: static void BufferToFrame(OIIO::ImageBuf* buf, FramePtr frame); + static PixelFormat::Format GetFormatFromOIIOBasetype(const OIIO::ImageSpec& spec); + private: #if OIIO_VERSION < 10903 OIIO::ImageInput* image_; diff --git a/app/node/param.cpp b/app/node/param.cpp index ff8e72b21..ec1eb8b65 100644 --- a/app/node/param.cpp +++ b/app/node/param.cpp @@ -185,6 +185,58 @@ NodeEdgePtr NodeParam::DisconnectForNewOutput(NodeInput *input) return nullptr; } +QString NodeParam::GetPrettyDataTypeName(const NodeParam::DataType &type) +{ + switch (type) { + case kNone: + return tr("None"); + case kInt: + case kCombo: + return tr("Integer"); + case kFloat: + return tr("Float"); + case kRational: + return tr("Rational"); + case kBoolean: + return tr("Boolean"); + case kColor: + return tr("Color"); + case kMatrix: + return tr("Matrix"); + case kText: + return tr("Text"); + case kFont: + return tr("Font"); + case kFile: + return tr("File"); + case kTexture: + return tr("Texture"); + case kSamples: + return tr("Samples"); + case kFootage: + return tr("Footage"); + case kVec2: + return tr("Vector 2D"); + case kVec3: + return tr("Vector 3D"); + case kVec4: + return tr("Vector 4D"); + + case kDecimal: + case kNumber: + case kString: + case kBuffer: + case kVector: + case kShaderJob: + case kSampleJob: + case kGenerateJob: + case kAny: + break; + } + + return tr("Unknown"); +} + QByteArray NodeParam::ValueToBytes(const NodeParam::DataType &type, const QVariant &value) { switch (type) { @@ -222,39 +274,6 @@ QByteArray NodeParam::ValueToBytes(const NodeParam::DataType &type, const QVaria return QByteArray(); } -NodeParam::DataType NodeParam::StringToDataType(const QString &s) -{ - QString type_id = s.toLower(); - - if (type_id == QStringLiteral("float")) { - return kFloat; - } else if (type_id == QStringLiteral("int")) { - return kInt; - } else if (type_id == QStringLiteral("rational")) { - return kRational; - } else if (type_id == QStringLiteral("bool")) { - return kBoolean; - } else if (type_id == QStringLiteral("color")) { - return kColor; - } else if (type_id == QStringLiteral("matrix")) { - return kMatrix; - } else if (type_id == QStringLiteral("text")) { - return kText; - } else if (type_id == QStringLiteral("texture")) { - return kTexture; - } else if (type_id == QStringLiteral("vec2")) { - return kVec2; - } else if (type_id == QStringLiteral("vec3")) { - return kVec3; - } else if (type_id == QStringLiteral("vec4")) { - return kVec4; - } else if (type_id == QStringLiteral("combo")) { - return kCombo; - } - - return kAny; -} - template QByteArray NodeParam::ValueToBytesInternal(const QVariant &v) { diff --git a/app/node/param.h b/app/node/param.h index 40e5a9716..03e667936 100644 --- a/app/node/param.h +++ b/app/node/param.h @@ -378,18 +378,13 @@ public: /** * @brief Get a human-readable translated name for a certain data type */ - static QString GetDefaultDataTypeName(const DataType &type); + static QString GetPrettyDataTypeName(const DataType &type); /** * @brief Convert a value from a NodeParam into bytes */ static QByteArray ValueToBytes(const DataType &type, const QVariant& value); - /** - * @brief Convert a string to a data type - */ - static DataType StringToDataType(const QString& s); - signals: /** * @brief Signal emitted when an edge is added to this parameter diff --git a/app/node/value.cpp b/app/node/value.cpp index d487f0a3d..e2f9913c5 100644 --- a/app/node/value.cpp +++ b/app/node/value.cpp @@ -61,26 +61,11 @@ NodeValue::NodeValue(const NodeParam::DataType &type, const QVariant &data, cons { } -const NodeParam::DataType &NodeValue::type() const -{ - return type_; -} - -const QString &NodeValue::tag() const -{ - return tag_; -} - bool NodeValue::operator==(const NodeValue &rhs) const { return type_ == rhs.type_ && tag_ == rhs.tag_ && data_ == rhs.data_; } -const QVariant &NodeValue::data() const -{ - return data_; -} - QVariant NodeValueTable::Get(const NodeParam::DataType &type, const QString &tag) const { return GetWithMeta(type, tag).data(); diff --git a/app/node/value.h b/app/node/value.h index 387117993..7efb8592a 100644 --- a/app/node/value.h +++ b/app/node/value.h @@ -33,9 +33,25 @@ public: NodeValue(); NodeValue(const NodeParam::DataType& type, const QVariant& data, const Node* from, const QString& tag = QString()); - const NodeParam::DataType& type() const; - const QVariant& data() const; - const QString& tag() const; + const NodeParam::DataType& type() const + { + return type_; + } + + const QVariant& data() const + { + return data_; + } + + const QString& tag() const + { + return tag_; + } + + const Node* source() const + { + return from_; + } bool operator==(const NodeValue& rhs) const; @@ -90,6 +106,16 @@ public: NodeValueTable Merge() const; + using const_iterator = QHash::const_iterator; + + inline QHash::const_iterator begin() const { + return tables_.cbegin(); + } + + inline QHash::const_iterator end() const { + return tables_.cend(); + } + private: QHash tables_; diff --git a/app/panel/CMakeLists.txt b/app/panel/CMakeLists.txt index 0c6423398..75c1c7738 100644 --- a/app/panel/CMakeLists.txt +++ b/app/panel/CMakeLists.txt @@ -23,6 +23,7 @@ add_subdirectory(pixelsampler) add_subdirectory(project) add_subdirectory(scope) add_subdirectory(sequenceviewer) +add_subdirectory(table) add_subdirectory(taskmanager) add_subdirectory(timebased) add_subdirectory(timeline) diff --git a/app/panel/table/CMakeLists.txt b/app/panel/table/CMakeLists.txt new file mode 100644 index 000000000..f332aa646 --- /dev/null +++ b/app/panel/table/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + panel/table/table.h + panel/table/table.cpp + PARENT_SCOPE +) diff --git a/app/panel/table/table.cpp b/app/panel/table/table.cpp new file mode 100644 index 000000000..7183e9c23 --- /dev/null +++ b/app/panel/table/table.cpp @@ -0,0 +1,44 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "table.h" + +OLIVE_NAMESPACE_ENTER + +NodeTablePanel::NodeTablePanel(QWidget* parent) : + TimeBasedPanel(QStringLiteral("NodeTablePanel"), parent) +{ + view_ = new NodeTableWidget(); + SetTimeBasedWidget(view_); + + Retranslate(); +} + +void NodeTablePanel::SetNodes(const QList &nodes) +{ + view_->SetNodes(nodes); +} + +void NodeTablePanel::Retranslate() +{ + SetTitle(tr("Table View")); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/panel/table/table.h b/app/panel/table/table.h new file mode 100644 index 000000000..0587cf51b --- /dev/null +++ b/app/panel/table/table.h @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLEPANEL_H +#define NODETABLEPANEL_H + +#include "panel/timebased/timebased.h" +#include "widget/nodetableview/nodetablewidget.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTablePanel : public TimeBasedPanel +{ +public: + NodeTablePanel(QWidget* parent); + +public slots: + void SetNodes(const QList& nodes); + +private: + virtual void Retranslate() override; + + NodeTableWidget* view_; +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLEPANEL_H diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp index beb5f3b3b..41a9d0d23 100644 --- a/app/project/item/footage/imagestream.cpp +++ b/app/project/item/footage/imagestream.cpp @@ -72,26 +72,6 @@ QString ImageStream::description() const QString::number(height())); } -const int &ImageStream::width() const -{ - return width_; -} - -void ImageStream::set_width(const int &width) -{ - width_ = width; -} - -const int &ImageStream::height() const -{ - return height_; -} - -void ImageStream::set_height(const int &height) -{ - height_ = height; -} - bool ImageStream::premultiplied_alpha() const { return premultiplied_alpha_; diff --git a/app/project/item/footage/imagestream.h b/app/project/item/footage/imagestream.h index 9e3b6f72b..30fc712a2 100644 --- a/app/project/item/footage/imagestream.h +++ b/app/project/item/footage/imagestream.h @@ -21,6 +21,7 @@ #ifndef IMAGESTREAM_H #define IMAGESTREAM_H +#include "render/pixelformat.h" #include "stream.h" OLIVE_NAMESPACE_ENTER @@ -36,11 +37,35 @@ public: virtual QString description() const override; - const int& width() const; - void set_width(const int& width); + const int& width() const + { + return width_; + } - const int& height() const; - void set_height(const int& height); + void set_width(const int& width) + { + width_ = width; + } + + const int& height() const + { + return height_; + } + + void set_height(const int& height) + { + height_ = height; + } + + const PixelFormat::Format& format() const + { + return format_; + } + + void set_format(const PixelFormat::Format& format) + { + format_ = format; + } bool premultiplied_alpha() const; void set_premultiplied_alpha(bool e); @@ -63,6 +88,8 @@ private: bool premultiplied_alpha_; QString colorspace_; + PixelFormat::Format format_; + private slots: void ColorConfigChanged(); diff --git a/app/render/videoparams.h b/app/render/videoparams.h index b108874d5..56534ed3c 100644 --- a/app/render/videoparams.h +++ b/app/render/videoparams.h @@ -91,4 +91,6 @@ private: OLIVE_NAMESPACE_EXIT +Q_DECLARE_METATYPE(OLIVE_NAMESPACE::VideoParams) + #endif // VIDEOPARAMS_H diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt index dc068e82b..67c20349e 100644 --- a/app/widget/CMakeLists.txt +++ b/app/widget/CMakeLists.txt @@ -31,6 +31,7 @@ add_subdirectory(nodecombobox) add_subdirectory(nodecopypaste) add_subdirectory(nodeview) add_subdirectory(nodeparamview) +add_subdirectory(nodetableview) add_subdirectory(panel) add_subdirectory(pixelsampler) add_subdirectory(playbackcontrols) diff --git a/app/widget/nodetableview/CMakeLists.txt b/app/widget/nodetableview/CMakeLists.txt new file mode 100644 index 000000000..f12dff040 --- /dev/null +++ b/app/widget/nodetableview/CMakeLists.txt @@ -0,0 +1,26 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/nodetableview/nodetabletraverser.h + widget/nodetableview/nodetabletraverser.cpp + widget/nodetableview/nodetableview.h + widget/nodetableview/nodetableview.cpp + widget/nodetableview/nodetablewidget.h + widget/nodetableview/nodetablewidget.cpp + PARENT_SCOPE +) diff --git a/app/widget/nodetableview/nodetabletraverser.cpp b/app/widget/nodetableview/nodetabletraverser.cpp new file mode 100644 index 000000000..1ca0e1080 --- /dev/null +++ b/app/widget/nodetableview/nodetabletraverser.cpp @@ -0,0 +1,44 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodetabletraverser.h" + +OLIVE_NAMESPACE_ENTER + +QVariant NodeTableTraverser::ProcessVideoFootage(StreamPtr stream, const rational &input_time) +{ + ImageStreamPtr video_stream = std::static_pointer_cast(stream); + + return QVariant::fromValue(VideoParams(video_stream->width(), + video_stream->height(), + video_stream->timebase(), + video_stream->format())); +} + +QVariant NodeTableTraverser::ProcessAudioFootage(StreamPtr stream, const TimeRange &input_time) +{ + AudioStreamPtr audio_stream = std::static_pointer_cast(stream); + + return QVariant::fromValue(AudioParams(audio_stream->sample_rate(), + audio_stream->channel_layout(), + SampleFormat::kInternalFormat)); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetabletraverser.h b/app/widget/nodetableview/nodetabletraverser.h new file mode 100644 index 000000000..20dae8e2e --- /dev/null +++ b/app/widget/nodetableview/nodetabletraverser.h @@ -0,0 +1,42 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLETRAVERSER_H +#define NODETABLETRAVERSER_H + +#include "node/traverser.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTableTraverser : public NodeTraverser +{ +public: + NodeTableTraverser() = default; + +protected: + virtual QVariant ProcessVideoFootage(StreamPtr stream, const rational &input_time); + + virtual QVariant ProcessAudioFootage(StreamPtr stream, const TimeRange &input_time); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLETRAVERSER_H diff --git a/app/widget/nodetableview/nodetableview.cpp b/app/widget/nodetableview/nodetableview.cpp new file mode 100644 index 000000000..8e9f5aab0 --- /dev/null +++ b/app/widget/nodetableview/nodetableview.cpp @@ -0,0 +1,113 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodetableview.h" + +#include + +#include "node/param.h" +#include "nodetabletraverser.h" + +OLIVE_NAMESPACE_ENTER + +NodeTableView::NodeTableView(QWidget* parent) : + QTreeWidget(parent) +{ + setColumnCount(3); + setHeaderLabels({tr("Type"), tr("Value"), tr("Source")}); +} + +void NodeTableView::SetNode(Node *n, const rational &time) +{ + clear(); + + NodeTableTraverser traverser; + NodeValueDatabase db = traverser.GenerateDatabase(n, TimeRange(time, time)); + + NodeValueDatabase::const_iterator i; + + for (i=db.begin(); i!=db.end(); i++) { + const NodeValueTable& table = i.value(); + + NodeInput* input = n->GetInputWithID(i.key()); + if (!input) { + // Filters out table entries that aren't inputs (like "global") + continue; + } + + QTreeWidgetItem* top_item = new QTreeWidgetItem(); + top_item->setText(0, input->name()); + top_item->setFirstColumnSpanned(true); + this->addTopLevelItem(top_item); + + for (int j=table.Count()-1; j>=0; j--) { + const NodeValue& value = table.at(j); + + QString value_str = NodeInput::ValueToString(value.type(), value.data()); + + QString source_name; + if (value.source()) { + source_name = value.source()->Name(); + } else { + source_name = tr("(unknown)"); + } + + QTreeWidgetItem* sub_item = new QTreeWidgetItem(); + sub_item->setText(0, NodeParam::GetPrettyDataTypeName(value.type())); + sub_item->setText(1, value_str); + sub_item->setText(2, source_name); + top_item->addChild(sub_item); + + // Special cases + if (value.type() == NodeParam::kTexture) { + // NodeTableTraverser converts footage to VideoParams + QTreeWidgetItem* red_channel = new QTreeWidgetItem(); + red_channel->setText(0, tr("Red")); + sub_item->addChild(red_channel); + + QTreeWidgetItem* green_channel = new QTreeWidgetItem(); + green_channel->setText(0, tr("Green")); + sub_item->addChild(green_channel); + + QTreeWidgetItem* blue_channel = new QTreeWidgetItem(); + blue_channel->setText(0, tr("Blue")); + sub_item->addChild(blue_channel); + + if (PixelFormat::FormatHasAlphaChannel(value.data().value().format())) { + QTreeWidgetItem* alpha_channel = new QTreeWidgetItem(); + alpha_channel->setText(0, tr("Alpha")); + sub_item->addChild(alpha_channel); + } + } + } + } +} + +void NodeTableView::SetMultipleNodeMessage() +{ + this->clear(); + + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setText(0, tr("Multiple nodes selected")); + item->setFirstColumnSpanned(true); + this->addTopLevelItem(item); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetableview.h b/app/widget/nodetableview/nodetableview.h new file mode 100644 index 000000000..e453903b7 --- /dev/null +++ b/app/widget/nodetableview/nodetableview.h @@ -0,0 +1,43 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLEVIEW_H +#define NODETABLEVIEW_H + +#include + +#include "node/node.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTableView : public QTreeWidget +{ +public: + NodeTableView(QWidget* parent = nullptr); + + void SetNode(Node* n, const rational& time); + + void SetMultipleNodeMessage(); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLEVIEW_H diff --git a/app/widget/nodetableview/nodetablewidget.cpp b/app/widget/nodetableview/nodetablewidget.cpp new file mode 100644 index 000000000..20db058c2 --- /dev/null +++ b/app/widget/nodetableview/nodetablewidget.cpp @@ -0,0 +1,49 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "nodetablewidget.h" + +#include + +OLIVE_NAMESPACE_ENTER + +NodeTableWidget::NodeTableWidget(QWidget* parent) : + TimeBasedWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setSpacing(0); + layout->setMargin(0); + + view_ = new NodeTableView(); + layout->addWidget(view_); +} + +void NodeTableWidget::SetNodes(const QList &nodes) +{ + if (nodes.isEmpty()) { + view_->clear(); + } else if (nodes.size() == 1) { + view_->SetNode(nodes.first(), rational()); + } else { + view_->SetMultipleNodeMessage(); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetablewidget.h b/app/widget/nodetableview/nodetablewidget.h new file mode 100644 index 000000000..ae0c82cc4 --- /dev/null +++ b/app/widget/nodetableview/nodetablewidget.h @@ -0,0 +1,43 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef NODETABLEWIDGET_H +#define NODETABLEWIDGET_H + +#include "nodetableview.h" +#include "widget/timebased/timebased.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTableWidget : public TimeBasedWidget +{ +public: + NodeTableWidget(QWidget* parent = nullptr); + + void SetNodes(const QList& nodes); + +private: + NodeTableView* view_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETABLEWIDGET_H diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 31239436d..e6b0f8d34 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -72,6 +72,7 @@ MainWindow::MainWindow(QWidget *parent) : node_panel_ = PanelManager::instance()->CreatePanel(this); footage_viewer_panel_ = PanelManager::instance()->CreatePanel(this); param_panel_ = PanelManager::instance()->CreatePanel(this); + table_panel_ = PanelManager::instance()->CreatePanel(this); sequence_viewer_panel_ = PanelManager::instance()->CreatePanel(this); pixel_sampler_panel_ = PanelManager::instance()->CreatePanel(this); AppendProjectPanel(); @@ -82,6 +83,7 @@ MainWindow::MainWindow(QWidget *parent) : // Make connections to sequence viewer connect(node_panel_, &NodePanel::SelectionChanged, param_panel_, &ParamPanel::SetNodes); + connect(node_panel_, &NodePanel::SelectionChanged, table_panel_, &NodeTablePanel::SetNodes); connect(param_panel_, &ParamPanel::RequestSelectNode, node_panel_, &NodePanel::Select); connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp); connect(param_panel_, &ParamPanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp); @@ -590,6 +592,10 @@ void MainWindow::SetDefaultLayout() tabifyDockWidget(footage_viewer_panel_, param_panel_); footage_viewer_panel_->raise(); + table_panel_->hide(); + table_panel_->setFloating(true); + addDockWidget(Qt::TopDockWidgetArea, table_panel_); + sequence_viewer_panel_->show(); addDockWidget(Qt::TopDockWidgetArea, sequence_viewer_panel_); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 21a6d561c..787eb2899 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -30,6 +30,7 @@ #include "panel/param/param.h" #include "panel/project/project.h" #include "panel/scope/scope.h" +#include "panel/table/table.h" #include "panel/taskmanager/taskmanager.h" #include "panel/timeline/timeline.h" #include "panel/tool/tool.h" @@ -145,6 +146,7 @@ private: QList curve_panels_; PixelSamplerPanel* pixel_sampler_panel_; QList scope_panels_; + NodeTablePanel* table_panel_; #ifdef Q_OS_WINDOWS unsigned int taskbar_btn_id_; From 2261f1147d3a0610c4d05165fca25cf069759461 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 17:48:41 +1000 Subject: [PATCH 007/138] renderer: catch failure to load pre-cached frame --- app/render/backend/renderworker.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 7142eae07..1ff861658 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -244,14 +244,16 @@ QVariant RenderWorker::GetCachedFrame(const Node* node, const rational& time) if (QFileInfo::exists(fn)) { FramePtr f = FrameHashCache::LoadCacheFrame(hash); - // The cached frame won't load with the correct divider by default, so we enforce it here - f->set_video_params(VideoParams(f->width() * video_params_.divider(), - f->height() * video_params_.divider(), - f->video_params().time_base(), - f->video_params().format(), - video_params_.divider())); + if (f) { + // The cached frame won't load with the correct divider by default, so we enforce it here + f->set_video_params(VideoParams(f->width() * video_params_.divider(), + f->height() * video_params_.divider(), + f->video_params().time_base(), + f->video_params().format(), + video_params_.divider())); - return CachedFrameToTexture(f); + return CachedFrameToTexture(f); + } } } @@ -316,9 +318,7 @@ QVariant RenderWorker::ProcessVideoFootage(StreamPtr stream, const rational &inp // Return a texture from the derived class value = FootageFrameToTexture(stream, frame); - if (value.isNull()) { - qDebug() << "Texture from derivative was blank"; - } else { + if (!value.isNull()) { // Put this into the image cache instead still_image_cache_.insert(stream.get(), {value, colorspace_match, @@ -326,8 +326,6 @@ QVariant RenderWorker::ProcessVideoFootage(StreamPtr stream, const rational &inp video_params_.divider(), time_match}); } - } else { - qDebug() << "Frame from decoder was blank"; } } From c3c26a8e240a504b75e00af58216acec76009146 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 17:49:54 +1000 Subject: [PATCH 008/138] cache: use openexr directly OIIO lacks fidelity over OpenEXR's threading. Using it directly gives us a little more control. --- app/render/framehashcache.cpp | 65 ++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/app/render/framehashcache.cpp b/app/render/framehashcache.cpp index 0bde0581a..58f18a957 100644 --- a/app/render/framehashcache.cpp +++ b/app/render/framehashcache.cpp @@ -206,34 +206,51 @@ FramePtr FrameHashCache::LoadCacheFrame(const QString &fn) FramePtr frame = nullptr; if (!fn.isEmpty() && QFileInfo::exists(fn)) { - auto input = OIIO::ImageInput::open(fn.toStdString()); + Imf::InputFile file(fn.toUtf8(), 0); - if (input) { - - PixelFormat::Format image_format = PixelFormat::OIIOFormatToOliveFormat(input->spec().format, - input->spec().nchannels == kRGBAChannels); - - frame = Frame::Create(); - frame->set_video_params(VideoParams(input->spec().width, - input->spec().height, - image_format)); - - frame->allocate(); - - input->read_image(input->spec().format, - frame->data(), - OIIO::AutoStride, - frame->linesize_bytes()); - - input->close(); - -#if OIIO_VERSION < 10903 - OIIO::ImageInput::destroy(input); -#endif + Imath::Box2i dw = file.header().dataWindow(); + Imf::PixelType pix_type = file.header().channels().begin().channel().type; + int width = dw.max.x - dw.min.x + 1; + int height = dw.max.y - dw.min.y + 1; + bool has_alpha = file.header().channels().findChannel("A"); + PixelFormat::Format image_format; + if (pix_type == Imf::HALF) { + if (has_alpha) { + image_format = PixelFormat::PIX_FMT_RGBA16F; + } else { + image_format = PixelFormat::PIX_FMT_RGB16F; + } } else { - qWarning() << "OIIO Error:" << OIIO::geterror().c_str(); + if (has_alpha) { + image_format = PixelFormat::PIX_FMT_RGBA32F; + } else { + image_format = PixelFormat::PIX_FMT_RGB32F; + } } + + frame = Frame::Create(); + frame->set_video_params(VideoParams(width, + height, + image_format)); + + frame->allocate(); + + int bpc = PixelFormat::BytesPerChannel(image_format); + + size_t xs = PixelFormat::ChannelCount(image_format) * bpc; + size_t ys = frame->linesize_bytes(); + + Imf::FrameBuffer framebuffer; + framebuffer.insert("R", Imf::Slice(pix_type, frame->data(), xs, ys)); + framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc, xs, ys)); + framebuffer.insert("B", Imf::Slice(pix_type, frame->data() + 2*bpc, xs, ys)); + if (has_alpha) { + framebuffer.insert("A", Imf::Slice(pix_type, frame->data() + 3*bpc, xs, ys)); + } + + file.setFrameBuffer(framebuffer); + file.readPixels(dw.min.y, dw.max.y); } return frame; From a818e17c551fd4ff4be1fb020cb6e3831ce62f91 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 18:06:50 +1000 Subject: [PATCH 009/138] export: made progress on headless export Still incomplete and unusable, but less so than before. --- app/cli/cliexport/cliexportmanager.cpp | 30 ++++ app/cli/cliexport/cliexportmanager.h | 36 ++++ app/cli/cliprogress/cliprogressdialog.cpp | 9 +- app/cli/cliprogress/cliprogressdialog.h | 4 +- app/cli/clitask/clitaskdialog.cpp | 10 +- app/cli/clitask/clitaskdialog.h | 6 + app/codec/encoder.cpp | 57 ++++++ app/codec/encoder.h | 3 + app/core.cpp | 206 ++++++++++++++++------ app/core.h | 18 +- app/main.cpp | 20 +-- app/task/export/exportparams.cpp | 22 +++ app/task/export/exportparams.h | 2 + app/task/project/load/load.cpp | 2 + 14 files changed, 337 insertions(+), 88 deletions(-) create mode 100644 app/cli/cliexport/cliexportmanager.cpp create mode 100644 app/cli/cliexport/cliexportmanager.h diff --git a/app/cli/cliexport/cliexportmanager.cpp b/app/cli/cliexport/cliexportmanager.cpp new file mode 100644 index 000000000..de0145729 --- /dev/null +++ b/app/cli/cliexport/cliexportmanager.cpp @@ -0,0 +1,30 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "cliexportmanager.h" + +OLIVE_NAMESPACE_ENTER + +CLIExportManager::CLIExportManager() +{ + +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/cli/cliexport/cliexportmanager.h b/app/cli/cliexport/cliexportmanager.h new file mode 100644 index 000000000..6d3fc346b --- /dev/null +++ b/app/cli/cliexport/cliexportmanager.h @@ -0,0 +1,36 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef CLIEXPORTMANAGER_H +#define CLIEXPORTMANAGER_H + +#include "task/export/export.h" + +OLIVE_NAMESPACE_ENTER + +class CLIExportManager : public QObject +{ +public: + CLIExportManager(); +}; + +OLIVE_NAMESPACE_EXIT + +#endif // CLIEXPORTMANAGER_H diff --git a/app/cli/cliprogress/cliprogressdialog.cpp b/app/cli/cliprogress/cliprogressdialog.cpp index fa846c1ff..88c0cfd57 100644 --- a/app/cli/cliprogress/cliprogressdialog.cpp +++ b/app/cli/cliprogress/cliprogressdialog.cpp @@ -27,9 +27,10 @@ OLIVE_NAMESPACE_ENTER CLIProgressDialog::CLIProgressDialog(const QString& title, QObject *parent) : QObject(parent), title_(title), - progress_(0), + progress_(-1), drawn_(false) { + SetProgress(0); } void CLIProgressDialog::Update() @@ -68,7 +69,7 @@ void CLIProgressDialog::Update() std::cout << "["; // Get UI bar progress - int bar_prog = qRound(progress_ * 0.01 * progress_bar_columns); + int bar_prog = qRound(progress_ * progress_bar_columns); // Draw filled in bar for (int i=0;iGetTitle(), parent) + CLIProgressDialog(task->GetTitle(), parent), + task_(task) { - // FIXME: Still developing this, don't try to use + connect(task_, &Task::ProgressChanged, this, &CLITaskDialog::SetProgress); +} + +bool CLITaskDialog::Run() +{ + return task_->Start(); } OLIVE_NAMESPACE_EXIT diff --git a/app/cli/clitask/clitaskdialog.h b/app/cli/clitask/clitaskdialog.h index 6408b238e..c574b6011 100644 --- a/app/cli/clitask/clitaskdialog.h +++ b/app/cli/clitask/clitaskdialog.h @@ -28,9 +28,15 @@ OLIVE_NAMESPACE_ENTER class CLITaskDialog : public CLIProgressDialog { + Q_OBJECT public: CLITaskDialog(Task *task, QObject* parent = nullptr); + bool Run(); + +private: + Task* task_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index 43e0ca1a6..388278525 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -158,6 +158,63 @@ void EncodingParams::SetExportLength(const rational &export_length) export_length_ = export_length; } +void EncodingParams::Save(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("encode")); + + writer->writeTextElement(QStringLiteral("filename"), filename_); + + writer->writeStartElement(QStringLiteral("video")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(video_enabled_)); + + if (video_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(video_codec_)); + writer->writeTextElement(QStringLiteral("width"), QString::number(video_params_.width())); + writer->writeTextElement(QStringLiteral("height"), QString::number(video_params_.height())); + writer->writeTextElement(QStringLiteral("format"), QString::number(video_params_.format())); + writer->writeTextElement(QStringLiteral("timebase"), video_params_.time_base().toString()); + writer->writeTextElement(QStringLiteral("divider"), QString::number(video_params_.divider())); + writer->writeTextElement(QStringLiteral("bitrate"), QString::number(video_bit_rate_)); + writer->writeTextElement(QStringLiteral("maxbitrate"), QString::number(video_max_bit_rate_)); + writer->writeTextElement(QStringLiteral("bufsize"), QString::number(video_buffer_size_)); + writer->writeTextElement(QStringLiteral("threads"), QString::number(video_threads_)); + + if (!video_opts_.isEmpty()) { + writer->writeStartElement(QStringLiteral("opts")); + + QHash::const_iterator i; + for (i=video_opts_.constBegin(); i!=video_opts_.constEnd(); i++) { + writer->writeStartElement(QStringLiteral("entry")); + + writer->writeTextElement(QStringLiteral("key"), i.key()); + writer->writeTextElement(QStringLiteral("value"), i.value()); + + writer->writeEndElement(); // entry + } + + writer->writeEndElement(); // opts + } + } + + writer->writeEndElement(); // video + + writer->writeStartElement(QStringLiteral("audio")); + + writer->writeAttribute(QStringLiteral("enabled"), QString::number(audio_enabled_)); + + if (audio_enabled_) { + writer->writeTextElement(QStringLiteral("codec"), QString::number(audio_codec_)); + writer->writeTextElement(QStringLiteral("samplerate"), QString::number(audio_params_.sample_rate())); + writer->writeTextElement(QStringLiteral("channellayout"), QString::number(audio_params_.channel_layout())); + writer->writeTextElement(QStringLiteral("format"), QString::number(audio_params_.format())); + } + + writer->writeEndElement(); // audio + + writer->writeEndElement(); // encode +} + Encoder* Encoder::CreateFromID(const QString &id, const EncodingParams& params) { Q_UNUSED(id) diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 8a60a75cc..260c735a3 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -23,6 +23,7 @@ #include #include +#include #include "codec/exportcodec.h" #include "codec/exportformat.h" @@ -69,6 +70,8 @@ public: const rational& GetExportLength() const; void SetExportLength(const rational& GetExportLength); + virtual void Save(QXmlStreamWriter* writer) const; + private: QString filename_; diff --git a/app/core.cpp b/app/core.cpp index 70fb71f6c..d737a0563 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -81,18 +81,17 @@ Core *Core::instance() return &instance_; } -bool Core::Start() +int Core::execute(QCoreApplication* a) { - // Reset config (Config sets to default on construction already, but we do it again here as a workaround that fixes - // the fact that some of the config paths set by default rely on the app name having been set (in main()) - Config::Current().SetDefaults(); + int exit_code = 1; + + // Start core + OLIVE_NAMESPACE::Core::instance()->Start(); // // Parse command line arguments // - QCoreApplication* app = QCoreApplication::instance(); - QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); @@ -111,7 +110,7 @@ bool Core::Start() parser.addOption(headless_export_option); // Parse options - parser.process(*app); + parser.process(*a); QStringList args = parser.positionalArguments(); @@ -120,6 +119,44 @@ bool Core::Start() startup_project_ = args.first(); } + gui_active_ = !parser.isSet(headless_export_option); + + if (gui_active_) { + + // Start GUI + StartGUI(parser.isSet(fullscreen_option)); + + // If we have a startup + QMetaObject::invokeMethod(this, "OpenStartupProject", Qt::QueuedConnection); + + // Run application loop and receive exit code + exit_code = a->exec(); + + } else { + + if (parser.isSet(headless_export_option)) { + // Start a headless export + if (StartHeadlessExport()) { + exit_code = 0; + } + } + + } + + + + // Clear core memory + OLIVE_NAMESPACE::Core::instance()->Stop(); + + return exit_code; +} + +void Core::Start() +{ + // Reset config (Config sets to default on construction already, but we do it again here as a workaround that fixes + // the fact that some of the config paths set by default rely on the app name having been set (in main()) + Config::Current().SetDefaults(); + // Declare custom types for Qt signal/slot system DeclareTypesForQt(); @@ -141,53 +178,6 @@ bool Core::Start() // qInfo() << "Using Qt version:" << qVersion(); - - gui_active_ = !parser.isSet(headless_export_option); - - if (gui_active_) { - - // Start GUI - StartGUI(parser.isSet(fullscreen_option)); - - // Load startup project - if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) { - QMessageBox::warning(main_window(), - tr("Failed to open startup file"), - tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_), - QMessageBox::Ok); - - startup_project_.clear(); - } - - if (startup_project_.isEmpty()) { - // If no load project is set, create a new one on open - CreateNewProject(); - } else { - OpenProjectInternal(startup_project_); - } - - return true; - - } else { - - if (parser.isSet(headless_export_option)) { - - if (startup_project_.isEmpty()) { - qCritical().noquote() << tr("You must specify a project file to export"); - } else { - OpenProjectInternal(startup_project_); - - qDebug() << "Ready for exporting!"; - - return true; - } - - } - - // Error fallback - return false; - - } } void Core::Stop() @@ -539,6 +529,106 @@ void Core::ProjectWasModified(bool e) } } +bool Core::StartHeadlessExport() +{ + if (startup_project_.isEmpty()) { + qCritical().noquote() << tr("You must specify a project file to export"); + return false; + } + + if (!QFileInfo::exists(startup_project_)) { + qCritical().noquote() << tr("Specified project does not exist"); + return false; + } + + // Start a load task and try running it + ProjectLoadTask plm(startup_project_); + CLITaskDialog task_dialog(&plm); + + if (task_dialog.Run()) { + ProjectPtr p = plm.GetLoadedProjects().first(); + QList items = p->get_items_of_type(Item::kSequence); + + // Check if this project contains sequences + if (items.isEmpty()) { + qCritical().noquote() << tr("Project contains no sequences, nothing to export"); + return false; + } + + SequencePtr sequence = nullptr; + + // Check if this project contains multiple sequences + if (items.size() > 1) { + qInfo().noquote() << tr("This project has multiple sequences. Which do you wish to export?"); + for (int i=0;iname().toStdString(); + } + + QTextStream stream(stdin); + QString sequence_read; + int sequence_index = -1; + QString quit_code = QStringLiteral("q"); + std::string prompt = tr("Enter number (or %1 to cancel): ").arg(quit_code).toStdString(); + forever { + std::cout << prompt; + + stream.readLineInto(&sequence_read); + + if (!QString::compare(sequence_read, quit_code, Qt::CaseInsensitive)) { + return false; + } + + bool ok; + sequence_index = sequence_read.toInt(&ok); + + if (ok && sequence_index >= 0 && sequence_index < items.size()) { + break; + } else { + qCritical().noquote() << tr("Invalid sequence number"); + } + } + + sequence = std::static_pointer_cast(items.at(sequence_index)); + } else { + sequence = std::static_pointer_cast(items.first()); + } + + ExportParams params; + ExportTask export_task(sequence->viewer_output(), p->color_manager(), params); + CLITaskDialog export_dialog(&export_task); + if (export_dialog.Run()) { + qInfo().noquote() << tr("Export succeeded"); + return true; + } else { + qInfo().noquote() << tr("Export failed: %1").arg(export_task.GetError()); + return false; + } + } else { + qCritical().noquote() << tr("Project failed to load: %1").arg(plm.GetError()); + return false; + } +} + +void Core::OpenStartupProject() +{ + // Load startup project + if (!startup_project_.isEmpty() && !QFileInfo::exists(startup_project_)) { + QMessageBox::warning(main_window_, + tr("Failed to open startup file"), + tr("The project \"%1\" doesn't exist. A new project will be started instead.").arg(startup_project_), + QMessageBox::Ok); + + startup_project_.clear(); + } + + if (startup_project_.isEmpty()) { + // If no load project is set, create a new one on open + CreateNewProject(); + } else { + OpenProjectInternal(startup_project_); + } +} + void Core::DeclareTypesForQt() { qRegisterMetaType(); @@ -559,6 +649,7 @@ void Core::DeclareTypesForQt() qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); } void Core::StartGUI(bool full_screen) @@ -877,6 +968,11 @@ bool Core::SaveProjectAs(ProjectPtr p) GetProjectFilter()); if (!fn.isEmpty()) { + QString extension(QStringLiteral(".ove")); + if (!fn.endsWith(extension, Qt::CaseInsensitive)) { + fn.append(extension); + } + p->set_filename(fn); SaveProjectInternal(p); @@ -927,7 +1023,7 @@ void Core::OpenProjectInternal(const QString &filename) //connect(plm, &ProjectLoadManager::ProjectLoaded, this, &Core::AddOpenProject); - CLITaskDialog task_dialog(plm); + } } diff --git a/app/core.h b/app/core.h index 07aeccb08..9b3d25728 100644 --- a/app/core.h +++ b/app/core.h @@ -66,12 +66,14 @@ public: */ static Core* instance(); + int execute(QCoreApplication *a); + /** * @brief Start Olive Core * * Main application launcher. Parses command line arguments and constructs main window (if entering a GUI mode). */ - bool Start(); + void Start(); /** * @brief Stop Olive Core @@ -405,11 +407,6 @@ private: */ void PushRecentlyOpenedProject(const QString &s); - /** - * @brief Internal project open - */ - void OpenProjectInternal(const QString& filename); - /** * @brief Declare custom types/classes for Qt's signal/slot system * @@ -507,6 +504,15 @@ private slots: void ProjectWasModified(bool e); + bool StartHeadlessExport(); + + void OpenStartupProject(); + + /** + * @brief Internal project open + */ + void OpenProjectInternal(const QString& filename); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/main.cpp b/app/main.cpp index e87f1fd34..0026395f0 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -86,23 +86,5 @@ int main(int argc, char *argv[]) { avfilter_register_all(); #endif - int exit_code; - - // Start core - if (OLIVE_NAMESPACE::Core::instance()->Start()) { - - // Run application loop and receive exit code - exit_code = a.exec(); - - } else { - - // Core failed to start, exit now - exit_code = 1; - - } - - // Clear core memory - OLIVE_NAMESPACE::Core::instance()->Stop(); - - return exit_code; + return OLIVE_NAMESPACE::Core::instance()->execute(&a); } diff --git a/app/task/export/exportparams.cpp b/app/task/export/exportparams.cpp index a603735ee..299c1fca3 100644 --- a/app/task/export/exportparams.cpp +++ b/app/task/export/exportparams.cpp @@ -100,4 +100,26 @@ QMatrix4x4 ExportParams::GenerateMatrix(ExportParams::VideoScalingMethod method, return preview_matrix; } +void ExportParams::Save(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("export")); + + writer->writeTextElement(QStringLiteral("encoder"), encoder_id_); + + writer->writeTextElement(QStringLiteral("vscale"), QString::number(video_scaling_method_)); + + writer->writeTextElement(QStringLiteral("range"), QString::number(has_custom_range_)); + + writer->writeTextElement(QStringLiteral("customrangein"), custom_range_.in().toString()); + + writer->writeTextElement(QStringLiteral("customrangeout"), custom_range_.out().toString()); + + // FIXME: Change this when color chains are implemented + writer->writeTextElement(QStringLiteral("color"), color_transform_.output()); + + EncodingParams::Save(writer); + + writer->writeEndElement(); // export +} + OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/exportparams.h b/app/task/export/exportparams.h index a74ea3c5d..ed6106d67 100644 --- a/app/task/export/exportparams.h +++ b/app/task/export/exportparams.h @@ -56,6 +56,8 @@ public: int source_width, int source_height, int dest_width, int dest_height); + virtual void Save(QXmlStreamWriter* writer) const override; + private: QString encoder_id_; diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 20d05ce7d..932a94c74 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -70,6 +70,8 @@ bool ProjectLoadTask::Run() project_file.close(); + emit ProgressChanged(1); + if (reader.hasError()) { SetError(reader.errorString()); return false; From b456b6b0f668b2c79cfee4496202462ea25fbb21 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 19:53:21 +1000 Subject: [PATCH 010/138] oiiodecoder: make sure spec is used instead of basetype Fixed issue with image alpha handling. --- app/codec/oiio/oiiodecoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/codec/oiio/oiiodecoder.cpp b/app/codec/oiio/oiiodecoder.cpp index 6d0804267..5a7650e89 100644 --- a/app/codec/oiio/oiiodecoder.cpp +++ b/app/codec/oiio/oiiodecoder.cpp @@ -379,7 +379,7 @@ bool OIIODecoder::OpenImageHandler(const QString &fn) is_rgba_ = (spec.nchannels == kRGBAChannels); - pix_fmt_ = GetFormatFromOIIOBasetype(spec.format); + pix_fmt_ = GetFormatFromOIIOBasetype(spec); if (pix_fmt_ == PixelFormat::PIX_FMT_INVALID) { qWarning() << "Failed to convert OIIO::ImageDesc to native pixel format"; From 57eb039b4caffb1a4fd7610445a34f1457952494 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 17 Jun 2020 20:50:03 +1000 Subject: [PATCH 011/138] debug: print graphics driver in debug log Might help with debugging GPU issues from users. --- app/window/mainwindow/mainwindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 5e61cd1d6..c3eaf7e75 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -650,6 +650,7 @@ void MainWindow::showEvent(QShowEvent *e) context.create(); context.makeCurrent(&surface); const char* vendor = reinterpret_cast(context.functions()->glGetString(GL_VENDOR)); + qDebug() << "Using graphics driver:" << vendor; if (!strcmp(vendor, "nouveau")) { QMetaObject::invokeMethod(this, "ShowNouveauWarning", Qt::QueuedConnection); } From 5397457c8db0226b03f9feab699ac50fd27259da Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 03:35:59 +1000 Subject: [PATCH 012/138] timeline: treat non-primary clicks as non-moving Fixes bug where drag would get stuck if the user right clicked on the timeline. --- app/widget/timelinewidget/timelinewidget.cpp | 6 ++++++ app/widget/timelinewidget/view/timelineviewbase.cpp | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 97f186cca..908d7e3a2 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -893,6 +893,12 @@ void TimelineWidget::ViewMousePressed(TimelineViewMouseEvent *event) if (GetConnectedNode() && active_tool_ != nullptr) { active_tool_->MousePress(event); } + + if (event->GetButton() != Qt::LeftButton) { + // Suspend tool immediately if the cursor isn't the primary button + active_tool_->MouseRelease(event); + active_tool_ = nullptr; + } } void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event) diff --git a/app/widget/timelinewidget/view/timelineviewbase.cpp b/app/widget/timelinewidget/view/timelineviewbase.cpp index 88798540c..c945f80db 100644 --- a/app/widget/timelinewidget/view/timelineviewbase.cpp +++ b/app/widget/timelinewidget/view/timelineviewbase.cpp @@ -134,7 +134,9 @@ bool TimelineViewBase::PlayheadPress(QMouseEvent *event) { QPointF scene_pos = mapToScene(event->pos()); - dragging_playhead_ = (scene_pos.x() >= playhead_scene_left_ && scene_pos.x() < playhead_scene_right_); + dragging_playhead_ = (event->button() == Qt::LeftButton + && scene_pos.x() >= playhead_scene_left_ + && scene_pos.x() < playhead_scene_right_); return dragging_playhead_; } From c5dd406ff165177af14c8300de0442b33f21b35c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 14:11:50 +1000 Subject: [PATCH 013/138] slider: improved ladder usability --- app/widget/slider/sliderbase.cpp | 9 +-- app/widget/slider/sliderbase.h | 2 +- app/widget/slider/sliderlabel.cpp | 4 -- app/widget/slider/sliderladder.cpp | 107 ++++++++++++++--------------- app/widget/slider/sliderladder.h | 20 ++---- 5 files changed, 65 insertions(+), 77 deletions(-) diff --git a/app/widget/slider/sliderbase.cpp b/app/widget/slider/sliderbase.cpp index 875d5f4e2..7818e4fd0 100644 --- a/app/widget/slider/sliderbase.cpp +++ b/app/widget/slider/sliderbase.cpp @@ -250,7 +250,8 @@ void SliderBase::LabelDragged() break; case kInteger: case kFloat: - drag_ladder_ = new SliderLadder(value_.toDouble(), drag_multiplier_, enable_ladder_ ? ladder_element_count_ : 0); + drag_ladder_ = new SliderLadder(drag_multiplier_, enable_ladder_ ? ladder_element_count_ : 0); + drag_ladder_->SetValue(ValueToString(value_)); drag_ladder_->show(); QPoint label_global_pos = label_->mapToGlobal(label_->pos()); @@ -263,7 +264,7 @@ void SliderBase::LabelDragged() } } -void SliderBase::LadderDragged(int value, double multiplier) +void SliderBase::LadderDragged(double value, double multiplier) { switch (mode_) { case kString: @@ -272,7 +273,7 @@ void SliderBase::LadderDragged(int value, double multiplier) case kInteger: case kFloat: { - dragged_diff_ += static_cast(value) * drag_multiplier_ * multiplier; + dragged_diff_ += value * drag_multiplier_ * multiplier; double drag_val = AdjustDragDistanceInternal(value_.toDouble(), dragged_diff_); @@ -291,7 +292,7 @@ void SliderBase::LadderDragged(int value, double multiplier) } UpdateLabel(temp_dragged_value_); - drag_ladder_->SetValue(temp_dragged_value_.toDouble()); + drag_ladder_->SetValue(ValueToString(temp_dragged_value_)); emit ValueChanged(temp_dragged_value_); break; } diff --git a/app/widget/slider/sliderbase.h b/app/widget/slider/sliderbase.h index fa956db22..cbc9630a8 100644 --- a/app/widget/slider/sliderbase.h +++ b/app/widget/slider/sliderbase.h @@ -133,7 +133,7 @@ private slots: void LabelDragged(); - void LadderDragged(int value, double multiplier); + void LadderDragged(double value, double multiplier); void LadderReleased(); diff --git a/app/widget/slider/sliderlabel.cpp b/app/widget/slider/sliderlabel.cpp index ab80988da..8fcf30592 100644 --- a/app/widget/slider/sliderlabel.cpp +++ b/app/widget/slider/sliderlabel.cpp @@ -24,10 +24,6 @@ #include #include -#ifdef Q_OS_MAC -#include -#endif - OLIVE_NAMESPACE_ENTER SliderLabel::SliderLabel(QWidget *parent) : diff --git a/app/widget/slider/sliderladder.cpp b/app/widget/slider/sliderladder.cpp index 41bc4d031..f21ab8852 100644 --- a/app/widget/slider/sliderladder.cpp +++ b/app/widget/slider/sliderladder.cpp @@ -26,16 +26,18 @@ #include #include +#ifdef Q_OS_MAC +#include +#endif + #include "common/clamp.h" #include "common/lerp.h" OLIVE_NAMESPACE_ENTER -SliderLadder::SliderLadder(double start_val, double drag_multiplier, int nb_outer_values, QWidget* parent) : +SliderLadder::SliderLadder(double drag_multiplier, int nb_outer_values, QWidget* parent) : QFrame(parent, Qt::Popup), - start_val_(start_val), - active_element_(nullptr), - relative_y_(-1) + y_mobility_(0) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setMargin(0); @@ -48,14 +50,17 @@ SliderLadder::SliderLadder(double start_val, double drag_multiplier, int nb_oute elements_.append(new SliderLadderElement(qPow(10, i + 1) * drag_multiplier)); } - elements_.append(new SliderLadderElement(drag_multiplier)); + // Create center entry + SliderLadderElement* start_element = new SliderLadderElement(drag_multiplier); + active_element_ = elements_.size(); + start_element->SetHighlighted(true); + elements_.append(start_element); for (int i=0;iSetValue(start_val_); layout->addWidget(e); } @@ -87,10 +92,10 @@ SliderLadder::~SliderLadder() #endif } -void SliderLadder::SetValue(double val) +void SliderLadder::SetValue(const QString &s) { foreach (SliderLadderElement* e, elements_) { - e->SetValue(val); + e->SetValue(s); } } @@ -105,38 +110,7 @@ void SliderLadder::showEvent(QShowEvent *event) { QWidget::showEvent(event); - QMetaObject::invokeMethod(this, "InitRelativeY", Qt::QueuedConnection); - QMetaObject::invokeMethod(&drag_timer_, "start", Qt::QueuedConnection); -} - -void SliderLadder::SetActiveElement() -{ - if (!active_element_ - || relative_y_ < active_element_->y() - || relative_y_ >= active_element_->y() + active_element_->height()) { - if (active_element_) { - // Un-highlight active element if one is set - active_element_->SetHighlighted(false); - } - - // Find new active element - foreach (SliderLadderElement* ele, elements_) { - if (relative_y_ >= ele->y() && relative_y_ < ele->y() + ele->height()) { - // This is the element! - active_element_ = ele; - active_element_->SetHighlighted(true); - relative_y_ = active_element_->y() + active_element_->height() / 2; - break; - } - } - } -} - -void SliderLadder::InitRelativeY() -{ - relative_y_ = QCursor::pos().y() - this->y(); - - SetActiveElement(); + drag_timer_.start(); } void SliderLadder::TimerUpdate() @@ -155,22 +129,45 @@ void SliderLadder::TimerUpdate() QCursor::setPos(drag_start_); #endif - int target = active_element_->y() + active_element_->height() / 2; - relative_y_ = lerp(relative_y_, static_cast(target), 0.1f); + int y_threshold = fontMetrics().height() / 2; - if (!x_mvmt && !y_mvmt) { - return; - } + if (qAbs(y_mvmt) > qAbs(x_mvmt) + || qApp->keyboardModifiers() & Qt::ControlModifier) { + // Movement is vertical + y_mobility_ += y_mvmt; - // Determine which element we're in - relative_y_ = clamp(relative_y_ + y_mvmt, - static_cast(elements_.first()->y()), - static_cast(elements_.last()->y() + elements_.last()->height() - 1)); + if (qAbs(y_mobility_) > y_threshold) { + int new_active_element; - SetActiveElement(); + if (y_mvmt < 0) { + // Movement is UP + new_active_element = active_element_ - 1; + } else { + // Movement is DOWN + new_active_element = active_element_ + 1; + } - if (qAbs(x_mvmt) > qAbs(y_mvmt)) { - emit DraggedByValue(x_mvmt, active_element_->GetMultiplier()); + // Check if the proposed element is valid + if (new_active_element >= 0 && new_active_element < elements_.size()) { + elements_.at(active_element_)->SetHighlighted(false); + + active_element_ = new_active_element; + + elements_.at(active_element_)->SetHighlighted(true); + } + + y_mobility_ = 0; + } + } else { + // Movement is horizontal + emit DraggedByValue(x_mvmt , elements_.at(active_element_)->GetMultiplier()); + + // Reduce Y mobility + if (y_mobility_ > 0) { + y_mobility_--; + } else if (y_mobility_ < 0) { + y_mobility_++; + } } } @@ -210,7 +207,7 @@ void SliderLadderElement::SetHighlighted(bool e) UpdateLabel(); } -void SliderLadderElement::SetValue(double value) +void SliderLadderElement::SetValue(const QString &value) { value_ = value; @@ -230,13 +227,13 @@ void SliderLadderElement::UpdateLabel() QString val_text; if (highlighted_) { - val_text = QString::number(value_); + val_text = value_; } label_->setText(QStringLiteral("%1\n%2").arg(QString::number(multiplier_), val_text)); } else { - label_->setText(QString::number(value_)); + label_->setText(value_); } } diff --git a/app/widget/slider/sliderladder.h b/app/widget/slider/sliderladder.h index d404b1fb0..ce0ae6193 100644 --- a/app/widget/slider/sliderladder.h +++ b/app/widget/slider/sliderladder.h @@ -37,7 +37,7 @@ public: void SetHighlighted(bool e); - void SetValue(double value); + void SetValue(const QString& value); void SetMultiplierVisible(bool e); @@ -52,7 +52,7 @@ private: QLabel* label_; double multiplier_; - double value_; + QString value_; bool highlighted_; @@ -64,11 +64,11 @@ class SliderLadder : public QFrame { Q_OBJECT public: - SliderLadder(double start_val, double drag_multiplier, int nb_outer_values, QWidget* parent = nullptr); + SliderLadder(double drag_multiplier, int nb_outer_values, QWidget* parent = nullptr); virtual ~SliderLadder() override; - void SetValue(double val); + void SetValue(const QString& s); protected: virtual void mouseReleaseEvent(QMouseEvent *event) override; @@ -81,23 +81,17 @@ signals: void Released(); private: - void SetActiveElement(); - QPoint drag_start_; - double start_val_; - QList elements_; - SliderLadderElement* active_element_; - - float relative_y_; + int active_element_; QTimer drag_timer_; -private slots: - void InitRelativeY(); + int y_mobility_; +private slots: void TimerUpdate(); }; From 897b9ca3dde162a85d22e1f7de7bdbe9651b9a8c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 15:06:07 +1000 Subject: [PATCH 014/138] statusbar: disconnect old task when connecting a new one --- app/window/mainwindow/mainstatusbar.cpp | 21 ++++++++++++++++++--- app/window/mainwindow/mainstatusbar.h | 4 ++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/window/mainwindow/mainstatusbar.cpp b/app/window/mainwindow/mainstatusbar.cpp index bf3532dc6..fada2d6e7 100644 --- a/app/window/mainwindow/mainstatusbar.cpp +++ b/app/window/mainwindow/mainstatusbar.cpp @@ -26,7 +26,8 @@ OLIVE_NAMESPACE_ENTER MainStatusBar::MainStatusBar(QWidget *parent) : QStatusBar(parent), - manager_(nullptr) + manager_(nullptr), + connected_task_(nullptr) { setSizeGripEnabled(false); @@ -37,7 +38,8 @@ MainStatusBar::MainStatusBar(QWidget *parent) : bar_->setMaximum(100); bar_->setVisible(false); - showMessage(tr("Welcome to %1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion())); + showMessage(tr("Welcome to %1 %2").arg(QCoreApplication::applicationName(), + QCoreApplication::applicationVersion())); } void MainStatusBar::ConnectTaskManager(TaskManager *manager) @@ -73,7 +75,15 @@ void MainStatusBar::UpdateStatus() } bar_->setVisible(true); - connect(t, &Task::ProgressChanged, this, &MainStatusBar::SetProgressBarValue); + + if (connected_task_) { + disconnect(connected_task_, &Task::ProgressChanged, this, &MainStatusBar::SetProgressBarValue); + disconnect(connected_task_, &Task::destroyed, this, &MainStatusBar::ConnectedTaskDeleted); + } + + connected_task_ = t; + connect(connected_task_, &Task::ProgressChanged, this, &MainStatusBar::SetProgressBarValue); + connect(connected_task_, &Task::destroyed, this, &MainStatusBar::ConnectedTaskDeleted); } } @@ -82,6 +92,11 @@ void MainStatusBar::SetProgressBarValue(double d) bar_->setValue(qRound(100.0 * d)); } +void MainStatusBar::ConnectedTaskDeleted() +{ + connected_task_ = nullptr; +} + void MainStatusBar::mouseDoubleClickEvent(QMouseEvent* e) { QStatusBar::mouseDoubleClickEvent(e); diff --git a/app/window/mainwindow/mainstatusbar.h b/app/window/mainwindow/mainstatusbar.h index 5951c2dd7..a1bd8fb27 100644 --- a/app/window/mainwindow/mainstatusbar.h +++ b/app/window/mainwindow/mainstatusbar.h @@ -50,11 +50,15 @@ private slots: void SetProgressBarValue(double d); + void ConnectedTaskDeleted(); + private: TaskManager* manager_; QProgressBar* bar_; + Task* connected_task_; + }; OLIVE_NAMESPACE_EXIT From a717742cbca7ecfa9bc168ee6d57cf0f5f0214cb Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 15:07:47 +1000 Subject: [PATCH 015/138] nodetable: show values in separate columns Works for vector coordinates (e.g. XY, XYZ, etc.) as well as displays checkboxes for RGB/A. Still non-functional. --- app/widget/nodetableview/nodetableview.cpp | 58 ++++++++++++---------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/app/widget/nodetableview/nodetableview.cpp b/app/widget/nodetableview/nodetableview.cpp index 8e9f5aab0..f876b82d5 100644 --- a/app/widget/nodetableview/nodetableview.cpp +++ b/app/widget/nodetableview/nodetableview.cpp @@ -20,6 +20,7 @@ #include "nodetableview.h" +#include #include #include "node/param.h" @@ -31,7 +32,12 @@ NodeTableView::NodeTableView(QWidget* parent) : QTreeWidget(parent) { setColumnCount(3); - setHeaderLabels({tr("Type"), tr("Value"), tr("Source")}); + setHeaderLabels({tr("Type"), + tr("Source"), + tr("R/X"), + tr("G/Y"), + tr("B/Z"), + tr("A/W")}); } void NodeTableView::SetNode(Node *n, const rational &time) @@ -60,41 +66,41 @@ void NodeTableView::SetNode(Node *n, const rational &time) for (int j=table.Count()-1; j>=0; j--) { const NodeValue& value = table.at(j); - QString value_str = NodeInput::ValueToString(value.type(), value.data()); + // Create item + QTreeWidgetItem* sub_item = new QTreeWidgetItem(); + top_item->addChild(sub_item); + // Set data type name + sub_item->setText(0, NodeParam::GetPrettyDataTypeName(value.type())); + + // Determine source QString source_name; if (value.source()) { source_name = value.source()->Name(); } else { source_name = tr("(unknown)"); } + sub_item->setText(1, source_name); - QTreeWidgetItem* sub_item = new QTreeWidgetItem(); - sub_item->setText(0, NodeParam::GetPrettyDataTypeName(value.type())); - sub_item->setText(1, value_str); - sub_item->setText(2, source_name); - top_item->addChild(sub_item); + switch (value.type()) { + case NodeParam::kTexture: + { + // NodeTableTraverser puts video params in here + VideoParams p = value.data().value(); + int channel_count = PixelFormat::ChannelCount(p.format()); - // Special cases - if (value.type() == NodeParam::kTexture) { - // NodeTableTraverser converts footage to VideoParams - QTreeWidgetItem* red_channel = new QTreeWidgetItem(); - red_channel->setText(0, tr("Red")); - sub_item->addChild(red_channel); - - QTreeWidgetItem* green_channel = new QTreeWidgetItem(); - green_channel->setText(0, tr("Green")); - sub_item->addChild(green_channel); - - QTreeWidgetItem* blue_channel = new QTreeWidgetItem(); - blue_channel->setText(0, tr("Blue")); - sub_item->addChild(blue_channel); - - if (PixelFormat::FormatHasAlphaChannel(value.data().value().format())) { - QTreeWidgetItem* alpha_channel = new QTreeWidgetItem(); - alpha_channel->setText(0, tr("Alpha")); - sub_item->addChild(alpha_channel); + for (int k=0;ksetItemWidget(sub_item, 2 + k, new QCheckBox()); } + break; + } + default: + { + QVector split_values = input->split_normal_value_into_track_values(value.data()); + for (int k=0;ksetText(2 + k, NodeInput::ValueToString(value.type(), split_values.at(k))); + } + } } } } From e47c985baaebd297509fe31eee2c26244fa1dd92 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 16:30:36 +1000 Subject: [PATCH 016/138] slider: if a ladder appears, signal the label that the drag has finished --- app/widget/slider/sliderbase.cpp | 2 ++ app/widget/slider/sliderlabel.h | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/app/widget/slider/sliderbase.cpp b/app/widget/slider/sliderbase.cpp index 7818e4fd0..67ff5b7de 100644 --- a/app/widget/slider/sliderbase.cpp +++ b/app/widget/slider/sliderbase.cpp @@ -260,6 +260,8 @@ void SliderBase::LabelDragged() connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &SliderBase::LadderDragged); connect(drag_ladder_, &SliderLadder::Released, this, &SliderBase::LadderReleased); + + label_->CancelDrag(); break; } } diff --git a/app/widget/slider/sliderlabel.h b/app/widget/slider/sliderlabel.h index 081c5eafa..6ac8f9e5a 100644 --- a/app/widget/slider/sliderlabel.h +++ b/app/widget/slider/sliderlabel.h @@ -33,6 +33,11 @@ class SliderLabel : public QLabel public: SliderLabel(QWidget* parent); + void CancelDrag() + { + dragging_ = false; + } + protected: virtual void mousePressEvent(QMouseEvent *ev) override; From 57f858dd92daae08c91b6df07a25f80ee263c497 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 16:30:57 +1000 Subject: [PATCH 017/138] nodetable: correctly update with time --- app/node/value.cpp | 8 ++- app/node/value.h | 11 +++- app/panel/table/table.h | 1 + app/widget/nodetableview/nodetableview.cpp | 56 ++++++++++++++++---- app/widget/nodetableview/nodetableview.h | 3 ++ app/widget/nodetableview/nodetablewidget.cpp | 27 +++++++++- app/widget/nodetableview/nodetablewidget.h | 7 +++ app/window/mainwindow/mainwindow.cpp | 3 ++ 8 files changed, 100 insertions(+), 16 deletions(-) diff --git a/app/node/value.cpp b/app/node/value.cpp index e2f9913c5..ef5ac05bd 100644 --- a/app/node/value.cpp +++ b/app/node/value.cpp @@ -44,7 +44,12 @@ void NodeValueDatabase::Insert(const NodeInput *key, const NodeValueTable &value NodeValueTable NodeValueDatabase::Merge() const { - return NodeValueTable::Merge(tables_.values()); + QHash copy = tables_; + + // Kinda hacky, but we don't need this table to slipstream + copy.remove(QStringLiteral("global")); + + return NodeValueTable::Merge(copy.values()); } NodeValue::NodeValue() : @@ -176,7 +181,6 @@ NodeValueTable NodeValueTable::Merge(QList tables) NodeValueTable merged_table; // Slipstreams all tables together - // FIXME: I don't actually know if this is the right approach... foreach (const NodeValueTable& t, tables) { if (row >= t.Count()) { continue; diff --git a/app/node/value.h b/app/node/value.h index 7efb8592a..448c1c54d 100644 --- a/app/node/value.h +++ b/app/node/value.h @@ -108,14 +108,21 @@ public: using const_iterator = QHash::const_iterator; - inline QHash::const_iterator begin() const { + inline QHash::const_iterator begin() const + { return tables_.cbegin(); } - inline QHash::const_iterator end() const { + inline QHash::const_iterator end() const + { return tables_.cend(); } + inline bool contains(const QString& s) const + { + return tables_.contains(s); + } + private: QHash tables_; diff --git a/app/panel/table/table.h b/app/panel/table/table.h index 0587cf51b..d08fe0de3 100644 --- a/app/panel/table/table.h +++ b/app/panel/table/table.h @@ -28,6 +28,7 @@ OLIVE_NAMESPACE_ENTER class NodeTablePanel : public TimeBasedPanel { + Q_OBJECT public: NodeTablePanel(QWidget* parent); diff --git a/app/widget/nodetableview/nodetableview.cpp b/app/widget/nodetableview/nodetableview.cpp index f876b82d5..9d6590cbc 100644 --- a/app/widget/nodetableview/nodetableview.cpp +++ b/app/widget/nodetableview/nodetableview.cpp @@ -29,7 +29,8 @@ OLIVE_NAMESPACE_ENTER NodeTableView::NodeTableView(QWidget* parent) : - QTreeWidget(parent) + QTreeWidget(parent), + last_set_node_(nullptr) { setColumnCount(3); setHeaderLabels({tr("Type"), @@ -42,11 +43,23 @@ NodeTableView::NodeTableView(QWidget* parent) : void NodeTableView::SetNode(Node *n, const rational &time) { - clear(); + if (last_set_node_ != n) { + // Clear everything if the node has changed + clear(); + } + last_set_node_ = n; NodeTableTraverser traverser; NodeValueDatabase db = traverser.GenerateDatabase(n, TimeRange(time, time)); + // Remove top items if necessary + for (int i=0;itopLevelItemCount();i++) { + if (!db.contains(this->topLevelItem(i)->data(0, Qt::UserRole).toString())) { + delete this->takeTopLevelItem(i); + i--; + } + } + NodeValueDatabase::const_iterator i; for (i=db.begin(); i!=db.end(); i++) { @@ -58,17 +71,40 @@ void NodeTableView::SetNode(Node *n, const rational &time) continue; } - QTreeWidgetItem* top_item = new QTreeWidgetItem(); - top_item->setText(0, input->name()); - top_item->setFirstColumnSpanned(true); - this->addTopLevelItem(top_item); + QTreeWidgetItem* top_item = nullptr; - for (int j=table.Count()-1; j>=0; j--) { - const NodeValue& value = table.at(j); + for (int j=0;jtopLevelItemCount();j++) { + QTreeWidgetItem* compare = this->topLevelItem(j); + + if (compare->data(0, Qt::UserRole).toString() == input->id()) { + top_item = compare; + break; + } + } + + if (!top_item) { + top_item = new QTreeWidgetItem(); + top_item->setText(0, input->name()); + top_item->setData(0, Qt::UserRole, input->id()); + top_item->setFirstColumnSpanned(true); + this->addTopLevelItem(top_item); + } + + // Create children if necessary + while (top_item->childCount() < table.Count()) { + top_item->addChild(new QTreeWidgetItem()); + } + + // Remove children if necessary + while (top_item->childCount() > table.Count()) { + delete top_item->takeChild(top_item->childCount() - 1); + } + + for (int j=0;jaddChild(sub_item); + QTreeWidgetItem* sub_item = top_item->child(j); // Set data type name sub_item->setText(0, NodeParam::GetPrettyDataTypeName(value.type())); diff --git a/app/widget/nodetableview/nodetableview.h b/app/widget/nodetableview/nodetableview.h index e453903b7..1ce906c09 100644 --- a/app/widget/nodetableview/nodetableview.h +++ b/app/widget/nodetableview/nodetableview.h @@ -36,6 +36,9 @@ public: void SetMultipleNodeMessage(); +private: + Node* last_set_node_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetablewidget.cpp b/app/widget/nodetableview/nodetablewidget.cpp index 20db058c2..0c50794ab 100644 --- a/app/widget/nodetableview/nodetablewidget.cpp +++ b/app/widget/nodetableview/nodetablewidget.cpp @@ -25,7 +25,8 @@ OLIVE_NAMESPACE_ENTER NodeTableWidget::NodeTableWidget(QWidget* parent) : - TimeBasedWidget(parent) + TimeBasedWidget(parent), + node_(nullptr) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setSpacing(0); @@ -37,13 +38,35 @@ NodeTableWidget::NodeTableWidget(QWidget* parent) : void NodeTableWidget::SetNodes(const QList &nodes) { + node_ = nullptr; + if (nodes.isEmpty()) { view_->clear(); } else if (nodes.size() == 1) { - view_->SetNode(nodes.first(), rational()); + node_ = nodes.first(); + + ViewerOutput* viewer = node_->FindOutputNode(); + if (viewer) { + qDebug() << "Found timebase"; + SetTimebase(viewer->video_params().time_base()); + } + + UpdateView(); } else { view_->SetMultipleNodeMessage(); } } +void NodeTableWidget::TimeChangedEvent(const int64_t &) +{ + UpdateView(); +} + +void NodeTableWidget::UpdateView() +{ + if (node_) { + view_->SetNode(node_, GetTime()); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetableview/nodetablewidget.h b/app/widget/nodetableview/nodetablewidget.h index ae0c82cc4..bc05d768d 100644 --- a/app/widget/nodetableview/nodetablewidget.h +++ b/app/widget/nodetableview/nodetablewidget.h @@ -33,9 +33,16 @@ public: void SetNodes(const QList& nodes); +protected: + virtual void TimeChangedEvent(const int64_t& ts) override; + private: + void UpdateView(); + NodeTableView* view_; + Node* node_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index a917143f6..09286c316 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -86,7 +86,9 @@ MainWindow::MainWindow(QWidget *parent) : connect(node_panel_, &NodePanel::SelectionChanged, table_panel_, &NodeTablePanel::SetNodes); connect(param_panel_, &ParamPanel::RequestSelectNode, node_panel_, &NodePanel::Select); connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp); + connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp); connect(param_panel_, &ParamPanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp); + connect(param_panel_, &ParamPanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp); connect(param_panel_, &ParamPanel::FoundGizmos, sequence_viewer_panel_, &SequenceViewerPanel::SetGizmos); connect(PanelManager::instance(), &PanelManager::FocusedPanelChanged, this, &MainWindow::FocusedPanelChanged); @@ -518,6 +520,7 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::TimelineCloseRequested); connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTimestamp); + connect(panel, &TimelinePanel::TimeChanged, table_panel_, &NodeTablePanel::SetTimestamp); connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTimestamp); connect(panel, &TimelinePanel::SelectionChanged, node_panel_, &NodePanel::SelectBlocks); connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTimestamp); From e8c8fedcdb6d0a9d8affae2f4a09681b8a843d35 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 17:50:28 +1000 Subject: [PATCH 018/138] slider: stop ladder drag timer on release --- app/widget/slider/sliderladder.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/widget/slider/sliderladder.cpp b/app/widget/slider/sliderladder.cpp index f21ab8852..bbfd50575 100644 --- a/app/widget/slider/sliderladder.cpp +++ b/app/widget/slider/sliderladder.cpp @@ -103,6 +103,8 @@ void SliderLadder::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) + drag_timer_.stop(); + emit Released(); } From 115e62f1403b03a326fe98e099ae56cce528a5e5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 18 Jun 2020 18:52:13 +1000 Subject: [PATCH 019/138] slider: further behavior improvements Ladders appears offset on sliders with multipliers. Ladder also appears on initial press (but still shows line edit if mouse wasn't moved) --- app/dialog/richtext/richtext.cpp | 1 - app/widget/colorwheel/colorvalueswidget.cpp | 1 - .../nodeparamviewwidgetbridge.cpp | 4 +- app/widget/slider/sliderbase.cpp | 96 ++++++++++++------- app/widget/slider/sliderbase.h | 17 ++-- app/widget/slider/sliderlabel.cpp | 19 +--- app/widget/slider/sliderlabel.h | 16 ---- app/widget/slider/sliderladder.cpp | 4 + 8 files changed, 73 insertions(+), 85 deletions(-) diff --git a/app/dialog/richtext/richtext.cpp b/app/dialog/richtext/richtext.cpp index 5b4303530..8807156b9 100644 --- a/app/dialog/richtext/richtext.cpp +++ b/app/dialog/richtext/richtext.cpp @@ -50,7 +50,6 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : toolbar_layout->addWidget(font_combo_); size_slider_ = new FloatSlider(); size_slider_->SetMinimum(0.1); - size_slider_->SetLadderEnabled(true); size_slider_->SetLadderElementCount(1); size_slider_->setToolTip(tr("Font Size")); toolbar_layout->addWidget(size_slider_); diff --git a/app/widget/colorwheel/colorvalueswidget.cpp b/app/widget/colorwheel/colorvalueswidget.cpp index 1b7fd76fc..43b328949 100644 --- a/app/widget/colorwheel/colorvalueswidget.cpp +++ b/app/widget/colorwheel/colorvalueswidget.cpp @@ -203,7 +203,6 @@ FloatSlider *ColorValuesTab::CreateColorSlider() FloatSlider* fs = new FloatSlider(); fs->SetDragMultiplier(0.01); fs->SetDecimalPlaces(5); - fs->SetLadderEnabled(true); fs->SetLadderElementCount(1); connect(fs, &FloatSlider::ValueChanged, this, &ColorValuesTab::SliderChanged); return fs; diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index dc2638ccd..4e4fabba3 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -96,7 +96,7 @@ void NodeParamViewWidgetBridge::CreateWidgets() { IntegerSlider* slider = new IntegerSlider(); slider->SetDefaultValue(input_->GetDefaultValue()); - slider->SetLadderEnabled(true); + slider->SetLadderElementCount(2); widgets_.append(slider); connect(slider, &IntegerSlider::ValueChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; @@ -373,7 +373,7 @@ void NodeParamViewWidgetBridge::CreateSliders(int count) for (int i=0;iSetDefaultValue(input_->GetDefaultValueForTrack(i)); - fs->SetLadderEnabled(true); + fs->SetLadderElementCount(2); widgets_.append(fs); connect(fs, &FloatSlider::ValueChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); } diff --git a/app/widget/slider/sliderbase.cpp b/app/widget/slider/sliderbase.cpp index 67ff5b7de..727b2aa01 100644 --- a/app/widget/slider/sliderbase.cpp +++ b/app/widget/slider/sliderbase.cpp @@ -40,8 +40,8 @@ SliderBase::SliderBase(Mode mode, QWidget *parent) : require_valid_input_(true), tristate_(false), drag_ladder_(nullptr), - enable_ladder_(false), - ladder_element_count_(2) + ladder_element_count_(0), + dragged_(false) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); @@ -52,9 +52,8 @@ SliderBase::SliderBase(Mode mode, QWidget *parent) : editor_ = new FocusableLineEdit(this); addWidget(editor_); - connect(label_, &SliderLabel::LabelMoved, this, &SliderBase::LabelDragged); - connect(label_, &SliderLabel::LabelReleased, this, &SliderBase::LabelClicked); - connect(label_, &SliderLabel::focused, this, &SliderBase::LabelClicked); + connect(label_, &SliderLabel::LabelPressed, this, &SliderBase::LabelPressed); + connect(label_, &SliderLabel::focused, this, &SliderBase::ShowEditor); connect(label_, &SliderLabel::RequestReset, this, &SliderBase::ResetValue); connect(editor_, &FocusableLineEdit::Confirmed, this, &SliderBase::LineEditConfirmed); connect(editor_, &FocusableLineEdit::Cancelled, this, &SliderBase::LineEditCancelled); @@ -201,6 +200,22 @@ QString SliderBase::GetFormat() const } } +void SliderBase::RepositionLadder() +{ + QPoint label_global_pos = label_->mapToGlobal(label_->pos()); + int text_width = QFontMetricsWidth(label_->fontMetrics(), label_->text()); + QPoint ladder_pos(label_global_pos.x(), + label_global_pos.y() + label_->height() / 2 - drag_ladder_->height() / 2); + + if (ladder_element_count_ > 0) { + ladder_pos.setX(ladder_pos.x() + text_width + QFontMetricsWidth(label_->fontMetrics(), QStringLiteral("H"))); + } else { + ladder_pos.setX(ladder_pos.x() + text_width / 2 - drag_ladder_->width() / 2); + } + + drag_ladder_->move(ladder_pos); +} + void SliderBase::UpdateLabel(const QVariant &v) { if (tristate_) { @@ -226,23 +241,21 @@ QVariant SliderBase::StringToValue(const QString &s, bool *ok) return s; } -void SliderBase::LabelClicked() +void SliderBase::ShowEditor() { - if (!drag_ladder_) { - // This was a simple click - // Load label's text into editor - editor_->setText(ValueToString(value_)); + // This was a simple click + // Load label's text into editor + editor_->setText(ValueToString(value_)); - // Show editor - setCurrentWidget(editor_); + // Show editor + setCurrentWidget(editor_); - // Select all text in the editor - editor_->setFocus(); - editor_->selectAll(); - } + // Select all text in the editor + editor_->setFocus(); + editor_->selectAll(); } -void SliderBase::LabelDragged() +void SliderBase::LabelPressed() { switch (mode_) { case kString: @@ -250,24 +263,24 @@ void SliderBase::LabelDragged() break; case kInteger: case kFloat: - drag_ladder_ = new SliderLadder(drag_multiplier_, enable_ladder_ ? ladder_element_count_ : 0); + { + drag_ladder_ = new SliderLadder(drag_multiplier_, ladder_element_count_); drag_ladder_->SetValue(ValueToString(value_)); drag_ladder_->show(); - QPoint label_global_pos = label_->mapToGlobal(label_->pos()); - drag_ladder_->move(label_global_pos.x() + QFontMetricsWidth(label_->fontMetrics(), label_->text()) / 2 - drag_ladder_->width() / 2, - label_global_pos.y() + label_->height() / 2 - drag_ladder_->height() / 2); + RepositionLadder(); connect(drag_ladder_, &SliderLadder::DraggedByValue, this, &SliderBase::LadderDragged); connect(drag_ladder_, &SliderLadder::Released, this, &SliderBase::LadderReleased); - - label_->CancelDrag(); break; } + } } -void SliderBase::LadderDragged(double value, double multiplier) +void SliderBase::LadderDragged(int value, double multiplier) { + dragged_ = true; + switch (mode_) { case kString: // No dragging supported for strings @@ -294,7 +307,10 @@ void SliderBase::LadderDragged(double value, double multiplier) } UpdateLabel(temp_dragged_value_); + drag_ladder_->SetValue(ValueToString(temp_dragged_value_)); + RepositionLadder(); + emit ValueChanged(temp_dragged_value_); break; } @@ -307,20 +323,26 @@ void SliderBase::LadderReleased() drag_ladder_ = nullptr; dragged_diff_ = 0; - // This was a drag - switch (mode_) { - case kString: - // No-op - break; - case kInteger: - SetValue(temp_dragged_value_.toInt()); - break; - case kFloat: - SetValue(temp_dragged_value_.toDouble()); - break; - } + if (dragged_) { + // This was a drag + switch (mode_) { + case kString: + // No-op + break; + case kInteger: + SetValue(temp_dragged_value_.toInt()); + break; + case kFloat: + SetValue(temp_dragged_value_.toDouble()); + break; + } - emit ValueChanged(value_); + emit ValueChanged(value_); + + dragged_ = false; + } else { + ShowEditor(); + } } void SliderBase::LineEditConfirmed() diff --git a/app/widget/slider/sliderbase.h b/app/widget/slider/sliderbase.h index cbc9630a8..7e8062694 100644 --- a/app/widget/slider/sliderbase.h +++ b/app/widget/slider/sliderbase.h @@ -56,11 +56,6 @@ public: void SetFormat(const QString& s); void ClearFormat(); - void SetLadderEnabled(bool e) - { - enable_ladder_ = e; - } - void SetLadderElementCount(int b) { ladder_element_count_ = b; @@ -97,6 +92,8 @@ private: QString GetFormat() const; + void RepositionLadder(); + SliderLabel* label_; FocusableLineEdit* editor_; @@ -124,16 +121,16 @@ private: SliderLadder* drag_ladder_; - bool enable_ladder_; - int ladder_element_count_; + bool dragged_; + private slots: - void LabelClicked(); + void ShowEditor(); - void LabelDragged(); + void LabelPressed(); - void LadderDragged(double value, double multiplier); + void LadderDragged(int value, double multiplier); void LadderReleased(); diff --git a/app/widget/slider/sliderlabel.cpp b/app/widget/slider/sliderlabel.cpp index 8fcf30592..319398a91 100644 --- a/app/widget/slider/sliderlabel.cpp +++ b/app/widget/slider/sliderlabel.cpp @@ -27,8 +27,7 @@ OLIVE_NAMESPACE_ENTER SliderLabel::SliderLabel(QWidget *parent) : - QLabel(parent), - dragging_(false) + QLabel(parent) { QPalette p = palette(); @@ -55,26 +54,10 @@ void SliderLabel::mousePressEvent(QMouseEvent *e) if (e->modifiers() & Qt::AltModifier) { emit RequestReset(); } else { - dragging_ = true; emit LabelPressed(); } } -void SliderLabel::mouseMoveEvent(QMouseEvent *) -{ - if (dragging_) { - emit LabelMoved(); - } -} - -void SliderLabel::mouseReleaseEvent(QMouseEvent *) -{ - if (dragging_) { - emit LabelReleased(); - dragging_ = false; - } -} - void SliderLabel::focusInEvent(QFocusEvent *event) { QWidget::focusInEvent(event); diff --git a/app/widget/slider/sliderlabel.h b/app/widget/slider/sliderlabel.h index 6ac8f9e5a..68d0666e3 100644 --- a/app/widget/slider/sliderlabel.h +++ b/app/widget/slider/sliderlabel.h @@ -33,34 +33,18 @@ class SliderLabel : public QLabel public: SliderLabel(QWidget* parent); - void CancelDrag() - { - dragging_ = false; - } - protected: virtual void mousePressEvent(QMouseEvent *ev) override; - virtual void mouseMoveEvent(QMouseEvent *ev) override; - - virtual void mouseReleaseEvent(QMouseEvent *ev) override; - virtual void focusInEvent(QFocusEvent *event) override; signals: void LabelPressed(); - void LabelMoved(); - - void LabelReleased(); - void focused(); void RequestReset(); -private: - bool dragging_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/slider/sliderladder.cpp b/app/widget/slider/sliderladder.cpp index bbfd50575..2c8e6128e 100644 --- a/app/widget/slider/sliderladder.cpp +++ b/app/widget/slider/sliderladder.cpp @@ -131,6 +131,10 @@ void SliderLadder::TimerUpdate() QCursor::setPos(drag_start_); #endif + if (!x_mvmt && !y_mvmt) { + return; + } + int y_threshold = fontMetrics().height() / 2; if (qAbs(y_mvmt) > qAbs(x_mvmt) From 0280ade20d9ebb55b84944e5bcd5eb91d202d7a0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 19 Jun 2020 02:23:06 +1000 Subject: [PATCH 020/138] project: improved main window layout loading There were a lot of issues that arose from trying to make GUI changes from another thread (even though we ran those functions in the right thread). Now we store layout information until the end of the load and make the changes then. This works much better from both a business logic and user experience perspective. Also prevents multiple sequences from taking focus during load and starting a render job. --- app/core.cpp | 69 ++++++------- app/project/item/sequence/sequence.cpp | 2 +- app/project/project.cpp | 12 ++- app/project/project.h | 3 +- app/task/project/load/load.cpp | 5 +- app/task/project/load/load.h | 10 +- app/window/mainwindow/CMakeLists.txt | 2 + app/window/mainwindow/mainwindow.cpp | 92 ++++-------------- app/window/mainwindow/mainwindow.h | 9 +- .../mainwindow/mainwindowlayoutinfo.cpp | 96 +++++++++++++++++++ app/window/mainwindow/mainwindowlayoutinfo.h | 52 ++++++++++ 11 files changed, 231 insertions(+), 121 deletions(-) create mode 100644 app/window/mainwindow/mainwindowlayoutinfo.cpp create mode 100644 app/window/mainwindow/mainwindowlayoutinfo.h diff --git a/app/core.cpp b/app/core.cpp index d737a0563..55f0b2c49 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -151,6 +151,30 @@ int Core::execute(QCoreApplication* a) return exit_code; } +void Core::DeclareTypesForQt() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); +} + void Core::Start() { // Reset config (Config sets to default on construction already, but we do it again here as a workaround that fixes @@ -480,9 +504,11 @@ void Core::AddOpenProject(ProjectPtr p) void Core::AddOpenProjectFromTask(Task *task) { QList projects = static_cast(task)->GetLoadedProjects(); + QList layouts = static_cast(task)->GetLoadedLayouts(); - foreach (ProjectPtr p, projects) { - AddOpenProject(p); + for (int i=0; iLoadLayout(layouts.at(i)); } } @@ -629,29 +655,6 @@ void Core::OpenStartupProject() } } -void Core::DeclareTypesForQt() -{ - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); -} - void Core::StartGUI(bool full_screen) { // Set UI style @@ -1011,21 +1014,11 @@ void Core::OpenProjectInternal(const QString &filename) ProjectLoadTask* plm = new ProjectLoadTask(filename); - if (gui_active_) { + TaskDialog* task_dialog = new TaskDialog(plm, tr("Load Project"), main_window()); - TaskDialog* task_dialog = new TaskDialog(plm, tr("Load Project"), main_window()); + connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask); - connect(task_dialog, &TaskDialog::TaskSucceeded, this, &Core::AddOpenProjectFromTask); - - task_dialog->open(); - - } else { - - //connect(plm, &ProjectLoadManager::ProjectLoaded, this, &Core::AddOpenProject); - - - - } + task_dialog->open(); } int Core::CountFilesInFileList(const QFileInfoList &filenames) diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index b2c5a0c22..9e89a0aaa 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -149,7 +149,7 @@ void Sequence::Save(QXmlStreamWriter *writer) const writer->writeAttribute(QStringLiteral("name"), name()); - writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(viewer_output_))); + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); writer->writeStartElement(QStringLiteral("video")); diff --git a/app/project/project.cpp b/app/project/project.cpp index 014489dd6..653202c02 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -37,7 +37,7 @@ Project::Project() : root_.set_project(this); } -void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) +void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const QAtomicInt* cancelled) { XMLNodeData xml_node_data; @@ -62,7 +62,12 @@ void Project::Load(QXmlStreamReader *reader, const QAtomicInt* cancelled) } else if (reader->name() == QStringLiteral("layout")) { - Core::instance()->main_window()->LoadLayout(reader, xml_node_data); + // Since the main window's functions have to occur in the GUI thread (and we're likely + // loading in a secondary thread), we load all necessary data into a separate struct so we + // can continue loading and queue it with the main window so it can handle the data + // appropriately in its own thread. + + *layout = MainWindowLayoutInfo::fromXml(reader, xml_node_data); } else { reader->skipCurrentElement(); @@ -93,7 +98,8 @@ void Project::Save(QXmlStreamWriter *writer) const writer->writeEndElement(); // colormanagement // Save main window project layout - Core::instance()->main_window()->SaveLayout(writer); + MainWindowLayoutInfo main_window_info = Core::instance()->main_window()->SaveLayout(); + main_window_info.toXml(writer); writer->writeEndElement(); // project } diff --git a/app/project/project.h b/app/project/project.h index f61fc50da..ff3f8ce35 100644 --- a/app/project/project.h +++ b/app/project/project.h @@ -26,6 +26,7 @@ #include "render/colormanager.h" #include "project/item/folder/folder.h" +#include "window/mainwindow/mainwindowlayoutinfo.h" OLIVE_NAMESPACE_ENTER @@ -46,7 +47,7 @@ class Project : public QObject public: Project(); - void Load(QXmlStreamReader* reader, const QAtomicInt* cancelled); + void Load(QXmlStreamReader* reader, MainWindowLayoutInfo *layout, const QAtomicInt* cancelled); void Save(QXmlStreamWriter* writer) const; diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 932a94c74..27dff9e11 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -51,13 +51,16 @@ bool ProjectLoadTask::Run() project->set_filename(filename_); - project->Load(&reader, &IsCancelled()); + MainWindowLayoutInfo layout; + + project->Load(&reader, &layout, &IsCancelled()); // Ensure project is in main thread moveToThread(qApp->thread()); if (!IsCancelled()) { projects_.append(project); + layout_info_.append(layout); } } else { reader.skipCurrentElement(); diff --git a/app/task/project/load/load.h b/app/task/project/load/load.h index fa203abfe..398853028 100644 --- a/app/task/project/load/load.h +++ b/app/task/project/load/load.h @@ -23,6 +23,7 @@ #include "project/project.h" #include "task/task.h" +#include "window/mainwindow/mainwindowlayoutinfo.h" OLIVE_NAMESPACE_ENTER @@ -32,17 +33,24 @@ class ProjectLoadTask : public Task public: ProjectLoadTask(const QString& filename); - const QList& GetLoadedProjects() + const QList& GetLoadedProjects() const { return projects_; } + const QList& GetLoadedLayouts() const + { + return layout_info_; + } + protected: virtual bool Run() override; private: QList projects_; + QList layout_info_; + QString filename_; }; diff --git a/app/window/mainwindow/CMakeLists.txt b/app/window/mainwindow/CMakeLists.txt index 151883684..4b70b8f9e 100644 --- a/app/window/mainwindow/CMakeLists.txt +++ b/app/window/mainwindow/CMakeLists.txt @@ -22,5 +22,7 @@ set(OLIVE_SOURCES window/mainwindow/mainstatusbar.cpp window/mainwindow/mainwindow.h window/mainwindow/mainwindow.cpp + window/mainwindow/mainwindowlayoutinfo.h + window/mainwindow/mainwindowlayoutinfo.cpp PARENT_SCOPE ) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 09286c316..26b765b0d 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -111,43 +111,37 @@ MainWindow::~MainWindow() #endif } -void MainWindow::LoadLayout(QXmlStreamReader *reader, XMLNodeData &xml_data) +void MainWindow::LoadLayout(const MainWindowLayoutInfo &info) { - QMetaObject::invokeMethod(this, - "LoadLayoutInternal", - Qt::BlockingQueuedConnection, - Q_ARG(QXmlStreamReader*, reader), - Q_ARG(XMLNodeData*, &xml_data)); + foreach (Folder* folder, info.open_folders()) { + FolderOpen(folder->project(), folder, true); + } + + foreach (Sequence* sequence, info.open_sequences()) { + OpenSequence(sequence, false); + } + + restoreState(info.state()); } -void MainWindow::SaveLayout(QXmlStreamWriter *writer) const +MainWindowLayoutInfo MainWindow::SaveLayout() const { - writer->writeStartElement(QStringLiteral("layout")); - - writer->writeStartElement(QStringLiteral("folders")); + MainWindowLayoutInfo info; foreach (ProjectPanel* panel, folder_panels_) { - writer->writeTextElement(QStringLiteral("folder"), - QString::number(reinterpret_cast(panel->get_root_index().internalPointer()))); + info.add_folder(static_cast(panel->get_root_index().internalPointer())); } - writer->writeEndElement(); // folders - - writer->writeStartElement(QStringLiteral("timeline")); - foreach (TimelinePanel* panel, timeline_panels_) { - writer->writeTextElement(QStringLiteral("sequence"), - QString::number(reinterpret_cast(panel->GetConnectedViewer()))); + info.add_sequence(static_cast(panel->GetConnectedViewer()->parent())); } - writer->writeEndElement(); // timeline + info.set_state(saveState()); - writer->writeTextElement(QStringLiteral("state"), QString(saveState().toBase64())); - - writer->writeEndElement(); // layout + return info; } -void MainWindow::OpenSequence(Sequence *sequence) +void MainWindow::OpenSequence(Sequence *sequence, bool enable_focus) { // See if this sequence is already open, and switch to it if so foreach (TimelinePanel* tl, timeline_panels_) { @@ -164,11 +158,14 @@ void MainWindow::OpenSequence(Sequence *sequence) panel = timeline_panels_.first(); } else { panel = AppendTimelinePanel(); + enable_focus = false; } panel->ConnectViewerNode(sequence->viewer_output()); - TimelineFocused(sequence->viewer_output()); + if (enable_focus) { + TimelineFocused(sequence->viewer_output()); + } } void MainWindow::CloseSequence(Sequence *sequence) @@ -467,53 +464,6 @@ void MainWindow::FloatingPanelCloseRequested() panel->deleteLater(); } -void MainWindow::LoadLayoutInternal(QXmlStreamReader *reader, XMLNodeData *xml_data) -{ - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("folders")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("folder")) { - quintptr item_id = reader->readElementText().toULongLong(); - - Item* open_item = xml_data->item_ptrs.value(item_id); - - if (open_item) { - FolderOpen(open_item->project(), open_item, true); - } - } else { - reader->skipCurrentElement(); - } - } - - } else if (reader->name() == QStringLiteral("timeline")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("sequence")) { - quintptr item_id = reader->readElementText().toULongLong(); - - Sequence* open_seq = dynamic_cast(xml_data->item_ptrs.value(item_id)); - - if (open_seq) { - OpenSequence(open_seq); - } - } else { - reader->skipCurrentElement(); - } - } - - } else if (reader->name() == QStringLiteral("state")) { - - QByteArray state = QByteArray::fromBase64(reader->readElementText().toLatin1()); - - restoreState(state); - - } else { - reader->skipCurrentElement(); - } - } -} - TimelinePanel* MainWindow::AppendTimelinePanel() { TimelinePanel* panel = AppendPanelInternal(timeline_panels_); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index c9f349051..f06b8c0fe 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -23,6 +23,7 @@ #include +#include "mainwindowlayoutinfo.h" #include "panel/panelmanager.h" #include "panel/audiomonitor/audiomonitor.h" #include "panel/curve/curve.h" @@ -55,11 +56,11 @@ public: virtual ~MainWindow() override; - void LoadLayout(QXmlStreamReader* reader, XMLNodeData& xml_data); + void LoadLayout(const MainWindowLayoutInfo &info); - void SaveLayout(QXmlStreamWriter* writer) const; + MainWindowLayoutInfo SaveLayout() const; - void OpenSequence(Sequence* sequence); + void OpenSequence(Sequence* sequence, bool enable_focus = true); void CloseSequence(Sequence* sequence); @@ -167,8 +168,6 @@ private slots: void FloatingPanelCloseRequested(); - void LoadLayoutInternal(QXmlStreamReader* reader, XMLNodeData *xml_data); - void StatusBarDoubleClicked(); #ifdef Q_OS_LINUX diff --git a/app/window/mainwindow/mainwindowlayoutinfo.cpp b/app/window/mainwindow/mainwindowlayoutinfo.cpp new file mode 100644 index 000000000..168978f79 --- /dev/null +++ b/app/window/mainwindow/mainwindowlayoutinfo.cpp @@ -0,0 +1,96 @@ +#include "mainwindowlayoutinfo.h" + +OLIVE_NAMESPACE_ENTER + +void MainWindowLayoutInfo::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("layout")); + + writer->writeStartElement(QStringLiteral("folders")); + + foreach (Folder* folder, open_folders_) { + writer->writeTextElement(QStringLiteral("folder"), + QString::number(reinterpret_cast(folder))); + } + + writer->writeEndElement(); // folders + + writer->writeStartElement(QStringLiteral("timeline")); + + foreach (Sequence* sequence, open_sequences_) { + writer->writeTextElement(QStringLiteral("sequence"), + QString::number(reinterpret_cast(sequence))); + } + + writer->writeEndElement(); // timeline + + writer->writeTextElement(QStringLiteral("state"), QString(state_.toBase64())); + + writer->writeEndElement(); // layout +} + +MainWindowLayoutInfo MainWindowLayoutInfo::fromXml(QXmlStreamReader *reader, XMLNodeData &xml_data) +{ + MainWindowLayoutInfo info; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("folders")) { + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("folder")) { + quintptr item_id = reader->readElementText().toULongLong(); + + Item* open_item = xml_data.item_ptrs.value(item_id); + + if (open_item) { + info.open_folders_.append(static_cast(open_item)); + } + } else { + reader->skipCurrentElement(); + } + } + + } else if (reader->name() == QStringLiteral("timeline")) { + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("sequence")) { + quintptr item_id = reader->readElementText().toULongLong(); + + Sequence* open_seq = dynamic_cast(xml_data.item_ptrs.value(item_id)); + + if (open_seq) { + info.open_sequences_.append(open_seq); + } + } else { + reader->skipCurrentElement(); + } + } + + } else if (reader->name() == QStringLiteral("state")) { + + info.state_ = QByteArray::fromBase64(reader->readElementText().toLatin1()); + + } else { + reader->skipCurrentElement(); + } + } + + return info; +} + +void MainWindowLayoutInfo::add_folder(olive::Folder *f) +{ + open_folders_.append(f); +} + +void MainWindowLayoutInfo::add_sequence(Sequence *s) +{ + open_sequences_.append(s); +} + +void MainWindowLayoutInfo::set_state(const QByteArray &layout) +{ + state_ = layout; +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/window/mainwindow/mainwindowlayoutinfo.h b/app/window/mainwindow/mainwindowlayoutinfo.h new file mode 100644 index 000000000..5f76cbc2b --- /dev/null +++ b/app/window/mainwindow/mainwindowlayoutinfo.h @@ -0,0 +1,52 @@ +#ifndef MAINWINDOWLAYOUTINFO_H +#define MAINWINDOWLAYOUTINFO_H + +#include "project/item/folder/folder.h" +#include "project/item/sequence/sequence.h" + +OLIVE_NAMESPACE_ENTER + +class MainWindowLayoutInfo +{ +public: + MainWindowLayoutInfo() = default; + + void toXml(QXmlStreamWriter* writer) const; + + static MainWindowLayoutInfo fromXml(QXmlStreamReader* reader, XMLNodeData &xml_data); + + void add_folder(Folder* f); + + void add_sequence(Sequence* s); + + void set_state(const QByteArray& layout); + + const QList& open_folders() const + { + return open_folders_; + } + + const QList& open_sequences() const + { + return open_sequences_; + } + + const QByteArray& state() const + { + return state_; + } + +private: + QByteArray state_; + + QList open_folders_; + + QList open_sequences_; + +}; + +OLIVE_NAMESPACE_EXIT + +Q_DECLARE_METATYPE(OLIVE_NAMESPACE::MainWindowLayoutInfo) + +#endif // MAINWINDOWLAYOUTINFO_H From f8a8af9ec3426521a72265d803d40f8260a9614f Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 19 Jun 2020 02:26:55 +1000 Subject: [PATCH 021/138] project: focus sequence if there is only one --- app/window/mainwindow/mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 26b765b0d..98538cc77 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -118,7 +118,7 @@ void MainWindow::LoadLayout(const MainWindowLayoutInfo &info) } foreach (Sequence* sequence, info.open_sequences()) { - OpenSequence(sequence, false); + OpenSequence(sequence, info.open_sequences().size() == 1); } restoreState(info.state()); From fd8116a9de638c6fd6a4f3d263dca2997e03f018 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 19 Jun 2020 02:36:09 +1000 Subject: [PATCH 022/138] slider: use ctrl to switch axes --- app/widget/slider/sliderladder.cpp | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/app/widget/slider/sliderladder.cpp b/app/widget/slider/sliderladder.cpp index 2c8e6128e..5a9c4d235 100644 --- a/app/widget/slider/sliderladder.cpp +++ b/app/widget/slider/sliderladder.cpp @@ -135,14 +135,11 @@ void SliderLadder::TimerUpdate() return; } - int y_threshold = fontMetrics().height() / 2; - - if (qAbs(y_mvmt) > qAbs(x_mvmt) - || qApp->keyboardModifiers() & Qt::ControlModifier) { + if (qApp->keyboardModifiers() & Qt::ControlModifier) { // Movement is vertical y_mobility_ += y_mvmt; - if (qAbs(y_mobility_) > y_threshold) { + if (qAbs(y_mobility_) > fontMetrics().height()) { int new_active_element; if (y_mvmt < 0) { @@ -165,15 +162,9 @@ void SliderLadder::TimerUpdate() y_mobility_ = 0; } } else { - // Movement is horizontal - emit DraggedByValue(x_mvmt , elements_.at(active_element_)->GetMultiplier()); + y_mobility_ = 0; - // Reduce Y mobility - if (y_mobility_ > 0) { - y_mobility_--; - } else if (y_mobility_ < 0) { - y_mobility_++; - } + emit DraggedByValue(x_mvmt + y_mvmt, elements_.at(active_element_)->GetMultiplier()); } } From ab74c1579ef70dd3d1ec054f2b2397fe30d2c10f Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 19 Jun 2020 02:44:26 +1000 Subject: [PATCH 023/138] mathnode: temporarily disable identity matrix detection This code was faulty and needs extra functionality elsewhere to actually work correctly. --- app/node/math/math/mathbase.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/node/math/math/mathbase.cpp b/app/node/math/math/mathbase.cpp index 50ae6c2d8..b5ee9d76d 100644 --- a/app/node/math/math/mathbase.cpp +++ b/app/node/math/math/mathbase.cpp @@ -285,8 +285,13 @@ NodeValueTable MathNodeBase::ValueInternal(NodeValueDatabase &value, Operation o } } else if (pairing == kPairTextureMatrix) { // Only allow matrix multiplication - if (operation != kOpMultiply - || number_val.data().value().isIdentity()) { + bool matrix_is_identity = false; + + // FIXME: The matrix in the shader is transformed around footage+sequence resolution so we + // need to do that here to determine if the matrix is truly identity. But to do that, + // we need access to the texture parameters which is currently not possible. + + if (operation != kOpMultiply || matrix_is_identity) { operation_is_noop = true; } else { // It's likely an alpha channel will result from this operation From b2a3cede2cdc8aa2fe973cd31471d2aab8904a59 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 19 Jun 2020 17:06:14 +1000 Subject: [PATCH 024/138] renderer: re-use the same opengl instance for all background rendering Previously the OpenGL instance was tied to each render/cache task, creating and destroying it each time one started and stopped. This was completely unnecessary since the instance holds no state and can be shared by all of the render tasks without having to expensively start a new one. --- app/core.cpp | 10 +++++--- app/render/backend/opengl/openglbackend.cpp | 23 +----------------- app/render/backend/opengl/openglbackend.h | 5 ---- app/render/backend/opengl/openglproxy.cpp | 26 +++++++++++++++++++++ app/render/backend/opengl/openglproxy.h | 11 +++++++++ app/render/backend/opengl/openglworker.cpp | 13 +++++------ app/render/backend/opengl/openglworker.h | 5 +--- 7 files changed, 52 insertions(+), 41 deletions(-) diff --git a/app/core.cpp b/app/core.cpp index 55f0b2c49..e8f34fcf4 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -181,6 +181,9 @@ void Core::Start() // the fact that some of the config paths set by default rely on the app name having been set (in main()) Config::Current().SetDefaults(); + // Load application config + Config::Load(); + // Declare custom types for Qt signal/slot system DeclareTypesForQt(); @@ -193,9 +196,8 @@ void Core::Start() // Initialize task manager TaskManager::CreateInstance(); - // Load application config - Config::Load(); - + // Initialize OpenGL service + OpenGLProxy::CreateInstance(); // // Start application @@ -223,6 +225,8 @@ void Core::Stop() } } + OpenGLProxy::DestroyInstance(); + MenuShared::DestroyInstance(); TaskManager::DestroyInstance(); diff --git a/app/render/backend/opengl/openglbackend.cpp b/app/render/backend/opengl/openglbackend.cpp index a66fa8928..60e81f914 100644 --- a/app/render/backend/opengl/openglbackend.cpp +++ b/app/render/backend/opengl/openglbackend.cpp @@ -27,38 +27,17 @@ OLIVE_NAMESPACE_ENTER OpenGLBackend::OpenGLBackend(QObject* parent) : RenderBackend(parent) { - proxy_ = new OpenGLProxy(); - QThread* proxy_thread = new QThread(); - proxy_thread->start(QThread::IdlePriority); - proxy_->moveToThread(proxy_thread); - - if (!proxy_->Init()) { - ClearProxy(); - } } OpenGLBackend::~OpenGLBackend() { Close(); - - ClearProxy(); } RenderWorker *OpenGLBackend::CreateNewWorker() { - return new OpenGLWorker(this, proxy_); -} - -void OpenGLBackend::ClearProxy() -{ - if (proxy_) { - proxy_->thread()->quit(); - proxy_->thread()->wait(); - proxy_->thread()->deleteLater(); - proxy_->deleteLater(); - proxy_ = nullptr; - } + return new OpenGLWorker(this); } OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglbackend.h b/app/render/backend/opengl/openglbackend.h index a1df6dcab..7d88611f3 100644 --- a/app/render/backend/opengl/openglbackend.h +++ b/app/render/backend/opengl/openglbackend.h @@ -36,11 +36,6 @@ public: protected: virtual RenderWorker* CreateNewWorker() override; -private: - void ClearProxy(); - - OpenGLProxy* proxy_; - }; OLIVE_NAMESPACE_EXIT diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index aff4dac42..c96e52d7a 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -33,6 +33,8 @@ OLIVE_NAMESPACE_ENTER +OpenGLProxy* OpenGLProxy::instance_ = nullptr; + OpenGLProxy::OpenGLProxy(QObject *parent) : QObject(parent), ctx_(nullptr), @@ -48,6 +50,30 @@ OpenGLProxy::~OpenGLProxy() surface_.destroy(); } +void OpenGLProxy::CreateInstance() +{ + instance_ = new OpenGLProxy(); + + QThread* proxy_thread = new QThread(); + proxy_thread->start(QThread::IdlePriority); + instance_->moveToThread(proxy_thread); + + if (!instance_->Init()) { + DestroyInstance(); + } +} + +void OpenGLProxy::DestroyInstance() +{ + if (instance_) { + instance_->thread()->quit(); + instance_->thread()->wait(); + instance_->thread()->deleteLater(); + instance_->deleteLater(); + instance_ = nullptr; + } +} + bool OpenGLProxy::Init() { // Create context object diff --git a/app/render/backend/opengl/openglproxy.h b/app/render/backend/opengl/openglproxy.h index 7c6878626..ff59f76b9 100644 --- a/app/render/backend/opengl/openglproxy.h +++ b/app/render/backend/opengl/openglproxy.h @@ -41,6 +41,15 @@ public: virtual ~OpenGLProxy() override; + static void CreateInstance(); + + static void DestroyInstance(); + + static OpenGLProxy* instance() + { + return instance_; + } + /** * @brief Initialize OpenGL instance in whatever thread this object is a part of * @@ -101,6 +110,8 @@ private: OpenGLTextureCache texture_cache_; + static OpenGLProxy* instance_; + private slots: void FinishInit(); diff --git a/app/render/backend/opengl/openglworker.cpp b/app/render/backend/opengl/openglworker.cpp index b3e10b71c..ec518ffc7 100644 --- a/app/render/backend/opengl/openglworker.cpp +++ b/app/render/backend/opengl/openglworker.cpp @@ -22,15 +22,14 @@ OLIVE_NAMESPACE_ENTER -OpenGLWorker::OpenGLWorker(RenderBackend *parent, OpenGLProxy* proxy) : - RenderWorker(parent), - proxy_(proxy) +OpenGLWorker::OpenGLWorker(RenderBackend *parent) : + RenderWorker(parent) { } void OpenGLWorker::TextureToFrame(const QVariant &texture, FramePtr frame, const QMatrix4x4& mat) const { - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "TextureToBuffer", Qt::BlockingQueuedConnection, Q_ARG(const QVariant&, texture), @@ -42,7 +41,7 @@ QVariant OpenGLWorker::FootageFrameToTexture(StreamPtr stream, FramePtr frame) c { QVariant value; - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "FrameToValue", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, value), @@ -58,7 +57,7 @@ QVariant OpenGLWorker::CachedFrameToTexture(FramePtr frame) const { QVariant value; - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "PreCachedFrameToValue", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, value), @@ -71,7 +70,7 @@ QVariant OpenGLWorker::ProcessShader(const Node *node, const TimeRange &range, c { QVariant value; - QMetaObject::invokeMethod(proxy_, + QMetaObject::invokeMethod(OpenGLProxy::instance(), "RunNodeAccelerated", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, value), diff --git a/app/render/backend/opengl/openglworker.h b/app/render/backend/opengl/openglworker.h index 23b5e787f..75eed65f8 100644 --- a/app/render/backend/opengl/openglworker.h +++ b/app/render/backend/opengl/openglworker.h @@ -29,7 +29,7 @@ OLIVE_NAMESPACE_ENTER class OpenGLWorker : public RenderWorker { public: - OpenGLWorker(RenderBackend* parent, OpenGLProxy* proxy); + OpenGLWorker(RenderBackend* parent); protected: virtual void TextureToFrame(const QVariant& texture, FramePtr frame, const QMatrix4x4 &mat) const override; @@ -42,9 +42,6 @@ protected: virtual bool TextureHasAlpha(const QVariant& v) const override; -private: - OpenGLProxy* proxy_; - }; OLIVE_NAMESPACE_EXIT From 65ccac1dfede05d784f016dc6b340749dc4d9b74 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 19 Jun 2020 20:25:05 +1000 Subject: [PATCH 025/138] renderer: fixed bug that would render frames even after hash matching Also enables the -Wshadow GCC warning to warn against the code bug that caused this issue (and has caused other issues like it in the past). --- app/CMakeLists.txt | 1 + app/node/input.cpp | 14 +++++++----- app/node/output/track/tracklist.cpp | 6 ++--- app/render/backend/opengl/openglproxy.cpp | 24 ++++++++++---------- app/render/backend/renderbackend.cpp | 6 ++--- app/task/render/render.cpp | 21 +++++++++-------- app/widget/audiomonitor/audiomonitor.cpp | 4 ++-- app/widget/audiomonitor/audiomonitor.h | 2 +- app/widget/menu/menu.cpp | 6 ++--- app/widget/menu/menu.h | 2 +- app/widget/timelinewidget/timelinewidget.cpp | 6 ++--- 11 files changed, 48 insertions(+), 44 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 6050fabfe..6f0395705 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -107,6 +107,7 @@ else() -Wall -Wextra -Wno-unused-parameter + -Wshadow ) endif() diff --git a/app/node/input.cpp b/app/node/input.cpp index 3ec99b36f..d17656194 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -81,13 +81,15 @@ QString NodeInput::name() void NodeInput::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled) { - XMLAttributeLoop(reader, attr) { - if (cancelled && *cancelled) { - return; - } + { + XMLAttributeLoop(reader, attr) { + if (cancelled && *cancelled) { + return; + } - if (attr.name() == QStringLiteral("keyframing")) { - set_is_keyframing(attr.value() == QStringLiteral("1")); + if (attr.name() == QStringLiteral("keyframing")) { + set_is_keyframing(attr.value() == QStringLiteral("1")); + } } } diff --git a/app/node/output/track/tracklist.cpp b/app/node/output/track/tracklist.cpp index 67a19c91b..abe36e1e7 100644 --- a/app/node/output/track/tracklist.cpp +++ b/app/node/output/track/tracklist.cpp @@ -151,9 +151,9 @@ void TrackList::RemoveTrack() void TrackList::TrackConnected(NodeEdgePtr edge) { - int track_index = track_input_->IndexOfSubParameter(edge->input()); + int input_index = track_input_->IndexOfSubParameter(edge->input()); - Q_ASSERT(track_index >= 0); + Q_ASSERT(input_index >= 0); Node* connected_node = edge->output()->parentNode(); @@ -163,7 +163,7 @@ void TrackList::TrackConnected(NodeEdgePtr edge) { // Find "real" index TrackOutput* next = nullptr; - for (int i=track_index+1; iGetSize(); i++) { + for (int i=input_index+1; iGetSize(); i++) { Node* that_track = track_input_->At(i)->get_connected_node(); if (that_track && that_track->IsTrack()) { diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index c96e52d7a..08aa8864c 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -276,21 +276,21 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->bind(); - NodeValueMap::const_iterator i; - for (i=job.GetValues().constBegin(); i!=job.GetValues().constEnd(); i++) { + NodeValueMap::const_iterator it; + for (it=job.GetValues().constBegin(); it!=job.GetValues().constEnd(); it++) { // See if the shader has takes this parameter as an input - int variable_location = shader->uniformLocation(i.key()->id()); + int variable_location = shader->uniformLocation(it.key()->id()); if (variable_location == -1) { continue; } // This variable is used in the shader, let's set it - const QVariant& value = i.value().data(); + const QVariant& value = it.value().data(); - const NodeParam::DataType& data_type = (i.value().type() != NodeParam::kNone) - ? i.value().type() - : i.key()->data_type(); + const NodeParam::DataType& data_type = (it.value().type() != NodeParam::kNone) + ? it.value().type() + : it.key()->data_type(); switch (data_type) { case NodeInput::kInt: @@ -300,7 +300,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValue(variable_location, value.toFloat()); break; case NodeInput::kVec2: - if (i.key()->IsArray()) { + if (it.key()->IsArray()) { QVector nv = value.value< QVector >(); QVector a(nv.size()); @@ -310,7 +310,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValueArray(variable_location, a.constData(), a.size()); - int count_location = shader->uniformLocation(QStringLiteral("%1_count").arg(i.key()->id())); + int count_location = shader->uniformLocation(QStringLiteral("%1_count").arg(it.key()->id())); if (count_location > -1) { shader->setUniformValue(count_location, a.size()); } @@ -354,7 +354,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValue(variable_location, textures_to_bind.size()); // If this texture binding is the iterative input, set it here - if (i.key() == job.GetIterativeInput()) { + if (it.key() == job.GetIterativeInput()) { iterative_input = textures_to_bind.size(); } @@ -362,7 +362,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, textures_to_bind.append(tex_id); // Set enable flag if shader wants it - int enable_param_location = shader->uniformLocation(QStringLiteral("%1_enabled").arg(i.key()->id())); + int enable_param_location = shader->uniformLocation(QStringLiteral("%1_enabled").arg(it.key()->id())); if (enable_param_location > -1) { shader->setUniformValue(enable_param_location, tex_id > 0); @@ -370,7 +370,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, if (tex_id > 0) { // Set texture resolution if shader wants it - int res_param_location = shader->uniformLocation(QStringLiteral("%1_resolution").arg(i.key()->id())); + int res_param_location = shader->uniformLocation(QStringLiteral("%1_resolution").arg(it.key()->id())); if (res_param_location > -1) { shader->setUniformValue(res_param_location, static_cast(texture->texture()->width() * texture->texture()->divider()), diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index df96c9efc..b9f90e5e4 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -115,13 +115,13 @@ void RenderBackend::ClearVideoQueue() QFuture > RenderBackend::Hash(const QVector ×) { - return QtConcurrent::run(&pool_, [this](const QVector ×){ - QVector hashes(times.size()); + return QtConcurrent::run(&pool_, [this](const QVector &t){ + QVector hashes(t.size()); for (int i=0;itexture_input()->get_connected_node(), video_params_, - times.at(i)); + t.at(i)); } return hashes; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 133a6bf1d..f0da1ff54 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -93,12 +93,10 @@ void RenderTask::Render(const TimeRangeList& video_range, if (!video_range.isEmpty()) { QList existing_hashes; - foreach (const TimeRange& r, video_range) { - total_length += r.length().toDouble(); - } - times = viewer_->video_frame_cache()->GetFrameListFromTimeRange(video_range); + total_length += video_frame_sz * times.size(); + QFuture > hash_future = backend_.Hash(times); hashes = hash_future.result(); @@ -125,7 +123,7 @@ void RenderTask::Render(const TimeRangeList& video_range, || !download_futures.empty() || !audio_lookup_table.empty())) { - if (!frame_queue.empty()) { + if (!IsCancelled() && !frame_queue.empty()) { // Pop another frame off the frame queue const HashTimePair& p = frame_queue.front(); @@ -138,11 +136,14 @@ void RenderTask::Render(const TimeRangeList& video_range, bool hash_exists = false; if (use_disk_cache) { - bool hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), p.hash) != existing_hashes.end()); + // Check if this hash is in our "existing hashes" list + hash_exists = (std::find(existing_hashes.begin(), existing_hashes.end(), p.hash) != existing_hashes.end()); + // If not, check if it's in the filesystem if (!hash_exists) { hash_exists = QFileInfo::exists(viewer_->video_frame_cache()->CachePathName(p.hash)); + // If so, add it to the list so we don't have to check the filesystem again later if (hash_exists) { existing_hashes.push_back(p.hash); } @@ -167,7 +168,7 @@ void RenderTask::Render(const TimeRangeList& video_range, frame_queue.pop_front(); } - if (!audio_queue.empty()) { + if (!IsCancelled() && !audio_queue.empty()) { audio_lookup_table.push_back({audio_queue.front(), backend_.RenderAudio(audio_queue.front())}); audio_queue.pop_front(); } @@ -194,9 +195,9 @@ void RenderTask::Render(const TimeRangeList& video_range, // Place it in the cache std::list times_with_hash; - for (int k=0;khash) { - times_with_hash.push_back(times.at(k)); + for (int hash_index=0;hash_indexhash) { + times_with_hash.push_back(times.at(hash_index)); } } diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index 46a05a26e..86b53a8df 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -90,11 +90,11 @@ void AudioMonitor::Stop() } } -void AudioMonitor::OutputPushed(const QByteArray &data) +void AudioMonitor::OutputPushed(const QByteArray &d) { QVector v(params_.channel_count(), 0); - BytesToSampleSummary(data, v); + BytesToSampleSummary(d, v); PushValue(v); diff --git a/app/widget/audiomonitor/audiomonitor.h b/app/widget/audiomonitor/audiomonitor.h index d2f55f287..2b290ff14 100644 --- a/app/widget/audiomonitor/audiomonitor.h +++ b/app/widget/audiomonitor/audiomonitor.h @@ -45,7 +45,7 @@ public slots: void Stop(); - void OutputPushed(const QByteArray& data); + void OutputPushed(const QByteArray& d); protected: //virtual void paintEvent(QPaintEvent* event) override; diff --git a/app/widget/menu/menu.cpp b/app/widget/menu/menu.cpp index b744c3d32..5d1f7321c 100644 --- a/app/widget/menu/menu.cpp +++ b/app/widget/menu/menu.cpp @@ -50,13 +50,13 @@ Menu::Menu(const QString &s, QWidget *parent) : Init(); } -QAction *Menu::AddActionWithData(const QString &text, const QVariant &data, const QVariant &compare) +QAction *Menu::AddActionWithData(const QString &text, const QVariant &d, const QVariant &compare) { QAction* a = addAction(text); - a->setData(data); + a->setData(d); a->setCheckable(true); - a->setChecked(data == compare); + a->setChecked(d == compare); return a; } diff --git a/app/widget/menu/menu.h b/app/widget/menu/menu.h index edafaf85d..777143efc 100644 --- a/app/widget/menu/menu.h +++ b/app/widget/menu/menu.h @@ -132,7 +132,7 @@ public: } QAction* AddActionWithData(const QString& text, - const QVariant& data, + const QVariant& d, const QVariant& compare); QAction *InsertAlphabetically(const QString& s); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 908d7e3a2..b89ffe00d 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -1584,9 +1584,9 @@ bool TimelineWidget::SnapPoint(QList start_times, rational* movement, // Find all points at this movement QList snap_times; - foreach (const SnapData& data, potential_snaps) { - if (data.movement == *movement) { - snap_times.append(data.time); + foreach (const SnapData& d, potential_snaps) { + if (d.movement == *movement) { + snap_times.append(d.time); } } From a94caa57378f8209e4b25225490231a79df6e1bc Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 19 Jun 2020 20:25:49 +1000 Subject: [PATCH 026/138] export: fixed bug that broke audio on export --- app/render/playbackcache.cpp | 1 + app/task/export/export.cpp | 14 +++----------- app/task/export/export.h | 2 -- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/app/render/playbackcache.cpp b/app/render/playbackcache.cpp index b94ed08e0..9bce6a504 100644 --- a/app/render/playbackcache.cpp +++ b/app/render/playbackcache.cpp @@ -71,6 +71,7 @@ void PlaybackCache::SetLength(const rational &r) } else if (r > length_) { // If new length is greater, simply extend the invalidated range for now invalidated_.InsertTimeRange(range_diff); + jobs_.append({range_diff, QDateTime::currentMSecsSinceEpoch()}); } else { // If new length is smaller, removed hashes invalidated_.RemoveTimeRange(range_diff); diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 25b65aa27..5cc667a9e 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -96,21 +96,13 @@ bool ExportTask::Run() if (params_.audio_enabled()) { audio_range.append(range); + audio_data_.SetLength(range.length()); } Render(video_range, audio_range, mat, false); bool success = true; - foreach (QFuture f, write_frame_futures_) { - f.waitForFinished(); - - if (!f.result()) { - SetError(tr("Failed to write AVFrame")); - success = false; - } - } - if (params_.audio_enabled()) { // Write audio data now encoder_->WriteAudio(audio_params(), audio_data_.GetCacheFilename()); @@ -165,7 +157,7 @@ void ExportTask::FrameDownloaded(const QByteArray &hash, const std::listWriteFrame(time_map_.value(real_time), real_time); + encoder_->WriteFrame(time_map_.take(real_time), real_time); frame_time_++; @@ -180,7 +172,7 @@ void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples adjusted_range -= params_.custom_range().in(); } - audio_data_.WritePCM(adjusted_range, samples, job_time()); + audio_data_.WritePCM(adjusted_range, samples, QDateTime::currentMSecsSinceEpoch()); } OLIVE_NAMESPACE_EXIT diff --git a/app/task/export/export.h b/app/task/export/export.h index dcbae30f0..5c92adea3 100644 --- a/app/task/export/export.h +++ b/app/task/export/export.h @@ -49,8 +49,6 @@ private: QHash time_map_; - QList< QFuture > write_frame_futures_; - ColorManager* color_manager_; ExportParams params_; From 5f6e5916bcda5ecab29c40297ae8fc3250472799 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 20 Jun 2020 01:56:41 +1000 Subject: [PATCH 027/138] curvewidget: allow zooming only one axis and allow hiding keyframe tracks --- app/widget/curvewidget/curveview.cpp | 43 ++++++++++++++++++-- app/widget/curvewidget/curveview.h | 4 ++ app/widget/curvewidget/curvewidget.cpp | 19 +++++++-- app/widget/curvewidget/curvewidget.h | 3 ++ app/widget/keyframeview/keyframeviewbase.cpp | 26 ++++++++++++ app/widget/keyframeview/keyframeviewbase.h | 4 ++ 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index a78f97e13..52a7e8eab 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -61,6 +61,16 @@ void CurveView::Clear() void CurveView::SetTrackCount(int count) { track_count_ = count; + + track_visible_.resize(track_count_); + track_visible_.fill(true); +} + +void CurveView::SetTrackVisible(int track, bool visible) +{ + track_visible_[track] = visible; + + SetKeyframeTrackVisible(track, visible); } void CurveView::drawBackground(QPainter *painter, const QRectF &rect) @@ -116,6 +126,10 @@ void CurveView::drawBackground(QPainter *painter, const QRectF &rect) // Draw keyframe lines for (int j=0;jsetPen(QPen(GetKeyframeColor(j), qMax(1, fontMetrics().height() / 4))); QList keys = GetKeyframesSortedByTime(j); @@ -239,12 +253,33 @@ void CurveView::wheelEvent(QWheelEvent *event) { if (WheelEventIsAZoomEvent(event)) { if (!event->angleDelta().isNull()) { + bool only_vertical = false; + bool only_horizontal = false; + + if (event->modifiers() & Qt::ShiftModifier) { + if (event->modifiers() & Qt::AltModifier) { + only_horizontal = true; + } else { + only_vertical = true; + } + } + if (event->angleDelta().x() + event->angleDelta().y() > 0) { - emit ScaleChanged(GetScale() * 1.1); - SetYScale(GetYScale() * 1.1); + if (!only_vertical) { + emit ScaleChanged(GetScale() * 1.1); + } + + if (!only_horizontal) { + SetYScale(GetYScale() * 1.1); + } } else { - emit ScaleChanged(GetScale() * 0.9); - SetYScale(GetYScale() * 0.9); + if (!only_vertical) { + emit ScaleChanged(GetScale() * 0.9); + } + + if (!only_horizontal) { + SetYScale(GetYScale() *0.9); + } } } } else { diff --git a/app/widget/curvewidget/curveview.h b/app/widget/curvewidget/curveview.h index 2c767acd6..67c2e25c3 100644 --- a/app/widget/curvewidget/curveview.h +++ b/app/widget/curvewidget/curveview.h @@ -39,6 +39,8 @@ public: void SetTrackCount(int count); + void SetTrackVisible(int track, bool visible); + public slots: void AddKeyframe(NodeKeyframePtr key); @@ -76,6 +78,8 @@ private: QList bezier_control_points_; + QVector track_visible_; + int track_count_; private slots: diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index 4cd9dba3e..2453618f9 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -117,12 +117,17 @@ void CurveWidget::SetInput(NodeInput *input) { if (bridge_) { foreach (QWidget* bridge_widget, bridge_->widgets()) { - delete bridge_widget; + bridge_widget->deleteLater(); } - delete bridge_; + bridge_->deleteLater(); bridge_ = nullptr; } + foreach (QCheckBox* box, checkboxes_) { + box->deleteLater(); + } + checkboxes_.clear(); + if (input_) { disconnect(input_, &NodeInput::KeyframeAdded, view_, &CurveView::AddKeyframe); disconnect(input_, &NodeInput::KeyframeRemoved, view_, &CurveView::RemoveKeyframe); @@ -142,7 +147,15 @@ void CurveWidget::SetInput(NodeInput *input) for (int i=0;iwidgets().size();i++) { // Insert between two stretches to center the widget - widget_bridge_layout_->insertWidget(2 + i, bridge_->widgets().at(i)); + QCheckBox* checkbox = new QCheckBox(); + checkbox->setChecked(true); + widget_bridge_layout_->insertWidget(2 + i*2, checkbox); + checkboxes_.append(checkbox); + connect(checkbox, &QCheckBox::clicked, this, [this](bool e){ + view_->SetTrackVisible(checkboxes_.indexOf(static_cast(sender())), e); + }); + + widget_bridge_layout_->insertWidget(2 + i*2 + 1, bridge_->widgets().at(i)); } connect(input_, &NodeInput::KeyframeAdded, view_, &CurveView::AddKeyframe); diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index 21e0400d0..a1c2035c0 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -21,6 +21,7 @@ #ifndef CURVEWIDGET_H #define CURVEWIDGET_H +#include #include #include #include @@ -87,6 +88,8 @@ private: NodeParamViewKeyframeControl* key_control_; + QList checkboxes_; + private slots: void SelectionChanged(); diff --git a/app/widget/keyframeview/keyframeviewbase.cpp b/app/widget/keyframeview/keyframeviewbase.cpp index 0bf903b39..257a12e09 100644 --- a/app/widget/keyframeview/keyframeviewbase.cpp +++ b/app/widget/keyframeview/keyframeviewbase.cpp @@ -106,6 +106,11 @@ KeyframeViewItem *KeyframeViewBase::AddKeyframeInternal(NodeKeyframePtr key) item->SetScale(GetScale()); item_map_.insert(key.get(), item); scene()->addItem(item); + + if (hidden_tracks_.contains(key->track())) { + item->setVisible(false); + } + return item; } @@ -306,6 +311,27 @@ void KeyframeViewBase::SetYAxisEnabled(bool e) y_axis_enabled_ = e; } +void KeyframeViewBase::SetKeyframeTrackVisible(int track, bool visible) +{ + if (!visible == hidden_tracks_.contains(track)) { + return; + } + + QMap::const_iterator i; + + for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { + if (i.key()->track() == track) { + i.value()->setVisible(visible); + } + } + + if (visible) { + hidden_tracks_.removeOne(track); + } else { + hidden_tracks_.append(track); + } +} + rational KeyframeViewBase::CalculateNewTimeFromScreen(const rational &old_time, double cursor_diff) { return rational::fromDouble(old_time.toDouble() + cursor_diff); diff --git a/app/widget/keyframeview/keyframeviewbase.h b/app/widget/keyframeview/keyframeviewbase.h index 04e7d6096..705e845c6 100644 --- a/app/widget/keyframeview/keyframeviewbase.h +++ b/app/widget/keyframeview/keyframeviewbase.h @@ -64,6 +64,8 @@ protected: void SetYAxisEnabled(bool e); + void SetKeyframeTrackVisible(int track, bool visible); + private: rational CalculateNewTimeFromScreen(const rational& old_time, double cursor_diff); @@ -100,6 +102,8 @@ private: bool currently_autoselecting_; + QList hidden_tracks_; + private slots: void ShowContextMenu(); From e97b5a9a1438fba0cceaaa2f2746a60b1a5bf468 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 21 Jun 2020 13:12:07 +1000 Subject: [PATCH 028/138] nodeparamview: set time on node set Fixes bug where keyframes would land on 0 unless the time is set a second time. --- app/widget/nodeparamview/nodeparamview.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index eaae8d0eb..bc7f1e07d 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -177,18 +177,21 @@ void NodeParamView::SetNodes(QList nodes) if (viewer) { SetTimebase(viewer->video_params().time_base()); + rational time = Timecode::timestamp_to_time(this->GetTimestamp(), timebase()); + // Set viewer as a time target keyframe_view_->SetTimeTarget(viewer); foreach (NodeParamViewItem* item, items_) { item->SetTimeTarget(viewer); + item->SetTime(time); } emit TimeTargetChanged(viewer); } // Forces the scroll to update to this time - keyframe_view_->SetTime(ruler()->GetTime()); + keyframe_view_->SetTime(GetTimestamp()); } } @@ -239,7 +242,7 @@ void NodeParamView::DeleteSelected() void NodeParamView::UpdateItemTime(const int64_t ×tamp) { - rational time = Timecode::timestamp_to_time(timestamp, keyframe_view_->timebase()); + rational time = Timecode::timestamp_to_time(timestamp, timebase()); foreach (NodeParamViewItem* item, items_) { item->SetTime(time); From 1725f01459b77250ef7100b27d4c7f9ea70024a3 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 22 Jun 2020 00:38:00 +1000 Subject: [PATCH 029/138] paramview: changed to static viewer-bound and fixed various scrolling issues --- app/panel/param/param.cpp | 2 - app/panel/param/param.h | 2 - app/widget/keyframeview/keyframeview.cpp | 9 ++- app/widget/keyframeview/keyframeview.h | 10 +++ app/widget/keyframeview/keyframeviewbase.cpp | 18 +++-- app/widget/nodeparamview/nodeparamview.cpp | 78 +++++++++---------- app/widget/nodeparamview/nodeparamview.h | 15 ++-- app/widget/timebased/timebased.h | 4 +- .../timelinewidget/view/timelineview.cpp | 1 - .../timelinewidget/view/timelineviewbase.cpp | 7 -- .../timelinewidget/view/timelineviewbase.h | 4 - app/window/mainwindow/mainwindow.cpp | 1 + 12 files changed, 81 insertions(+), 70 deletions(-) diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index 649998cdf..47d81df49 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -29,7 +29,6 @@ ParamPanel::ParamPanel(QWidget* parent) : { NodeParamView* view = new NodeParamView(); connect(view, &NodeParamView::InputDoubleClicked, this, &ParamPanel::CreateCurvePanel); - connect(view, &NodeParamView::TimeTargetChanged, this, &ParamPanel::TimeTargetChanged); connect(view, &NodeParamView::RequestSelectNode, this, &ParamPanel::RequestSelectNode); connect(view, &NodeParamView::OpenedNode, this, &ParamPanel::OpeningNode); connect(view, &NodeParamView::ClosedNode, this, &ParamPanel::ClosingNode); @@ -104,7 +103,6 @@ void ParamPanel::CreateCurvePanel(NodeInput *input) connect(view, &NodeParamView::TimebaseChanged, panel, &CurvePanel::SetTimebase); connect(view, &NodeParamView::TimeChanged, panel, &CurvePanel::SetTimestamp); - connect(view, &NodeParamView::TimeTargetChanged, panel, &CurvePanel::SetTimeTarget); connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::SetTimestamp); connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::TimeChanged); connect(panel, &CurvePanel::CloseRequested, this, &ParamPanel::ClosingCurvePanel); diff --git a/app/panel/param/param.h b/app/panel/param/param.h index 0227e45ba..e75350d6b 100644 --- a/app/panel/param/param.h +++ b/app/panel/param/param.h @@ -41,8 +41,6 @@ public slots: virtual void DeleteSelected() override; signals: - void TimeTargetChanged(Node* node); - void RequestSelectNode(const QList& target); void FoundGizmos(Node* node); diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index cfbac331f..2d6b880e9 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -23,7 +23,8 @@ OLIVE_NAMESPACE_ENTER KeyframeView::KeyframeView(QWidget *parent) : - KeyframeViewBase(parent) + KeyframeViewBase(parent), + max_scroll_(0) { setAlignment(Qt::AlignLeft | Qt::AlignTop); } @@ -35,6 +36,12 @@ void KeyframeView::wheelEvent(QWheelEvent *event) } } +void KeyframeView::SceneRectUpdateEvent(QRectF &rect) +{ + rect.setY(0); + rect.setHeight(max_scroll_); +} + void KeyframeView::AddKeyframe(NodeKeyframePtr key, int y) { QPoint global_pt(0, y); diff --git a/app/widget/keyframeview/keyframeview.h b/app/widget/keyframeview/keyframeview.h index 505ea4f58..90361c8e5 100644 --- a/app/widget/keyframeview/keyframeview.h +++ b/app/widget/keyframeview/keyframeview.h @@ -31,12 +31,22 @@ class KeyframeView : public KeyframeViewBase public: KeyframeView(QWidget* parent = nullptr); + void SetMaxScroll(int i) + { + max_scroll_ = i; + } + protected: virtual void wheelEvent(QWheelEvent* event) override; + virtual void SceneRectUpdateEvent(QRectF& rect) override; + public slots: void AddKeyframe(NodeKeyframePtr key, int y); +private: + int max_scroll_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/keyframeview/keyframeviewbase.cpp b/app/widget/keyframeview/keyframeviewbase.cpp index 257a12e09..e6c4ad480 100644 --- a/app/widget/keyframeview/keyframeviewbase.cpp +++ b/app/widget/keyframeview/keyframeviewbase.cpp @@ -101,14 +101,18 @@ void KeyframeViewBase::RemoveKeyframe(NodeKeyframePtr key) KeyframeViewItem *KeyframeViewBase::AddKeyframeInternal(NodeKeyframePtr key) { - KeyframeViewItem* item = new KeyframeViewItem(key); - item->SetTimeTarget(GetTimeTarget()); - item->SetScale(GetScale()); - item_map_.insert(key.get(), item); - scene()->addItem(item); + KeyframeViewItem* item = item_map_.value(key.get()); - if (hidden_tracks_.contains(key->track())) { - item->setVisible(false); + if (!item) { + item = new KeyframeViewItem(key); + item->SetTimeTarget(GetTimeTarget()); + item->SetScale(GetScale()); + item_map_.insert(key.get(), item); + scene()->addItem(item); + + if (hidden_tracks_.contains(key->track())) { + item->setVisible(false); + } } return item; diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index bc7f1e07d..eab4bd815 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -43,18 +43,19 @@ NodeParamView::NodeParamView(QWidget *parent) : // Set up scroll area for params QScrollArea* scroll_area = new QScrollArea(); + scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll_area->setWidgetResizable(true); splitter->addWidget(scroll_area); // Param widget - QWidget* param_widget_area = new QWidget(); - scroll_area->setWidget(param_widget_area); + param_widget_area_ = new QWidget(); + scroll_area->setWidget(param_widget_area_); // Set up scroll area layout - param_layout_ = new QVBoxLayout(param_widget_area); + param_layout_ = new QVBoxLayout(param_widget_area_); param_layout_->setSpacing(0); - param_layout_->setMargin(0); + param_layout_->setContentsMargins(0, ruler()->height(), 0, 0); // Add a stretch to allow empty space at the bottom of the layout param_layout_->addStretch(); @@ -73,7 +74,6 @@ NodeParamView::NodeParamView(QWidget *parent) : keyframe_view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ConnectTimelineView(keyframe_view_); connect(keyframe_view_, &KeyframeView::RequestCenterScrollOnPlayhead, this, &NodeParamView::CenterScrollOnPlayhead); - bottom_item_ = keyframe_view_->scene()->addRect(0, 0, 1, 1); keyframe_area_layout->addWidget(keyframe_view_); // Connect ruler and keyframe view together @@ -122,19 +122,15 @@ NodeParamView::NodeParamView(QWidget *parent) : void NodeParamView::SetNodes(QList nodes) { - ConnectViewerNode(nullptr); - // If we already have item widgets, delete them all now foreach (NodeParamViewItem* item, items_) { emit ClosedNode(item->GetNode()); emit FoundGizmos(nullptr); - delete item; + item->deleteLater(); } items_.clear(); - emit TimeTargetChanged(nullptr); // Reset keyframe view - SetTimebase(rational()); keyframe_view_->Clear(); // Set the internal list to the one we've received @@ -158,10 +154,6 @@ void NodeParamView::SetNodes(QList nodes) items_.append(item); - QMetaObject::invokeMethod(item, - "SignalAllKeyframes", - Qt::QueuedConnection); - emit OpenedNode(node); if (!found_gizmos && node->HasGizmos()) { @@ -170,28 +162,7 @@ void NodeParamView::SetNodes(QList nodes) } } - ViewerOutput* viewer = nodes_.first()->FindOutputNode(); - - ConnectViewerNode(viewer); - - if (viewer) { - SetTimebase(viewer->video_params().time_base()); - - rational time = Timecode::timestamp_to_time(this->GetTimestamp(), timebase()); - - // Set viewer as a time target - keyframe_view_->SetTimeTarget(viewer); - - foreach (NodeParamViewItem* item, items_) { - item->SetTimeTarget(viewer); - item->SetTime(time); - } - - emit TimeTargetChanged(viewer); - } - - // Forces the scroll to update to this time - keyframe_view_->SetTime(GetTimestamp()); + QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection); } } @@ -214,6 +185,8 @@ void NodeParamView::TimebaseChangedEvent(const rational &timebase) TimeBasedWidget::TimebaseChangedEvent(timebase); keyframe_view_->SetTimebase(timebase); + + UpdateItemTime(GetTimestamp()); } void NodeParamView::TimeChangedEvent(const int64_t ×tamp) @@ -225,6 +198,28 @@ void NodeParamView::TimeChangedEvent(const int64_t ×tamp) UpdateItemTime(timestamp); } +void NodeParamView::ConnectedNodeChanged(ViewerOutput *n) +{ + // Set viewer as a time target + keyframe_view_->SetTimeTarget(n); + + foreach (NodeParamViewItem* item, items_) { + item->SetTimeTarget(n); + } +} + +void NodeParamView::ConnectNodeInternal(ViewerOutput *n) +{ + SetTimebase(n->video_params().time_base()); +} + +void NodeParamView::DisconnectNodeInternal(ViewerOutput *n) +{ + Q_UNUSED(n) + + SetTimebase(rational()); +} + const QList &NodeParamView::nodes() { return nodes_; @@ -254,11 +249,16 @@ void NodeParamView::ItemRequestedTimeChanged(const rational &time) SetTimeAndSignal(Timecode::time_to_timestamp(time, keyframe_view_->timebase())); } -void NodeParamView::ForceKeyframeViewToScroll(int min, int max) +void NodeParamView::ForceKeyframeViewToScroll() { - Q_UNUSED(min) + keyframe_view_->SetMaxScroll(param_widget_area_->height() - ruler()->height()); +} - bottom_item_->setY(keyframe_view_->viewport()->height() + max); +void NodeParamView::PlaceKeyframesOnView() +{ + foreach (NodeParamViewItem* item, items_) { + QMetaObject::invokeMethod(item, "SignalAllKeyframes", Qt::QueuedConnection); + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index fd6fe4220..71d4edcfd 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -47,8 +47,6 @@ public: signals: void InputDoubleClicked(NodeInput* input); - void TimeTargetChanged(Node* target); - void RequestSelectNode(const QList& target); void OpenedNode(Node* n); @@ -64,6 +62,11 @@ protected: virtual void TimebaseChangedEvent(const rational&) override; virtual void TimeChangedEvent(const int64_t &) override; + virtual void ConnectedNodeChanged(ViewerOutput* n) override; + + virtual void ConnectNodeInternal(ViewerOutput* n) override; + virtual void DisconnectNodeInternal(ViewerOutput* n) override; + private: void UpdateItemTime(const int64_t ×tamp); @@ -77,14 +80,16 @@ private: QScrollBar* vertical_scrollbar_; - QGraphicsRectItem* bottom_item_; - int last_scroll_val_; + QWidget* param_widget_area_; + private slots: void ItemRequestedTimeChanged(const rational& time); - void ForceKeyframeViewToScroll(int min, int max); + void ForceKeyframeViewToScroll(); + + void PlaceKeyframesOnView(); }; diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index 49012af1c..bf5b8ac40 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -52,6 +52,8 @@ public: void SetScaleAndCenterOnPlayhead(const double& scale); + TimeRuler* ruler() const; + public slots: void SetTimestamp(int64_t timestamp); @@ -89,8 +91,6 @@ public slots: void GoToOut(); - TimeRuler* ruler() const; - protected slots: void SetTimeAndSignal(const int64_t& t); diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index 2d3cc4511..d3ae73c56 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -45,7 +45,6 @@ TimelineView::TimelineView(Qt::Alignment vertical_alignment, QWidget *parent) : setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setBackgroundRole(QPalette::Window); setContextMenuPolicy(Qt::CustomContextMenu); - SetLimitYAxis(true); viewport()->setMouseTracking(true); connect(scene(), &QGraphicsScene::selectionChanged, this, &TimelineView::SelectionChanged); diff --git a/app/widget/timelinewidget/view/timelineviewbase.cpp b/app/widget/timelinewidget/view/timelineviewbase.cpp index c945f80db..c044f425e 100644 --- a/app/widget/timelinewidget/view/timelineviewbase.cpp +++ b/app/widget/timelinewidget/view/timelineviewbase.cpp @@ -39,7 +39,6 @@ TimelineViewBase::TimelineViewBase(QWidget *parent) : playhead_scene_left_(-1), playhead_scene_right_(-1), dragging_playhead_(false), - limit_y_axis_(false), snapped_(false), snap_service_(nullptr) { @@ -271,10 +270,4 @@ bool TimelineViewBase::WheelEventIsAZoomEvent(QWheelEvent *event) return (static_cast(event->modifiers() & Qt::ControlModifier) == !Config::Current()["ScrollZooms"].toBool()); } -void TimelineViewBase::SetLimitYAxis(bool) -{ - limit_y_axis_ = true; - UpdateSceneRect(); -} - OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/view/timelineviewbase.h b/app/widget/timelinewidget/view/timelineviewbase.h index e664f5178..cb667dd41 100644 --- a/app/widget/timelinewidget/view/timelineviewbase.h +++ b/app/widget/timelinewidget/view/timelineviewbase.h @@ -73,8 +73,6 @@ protected: bool WheelEventIsAZoomEvent(QWheelEvent* event); - void SetLimitYAxis(bool e); - rational GetPlayheadTime() const; bool PlayheadPress(QMouseEvent* event); @@ -97,8 +95,6 @@ private: QGraphicsScene scene_; - bool limit_y_axis_; - bool snapped_; QList snap_time_; diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 98538cc77..38f364cb2 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -518,6 +518,7 @@ void MainWindow::RemoveProjectPanel(ProjectPanel *panel) void MainWindow::TimelineFocused(ViewerOutput* viewer) { sequence_viewer_panel_->ConnectViewerNode(viewer); + param_panel_->ConnectViewerNode(viewer); Sequence* seq = nullptr; From 6ebcb7162e4b05e79dc7230ce9587c6d64b412c0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 22 Jun 2020 00:39:38 +1000 Subject: [PATCH 030/138] audiovisualwaveform: clamp audio values so they stay inbounds --- app/audio/audiovisualwaveform.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp index 3beeb2cbb..08c6c27b0 100644 --- a/app/audio/audiovisualwaveform.cpp +++ b/app/audio/audiovisualwaveform.cpp @@ -231,10 +231,13 @@ void AudioVisualWaveform::DrawSample(QPainter *painter, const QVector(1.0f)); + qfloat16 min = qMax(sample.at(i).min, static_cast(-1.0)); + if (Config::Current()[QStringLiteral("RectifiedWaveforms")].toBool()) { int channel_bottom = y + channel_height * (i + 1); - int diff = qRound((sample.at(i).max - sample.at(i).min) * channel_half_height); + int diff = qRound((max - min) * channel_half_height); painter->drawLine(x, channel_bottom - diff, @@ -244,9 +247,9 @@ void AudioVisualWaveform::DrawSample(QPainter *painter, const QVectordrawLine(x, - channel_mid + qRound(sample.at(i).min * static_cast(channel_half_height)), + channel_mid + qRound(min * static_cast(channel_half_height)), x, - channel_mid + qRound(sample.at(i).max * static_cast(channel_half_height))); + channel_mid + qRound(max * static_cast(channel_half_height))); } } } From 1ec0d06e9fe1206d0e8366956933fd97ae009968 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 22 Jun 2020 00:40:19 +1000 Subject: [PATCH 031/138] timebasedpanel: check if node is the same Minor optimization. --- app/panel/timebased/timebased.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index 37a0959fa..5e95e944d 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -125,6 +125,10 @@ TimeRuler *TimeBasedPanel::ruler() const void TimeBasedPanel::ConnectViewerNode(ViewerOutput *node) { + if (widget_->GetConnectedNode() == node) { + return; + } + if (widget_->GetConnectedNode()) { disconnect(widget_->GetConnectedNode(), &ViewerOutput::MediaNameChanged, this, &TimeBasedPanel::SetSubtitle); } From 2207a915a6a239928326551ab43b8d5e04878244 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 23 Jun 2020 05:15:48 +1000 Subject: [PATCH 032/138] viewernode: check if timebase/size have actually changed before signalling Minor optimization. --- app/node/output/viewer/viewer.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 8078ade32..45fd6b23d 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -129,12 +129,20 @@ void ViewerOutput::InvalidateCache(const TimeRange &range, NodeInput *from, Node void ViewerOutput::set_video_params(const VideoParams &video) { + bool size_changed = video_params_.width() != video.width() || video_params_.height() != video.height(); + bool timebase_changed = video_params_.time_base() != video.time_base(); + video_params_ = video; - video_frame_cache_.SetTimebase(video_params_.time_base()); + if (size_changed) { + emit SizeChanged(video_params_.width(), video_params_.height()); + } + + if (timebase_changed) { + video_frame_cache_.SetTimebase(video_params_.time_base()); + emit TimebaseChanged(video_params_.time_base()); + } - emit SizeChanged(video_params_.width(), video_params_.height()); - emit TimebaseChanged(video_params_.time_base()); emit ParamsChanged(); } From e681b87988493978419bb0311685e1c098cd9948 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 23 Jun 2020 05:22:58 +1000 Subject: [PATCH 033/138] curve/param/panels: leave curve panel open and have option for timebasedwidget to auto-set timebase --- app/panel/curve/curve.cpp | 5 -- app/panel/curve/curve.h | 2 - app/panel/param/param.cpp | 77 +++++++++----------- app/panel/param/param.h | 9 ++- app/widget/curvewidget/curvewidget.cpp | 11 +-- app/widget/curvewidget/curvewidget.h | 2 + app/widget/nodeparamview/nodeparamview.cpp | 12 --- app/widget/nodeparamview/nodeparamview.h | 3 - app/widget/timebased/timebased.cpp | 25 ++++++- app/widget/timebased/timebased.h | 8 ++ app/widget/timelinewidget/timelinewidget.cpp | 4 +- app/widget/viewer/viewer.cpp | 12 --- 12 files changed, 81 insertions(+), 89 deletions(-) diff --git a/app/panel/curve/curve.cpp b/app/panel/curve/curve.cpp index 2c9fc0e4b..b6179d258 100644 --- a/app/panel/curve/curve.cpp +++ b/app/panel/curve/curve.cpp @@ -49,11 +49,6 @@ void CurvePanel::SetInput(NodeInput *input) Retranslate(); } -void CurvePanel::SetTimeTarget(Node *target) -{ - static_cast(GetTimeBasedWidget())->SetTimeTarget(target); -} - void CurvePanel::IncreaseTrackHeight() { CurveWidget* c = static_cast(GetTimeBasedWidget()); diff --git a/app/panel/curve/curve.h b/app/panel/curve/curve.h index 4b25b2b22..982a4d0b4 100644 --- a/app/panel/curve/curve.h +++ b/app/panel/curve/curve.h @@ -39,8 +39,6 @@ public: public slots: void SetInput(NodeInput* input); - void SetTimeTarget(Node* target); - virtual void IncreaseTrackHeight() override; virtual void DecreaseTrackHeight() override; diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index 47d81df49..1bd3355d7 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -30,8 +30,6 @@ ParamPanel::ParamPanel(QWidget* parent) : NodeParamView* view = new NodeParamView(); connect(view, &NodeParamView::InputDoubleClicked, this, &ParamPanel::CreateCurvePanel); connect(view, &NodeParamView::RequestSelectNode, this, &ParamPanel::RequestSelectNode); - connect(view, &NodeParamView::OpenedNode, this, &ParamPanel::OpeningNode); - connect(view, &NodeParamView::ClosedNode, this, &ParamPanel::ClosingNode); connect(view, &NodeParamView::FoundGizmos, this, &ParamPanel::FoundGizmos); SetTimeBasedWidget(view); @@ -50,13 +48,7 @@ void ParamPanel::SetTimestamp(const int64_t ×tamp) TimeBasedPanel::SetTimestamp(timestamp); // Ensure all CurvePanels are updated with this time too - QHash::const_iterator i; - - for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { - if (i.value() && i.value() != sender()) { - i.value()->SetTimestamp(timestamp); - } - } + ParamViewTimeChanged(timestamp); } void ParamPanel::DeleteSelected() @@ -97,50 +89,51 @@ void ParamPanel::CreateCurvePanel(NodeInput *input) panel = Core::instance()->main_window()->AppendCurvePanel(); panel->SetInput(input); - panel->SetTimebase(view->timebase()); + panel->ConnectViewerNode(view->GetConnectedNode()); panel->SetTimestamp(view->GetTimestamp()); - panel->SetTimeTarget(view->GetTimeTarget()); - connect(view, &NodeParamView::TimebaseChanged, panel, &CurvePanel::SetTimebase); - connect(view, &NodeParamView::TimeChanged, panel, &CurvePanel::SetTimestamp); - connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::SetTimestamp); - connect(panel, &CurvePanel::TimeChanged, view, &NodeParamView::TimeChanged); + connect(view, &NodeParamView::TimeChanged, this, &ParamPanel::ParamViewTimeChanged); + connect(panel, &CurvePanel::TimeChanged, this, &ParamPanel::CurvePanelTimeChanged); connect(panel, &CurvePanel::CloseRequested, this, &ParamPanel::ClosingCurvePanel); open_curve_panels_.insert(input, panel); } -void ParamPanel::OpeningNode(Node *n) -{ - QList inputs = n->GetInputsIncludingArrays(); - - foreach (NodeInput* i, inputs) { - if (open_curve_panels_.contains(i)) { - // We had a CurvePanel open for this input that was closed in ClosingNode(), re-open it - CreateCurvePanel(i); - } - } -} - -void ParamPanel::ClosingNode(Node *n) -{ - QList inputs = n->GetInputsIncludingArrays(); - - foreach (NodeInput* i, inputs) { - CurvePanel* panel = open_curve_panels_.value(i); - - // Close the panel (this also destroys it), but keep a reference in the hash - if (panel) { - panel->close(); - open_curve_panels_.insert(i, nullptr); - } - } -} - void ParamPanel::ClosingCurvePanel() { CurvePanel* panel = static_cast(sender()); open_curve_panels_.remove(panel->GetInput()); } +void ParamPanel::ParamViewTimeChanged(const int64_t &time) +{ + // Ensure all CurvePanels are updated with this time too + QHash::const_iterator i; + + for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { + // If connected viewers are the same, set the timestamp + if (i.value()->GetConnectedViewer() == GetConnectedViewer()) { + i.value()->SetTimestamp(time); + } + } +} + +void ParamPanel::CurvePanelTimeChanged(const int64_t &time) +{ + GetTimeBasedWidget()->SetTimestamp(time); + emit GetTimeBasedWidget()->TimeChanged(time); + + CurvePanel* src = static_cast(sender()); + + // Ensure all CurvePanels are updated with this time too + QHash::const_iterator i; + + for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { + // If connected viewers are the same and the panel isn't the source, set the timestamp + if (i.value() != src && i.value()->GetConnectedViewer() == src->GetConnectedViewer()) { + i.value()->SetTimestamp(time); + } + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/panel/param/param.h b/app/panel/param/param.h index e75350d6b..12bc1715e 100644 --- a/app/panel/param/param.h +++ b/app/panel/param/param.h @@ -51,15 +51,16 @@ protected: private slots: void CreateCurvePanel(NodeInput* input); - void OpeningNode(Node* n); - - void ClosingNode(Node* n); - void ClosingCurvePanel(); private: QHash open_curve_panels_; +private slots: + void ParamViewTimeChanged(const int64_t& time); + + void CurvePanelTimeChanged(const int64_t& time); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index 2453618f9..aa294deb2 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -218,8 +218,6 @@ void CurveWidget::ScaleChangedEvent(const double &scale) void CurveWidget::TimeTargetChangedEvent(Node *target) { - ConnectViewerNode(nullptr); - key_control_->SetTimeTarget(target); view_->SetTimeTarget(target); @@ -227,12 +225,11 @@ void CurveWidget::TimeTargetChangedEvent(Node *target) if (bridge_) { bridge_->SetTimeTarget(target); } +} - // FIXME: If a non-viewer node is ever set here, it will fail to update the length - ViewerOutput* viewer = dynamic_cast(target); - if (viewer) { - ConnectViewerNode(viewer); - } +void CurveWidget::ConnectedNodeChanged(ViewerOutput *n) +{ + SetTimeTarget(n); } void CurveWidget::UpdateInputLabel() diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index a1c2035c0..7c33b49c7 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -59,6 +59,8 @@ protected: virtual void TimeTargetChangedEvent(Node* target) override; + virtual void ConnectedNodeChanged(ViewerOutput* n) override; + private: void UpdateInputLabel(); diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index eab4bd815..03ce22f18 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -208,18 +208,6 @@ void NodeParamView::ConnectedNodeChanged(ViewerOutput *n) } } -void NodeParamView::ConnectNodeInternal(ViewerOutput *n) -{ - SetTimebase(n->video_params().time_base()); -} - -void NodeParamView::DisconnectNodeInternal(ViewerOutput *n) -{ - Q_UNUSED(n) - - SetTimebase(rational()); -} - const QList &NodeParamView::nodes() { return nodes_; diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index 71d4edcfd..9fe697d97 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -64,9 +64,6 @@ protected: virtual void ConnectedNodeChanged(ViewerOutput* n) override; - virtual void ConnectNodeInternal(ViewerOutput* n) override; - virtual void DisconnectNodeInternal(ViewerOutput* n) override; - private: void UpdateItemTime(const int64_t ×tamp); diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index 6c83257d2..b2e15b03b 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -36,7 +36,8 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_statu viewer_node_(nullptr), auto_max_scrollbar_(false), points_(nullptr), - toggle_show_all_(false) + toggle_show_all_(false), + auto_set_timebase_(true) { ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this); connect(ruler_, &TimeRuler::TimeChanged, this, &TimeBasedWidget::SetTimeAndSignal); @@ -79,6 +80,11 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) DisconnectNodeInternal(viewer_node_); disconnect(viewer_node_, &ViewerOutput::LengthChanged, this, &TimeBasedWidget::UpdateMaximumScroll); + disconnect(viewer_node_, &ViewerOutput::TimebaseChanged, this, &TimeBasedWidget::SetTimebase); + + if (auto_set_timebase_) { + SetTimebase(rational()); + } points_ = nullptr; ruler()->ConnectTimelinePoints(nullptr); @@ -95,6 +101,18 @@ void TimeBasedWidget::ConnectViewerNode(ViewerOutput *node) ruler()->ConnectTimelinePoints(points_); } + if (auto_set_timebase_) { + if (!viewer_node_->video_params().time_base().isNull()) { + SetTimebase(viewer_node_->video_params().time_base()); + } else if (viewer_node_->audio_params().sample_rate() > 0) { + SetTimebase(viewer_node_->audio_params().time_base()); + } else { + SetTimebase(rational()); + } + + connect(viewer_node_, &ViewerOutput::TimebaseChanged, this, &TimeBasedWidget::SetTimebase); + } + ConnectNodeInternal(viewer_node_); } @@ -332,6 +350,11 @@ void TimeBasedWidget::CenterScrollOnPlayhead() scrollbar_->setValue(qRound(TimeToScene(Timecode::timestamp_to_time(ruler_->GetTime(), timebase()))) - scrollbar_->width()/2); } +void TimeBasedWidget::SetAutoSetTimebase(bool e) +{ + auto_set_timebase_ = e; +} + void TimeBasedWidget::SetPoint(Timeline::MovementMode m, const rational& time) { if (!points_) { diff --git a/app/widget/timebased/timebased.h b/app/widget/timebased/timebased.h index bf5b8ac40..008514d26 100644 --- a/app/widget/timebased/timebased.h +++ b/app/widget/timebased/timebased.h @@ -127,6 +127,12 @@ protected slots: */ void CenterScrollOnPlayhead(); + /** + * @brief By default, TimeBasedWidget will set the timebase to the viewer node's video timebase. + * Set this to false if you want to set your own timebase. + */ + void SetAutoSetTimebase(bool e); + signals: void TimeChanged(const int64_t&); @@ -170,6 +176,8 @@ private: double toggle_show_all_old_scale_; int toggle_show_all_old_scroll_; + bool auto_set_timebase_; + private slots: void UpdateMaximumScroll(); diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index b89ffe00d..504293a75 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -147,8 +147,10 @@ TimelineWidget::TimelineWidget(QWidget *parent) : view_splitter->setSizes({INT_MAX, INT_MAX}); // FIXME: Magic number - SetMaximumScale(TimelineViewBase::kMaximumScale); SetScale(90.0); + + SetMaximumScale(TimelineViewBase::kMaximumScale); + SetAutoSetTimebase(false); } TimelineWidget::~TimelineWidget() diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 6280ad88f..77ffb8102 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -168,15 +168,6 @@ void ViewerWidget::TimeChangedEvent(const int64_t &i) void ViewerWidget::ConnectNodeInternal(ViewerOutput *n) { - if (!n->video_params().time_base().isNull()) { - SetTimebase(n->video_params().time_base()); - } else if (n->audio_params().sample_rate() > 0) { - SetTimebase(n->audio_params().time_base()); - } else { - SetTimebase(rational()); - } - - connect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase); connect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot); connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); connect(n, &ViewerOutput::ParamsChanged, this, &ViewerWidget::UpdateRendererParameters); @@ -231,9 +222,6 @@ void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n) } cache_wait_timer_.stop(); - SetTimebase(rational()); - - disconnect(n, &ViewerOutput::TimebaseChanged, this, &ViewerWidget::SetTimebase); disconnect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SizeChangedSlot); disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot); disconnect(n, &ViewerOutput::ParamsChanged, this, &ViewerWidget::UpdateRendererParameters); From 8bf653925f3de184bf4a25ba5a503a0bdfb2422c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 23 Jun 2020 05:25:34 +1000 Subject: [PATCH 034/138] curve/timeline: improved user interface behavior * Implements "auto-fit" setting for curve view and sets it on open. * Improves scroll zooming on all TimelineViewBase derivatives. * Improves code sharing for better maintenance. --- app/widget/curvewidget/curveview.cpp | 86 ++++++++++++------- app/widget/curvewidget/curveview.h | 6 ++ app/widget/curvewidget/curvewidget.cpp | 2 + app/widget/keyframeview/keyframeviewbase.cpp | 44 +++------- app/widget/keyframeview/keyframeviewbase.h | 16 +--- app/widget/nodeparamview/nodeparamview.cpp | 4 + app/widget/timebased/timebased.cpp | 4 +- .../timelinewidget/timelinescaledobject.cpp | 17 ++++ .../timelinewidget/timelinescaledobject.h | 6 ++ .../timelinewidget/view/timelineviewbase.cpp | 76 +++++++++++++++- .../timelinewidget/view/timelineviewbase.h | 19 ++++ 11 files changed, 198 insertions(+), 82 deletions(-) diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index 52a7e8eab..e5fc65cdd 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -20,6 +20,7 @@ #include "curveview.h" +#include #include #include @@ -251,42 +252,22 @@ void CurveView::VerticalScaleChangedEvent(double scale) void CurveView::wheelEvent(QWheelEvent *event) { - if (WheelEventIsAZoomEvent(event)) { - if (!event->angleDelta().isNull()) { - bool only_vertical = false; - bool only_horizontal = false; - - if (event->modifiers() & Qt::ShiftModifier) { - if (event->modifiers() & Qt::AltModifier) { - only_horizontal = true; - } else { - only_vertical = true; - } - } - - if (event->angleDelta().x() + event->angleDelta().y() > 0) { - if (!only_vertical) { - emit ScaleChanged(GetScale() * 1.1); - } - - if (!only_horizontal) { - SetYScale(GetYScale() * 1.1); - } - } else { - if (!only_vertical) { - emit ScaleChanged(GetScale() * 0.9); - } - - if (!only_horizontal) { - SetYScale(GetYScale() *0.9); - } - } - } - } else { + if (!HandleZoomFromScroll(event)) { KeyframeViewBase::wheelEvent(event); } } +void CurveView::ContextMenuEvent(Menu &m) +{ + m.addSeparator(); + + // View settings + QAction* zoom_fit_action = m.addAction(tr("Zoom to Fit")); + connect(zoom_fit_action, &QAction::triggered, this, &CurveView::ZoomToFit); + + //QAction* reset_zoom_action = m.addAction(tr("Reset Zoom")); +} + QList CurveView::GetKeyframesSortedByTime(int track) { QList sorted; @@ -320,7 +301,12 @@ QList CurveView::GetKeyframesSortedByTime(int track) qreal CurveView::GetItemYFromKeyframeValue(NodeKeyframe *key) { - return -key->value().toDouble() * GetYScale(); + return GetItemYFromKeyframeValue(key->value().toDouble()); +} + +qreal CurveView::GetItemYFromKeyframeValue(double value) +{ + return -value * GetYScale(); } void CurveView::SetItemYFromKeyframeValue(NodeKeyframe *key, KeyframeViewItem *item) @@ -402,6 +388,40 @@ void CurveView::BezierControlPointDestroyed() bezier_control_points_.removeOne(item); } +void CurveView::ZoomToFit() +{ + if (item_map().isEmpty()) { + // Prevent scaling to DBL_MIN/DBL_MAX + return; + } + + QMap::const_iterator i; + + rational min_time = RATIONAL_MAX; + rational max_time = RATIONAL_MIN; + + double min_val = DBL_MAX; + double max_val = DBL_MIN; + + for (i=item_map().constBegin(); i!=item_map().constEnd(); i++) { + min_time = qMin(i.key()->time(), min_time); + max_time = qMax(i.key()->time(), max_time); + + min_val = qMin(i.key()->value().toDouble(), min_val); + max_val = qMax(i.key()->value().toDouble(), max_val); + } + + double time_range = max_time.toDouble() - min_time.toDouble(); + double new_x_scale = CalculateScaleFromDimensions(this->width(), time_range); + double new_y_scale = CalculateScaleFromDimensions(this->height(), max_val - min_val); + + emit ScaleChanged(new_x_scale); + SetYScale(new_y_scale); + + horizontalScrollBar()->setValue(TimeToScene(min_time) - CalculatePaddingFromDimensionScale(this->width())); + verticalScrollBar()->setValue(GetItemYFromKeyframeValue(max_val) - CalculatePaddingFromDimensionScale(this->height())); +} + void CurveView::AddKeyframe(NodeKeyframePtr key) { KeyframeViewItem* item = AddKeyframeInternal(key); diff --git a/app/widget/curvewidget/curveview.h b/app/widget/curvewidget/curveview.h index 67c2e25c3..e051f4f04 100644 --- a/app/widget/curvewidget/curveview.h +++ b/app/widget/curvewidget/curveview.h @@ -30,6 +30,7 @@ OLIVE_NAMESPACE_ENTER class CurveView : public KeyframeViewBase { + Q_OBJECT public: CurveView(QWidget* parent = nullptr); @@ -44,6 +45,8 @@ public: public slots: void AddKeyframe(NodeKeyframePtr key); + void ZoomToFit(); + protected: virtual void drawBackground(QPainter* painter, const QRectF& rect) override; @@ -55,10 +58,13 @@ protected: virtual void wheelEvent(QWheelEvent* event) override; + virtual void ContextMenuEvent(Menu &m) override; + private: QList GetKeyframesSortedByTime(int track); qreal GetItemYFromKeyframeValue(NodeKeyframe* key); + qreal GetItemYFromKeyframeValue(double value); void SetItemYFromKeyframeValue(NodeKeyframe* key, KeyframeViewItem* item); diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index aa294deb2..da32088ec 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -169,6 +169,8 @@ void CurveWidget::SetInput(NodeInput *input) } UpdateInputLabel(); + + QMetaObject::invokeMethod(view_, "ZoomToFit", Qt::QueuedConnection); } const double &CurveWidget::GetVerticalScale() diff --git a/app/widget/keyframeview/keyframeviewbase.cpp b/app/widget/keyframeview/keyframeviewbase.cpp index e6c4ad480..c97a85372 100644 --- a/app/widget/keyframeview/keyframeviewbase.cpp +++ b/app/widget/keyframeview/keyframeviewbase.cpp @@ -35,8 +35,6 @@ OLIVE_NAMESPACE_ENTER KeyframeViewBase::KeyframeViewBase(QWidget *parent) : TimelineViewBase(parent), dragging_bezier_point_(nullptr), - y_axis_enabled_(false), - y_scale_(1.0), currently_autoselecting_(false) { SetDefaultDragMode(RubberBandDrag); @@ -57,22 +55,6 @@ void KeyframeViewBase::Clear() item_map_.clear(); } -const double &KeyframeViewBase::GetYScale() const -{ - return y_scale_; -} - -void KeyframeViewBase::SetYScale(const double &y_scale) -{ - y_scale_ = y_scale; - - if (y_axis_enabled_) { - VerticalScaleChangedEvent(y_scale_); - - viewport()->update(); - } -} - void KeyframeViewBase::DeleteSelected() { QUndoCommand* command = new QUndoCommand(); @@ -194,7 +176,7 @@ void KeyframeViewBase::mouseMoveEvent(QMouseEvent *event) keypair.key->key()->set_time(node_time); - if (y_axis_enabled_) { + if (IsYAxisEnabled()) { keypair.key->key()->set_value(keypair.value - mouse_diff_scaled.y()); } @@ -257,7 +239,7 @@ void KeyframeViewBase::mouseReleaseEvent(QMouseEvent *event) command); // Commit value if we're setting a value - if (y_axis_enabled_) { + if (IsYAxisEnabled()) { item->key()->set_value(keypair.value); new NodeParamSetKeyframeValueCommand(item->key(), keypair.value - mouse_diff_scaled.y(), @@ -288,10 +270,6 @@ void KeyframeViewBase::ScaleChangedEvent(const double &scale) } } -void KeyframeViewBase::VerticalScaleChangedEvent(double) -{ -} - const QMap &KeyframeViewBase::item_map() const { return item_map_; @@ -310,11 +288,6 @@ void KeyframeViewBase::TimeTargetChangedEvent(Node *target) } } -void KeyframeViewBase::SetYAxisEnabled(bool e) -{ - y_axis_enabled_ = e; -} - void KeyframeViewBase::SetKeyframeTrackVisible(int track, bool visible) { if (!visible == hidden_tracks_.contains(track)) { @@ -336,6 +309,11 @@ void KeyframeViewBase::SetKeyframeTrackVisible(int track, bool visible) } } +void KeyframeViewBase::ContextMenuEvent(Menu& m) +{ + Q_UNUSED(m) +} + rational KeyframeViewBase::CalculateNewTimeFromScreen(const rational &old_time, double cursor_diff) { return rational::fromDouble(old_time.toDouble() + cursor_diff); @@ -433,7 +411,7 @@ void KeyframeViewBase::ProcessBezierDrag(QPointF mouse_diff_scaled, bool include QPointF KeyframeViewBase::GetScaledCursorPos(const QPoint &cursor_pos) { return QPointF(static_cast(cursor_pos.x()) / GetScale(), - static_cast(cursor_pos.y()) / y_scale_); + static_cast(cursor_pos.y()) / GetYScale()); } void KeyframeViewBase::ShowContextMenu() @@ -480,7 +458,11 @@ void KeyframeViewBase::ShowContextMenu() break; } } + } + ContextMenuEvent(m); + + if (!items.isEmpty()) { m.addSeparator(); QAction* properties_action = m.addAction(tr("P&roperties")); @@ -532,7 +514,7 @@ void KeyframeViewBase::ShowKeyframePropertiesDialog() void KeyframeViewBase::AutoSelectKeyTimeNeighbors() { - if (currently_autoselecting_ || y_axis_enabled_) { + if (currently_autoselecting_ || IsYAxisEnabled()) { return; } diff --git a/app/widget/keyframeview/keyframeviewbase.h b/app/widget/keyframeview/keyframeviewbase.h index 705e845c6..743ef996f 100644 --- a/app/widget/keyframeview/keyframeviewbase.h +++ b/app/widget/keyframeview/keyframeviewbase.h @@ -23,9 +23,10 @@ #include "keyframeviewitem.h" #include "node/keyframe.h" -#include "widget/timetarget/timetarget.h" #include "widget/curvewidget/beziercontrolpointitem.h" +#include "widget/menu/menu.h" #include "widget/timelinewidget/view/timelineviewbase.h" +#include "widget/timetarget/timetarget.h" OLIVE_NAMESPACE_ENTER @@ -37,9 +38,6 @@ public: virtual void Clear(); - const double& GetYScale() const; - void SetYScale(const double& y_scale); - void DeleteSelected(); public slots: @@ -54,18 +52,16 @@ protected: virtual void ScaleChangedEvent(const double& scale) override; - virtual void VerticalScaleChangedEvent(double scale); - const QMap& item_map() const; virtual void KeyframeAboutToBeRemoved(NodeKeyframe* key); virtual void TimeTargetChangedEvent(Node*) override; - void SetYAxisEnabled(bool e); - void SetKeyframeTrackVisible(int track, bool visible); + virtual void ContextMenuEvent(Menu &m); + private: rational CalculateNewTimeFromScreen(const rational& old_time, double cursor_diff); @@ -96,10 +92,6 @@ private: QVector selected_keys_; - bool y_axis_enabled_; - - double y_scale_; - bool currently_autoselecting_; QList hidden_tracks_; diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 03ce22f18..24f32e7d6 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -55,6 +55,8 @@ NodeParamView::NodeParamView(QWidget *parent) : // Set up scroll area layout param_layout_ = new QVBoxLayout(param_widget_area_); param_layout_->setSpacing(0); + + // KeyframeView is offset by a ruler, so to stay synchronized with it, we should be too param_layout_->setContentsMargins(0, ruler()->height(), 0, 0); // Add a stretch to allow empty space at the bottom of the layout @@ -162,6 +164,8 @@ void NodeParamView::SetNodes(QList nodes) } } + UpdateItemTime(GetTimestamp()); + QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection); } } diff --git a/app/widget/timebased/timebased.cpp b/app/widget/timebased/timebased.cpp index b2e15b03b..5c83206c6 100644 --- a/app/widget/timebased/timebased.cpp +++ b/app/widget/timebased/timebased.cpp @@ -482,12 +482,12 @@ void TimeBasedWidget::ToggleShowAll() w = timeline_views_.first()->width(); } - w = w / 10 * 9; + toggle_show_all_old_scale_ = GetScale(); toggle_show_all_old_scroll_ = scrollbar_->value(); - SetScale(w / GetConnectedNode()->GetLength().toDouble()); + SetScaleFromDimensions(w, GetConnectedNode()->GetLength().toDouble()); scrollbar_->setValue(0); // Must explicitly do this because SetScale() will automatically set this to false diff --git a/app/widget/timelinewidget/timelinescaledobject.cpp b/app/widget/timelinewidget/timelinescaledobject.cpp index 979ecf048..d12a998f8 100644 --- a/app/widget/timelinewidget/timelinescaledobject.cpp +++ b/app/widget/timelinewidget/timelinescaledobject.cpp @@ -27,6 +27,8 @@ OLIVE_NAMESPACE_ENTER +const int TimelineScaledObject::kCalculateDimensionsPadding = 10; + TimelineScaledObject::TimelineScaledObject() : scale_(1.0), min_scale_(0), @@ -112,6 +114,21 @@ void TimelineScaledObject::SetScale(const double& scale) ScaleChangedEvent(scale_); } +void TimelineScaledObject::SetScaleFromDimensions(double viewport_width, double content_width) +{ + SetScale(CalculateScaleFromDimensions(viewport_width, content_width)); +} + +double TimelineScaledObject::CalculateScaleFromDimensions(double viewport_sz, double content_sz) +{ + return static_cast(viewport_sz / kCalculateDimensionsPadding * (kCalculateDimensionsPadding-1)) / static_cast(content_sz); +} + +double TimelineScaledObject::CalculatePaddingFromDimensionScale(double viewport_sz) +{ + return (viewport_sz / (kCalculateDimensionsPadding * 2)); +} + TimelineScaledWidget::TimelineScaledWidget(QWidget *parent) : QWidget(parent) { diff --git a/app/widget/timelinewidget/timelinescaledobject.h b/app/widget/timelinewidget/timelinescaledobject.h index c55cff296..c4aec301c 100644 --- a/app/widget/timelinewidget/timelinescaledobject.h +++ b/app/widget/timelinewidget/timelinescaledobject.h @@ -44,6 +44,10 @@ public: void SetScale(const double& scale); + void SetScaleFromDimensions(double viewport_width, double content_width); + static double CalculateScaleFromDimensions(double viewport_sz, double content_sz); + static double CalculatePaddingFromDimensionScale(double viewport_sz); + protected: double TimeToScene(const rational& time); rational SceneToTime(const double &x, bool round = false); @@ -67,6 +71,8 @@ private: double max_scale_; + static const int kCalculateDimensionsPadding; + }; class TimelineScaledWidget : public QWidget, public TimelineScaledObject diff --git a/app/widget/timelinewidget/view/timelineviewbase.cpp b/app/widget/timelinewidget/view/timelineviewbase.cpp index c044f425e..a5acb99e1 100644 --- a/app/widget/timelinewidget/view/timelineviewbase.cpp +++ b/app/widget/timelinewidget/view/timelineviewbase.cpp @@ -40,7 +40,9 @@ TimelineViewBase::TimelineViewBase(QWidget *parent) : playhead_scene_right_(-1), dragging_playhead_(false), snapped_(false), - snap_service_(nullptr) + snap_service_(nullptr), + y_axis_enabled_(false), + y_scale_(1.0) { setScene(&scene_); @@ -80,6 +82,26 @@ void TimelineViewBase::SetSnapService(SnapService *service) snap_service_ = service; } +const double &TimelineViewBase::GetYScale() const +{ + return y_scale_; +} + +void TimelineViewBase::VerticalScaleChangedEvent(double) +{ +} + +void TimelineViewBase::SetYScale(const double &y_scale) +{ + y_scale_ = y_scale; + + if (y_axis_enabled_) { + VerticalScaleChangedEvent(y_scale_); + + viewport()->update(); + } +} + void TimelineViewBase::SetTime(const int64_t time) { playhead_ = time; @@ -252,10 +274,56 @@ bool TimelineViewBase::HandleZoomFromScroll(QWheelEvent *event) if (WheelEventIsAZoomEvent(event)) { // If CTRL is held (or a preference is set to swap CTRL behavior), we zoom instead of scrolling if (!event->angleDelta().isNull()) { - if (event->angleDelta().x() + event->angleDelta().y() > 0) { - emit ScaleChanged(GetScale() * 1.1); + bool only_vertical = false; + bool only_horizontal = false; + + // Ctrl+Shift limits to only one axis + // Alt switches between horizontal only (alt held) or vertical only (alt not held) + if (y_axis_enabled_) { + if (event->modifiers() & Qt::ShiftModifier) { + if (event->modifiers() & Qt::AltModifier) { + only_horizontal = true; + } else { + only_vertical = true; + } + } } else { - emit ScaleChanged(GetScale() * 0.9); + only_horizontal = true; + } + + double scale_multiplier; + + if (event->angleDelta().x() + event->angleDelta().y() > 0) { + scale_multiplier = 1.1; + } else { + scale_multiplier = 0.9; + } + + QPointF cursor_pos; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + cursor_pos = event->position(); +#else + cursor_pos = event->posF(); +#endif + + if (!only_vertical) { + double new_x_scale = GetScale() * scale_multiplier; + + int new_x_scroll = qRound(horizontalScrollBar()->value() / GetScale() * new_x_scale + (cursor_pos.x() - cursor_pos.x() / new_x_scale * GetScale())); + + emit ScaleChanged(new_x_scale); + + horizontalScrollBar()->setValue(new_x_scroll); + } + + if (!only_horizontal) { + double new_y_scale = GetYScale() * scale_multiplier; + + int new_y_scroll = qRound(verticalScrollBar()->value() / GetYScale() * new_y_scale + (cursor_pos.y() - cursor_pos.y() / new_y_scale * GetYScale())); + + SetYScale(new_y_scale); + + verticalScrollBar()->setValue(new_y_scroll); } } diff --git a/app/widget/timelinewidget/view/timelineviewbase.h b/app/widget/timelinewidget/view/timelineviewbase.h index cb667dd41..d24af659e 100644 --- a/app/widget/timelinewidget/view/timelineviewbase.h +++ b/app/widget/timelinewidget/view/timelineviewbase.h @@ -48,6 +48,9 @@ public: void SetSnapService(SnapService* service); + const double& GetYScale() const; + void SetYScale(const double& y_scale); + public slots: void SetTime(const int64_t time); @@ -69,6 +72,8 @@ protected: virtual void SceneRectUpdateEvent(QRectF&){} + virtual void VerticalScaleChangedEvent(double scale); + bool HandleZoomFromScroll(QWheelEvent* event); bool WheelEventIsAZoomEvent(QWheelEvent* event); @@ -81,6 +86,16 @@ protected: virtual void TimebaseChangedEvent(const rational &) override; + bool IsYAxisEnabled() const + { + return y_axis_enabled_; + } + + void SetYAxisEnabled(bool e) + { + y_axis_enabled_ = e; + } + private: qreal GetPlayheadX(); @@ -102,6 +117,10 @@ private: SnapService* snap_service_; + bool y_axis_enabled_; + + double y_scale_; + private slots: /** * @brief Slot called whenever the view resizes or the scene contents change to enforce minimum scene sizes From d31c373bd0e92e225e445c8059a823cb68bde846 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 24 Jun 2020 01:29:57 +1000 Subject: [PATCH 035/138] curveview: transform times to target for zoom fit --- app/widget/curvewidget/curveview.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index e5fc65cdd..c57072e88 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -404,8 +404,13 @@ void CurveView::ZoomToFit() double max_val = DBL_MIN; for (i=item_map().constBegin(); i!=item_map().constEnd(); i++) { - min_time = qMin(i.key()->time(), min_time); - max_time = qMax(i.key()->time(), max_time); + rational transformed_time = GetAdjustedTime(i.key()->parent()->parentNode(), + GetTimeTarget(), + i.key()->time(), + NodeParam::kOutput); + + min_time = qMin(transformed_time, min_time); + max_time = qMax(transformed_time, max_time); min_val = qMin(i.key()->value().toDouble(), min_val); max_val = qMax(i.key()->value().toDouble(), max_val); From b0a5d4f4c3fb8b891e0004aeb8770157e248cece Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 24 Jun 2020 01:36:04 +1000 Subject: [PATCH 036/138] parampanel: set input after timestamp Default TimeBasedWidget behavior is to jump to the playhead, but we want the CurveView to zoom fit on open instead. So we set the input after the timestamp to make this behavior possible. --- app/panel/param/param.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index 1bd3355d7..e6731e9b0 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -88,9 +88,9 @@ void ParamPanel::CreateCurvePanel(NodeInput *input) panel = Core::instance()->main_window()->AppendCurvePanel(); - panel->SetInput(input); panel->ConnectViewerNode(view->GetConnectedNode()); panel->SetTimestamp(view->GetTimestamp()); + panel->SetInput(input); connect(view, &NodeParamView::TimeChanged, this, &ParamPanel::ParamViewTimeChanged); connect(panel, &CurvePanel::TimeChanged, this, &ParamPanel::CurvePanelTimeChanged); From e064d3798848c89c641a11114ccc5856eecca5aa Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 24 Jun 2020 02:14:51 +1000 Subject: [PATCH 037/138] mainwindow: check for connected items before saving Fixes segfault when saving an empty project/a project with no sequence open. --- app/window/mainwindow/mainwindow.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 38f364cb2..1c5f2b111 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -129,11 +129,15 @@ MainWindowLayoutInfo MainWindow::SaveLayout() const MainWindowLayoutInfo info; foreach (ProjectPanel* panel, folder_panels_) { - info.add_folder(static_cast(panel->get_root_index().internalPointer())); + if (panel->project()) { + info.add_folder(static_cast(panel->get_root_index().internalPointer())); + } } foreach (TimelinePanel* panel, timeline_panels_) { - info.add_sequence(static_cast(panel->GetConnectedViewer()->parent())); + if (panel->GetConnectedViewer()) { + info.add_sequence(static_cast(panel->GetConnectedViewer()->parent())); + } } info.set_state(saveState()); From 7523db099b623d104b7ba8a6ab658028c1ff2a8e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 26 Jun 2020 00:21:12 +1000 Subject: [PATCH 038/138] cmake: use improved FindOpenEXR module --- app/CMakeLists.txt | 2 +- cmake/FindOpenEXR.cmake | 230 +++++++++++++++++++++++++--------------- 2 files changed, 144 insertions(+), 88 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 6050fabfe..03b8f6e23 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -125,7 +125,7 @@ target_include_directories( ${FFMPEG_INCLUDE_DIRS} ${OCIO_INCLUDE_DIRS} ${OIIO_INCLUDE_DIRS} - ${OPENEXR_INCLUDE_DIRS} + ${OPENEXR_INCLUDE_DIR} ) # Set link libraries diff --git a/cmake/FindOpenEXR.cmake b/cmake/FindOpenEXR.cmake index b7ea7cbb8..86f1cd472 100644 --- a/cmake/FindOpenEXR.cmake +++ b/cmake/FindOpenEXR.cmake @@ -1,99 +1,155 @@ +# Module to find OpenEXR. # -# Copyright 2016 Pixar +# This module will set +# OPENEXR_FOUND true, if found +# OPENEXR_INCLUDES directory where headers are found +# OPENEXR_LIBRARIES libraries for OpenEXR + IlmBase +# ILMBASE_LIBRARIES libraries just IlmBase +# OPENEXR_VERSION OpenEXR version (accurate for >= 2.0.0, +# otherwise will just guess 1.6.1) # -# Licensed under the Apache License, Version 2.0 (the "Apache License") -# with the following modification; you may not use this file except in -# compliance with the Apache License and the following modification to it: -# Section 6. Trademarks. is deleted and replaced with: -# -# 6. Trademarks. This License does not grant permission to use the trade -# names, trademarks, service marks, or product names of the Licensor -# and its affiliates, except as required to comply with Section 4(c) of -# the License and to reproduce the content of the NOTICE file. -# -# You may obtain a copy of the Apache License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the Apache License with the above modification is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the Apache License for the specific -# language governing permissions and limitations under the Apache License. # -find_path(OPENEXR_INCLUDE_DIR - OpenEXR/half.h -HINTS - "${OPENEXR_LOCATION}" - "$ENV{OPENEXR_LOCATION}" -PATH_SUFFIXES - include/ -DOC - "OpenEXR headers path" -) +# Other standard issue macros +include (FindPackageHandleStandardArgs) +include (SelectLibraryConfigurations) -if(OPENEXR_INCLUDE_DIR) - set(openexr_config_file "${OPENEXR_INCLUDE_DIR}/OpenEXR/OpenEXRConfig.h") - if(EXISTS ${openexr_config_file}) - file(STRINGS - ${openexr_config_file} - TMP - REGEX "#define OPENEXR_VERSION_STRING.*$") - string(REGEX MATCHALL "[0-9.]+" OPENEXR_VERSION "${TMP}") +find_package (ZLIB REQUIRED) - file(STRINGS - ${openexr_config_file} - TMP - REGEX "#define OPENEXR_VERSION_MAJOR.*$") - string(REGEX MATCHALL "[0-9]" OPENEXR_MAJOR_VERSION "${TMP}") +# Link with pthreads if required +find_package (Threads) +if (CMAKE_USE_PTHREADS_INIT) + set (ILMBASE_PTHREADS ${CMAKE_THREAD_LIBS_INIT}) +endif () - file(STRINGS - ${openexr_config_file} - TMP - REGEX "#define OPENEXR_VERSION_MINOR.*$") - string(REGEX MATCHALL "[0-9]" OPENEXR_MINOR_VERSION "${TMP}") - endif() -endif() +# Attempt to find OpenEXR with pkgconfig +find_package(PkgConfig) +if (PKG_CONFIG_FOUND) + if (NOT Ilmbase_ROOT AND NOT ILMBASE_ROOT + AND NOT DEFINED ENV{Ilmbase_ROOT} AND NOT DEFINED ENV{ILMBASE_ROOT}) + pkg_check_modules(_ILMBASE QUIET IlmBase>=2.0.0) + endif () + if (NOT OpenEXR_ROOT AND NOT OPENEXR_ROOT + AND NOT DEFINED ENV{OpenEXR_ROOT} AND NOT DEFINED ENV{OPENEXR_ROOT}) + pkg_check_modules(_OPENEXR QUIET OpenEXR>=2.0.0) + endif () +endif (PKG_CONFIG_FOUND) -foreach(OPENEXR_LIB - Half - Iex - Imath - IlmImf - IlmThread +# List of likely places to find the headers -- note priority override of +# ${OPENEXR_ROOT}/include. +# ILMBASE is needed in case ilmbase an openexr are installed in separate +# directories, like NixOS does +set (GENERIC_INCLUDE_PATHS + ${OPENEXR_ROOT}/include + $ENV{OPENEXR_ROOT}/include + ${ILMBASE_ROOT}/include + $ENV{ILMBASE_ROOT}/include + ${_ILMBASE_INCLUDEDIR} + ${_OPENEXR_INCLUDEDIR} + /usr/local/include + /usr/include + /usr/include/${CMAKE_LIBRARY_ARCHITECTURE} + /sw/include + /opt/local/include ) + +# Find the include file locations. +find_path (ILMBASE_INCLUDE_PATH OpenEXR/IlmBaseConfig.h + HINTS ${ILMBASE_INCLUDE_DIR} ${OPENEXR_INCLUDE_DIR} + ${GENERIC_INCLUDE_PATHS} ) +find_path (OPENEXR_INCLUDE_PATH OpenEXR/OpenEXRConfig.h + HINTS ${OPENEXR_INCLUDE_DIR} + ${GENERIC_INCLUDE_PATHS} ) + +# Try to figure out version number +if (DEFINED _OPENEXR_VERSION AND NOT "${_OPENEXR_VERSION}" STREQUAL "") + set (OPENEXR_VERSION "${_OPENEXR_VERSION}") + string (REGEX REPLACE "([0-9]+)\\.[0-9\\.]+" "\\1" OPENEXR_VERSION_MAJOR "${_OPENEXR_VERSION}") + string (REGEX REPLACE "[0-9]+\\.([0-9]+)(\\.[0-9]+)?" "\\1" OPENEXR_VERSION_MINOR "${_OPENEXR_VERSION}") +elseif (EXISTS "${OPENEXR_INCLUDE_PATH}/OpenEXR/ImfMultiPartInputFile.h") + # Must be at least 2.0 + file(STRINGS "${OPENEXR_INCLUDE_PATH}/OpenEXR/OpenEXRConfig.h" TMP REGEX "^#define OPENEXR_VERSION_STRING .*$") + string (REGEX MATCHALL "[0-9]+[.0-9]+" OPENEXR_VERSION ${TMP}) + file(STRINGS "${OPENEXR_INCLUDE_PATH}/OpenEXR/OpenEXRConfig.h" TMP REGEX "^#define OPENEXR_VERSION_MAJOR .*$") + string (REGEX MATCHALL "[0-9]+" OPENEXR_VERSION_MAJOR ${TMP}) + file(STRINGS "${OPENEXR_INCLUDE_PATH}/OpenEXR/OpenEXRConfig.h" TMP REGEX "^#define OPENEXR_VERSION_MINOR .*$") + string (REGEX MATCHALL "[0-9]+" OPENEXR_VERSION_MINOR ${TMP}) +else () + # Assume an old one, predates 2.x that had versions + set (OPENEXR_VERSION 1.6.1) + set (OPENEXR_MAJOR 1) + set (OPENEXR_MINOR 6) +endif () + + +# List of likely places to find the libraries -- note priority override of +# ${OPENEXR_ROOT}/lib. +set (GENERIC_LIBRARY_PATHS + ${OPENEXR_ROOT}/lib + ${ILMBASE_ROOT}/lib + ${OPENEXR_INCLUDE_PATH}/../lib + ${ILMBASE_INCLUDE_PATH}/../lib + ${_ILMBASE_LIBDIR} + ${_OPENEXR_LIBDIR} + /usr/local/lib + /usr/local/lib/${CMAKE_LIBRARY_ARCHITECTURE} + /usr/lib + /usr/lib/${CMAKE_LIBRARY_ARCHITECTURE} + /sw/lib + /opt/local/lib + $ENV{PROGRAM_FILES}/OpenEXR/lib/static ) + +# message (STATUS "Generic lib paths: ${GENERIC_LIBRARY_PATHS}") + +# Handle request for static libs by altering CMAKE_FIND_LIBRARY_SUFFIXES. +# We will restore it at the end of this file. +set (_openexr_orig_suffixes ${CMAKE_FIND_LIBRARY_SUFFIXES}) +if (OpenEXR_USE_STATIC_LIBS) + if (WIN32) + set (CMAKE_FIND_LIBRARY_SUFFIXES .lib .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) + else () + set (CMAKE_FIND_LIBRARY_SUFFIXES .a) + endif () +endif () + +# Look for the libraries themselves, for all the components. +# This is complicated because the OpenEXR libraries may or may not be +# built with version numbers embedded. +set (_openexr_components IlmThread IlmImf Imath Iex Half) +foreach (COMPONENT ${_openexr_components}) + string (TOUPPER ${COMPONENT} UPPERCOMPONENT) + # First try with the version embedded + find_library (OPENEXR_${UPPERCOMPONENT}_LIBRARY + NAMES ${COMPONENT}-${OPENEXR_VERSION_MAJOR}_${OPENEXR_VERSION_MINOR} + ${COMPONENT} + ${COMPONENT}-${OPENEXR_VERSION_MAJOR}_${OPENEXR_VERSION_MINOR}_d + ${COMPONENT}_d + HINTS ${OPENEXR_LIBRARY_DIR} $ENV{OPENEXR_LIBRARY_DIR} + ${GENERIC_LIBRARY_PATHS} ) +endforeach () + +find_package_handle_standard_args (OpenEXR + REQUIRED_VARS ILMBASE_INCLUDE_PATH OPENEXR_INCLUDE_PATH + OPENEXR_IMATH_LIBRARY OPENEXR_ILMIMF_LIBRARY + OPENEXR_IEX_LIBRARY OPENEXR_HALF_LIBRARY + VERSION_VAR OPENEXR_VERSION ) - # OpenEXR libraries may be suffixed with the version number, so we search - # using both versioned and unversioned names. - find_library(OPENEXR_${OPENEXR_LIB}_LIBRARY - NAMES - ${OPENEXR_LIB}-${OPENEXR_MAJOR_VERSION}_${OPENEXR_MINOR_VERSION} - ${OPENEXR_LIB} - HINTS - "${OPENEXR_LOCATION}" - "$ENV{OPENEXR_LOCATION}" - PATH_SUFFIXES - lib/ - DOC - "OPENEXR's ${OPENEXR_LIB} library path" - ) +if (OPENEXR_FOUND) + set (ILMBASE_FOUND TRUE) + set (ILMBASE_INCLUDES ${ILMBASE_INCLUDE_PATH}) + set (OPENEXR_INCLUDES ${OPENEXR_INCLUDE_PATH}) + set (ILMBASE_INCLUDE_DIR ${ILMBASE_INCLUDE_PATH}) + set (OPENEXR_INCLUDE_DIR ${OPENEXR_INCLUDE_PATH}) + set (ILMBASE_LIBRARIES ${OPENEXR_IMATH_LIBRARY} ${OPENEXR_IEX_LIBRARY} ${OPENEXR_HALF_LIBRARY} ${OPENEXR_ILMTHREAD_LIBRARY} ${ILMBASE_PTHREADS} CACHE STRING "The libraries needed to use IlmBase") + set (OPENEXR_LIBRARIES ${OPENEXR_ILMIMF_LIBRARY} ${ILMBASE_LIBRARIES} ${ZLIB_LIBRARIES} CACHE STRING "The libraries needed to use OpenEXR") +endif () - if(OPENEXR_${OPENEXR_LIB}_LIBRARY) - list(APPEND OPENEXR_LIBRARIES ${OPENEXR_${OPENEXR_LIB}_LIBRARY}) - endif() -endforeach(OPENEXR_LIB) - -# So #include works -list(APPEND OPENEXR_INCLUDE_DIRS ${OPENEXR_INCLUDE_DIR}) -list(APPEND OPENEXR_INCLUDE_DIRS ${OPENEXR_INCLUDE_DIR}/OpenEXR) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(OpenEXR - REQUIRED_VARS - OPENEXR_INCLUDE_DIRS - OPENEXR_LIBRARIES - VERSION_VAR - OPENEXR_VERSION -) +mark_as_advanced( + OPENEXR_ILMIMF_LIBRARY + OPENEXR_IMATH_LIBRARY + OPENEXR_IEX_LIBRARY + OPENEXR_HALF_LIBRARY + OPENEXR_VERSION) +# Restore the original CMAKE_FIND_LIBRARY_SUFFIXES +set (CMAKE_FIND_LIBRARY_SUFFIXES ${_openexr_orig_suffixes}) From c324de804c961986ce0f5d09070f5b483c1cbd5a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 26 Jun 2020 00:21:34 +1000 Subject: [PATCH 039/138] audiomanager: create and delete future watchers when necessary --- app/audio/audiomanager.cpp | 38 +++++++++++++++++++++++++++++--------- app/audio/audiomanager.h | 4 ++-- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 4a8ac8435..f5a1a866d 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -48,18 +48,31 @@ AudioManager *AudioManager::instance() void AudioManager::RefreshDevices() { - output_watcher_.setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioOutput)); - input_watcher_.setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioInput)); + if (!is_refreshing_outputs_) { + QFutureWatcher< QList >* output_watcher = new QFutureWatcher< QList >(); + connect(output_watcher, &QFutureWatcher< QList >::finished, this, &AudioManager::OutputDevicesRefreshed); + output_watcher->setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioOutput)); + + is_refreshing_outputs_ = true; + } + + if (!is_refreshing_inputs_) { + QFutureWatcher< QList >* input_watcher = new QFutureWatcher< QList >(); + connect(input_watcher, &QFutureWatcher< QList >::finished, this, &AudioManager::InputDevicesRefreshed); + input_watcher->setFuture(QtConcurrent::run(QAudioDeviceInfo::availableDevices, QAudio::AudioInput)); + + is_refreshing_inputs_ = true; + } } bool AudioManager::IsRefreshingOutputs() { - return output_watcher_.isRunning(); + return is_refreshing_outputs_; } bool AudioManager::IsRefreshingInputs() { - return input_watcher_.isRunning(); + return is_refreshing_inputs_; } void AudioManager::PushToOutput(const QByteArray &samples) @@ -200,6 +213,8 @@ void AudioManager::ReverseBuffer(char *buffer, int buffer_size, int sample_size) } AudioManager::AudioManager() : + is_refreshing_inputs_(false), + is_refreshing_outputs_(false), output_is_set_(false), input_(nullptr), input_file_(nullptr) @@ -210,9 +225,6 @@ AudioManager::AudioManager() : output_manager_.moveToThread(&output_thread_); connect(&output_manager_, &AudioOutputManager::OutputNotified, this, &AudioManager::OutputNotified); - - connect(&output_watcher_, &QFutureWatcher< QList >::finished, this, &AudioManager::OutputDevicesRefreshed); - connect(&input_watcher_, &QFutureWatcher< QList >::finished, this, &AudioManager::InputDevicesRefreshed); } AudioManager::~AudioManager() @@ -224,7 +236,11 @@ AudioManager::~AudioManager() void AudioManager::OutputDevicesRefreshed() { - output_devices_ = output_watcher_.result(); + QFutureWatcher< QList >* watcher = static_cast >*>(sender()); + + output_devices_ = watcher->result(); + watcher->deleteLater(); + is_refreshing_outputs_ = false; QString preferred_audio_output = Config::Current()["PreferredAudioOutput"].toString(); @@ -247,7 +263,11 @@ void AudioManager::OutputDevicesRefreshed() void AudioManager::InputDevicesRefreshed() { - input_devices_ = input_watcher_.result(); + QFutureWatcher< QList >* watcher = static_cast >*>(sender()); + + input_devices_ = watcher->result(); + watcher->deleteLater(); + is_refreshing_inputs_ = false; QString preferred_audio_input = Config::Current()["PreferredAudioInput"].toString(); diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 07a5fb9a1..f0c6c7db0 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -102,8 +102,8 @@ private: QList input_devices_; QList output_devices_; - QFutureWatcher< QList > input_watcher_; - QFutureWatcher< QList > output_watcher_; + bool is_refreshing_inputs_; + bool is_refreshing_outputs_; static AudioManager* instance_; From b04fa02ab4e3281548f092c67d32c9edd08618e6 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Wed, 1 Jul 2020 14:18:12 +0100 Subject: [PATCH 040/138] Explicitly include limits header to fix build issue on Linux. --- app/widget/curvewidget/curveview.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index c57072e88..9ab395c4c 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "common/qtutils.h" @@ -400,8 +401,8 @@ void CurveView::ZoomToFit() rational min_time = RATIONAL_MAX; rational max_time = RATIONAL_MIN; - double min_val = DBL_MAX; - double max_val = DBL_MIN; + double min_val = std::numeric_limits::max(); + double max_val = std::numeric_limits::min(); for (i=item_map().constBegin(); i!=item_map().constEnd(); i++) { rational transformed_time = GetAdjustedTime(i.key()->parent()->parentNode(), From 098b40c98ea4777b487faad9ef07b151b764d363 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Thu, 2 Jul 2020 15:36:13 +0100 Subject: [PATCH 041/138] Use cfloat instead of limits --- app/widget/curvewidget/curveview.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index 9ab395c4c..e8b6884ef 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include "common/qtutils.h" @@ -401,8 +401,8 @@ void CurveView::ZoomToFit() rational min_time = RATIONAL_MAX; rational max_time = RATIONAL_MIN; - double min_val = std::numeric_limits::max(); - double max_val = std::numeric_limits::min(); + double min_val = DBL_MAX; + double max_val = DBL_MIN; for (i=item_map().constBegin(); i!=item_map().constEnd(); i++) { rational transformed_time = GetAdjustedTime(i.key()->parent()->parentNode(), From 936d0770777bcdf30abd86f6e3cdf975aee40a5d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 6 Jul 2020 14:03:22 +1000 Subject: [PATCH 042/138] project: use relative filenames if absolute don't exist --- app/common/xmlutils.h | 3 +++ app/project/item/footage/footage.cpp | 22 ++++++++++++++++++++++ app/project/project.cpp | 11 +++++++++++ 3 files changed, 36 insertions(+) diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h index d8c012559..ec406dbca 100644 --- a/app/common/xmlutils.h +++ b/app/common/xmlutils.h @@ -64,6 +64,9 @@ struct XMLNodeData { QList block_links; QHash item_ptrs; + QString real_project_url; + QString saved_project_url; + }; void XMLConnectNodes(const XMLNodeData& xml_node_data, QUndoCommand* command = nullptr); diff --git a/app/project/item/footage/footage.cpp b/app/project/item/footage/footage.cpp index 4acb16569..d2e62f46e 100644 --- a/app/project/item/footage/footage.cpp +++ b/app/project/item/footage/footage.cpp @@ -21,6 +21,7 @@ #include "footage.h" #include +#include #include "codec/decoder.h" #include "common/xmlutils.h" @@ -52,6 +53,27 @@ void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const Q } } + // Validate filename + if (!QFileInfo::exists(filename_)) { + // Absolute filename does not exist, use some heuristics to try relocating the file + + if (xml_node_data.real_project_url != xml_node_data.saved_project_url) { + // Project path has changed, check if the file we're looking for is the same relative to the + // new project path + QDir saved_dir(QFileInfo(xml_node_data.saved_project_url).dir()); + QDir true_dir(QFileInfo(xml_node_data.real_project_url).dir()); + + QString relative_filename = saved_dir.relativeFilePath(filename_); + QString transformed_abs_filename = true_dir.filePath(relative_filename); + + if (QFileInfo::exists(transformed_abs_filename)) { + // Use this file instead + qInfo() << "Footage" << filename_ << "doesn't exist, using relative file" << transformed_abs_filename; + set_filename(transformed_abs_filename); + } + } + } + Decoder::ProbeMedia(this, cancelled); while (XMLReadNextStartElement(reader)) { diff --git a/app/project/project.cpp b/app/project/project.cpp index 653202c02..5b14774d9 100644 --- a/app/project/project.cpp +++ b/app/project/project.cpp @@ -41,6 +41,9 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const { XMLNodeData xml_node_data; + // Set project filename (hacky) + xml_node_data.real_project_url = static_cast(reader->device())->fileName(); + while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("folder")) { @@ -69,8 +72,16 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, const *layout = MainWindowLayoutInfo::fromXml(reader, xml_node_data); + } else if (reader->name() == QStringLiteral("url")) { + + // This should be read in before most other elements + xml_node_data.saved_project_url = reader->readElementText(); + } else { + + // Skip this reader->skipCurrentElement(); + } } From a45f514f683c5a911c87da53200fe128027f25e5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 6 Jul 2020 14:22:17 +1000 Subject: [PATCH 043/138] renderer: share render backend for auto-cache tasks Since auto-cache events start and stop fairly frequently (and are somewhat heavy to create/destroy), we can save a lot of cycles over time by sharing the same backend between them all. --- app/node/node.cpp | 19 +++++++++++ app/node/node.h | 5 +++ app/render/backend/renderbackend.cpp | 50 +++++++++++++++------------- app/render/backend/renderbackend.h | 15 +++++++++ app/task/cache/cache.cpp | 24 +++++++++---- app/task/cache/cache.h | 3 ++ app/task/render/render.cpp | 47 ++++++++++++++++---------- app/task/render/render.h | 23 ++++++------- 8 files changed, 128 insertions(+), 58 deletions(-) diff --git a/app/node/node.cpp b/app/node/node.cpp index 78613fa9b..840406ed8 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -537,6 +537,25 @@ bool Node::OutputsTo(const QString &id, bool recursively) const return false; } +bool Node::OutputsTo(NodeInput *input, bool recursively) const +{ + QList outputs = GetOutputs(); + + foreach (NodeOutput* output, outputs) { + foreach (NodeEdgePtr edge, output->edges()) { + NodeInput* connected = edge->input(); + + if (connected == input) { + return true; + } else if (recursively && connected->parentNode()->OutputsTo(input, recursively)) { + return true; + } + } + } + + return false; +} + bool Node::InputsFrom(Node *n, bool recursively) const { QList inputs = GetInputsIncludingArrays(); diff --git a/app/node/node.h b/app/node/node.h index 91209581a..11a69704f 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -220,6 +220,11 @@ public: */ bool OutputsTo(const QString& id, bool recursively) const; + /** + * @brief Same as OutputsTo(Node*), but for a specific node input rather than just a node. + */ + bool OutputsTo(NodeInput* input, bool recursively) const; + /** * @brief Returns whether this node ever receives an input from a particular node instance */ diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index b9f90e5e4..e62c94a57 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -195,27 +195,37 @@ std::list RenderBackend::SplitRangeIntoChunks(const TimeRange &r) void RenderBackend::NodeGraphChanged(NodeInput *source) { - if (!graph_update_queue_.isEmpty()) { - // First, check if anything in our queue is a dependency of this input. If so, we should remove - // it and just update this input. + // We need to determine: + // - If we don't have this input, assume that it's coming soon and ignore it + // - If we do, is this input a child of another input we're already copying? + // - Or are any of the queued inputs children of this one? - // First we need to find our copy of the input being queued - Node* our_copy_node = copy_map_.value(source->parentNode()); + // First we need to find our copy of the input being queued + Node* our_copy_node = copy_map_.value(source->parentNode()); - if (our_copy_node) { - NodeInput* our_copy = our_copy_node->GetInputWithID(source->id()); - QList our_copy_deps = our_copy->GetDependencies(our_copy); + // If we don't have this node yet, assume it's coming in a later copy in which case it'll be + // copied then + if (!our_copy_node) { + // Assert that there are updates coming + Q_ASSERT(!graph_update_queue_.isEmpty()); + return; + } - for (int i=0;iparentNode()); + // If we're here, we must have this node. Determine if we're already copying a "parent" of this + for (int i=0; iparentNode()->OutputsTo(queued_input, true)) { + // In which case, no further copy is necessary + return; + } + + // Check if this input supersedes an already queued input + if (queued_input->parentNode()->OutputsTo(source, true)) { + // In which case, we don't need to queue it and can queue our own + graph_update_queue_.removeAt(i); + i--; } } @@ -329,15 +339,9 @@ void RenderBackend::RunNextJob() void RenderBackend::ProcessUpdateQueue() { - /* while (!graph_update_queue_.isEmpty()) { CopyNodeInputValue(graph_update_queue_.takeFirst()); } - */ - - // FIXME: SLOW DEBUGGING CODE - CopyNodeInputValue(viewer_node_->texture_input()); - CopyNodeInputValue(viewer_node_->samples_input()); } QByteArray RenderBackend::HashNode(const Node *n, const VideoParams ¶ms, const rational &time) diff --git a/app/render/backend/renderbackend.h b/app/render/backend/renderbackend.h index 8f9339691..59efca316 100644 --- a/app/render/backend/renderbackend.h +++ b/app/render/backend/renderbackend.h @@ -43,6 +43,11 @@ public: void Close(); + ViewerOutput* GetViewerNode() const + { + return viewer_node_; + } + void SetViewerNode(ViewerOutput* viewer_node); void SetUpdateWithGraph(bool e) @@ -81,6 +86,16 @@ public: */ RenderTicketPtr RenderAudio(const TimeRange& r); + const VideoParams& GetVideoParams() const + { + return video_params_; + } + + const AudioParams& GetAudioParams() const + { + return audio_params_; + } + void SetVideoParams(const VideoParams& params); void SetAudioParams(const AudioParams& params); diff --git a/app/task/cache/cache.cpp b/app/task/cache/cache.cpp index 563dec1b3..411a6d96a 100644 --- a/app/task/cache/cache.cpp +++ b/app/task/cache/cache.cpp @@ -27,16 +27,18 @@ OLIVE_NAMESPACE_ENTER +CacheTask::CacheTask(RenderBackend *backend, bool in_out_only) : + RenderTask(backend), + in_out_only_(in_out_only) +{ + Init(); +} + CacheTask::CacheTask(ViewerOutput* viewer, const VideoParams& vparams, const AudioParams &aparams, bool in_out_only) : RenderTask(viewer, vparams, aparams), in_out_only_(in_out_only) { - SetTitle(tr("Caching \"%1\"").arg(viewer->media_name())); - - backend()->EnablePreviewGeneration(job_time()); - - // Render fastest quality - backend()->SetRenderMode(RenderMode::kOffline); + Init(); } bool CacheTask::Run() @@ -83,4 +85,14 @@ void CacheTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples) } } +void CacheTask::Init() +{ + SetTitle(tr("Caching \"%1\"").arg(viewer()->media_name())); + + backend()->EnablePreviewGeneration(job_time()); + + // Render fastest quality + backend()->SetRenderMode(RenderMode::kOffline); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/task/cache/cache.h b/app/task/cache/cache.h index bab3cffc8..c17344713 100644 --- a/app/task/cache/cache.h +++ b/app/task/cache/cache.h @@ -31,6 +31,7 @@ class CacheTask : public RenderTask { Q_OBJECT public: + CacheTask(RenderBackend* backend, bool in_out_only); CacheTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams, @@ -46,6 +47,8 @@ protected: virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples) override; private: + void Init(); + bool in_out_only_; QThreadPool download_threads_; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index f0da1ff54..76b54ea98 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -24,18 +24,31 @@ OLIVE_NAMESPACE_ENTER -RenderTask::RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams) : - viewer_(viewer), - video_params_(vparams), - audio_params_(aparams) +RenderTask::RenderTask(RenderBackend *backend) : + backend_(backend) { job_time_ = QDateTime::currentMSecsSinceEpoch(); - // FIXME: This makes a full copy of the node graph every time it starts, there must be a better - // way. - backend_.SetViewerNode(viewer_); - backend_.SetVideoParams(video_params_); - backend_.SetAudioParams(audio_params_); + backend_is_ours_ = false; +} + +RenderTask::RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams) +{ + job_time_ = QDateTime::currentMSecsSinceEpoch(); + + backend_ = new OpenGLBackend(); + backend_->SetViewerNode(viewer); + backend_->SetVideoParams(vparams); + backend_->SetAudioParams(aparams); + + backend_is_ours_ = true; +} + +RenderTask::~RenderTask() +{ + if (backend_is_ours_) { + backend_->deleteLater(); + } } struct TimeHashFuturePair { @@ -68,11 +81,11 @@ void RenderTask::Render(const TimeRangeList& video_range, const QMatrix4x4& mat, bool use_disk_cache) { - backend_.SetVideoDownloadMatrix(mat); + backend_->SetVideoDownloadMatrix(mat); double progress_counter = 0; double total_length = 0; - double video_frame_sz = video_params_.time_base().toDouble(); + double video_frame_sz = video_params().time_base().toDouble(); std::list audio_queue; std::list audio_lookup_table; @@ -93,11 +106,11 @@ void RenderTask::Render(const TimeRangeList& video_range, if (!video_range.isEmpty()) { QList existing_hashes; - times = viewer_->video_frame_cache()->GetFrameListFromTimeRange(video_range); + times = viewer()->video_frame_cache()->GetFrameListFromTimeRange(video_range); total_length += video_frame_sz * times.size(); - QFuture > hash_future = backend_.Hash(times); + QFuture > hash_future = backend_->Hash(times); hashes = hash_future.result(); for (int i=0;ivideo_frame_cache()->CachePathName(p.hash)); + hash_exists = QFileInfo::exists(viewer()->video_frame_cache()->CachePathName(p.hash)); // If so, add it to the list so we don't have to check the filesystem again later if (hash_exists) { @@ -159,7 +172,7 @@ void RenderTask::Render(const TimeRangeList& video_range, // If no existing disk cache was found, queue it now if (!hash_exists) { - render_lookup_table.push_back({p.hash, backend_.RenderFrame(p.time)}); + render_lookup_table.push_back({p.hash, backend_->RenderFrame(p.time)}); running_hashes.push_back(p.hash); } } @@ -169,7 +182,7 @@ void RenderTask::Render(const TimeRangeList& video_range, } if (!IsCancelled() && !audio_queue.empty()) { - audio_lookup_table.push_back({audio_queue.front(), backend_.RenderAudio(audio_queue.front())}); + audio_lookup_table.push_back({audio_queue.front(), backend_->RenderAudio(audio_queue.front())}); audio_queue.pop_front(); } @@ -233,7 +246,7 @@ void RenderTask::Render(const TimeRangeList& video_range, } // `Close` will block until all jobs are done making a safe deletion - backend_.Close(); + backend_->Close(); } void RenderTask::SetAnchorPoint(const rational &r) diff --git a/app/task/render/render.h b/app/task/render/render.h index 9f9dd49e2..978a45982 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -32,8 +32,11 @@ OLIVE_NAMESPACE_ENTER class RenderTask : public Task { public: + RenderTask(RenderBackend* backend); RenderTask(ViewerOutput* viewer, const VideoParams &vparams, const AudioParams &aparams); + virtual ~RenderTask() override; + protected: void Render(const TimeRangeList &video_range, const TimeRangeList &audio_range, @@ -48,17 +51,17 @@ protected: ViewerOutput* viewer() const { - return viewer_; + return backend_->GetViewerNode(); } VideoParams video_params() const { - return video_params_; + return backend_->GetVideoParams(); } AudioParams audio_params() const { - return audio_params_; + return backend_->GetAudioParams(); } void SetAnchorPoint(const rational& r); @@ -68,21 +71,17 @@ protected: return job_time_; } - OpenGLBackend* backend() + RenderBackend* backend() { - return &backend_; + return backend_; } private: - ViewerOutput* viewer_; - - VideoParams video_params_; - - AudioParams audio_params_; - rational anchor_point_; - OpenGLBackend backend_; + RenderBackend* backend_; + + bool backend_is_ours_; qint64 job_time_; From 919314cce8058ffc52e0e027b9f4b779dd7b4136 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 7 Jul 2020 13:16:25 +1000 Subject: [PATCH 044/138] renderer: improve thread safety Since we share render backends between the viewer and cache tasks now, we should ensure the render backend is thread safe. --- app/render/backend/renderbackend.cpp | 4 ++-- app/render/backend/renderbackend.h | 4 ++-- app/task/cache/cache.cpp | 2 +- app/task/export/export.cpp | 16 ++++++++-------- app/task/render/render.cpp | 9 ++++----- app/task/render/render.h | 1 - 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index e62c94a57..87b68d78b 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -139,7 +139,7 @@ RenderTicketPtr RenderBackend::RenderFrame(const rational &time) render_queue_.push_back(ticket); - RunNextJob(); + QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); return ticket; } @@ -155,7 +155,7 @@ RenderTicketPtr RenderBackend::RenderAudio(const TimeRange &r) render_queue_.push_back(ticket); - RunNextJob(); + QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); return ticket; } diff --git a/app/render/backend/renderbackend.h b/app/render/backend/renderbackend.h index 59efca316..c31632aef 100644 --- a/app/render/backend/renderbackend.h +++ b/app/render/backend/renderbackend.h @@ -115,8 +115,6 @@ private: Node *CopyNodeConnections(Node *src_node); void CopyNodeMakeConnection(NodeInput *src_input, NodeInput *dst_input); - void RunNextJob(); - ViewerOutput* viewer_node_; // VIDEO MEMBERS @@ -150,6 +148,8 @@ private: private slots: void WorkerFinished(); + void RunNextJob(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/task/cache/cache.cpp b/app/task/cache/cache.cpp index 411a6d96a..bae2f6ab9 100644 --- a/app/task/cache/cache.cpp +++ b/app/task/cache/cache.cpp @@ -57,7 +57,7 @@ bool CacheTask::Run() } } - Render(video_range, audio_range, QMatrix4x4(), true); + Render(video_range, audio_range, true); download_threads_.waitForDone(); diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 5cc667a9e..fbd7f905e 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -64,17 +64,17 @@ bool ExportTask::Run() frame_time_ = Timecode::time_to_timestamp(range.in(), viewer()->video_params().time_base()); - QMatrix4x4 mat; - if (params_.video_enabled()) { // If a transformation matrix is applied to this video, create it here if (params_.video_scaling_method() != ExportParams::kStretch) { - mat = ExportParams::GenerateMatrix(params_.video_scaling_method(), - viewer()->video_params().width(), - viewer()->video_params().height(), - params_.video_params().width(), - params_.video_params().height()); + QMatrix4x4 mat = ExportParams::GenerateMatrix(params_.video_scaling_method(), + viewer()->video_params().width(), + viewer()->video_params().height(), + params_.video_params().width(), + params_.video_params().height()); + + backend()->SetVideoDownloadMatrix(mat); } // Create color processor @@ -99,7 +99,7 @@ bool ExportTask::Run() audio_data_.SetLength(range.length()); } - Render(video_range, audio_range, mat, false); + Render(video_range, audio_range, false); bool success = true; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 76b54ea98..31da11f17 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -78,11 +78,8 @@ struct HashDownloadFuturePair { void RenderTask::Render(const TimeRangeList& video_range, const TimeRangeList &audio_range, - const QMatrix4x4& mat, bool use_disk_cache) { - backend_->SetVideoDownloadMatrix(mat); - double progress_counter = 0; double total_length = 0; double video_frame_sz = video_params().time_base().toDouble(); @@ -245,8 +242,10 @@ void RenderTask::Render(const TimeRangeList& video_range, } } - // `Close` will block until all jobs are done making a safe deletion - backend_->Close(); + if (backend_is_ours_) { + // `Close` will block until all jobs are done making a safe deletion + backend_->Close(); + } } void RenderTask::SetAnchorPoint(const rational &r) diff --git a/app/task/render/render.h b/app/task/render/render.h index 978a45982..77b655c24 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -40,7 +40,6 @@ public: protected: void Render(const TimeRangeList &video_range, const TimeRangeList &audio_range, - const QMatrix4x4 &mat, bool use_disk_cache); virtual QFuture DownloadFrame(FramePtr frame, const QByteArray &hash) = 0; From cb93b6bfa14cb3bea045dd0d0e6a5652f0f76e0a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 7 Jul 2020 13:16:41 +1000 Subject: [PATCH 045/138] text: keep text confined within text-safe area --- app/node/generator/text/text.cpp | 33 +++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/app/node/generator/text/text.cpp b/app/node/generator/text/text.cpp index 656aaa8ca..993086bcb 100644 --- a/app/node/generator/text/text.cpp +++ b/app/node/generator/text/text.cpp @@ -110,23 +110,34 @@ void TextGenerator::GenerateFrame(FramePtr frame, const GenerateJob& job) const QTextDocument text_doc; text_doc.setHtml(job.GetValue(text_input_).data().toString()); - text_doc.setTextWidth(frame->video_params().width()); + + // Align to 80% width because that's considered the "title safe" area + int tenth_of_width = frame->video_params().width() / 10; + text_doc.setTextWidth(tenth_of_width * 8); // Draw rich text onto image QPainter p(&img); p.scale(1.0 / frame->video_params().divider(), 1.0 / frame->video_params().divider()); - TextVerticalAlign valign = static_cast(job.GetValue(valign_input_).data().toInt()); - if (valign != kVerticalAlignTop) { - int doc_height = text_doc.size().height(); + // Push 10% inwards to compensate for title safe area + p.translate(tenth_of_width, 0); - if (valign == kVerticalAlignCenter) { - // Center align - p.translate(0, frame->video_params().height() / 2 - doc_height / 2); - } else { - // Must be bottom align - p.translate(0, frame->video_params().height() - doc_height); - } + TextVerticalAlign valign = static_cast(job.GetValue(valign_input_).data().toInt()); + int doc_height = text_doc.size().height(); + + switch (valign) { + case kVerticalAlignTop: + // Push 10% inwards for title safe area + p.translate(0, frame->video_params().height() / 10); + break; + case kVerticalAlignCenter: + // Center align + p.translate(0, frame->video_params().height() / 2 - doc_height / 2); + break; + case kVerticalAlignBottom: + // Push 10% inwards for title safe area + p.translate(0, frame->video_params().height() - doc_height - frame->video_params().height() / 10); + break; } text_doc.drawContents(&p); From a239fa0e2f3e15b3bb67c062ffca70d5390d37b0 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 7 Jul 2020 18:06:49 +1000 Subject: [PATCH 046/138] timeline: fixed some ripple tool behavior bugs --- app/widget/timelinewidget/tool/pointer.cpp | 114 +++++++++++---------- app/widget/timelinewidget/tool/ripple.cpp | 17 +-- app/widget/timelinewidget/undo/undo.cpp | 10 +- 3 files changed, 77 insertions(+), 64 deletions(-) diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index e300daff8..76a2ae157 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -497,11 +497,6 @@ bool TimelineWidget::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, return true; } -rational GetEarliestPointForClip(Block* block) -{ - return qMax(rational(0), block->in() - block->media_in()); -} - rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, const QVector ghosts, bool prevent_overwriting) @@ -516,41 +511,43 @@ rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, rational earliest_in = RATIONAL_MIN; rational latest_in = ghost->Out(); - if (block->type() == Block::kTransition) { - // For transitions, validate with the attached block - TransitionBlock* transition = static_cast(block); - - if (transition->connected_in_block() && transition->connected_out_block()) { - // Here, we try to get the latest earliest point for both the in and out blocks, we do in here and out will - // be calculated later - earliest_in = GetEarliestPointForClip(transition->connected_in_block()); - - // We set the block to the out block since that will be before the in block and will be the one we use to - // prevent overwriting since we're trimming the in side of this transition - block = transition->connected_out_block(); - - latest_in = transition->in() + transition->out_offset(); - } else { - // Use whatever block is attached - block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); - } - } - - earliest_in = qMax(earliest_in, GetEarliestPointForClip(block)); - if (!ghost->CanHaveZeroLength()) { latest_in -= parent()->timebase(); } - if (prevent_overwriting) { - // Look for a Block in the way - Block* prev = block->previous(); - while (prev != nullptr) { - if (prev->type() == Block::kClip) { - earliest_in = qMax(earliest_in, prev->out()); - break; + if (block) { + /* FIXME: Rewrite transition logic + if (block->type() == Block::kTransition) { + // For transitions, validate with the attached block + TransitionBlock* transition = static_cast(block); + + if (transition->connected_in_block() && transition->connected_out_block()) { + // Here, we try to get the latest earliest point for both the in and out blocks, we do in here and out will + // be calculated later + earliest_in = GetEarliestPointForClip(transition->connected_in_block()); + + // We set the block to the out block since that will be before the in block and will be the one we use to + // prevent overwriting since we're trimming the in side of this transition + block = transition->connected_out_block(); + + latest_in = transition->in() + transition->out_offset(); + } else { + // Use whatever block is attached + block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); + } + } + */ + + if (prevent_overwriting) { + // Look for a Block in the way + Block* prev = block->previous(); + while (prev != nullptr) { + if (prev->type() == Block::kClip) { + earliest_in = qMax(earliest_in, prev->out()); + break; + } + prev = prev->previous(); } - prev = prev->previous(); } } @@ -586,33 +583,38 @@ rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement, rational latest_out = RATIONAL_MAX; - if (block->type() == Block::kTransition) { - // For transitions, validate with the attached block - TransitionBlock* transition = static_cast(block); + // Ripple tool creates block-less ghosts and creates gaps with them later + if (block) { + /* FIXME: Rewrite transition logic + if (block->type() == Block::kTransition) { + // For transitions, validate with the attached block + TransitionBlock* transition = static_cast(block); - if (transition->connected_in_block() && transition->connected_out_block()) { - // We set the block to the out block since that will be before the in block and will be the one we use to - // prevent overwriting since we're trimming the in side of this transition + if (transition->connected_in_block() && transition->connected_out_block()) { + // We set the block to the out block since that will be before the in block and will be the one we use to + // prevent overwriting since we're trimming the in side of this transition - // FIXME: At some point we may add some better logic to `latest_out` akin to the logic in ValidateInTrimming - // which is why this hasn't yet been collapsed into the ternary below. - block = transition->connected_in_block(); + // FIXME: At some point we may add some better logic to `latest_out` akin to the logic in ValidateInTrimming + // which is why this hasn't yet been collapsed into the ternary below. + block = transition->connected_in_block(); - earliest_out = transition->out() - transition->in_offset(); - } else { - block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); + earliest_out = transition->out() - transition->in_offset(); + } else { + block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); + } } - } + */ - if (prevent_overwriting) { - // Determine if there's a block in the way - Block* next = block->next(); - while (next != nullptr) { - if (next->type() == Block::kClip) { - latest_out = qMin(latest_out, next->in()); - break; + if (prevent_overwriting) { + // Determine if there's a block in the way + Block* next = block->next(); + while (next != nullptr) { + if (next->type() == Block::kClip) { + latest_out = qMin(latest_out, next->in()); + break; + } + next = next->next(); } - next = next->next(); } } diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index ed441b5de..9fb88ff40 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -87,13 +87,18 @@ void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_ite if (block_before_ripple->type() == Block::kGap) { // If this Block is already a Gap, ghost it now ghost = AddGhostFromBlock(block_before_ripple, track_ref, trim_mode); - } else { - // If there's no gap here, we'll need to create one - ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode); - ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); - } + } else if (block_before_ripple->next()) { + // Assuming this block is NOT at the end of the track (i.e. next != null) - ghost->SetInvisible(true); + // We're going to create a gap after it. If next is a gap, we can just use that + if (block_before_ripple->next()->type() == Block::kGap) { + ghost = AddGhostFromBlock(block_before_ripple->next(), track_ref, trim_mode); + } else { + // If next is NOT a gap, we'll need to create one, for which we'll use a null ghost + ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode); + ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); + } + } } } } diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 9c46b09ca..9b03b5bec 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -1398,8 +1398,12 @@ void TrackListRippleToolCommand::redo_internal() const RippleInfo& info = info_.at(i); if (info.block) { - new_latest_pt = qMax(new_latest_pt, info.block->out()); - } else { + if (info.new_length > 0) { + new_latest_pt = qMax(new_latest_pt, info.block->out()); + } else { + new_latest_pt = qMax(new_latest_pt, info.block->in()); + } + } else if (info.new_length > 0) { new_latest_pt = qMax(new_latest_pt, working_data_.at(i).created_gap->out()); } } @@ -1427,6 +1431,8 @@ void TrackListRippleToolCommand::redo_internal() void TrackListRippleToolCommand::undo_internal() { + // FIXME: Add cache shift optimization + // Clean created gaps for (int i=info_.size()-1; i>=0; i--) { const RippleInfo& info = info_.at(i); From af69f157a3d73afd5048d6205e6da62eef594386 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 7 Jul 2020 18:43:18 +1000 Subject: [PATCH 047/138] timeline: improved beam tool and its derivatives --- app/widget/timelinewidget/timelinewidget.h | 5 ++++- app/widget/timelinewidget/tool/add.cpp | 12 ++---------- app/widget/timelinewidget/tool/beam.cpp | 15 ++++++++++++++- app/widget/timelinewidget/tool/razor.cpp | 2 +- app/widget/timelinewidget/view/timelineview.cpp | 6 +++--- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index fad1da588..64bbbddb0 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -216,6 +216,9 @@ private: virtual void HoverMove(TimelineViewMouseEvent *event) override; + protected: + TimelineCoordinate ValidatedCoordinate(TimelineCoordinate coord); + }; class PointerTool : public Tool @@ -415,7 +418,7 @@ private: }; - class AddTool : public Tool + class AddTool : public BeamTool { public: AddTool(TimelineWidget* parent); diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index 8f1ae9c74..a7a34a1f7 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -27,7 +27,7 @@ OLIVE_NAMESPACE_ENTER TimelineWidget::AddTool::AddTool(TimelineWidget *parent) : - Tool(parent), + BeamTool(parent), ghost_(nullptr) { } @@ -63,15 +63,7 @@ void TimelineWidget::AddTool::MousePress(TimelineViewMouseEvent *event) if (add_type == Timeline::kTrackTypeNone || add_type == track.type()) { - drag_start_point_ = event->GetFrame(); - - if (Core::instance()->snapping()) { - rational movement; - parent()->SnapPoint({drag_start_point_}, &movement); - if (!movement.isNull()) { - drag_start_point_ += movement; - } - } + drag_start_point_ = ValidatedCoordinate(event->GetCoordinates(true)).GetFrame(); ghost_ = new TimelineViewGhostItem(); ghost_->SetIn(drag_start_point_); diff --git a/app/widget/timelinewidget/tool/beam.cpp b/app/widget/timelinewidget/tool/beam.cpp index a04d14b9e..e2fe1120e 100644 --- a/app/widget/timelinewidget/tool/beam.cpp +++ b/app/widget/timelinewidget/tool/beam.cpp @@ -29,7 +29,20 @@ TimelineWidget::BeamTool::BeamTool(TimelineWidget *parent) : void TimelineWidget::BeamTool::HoverMove(TimelineViewMouseEvent *event) { - parent()->SetViewBeamCursor(event->GetCoordinates(true)); + parent()->SetViewBeamCursor(ValidatedCoordinate(event->GetCoordinates(true))); +} + +TimelineCoordinate TimelineWidget::BeamTool::ValidatedCoordinate(TimelineCoordinate coord) +{ + if (Core::instance()->snapping()) { + rational movement; + parent()->SnapPoint({coord.GetFrame()}, &movement); + if (!movement.isNull()) { + coord.SetFrame(coord.GetFrame() + movement); + } + } + + return coord; } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/razor.cpp b/app/widget/timelinewidget/tool/razor.cpp index 298a36b3b..49177ce8a 100644 --- a/app/widget/timelinewidget/tool/razor.cpp +++ b/app/widget/timelinewidget/tool/razor.cpp @@ -37,7 +37,7 @@ void TimelineWidget::RazorTool::MousePress(TimelineViewMouseEvent *event) void TimelineWidget::RazorTool::MouseMove(TimelineViewMouseEvent *event) { if (!dragging_) { - drag_start_ = event->GetCoordinates(true); + drag_start_ = ValidatedCoordinate(event->GetCoordinates(true)); dragging_ = true; } diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index d3ae73c56..cf253104e 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -37,6 +37,7 @@ OLIVE_NAMESPACE_ENTER TimelineView::TimelineView(Qt::Alignment vertical_alignment, QWidget *parent) : TimelineViewBase(parent), + show_beam_cursor_(false), connected_track_list_(nullptr) { Q_ASSERT(vertical_alignment == Qt::AlignTop || vertical_alignment == Qt::AlignBottom); @@ -426,9 +427,8 @@ void TimelineView::ConnectTrackList(TrackList *list) void TimelineView::SetBeamCursor(const TimelineCoordinate &coord) { - bool update_required = true;/*(coord.GetTrack().type() == connected_track_list_->type() - || cursor_coord_.GetTrack().type() == connected_track_list_->type() - || !show_beam_cursor_);*/ + bool update_required = coord.GetTrack().type() == connected_track_list_->type() + || cursor_coord_.GetTrack().type() == connected_track_list_->type(); show_beam_cursor_ = true; cursor_coord_ = coord; From 8e7543230418d1a3797c5499ce1eb590753c74e2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 8 Jul 2020 19:19:32 +1000 Subject: [PATCH 048/138] project/timeline: save timeline tracklist state in project file --- app/panel/timeline/timeline.cpp | 10 +++++++ app/panel/timeline/timeline.h | 4 +++ app/widget/timelinewidget/timelinewidget.cpp | 20 ++++++++++---- app/widget/timelinewidget/timelinewidget.h | 6 +++++ app/window/mainwindow/mainwindow.cpp | 16 +++++++----- app/window/mainwindow/mainwindow.h | 2 +- .../mainwindow/mainwindowlayoutinfo.cpp | 26 ++++++++++++------- app/window/mainwindow/mainwindowlayoutinfo.h | 11 +++++--- 8 files changed, 71 insertions(+), 24 deletions(-) diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 4b42ea3f7..7d6cd4c8d 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -46,6 +46,16 @@ void TimelinePanel::SplitAtPlayhead() static_cast(GetTimeBasedWidget())->SplitAtPlayhead(); } +QByteArray TimelinePanel::SaveSplitterState() const +{ + return static_cast(GetTimeBasedWidget())->SaveSplitterState(); +} + +void TimelinePanel::RestoreSplitterState(const QByteArray &state) +{ + static_cast(GetTimeBasedWidget())->RestoreSplitterState(state); +} + void TimelinePanel::SelectAll() { static_cast(GetTimeBasedWidget())->SelectAll(); diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index bafc9b0d1..cf09322cb 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -39,6 +39,10 @@ public: void SplitAtPlayhead(); + QByteArray SaveSplitterState() const; + + void RestoreSplitterState(const QByteArray& state); + virtual void SelectAll() override; virtual void DeselectAll() override; diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 504293a75..e8ac028bd 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -63,9 +63,9 @@ TimelineWidget::TimelineWidget(QWidget *parent) : // Create list of TimelineViews - these MUST correspond to the ViewType enum - QSplitter* view_splitter = new QSplitter(Qt::Vertical); - view_splitter->setChildrenCollapsible(false); - vert_layout->addWidget(view_splitter); + view_splitter_ = new QSplitter(Qt::Vertical); + view_splitter_->setChildrenCollapsible(false); + vert_layout->addWidget(view_splitter_); // Video view views_.append(new TimelineAndTrackView(Qt::AlignBottom)); @@ -109,7 +109,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) : view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); view->SetSnapService(this); - view_splitter->addWidget(tview); + view_splitter_->addWidget(tview); ConnectTimelineView(view); @@ -144,7 +144,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) : } // Split viewer 50/50 - view_splitter->setSizes({INT_MAX, INT_MAX}); + view_splitter_->setSizes({INT_MAX, INT_MAX}); // FIXME: Magic number SetScale(90.0); @@ -1415,6 +1415,16 @@ void TimelineWidget::HideSnaps() } } +QByteArray TimelineWidget::SaveSplitterState() const +{ + return view_splitter_->saveState(); +} + +void TimelineWidget::RestoreSplitterState(const QByteArray &state) +{ + view_splitter_->restoreState(state); +} + void TimelineWidget::StartRubberBandSelect(bool enable_selecting, bool select_links) { drag_origin_ = QCursor::pos(); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 64bbbddb0..fbd76a47f 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -98,6 +98,10 @@ public: virtual void HideSnaps() override; + QByteArray SaveSplitterState() const; + + void RestoreSplitterState(const QByteArray& state); + signals: void SelectionChanged(const QList& selected_blocks); @@ -501,6 +505,8 @@ private: bool use_audio_time_units_; + QSplitter* view_splitter_; + int GetTrackY(const TrackReference& ref); int GetTrackHeight(const TrackReference& ref); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 1c5f2b111..c34ee5764 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -117,8 +117,9 @@ void MainWindow::LoadLayout(const MainWindowLayoutInfo &info) FolderOpen(folder->project(), folder, true); } - foreach (Sequence* sequence, info.open_sequences()) { - OpenSequence(sequence, info.open_sequences().size() == 1); + foreach (const MainWindowLayoutInfo::OpenSequence& sequence, info.open_sequences()) { + TimelinePanel* panel = OpenSequence(sequence.sequence, info.open_sequences().size() == 1); + panel->RestoreSplitterState(sequence.panel_state); } restoreState(info.state()); @@ -136,7 +137,8 @@ MainWindowLayoutInfo MainWindow::SaveLayout() const foreach (TimelinePanel* panel, timeline_panels_) { if (panel->GetConnectedViewer()) { - info.add_sequence(static_cast(panel->GetConnectedViewer()->parent())); + info.add_sequence({static_cast(panel->GetConnectedViewer()->parent()), + panel->SaveSplitterState()}); } } @@ -145,13 +147,13 @@ MainWindowLayoutInfo MainWindow::SaveLayout() const return info; } -void MainWindow::OpenSequence(Sequence *sequence, bool enable_focus) +TimelinePanel* MainWindow::OpenSequence(Sequence *sequence, bool enable_focus) { // See if this sequence is already open, and switch to it if so foreach (TimelinePanel* tl, timeline_panels_) { if (tl->GetConnectedViewer() == sequence->viewer_output()) { tl->raise(); - return; + return tl; } } @@ -170,6 +172,8 @@ void MainWindow::OpenSequence(Sequence *sequence, bool enable_focus) if (enable_focus) { TimelineFocused(sequence->viewer_output()); } + + return panel; } void MainWindow::CloseSequence(Sequence *sequence) @@ -354,7 +358,7 @@ void MainWindow::SetApplicationProgressStatus(ProgressStatus status) taskbar_interface_->SetProgressState(reinterpret_cast(this->winId()), TBPF_NORMAL); break; case kProgressNone: - taskbar_interface_->SetProgressState(reinterpret_cast(this->winId()), TBPF_NOPROGRESS); + taskbar_interface_->SetProgressState(reinterpret_ cast(this->winId()), TBPF_NOPROGRESS); break; case kProgressError: taskbar_interface_->SetProgressState(reinterpret_cast(this->winId()), TBPF_ERROR); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index f06b8c0fe..923a8f1b6 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -60,7 +60,7 @@ public: MainWindowLayoutInfo SaveLayout() const; - void OpenSequence(Sequence* sequence, bool enable_focus = true); + TimelinePanel *OpenSequence(Sequence* sequence, bool enable_focus = true); void CloseSequence(Sequence* sequence); diff --git a/app/window/mainwindow/mainwindowlayoutinfo.cpp b/app/window/mainwindow/mainwindowlayoutinfo.cpp index 168978f79..0b75a957f 100644 --- a/app/window/mainwindow/mainwindowlayoutinfo.cpp +++ b/app/window/mainwindow/mainwindowlayoutinfo.cpp @@ -17,9 +17,12 @@ void MainWindowLayoutInfo::toXml(QXmlStreamWriter *writer) const writer->writeStartElement(QStringLiteral("timeline")); - foreach (Sequence* sequence, open_sequences_) { + foreach (const OpenSequence& sequence, open_sequences_) { writer->writeTextElement(QStringLiteral("sequence"), - QString::number(reinterpret_cast(sequence))); + QString::number(reinterpret_cast(sequence.sequence))); + + writer->writeTextElement(QStringLiteral("state"), + QString(sequence.panel_state.toBase64())); } writer->writeEndElement(); // timeline @@ -52,20 +55,25 @@ MainWindowLayoutInfo MainWindowLayoutInfo::fromXml(QXmlStreamReader *reader, XML } else if (reader->name() == QStringLiteral("timeline")) { + Sequence* open_seq = nullptr; + QByteArray tl_state; + while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("sequence")) { quintptr item_id = reader->readElementText().toULongLong(); - Sequence* open_seq = dynamic_cast(xml_data.item_ptrs.value(item_id)); - - if (open_seq) { - info.open_sequences_.append(open_seq); - } + open_seq = dynamic_cast(xml_data.item_ptrs.value(item_id)); + } else if (reader->name() == QStringLiteral("state")) { + tl_state = QByteArray::fromBase64(reader->readElementText().toUtf8()); } else { reader->skipCurrentElement(); } } + if (open_seq) { + info.open_sequences_.append({open_seq, tl_state}); + } + } else if (reader->name() == QStringLiteral("state")) { info.state_ = QByteArray::fromBase64(reader->readElementText().toLatin1()); @@ -83,9 +91,9 @@ void MainWindowLayoutInfo::add_folder(olive::Folder *f) open_folders_.append(f); } -void MainWindowLayoutInfo::add_sequence(Sequence *s) +void MainWindowLayoutInfo::add_sequence(const OpenSequence &seq) { - open_sequences_.append(s); + open_sequences_.append(seq); } void MainWindowLayoutInfo::set_state(const QByteArray &layout) diff --git a/app/window/mainwindow/mainwindowlayoutinfo.h b/app/window/mainwindow/mainwindowlayoutinfo.h index 5f76cbc2b..59f357935 100644 --- a/app/window/mainwindow/mainwindowlayoutinfo.h +++ b/app/window/mainwindow/mainwindowlayoutinfo.h @@ -17,7 +17,12 @@ public: void add_folder(Folder* f); - void add_sequence(Sequence* s); + struct OpenSequence { + Sequence* sequence; + QByteArray panel_state; + }; + + void add_sequence(const OpenSequence& seq); void set_state(const QByteArray& layout); @@ -26,7 +31,7 @@ public: return open_folders_; } - const QList& open_sequences() const + const QList& open_sequences() const { return open_sequences_; } @@ -41,7 +46,7 @@ private: QList open_folders_; - QList open_sequences_; + QList open_sequences_; }; From a4acf5f99e897bdc40fc022b3815fccc40280c35 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 02:39:09 +1000 Subject: [PATCH 049/138] nodes: hash strings as their value rather than the struct Erroneously hashed strings incorrectly so the actual string contents weren't what was being hashed. --- app/node/param.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/node/param.cpp b/app/node/param.cpp index ec1eb8b65..b5604c413 100644 --- a/app/node/param.cpp +++ b/app/node/param.cpp @@ -243,10 +243,10 @@ QByteArray NodeParam::ValueToBytes(const NodeParam::DataType &type, const QVaria case kInt: return ValueToBytesInternal(value); case kFloat: return ValueToBytesInternal(value); case kColor: return ValueToBytesInternal(value); - case kText: return ValueToBytesInternal(value); + case kText: return value.toString().toUtf8(); case kBoolean: return ValueToBytesInternal(value); - case kFont: return ValueToBytesInternal(value); // FIXME: This should probably be a QFont? - case kFile: return ValueToBytesInternal(value); + case kFont: return value.toString().toUtf8(); + case kFile: return value.toString().toUtf8(); case kMatrix: return ValueToBytesInternal(value); case kRational: return ValueToBytesInternal(value); case kVec2: return ValueToBytesInternal(value); From 8e1be4a3ff7758c4c4c1d8068be11e55a6fb0500 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 02:40:09 +1000 Subject: [PATCH 050/138] nodeparamviewwidgetbridge: implemented editing font parameters --- app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 4e4fabba3..9950864d7 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -163,6 +163,7 @@ void NodeParamViewWidgetBridge::CreateWidgets() { QFontComboBox* font_combobox = new QFontComboBox(); widgets_.append(font_combobox); + connect(font_combobox, &QFontComboBox::currentFontChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; } case NodeParam::kFootage: @@ -350,7 +351,7 @@ void NodeParamViewWidgetBridge::WidgetCallback() case NodeParam::kFont: { // Widget is a QFontComboBox - SetInputValue(static_cast(sender())->currentFont(), 0); + SetInputValue(static_cast(sender())->currentFont().family(), 0); break; } case NodeParam::kFootage: @@ -459,7 +460,7 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() case NodeParam::kText: { NodeParamViewRichText* e = static_cast(widgets_.first()); - e->setText(input_->get_value_at_time(node_time).toString()); + e->setTextPreservingCursor(input_->get_value_at_time(node_time).toString()); break; } case NodeParam::kBoolean: @@ -467,7 +468,10 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() break; case NodeParam::kFont: { - // FIXME: Implement this + QFontComboBox* fc = static_cast(widgets_.first()); + fc->blockSignals(true); + fc->setCurrentFont(input_->get_value_at_time(node_time).toString()); + fc->blockSignals(false); break; } case NodeParam::kCombo: From 0b4a9f4650fe28e1a909bc0abe93ad48d913ce8d Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 02:40:23 +1000 Subject: [PATCH 051/138] timeline: fixed bug that caused I-beam to render incorrectly --- app/widget/timelinewidget/view/timelineview.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index cf253104e..b55ec6cba 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -250,17 +250,17 @@ void TimelineView::drawForeground(QPainter *painter, const QRectF &rect) if (show_beam_cursor_ && connected_track_list_ - && cursor_coord_.GetTrack().type() == connected_track_list_->type() - && cursor_coord_.GetTrack().index() < connected_track_list_->GetTrackCount()) { + && cursor_coord_.GetTrack().type() == connected_track_list_->type()) { painter->setPen(Qt::gray); double cursor_x = TimeToScene(cursor_coord_.GetFrame()); int track_index = cursor_coord_.GetTrack().index(); + int track_y = GetTrackY(track_index); painter->drawLine(cursor_x, - GetTrackY(track_index), + track_y, cursor_x, - GetTrackHeight(track_index)); + track_y + GetTrackHeight(track_index)); } } From 911b6c2887533265c376cd55b6f4b730a7f90723 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 02:40:52 +1000 Subject: [PATCH 052/138] textnode: added parameters for default font and font size Allows text node to be used without having to write HTML (but allows the option for HTML still). --- app/node/generator/text/text.cpp | 28 +++++++++++++++++++++++++--- app/node/generator/text/text.h | 4 ++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/node/generator/text/text.cpp b/app/node/generator/text/text.cpp index 993086bcb..de38f08bd 100644 --- a/app/node/generator/text/text.cpp +++ b/app/node/generator/text/text.cpp @@ -32,11 +32,9 @@ enum TextVerticalAlign { TextGenerator::TextGenerator() { - QString default_str = QStringLiteral("
%1
").arg(tr("Sample Text")); - text_input_ = new NodeInput(QStringLiteral("text_in"), NodeParam::kText, - default_str); + tr("Sample Text")); AddInput(text_input_); color_input_ = new NodeInput(QStringLiteral("color_in"), @@ -48,6 +46,15 @@ TextGenerator::TextGenerator() NodeParam::kCombo, 1); AddInput(valign_input_); + + font_input_ = new NodeInput(QStringLiteral("font_in"), + NodeParam::kFont); + AddInput(font_input_); + + font_size_input_ = new NodeInput(QStringLiteral("font_size_in"), + NodeParam::kFloat, + 72.0f); + AddInput(font_size_input_); } Node *TextGenerator::copy() const @@ -78,7 +85,10 @@ QString TextGenerator::Description() const void TextGenerator::Retranslate() { text_input_->set_name(tr("Text")); + font_input_->set_name(tr("Font")); + font_size_input_->set_name(tr("Font Size")); color_input_->set_name(tr("Color")); + valign_input_->set_name(tr("Vertical Align")); valign_input_->set_combobox_strings({tr("Top"), tr("Center"), tr("Bottom")}); } @@ -88,6 +98,8 @@ NodeValueTable TextGenerator::Value(NodeValueDatabase &value) const job.InsertValue(text_input_, value); job.InsertValue(color_input_, value); job.InsertValue(valign_input_, value); + job.InsertValue(font_input_, value); + job.InsertValue(font_size_input_, value); job.SetAlphaChannelRequired(true); NodeValueTable table = value.Merge(); @@ -109,6 +121,16 @@ void TextGenerator::GenerateFrame(FramePtr frame, const GenerateJob& job) const img.fill(0); QTextDocument text_doc; + + // Set default font + QFont default_font; + default_font.setFamily(job.GetValue(font_input_).data().toString()); + default_font.setPointSizeF(job.GetValue(font_size_input_).data().toFloat()); + text_doc.setDefaultFont(default_font); + + // Center by default + text_doc.setDefaultTextOption(QTextOption(Qt::AlignCenter)); + text_doc.setHtml(job.GetValue(text_input_).data().toString()); // Align to 80% width because that's considered the "title safe" area diff --git a/app/node/generator/text/text.h b/app/node/generator/text/text.h index b94399041..e4a14b868 100644 --- a/app/node/generator/text/text.h +++ b/app/node/generator/text/text.h @@ -50,6 +50,10 @@ private: NodeInput* valign_input_; + NodeInput* font_input_; + + NodeInput* font_size_input_; + }; OLIVE_NAMESPACE_EXIT From 343c39d66d57012a92a4cbdc6e405328edf5b7da Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 02:41:35 +1000 Subject: [PATCH 053/138] nodeparamview: preserve cursor position when text is edited --- app/widget/nodeparamview/nodeparamviewrichtext.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/widget/nodeparamview/nodeparamviewrichtext.h b/app/widget/nodeparamview/nodeparamviewrichtext.h index 88a2befef..974a742b4 100644 --- a/app/widget/nodeparamview/nodeparamviewrichtext.h +++ b/app/widget/nodeparamview/nodeparamviewrichtext.h @@ -45,6 +45,13 @@ public slots: line_edit_->setText(s); } + void setTextPreservingCursor(const QString &s) + { + int cursor_pos = line_edit_->cursorPosition(); + line_edit_->setText(s); + line_edit_->setCursorPosition(cursor_pos); + } + signals: void textEdited(const QString &); From 0f9d409fb08d10d45af3e20f0db7795c9db69ad1 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 03:37:29 +1000 Subject: [PATCH 054/138] code: removed unused function declaration --- app/render/backend/renderworker.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/render/backend/renderworker.h b/app/render/backend/renderworker.h index a8848c643..3f5af3f9d 100644 --- a/app/render/backend/renderworker.h +++ b/app/render/backend/renderworker.h @@ -84,18 +84,6 @@ public: preview_job_time_ = job_time; } - /** - * @brief Return a unique ID for the image generated at this time - * - * This hash should always be unique to this image and can therefore be used to match existing - * cached frames. - * - * @return - * - * SHA-1 hash or empty QByteArray if no viewer node is set. - */ - void Hash(RenderTicketPtr ticket, ViewerOutput *viewer, const QList& times); - /** * @brief Render the frame at this time * From 90bc678377412c3b3d39e44693a4cb206a6ac4cd Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 18:26:30 +1000 Subject: [PATCH 055/138] nodeinputarray: delete sub-params immediately instead of queuing Queuing the destructor caused a potential desync in the node graph that disconnected blocks after they were reconnected elsewhere. Destroying immediately keeps this logic synchronized. --- app/node/inputarray.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index 8ca54f435..b1ec0caa8 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -59,7 +59,7 @@ void NodeInputArray::SetSize(int size) if (size < old_size) { // If the new size is less, delete all extraneous parameters for (int i=size;ideleteLater(); + delete sub_params_.at(i); } } From df78e881500f121a3e13925709ee6366f15e400c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 18:30:03 +1000 Subject: [PATCH 056/138] cmake: disable optimizations for debug builds Makes debugging certain bugs easier. --- app/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 4e693987d..7ea42d7f5 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -100,7 +100,7 @@ else() target_compile_options( ${OLIVE_TARGET} PRIVATE - -O2 + "$<$:-O2>" -Werror -Wuninitialized -pedantic-errors From 36dd06d2dc07a139af9cf67590b2f4a9268c8632 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 18:30:39 +1000 Subject: [PATCH 057/138] viewer: set video and audio caches to children of the viewer Ensures their thread gets changed along with the viewer. The project gets loaded/created in a background thread so the GUI can remain responsive, and is then moved to the main thread for intended event handling. However if the caches aren't parented, the hierarchy breaks and the caches remain in a thread whose event loop is quickly destroyed. Now that they're parented, events can be properly queued on them once again. --- app/node/output/viewer/viewer.cpp | 4 +++- app/render/audioplaybackcache.cpp | 3 ++- app/render/audioplaybackcache.h | 2 +- app/render/framehashcache.h | 10 +++++++--- app/render/playbackcache.h | 5 ++++- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 45fd6b23d..b5b0f495b 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -24,7 +24,9 @@ OLIVE_NAMESPACE_ENTER -ViewerOutput::ViewerOutput() +ViewerOutput::ViewerOutput() : + video_frame_cache_(this), + audio_playback_cache_(this) { texture_input_ = new NodeInput("tex_in", NodeInput::kTexture); AddInput(texture_input_); diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 98503223e..73e76a067 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -28,7 +28,8 @@ OLIVE_NAMESPACE_ENTER -AudioPlaybackCache::AudioPlaybackCache() +AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : + PlaybackCache(parent) { quint32 r = std::rand(); UpdateFilename(QString::number(r)); diff --git a/app/render/audioplaybackcache.h b/app/render/audioplaybackcache.h index 9e1328fbc..5fdb224df 100644 --- a/app/render/audioplaybackcache.h +++ b/app/render/audioplaybackcache.h @@ -31,7 +31,7 @@ class AudioPlaybackCache : public PlaybackCache { Q_OBJECT public: - AudioPlaybackCache(); + AudioPlaybackCache(QObject* parent = nullptr); AudioParams GetParameters() { QMutexLocker locker(lock()); diff --git a/app/render/framehashcache.h b/app/render/framehashcache.h index a31c4335c..96878059e 100644 --- a/app/render/framehashcache.h +++ b/app/render/framehashcache.h @@ -35,12 +35,13 @@ class FrameHashCache : public PlaybackCache { Q_OBJECT public: - FrameHashCache() = default; + FrameHashCache(QObject* parent = nullptr) : + PlaybackCache(parent) + { + } QByteArray GetHash(const rational& time); - void SetHash(const rational& time, const QByteArray& hash, const qint64 &job_time); - void SetTimebase(const rational& tb); /** @@ -72,6 +73,9 @@ public: QVector GetFrameListFromTimeRange(const TimeRangeList &range); QVector GetInvalidatedFrames(); +public slots: + void SetHash(const OLIVE_NAMESPACE::rational& time, const QByteArray& hash, const qint64 &job_time); + protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; diff --git a/app/render/playbackcache.h b/app/render/playbackcache.h index f73cefb9f..ce54bdd7e 100644 --- a/app/render/playbackcache.h +++ b/app/render/playbackcache.h @@ -32,7 +32,10 @@ class PlaybackCache : public QObject { Q_OBJECT public: - PlaybackCache() = default; + PlaybackCache(QObject* parent = nullptr) : + QObject(parent) + { + } void Invalidate(const TimeRange& r); From efd90d5c8f154c8ce00a81fa4aac97f743ffc735 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 18:39:47 +1000 Subject: [PATCH 058/138] renderer: use shared backend when caching I think there was an earlier commit with a similar name but turns out I'd only done foundational work in that commit and never actually properly set it up. Of course once I did, there were several issues that needed fixing to make it work correctly, but now it works as expected. Heavily optimizes larger projects by allowing cache jobs to only copy what has changed. --- app/node/input.cpp | 8 +-- app/node/input.h | 2 +- app/render/backend/renderbackend.cpp | 75 ++++++++++++++++------------ app/render/backend/renderbackend.h | 4 +- app/render/backend/renderticket.cpp | 2 +- app/render/backend/renderticket.h | 7 +-- app/render/backend/renderworker.cpp | 35 ++++++++++++- app/render/backend/renderworker.h | 4 ++ app/task/render/render.cpp | 10 ++-- app/widget/viewer/viewer.cpp | 12 ++--- 10 files changed, 104 insertions(+), 55 deletions(-) diff --git a/app/node/input.cpp b/app/node/input.cpp index d17656194..4e75caa5b 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -1007,7 +1007,7 @@ void NodeInput::set_is_keyframable(bool k) keyframable_ = k; } -void NodeInput::CopyValues(NodeInput *source, NodeInput *dest, bool include_connections) +void NodeInput::CopyValues(NodeInput *source, NodeInput *dest, bool include_connections, bool traverse_arrays) { Q_ASSERT(source->id() == dest->id()); @@ -1037,8 +1037,10 @@ void NodeInput::CopyValues(NodeInput *source, NodeInput *dest, bool include_conn dst_array->SetSize(src_array->GetSize()); - for (int i=0;iGetSize();i++) { - CopyValues(src_array->At(i), dst_array->At(i), include_connections); + if (traverse_arrays) { + for (int i=0;iGetSize();i++) { + CopyValues(src_array->At(i), dst_array->At(i), include_connections); + } } } diff --git a/app/node/input.h b/app/node/input.h index 4a6da4822..cbf401250 100644 --- a/app/node/input.h +++ b/app/node/input.h @@ -235,7 +235,7 @@ public: /** * @brief Copy all values including keyframe information and connections from another NodeInput */ - static void CopyValues(NodeInput* source, NodeInput* dest, bool include_connections = true); + static void CopyValues(NodeInput* source, NodeInput* dest, bool include_connections = true, bool traverse_arrays = true); /** * @brief Set an arbitrary property on this input to influence a UI representation's behavior diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index 87b68d78b..2dddc700e 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -113,19 +113,20 @@ void RenderBackend::ClearVideoQueue() render_queue_.clear(); } -QFuture > RenderBackend::Hash(const QVector ×) +RenderTicketPtr RenderBackend::Hash(const QVector ×) { - return QtConcurrent::run(&pool_, [this](const QVector &t){ - QVector hashes(t.size()); + if (!viewer_node_) { + return nullptr; + } - for (int i=0;itexture_input()->get_connected_node(), - video_params_, - t.at(i)); - } + RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeHash, + QVariant::fromValue(times)); - return hashes; - }, times); + render_queue_.push_back(ticket); + + QMetaObject::invokeMethod(this, "RunNextJob", Qt::QueuedConnection); + + return ticket; } RenderTicketPtr RenderBackend::RenderFrame(const rational &time) @@ -135,7 +136,7 @@ RenderTicketPtr RenderBackend::RenderFrame(const rational &time) } RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeVideo, - TimeRange(time, time)); + QVariant::fromValue(time)); render_queue_.push_back(ticket); @@ -151,7 +152,7 @@ RenderTicketPtr RenderBackend::RenderAudio(const TimeRange &r) } RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeAudio, - r); + QVariant::fromValue(r)); render_queue_.push_back(ticket); @@ -215,6 +216,11 @@ void RenderBackend::NodeGraphChanged(NodeInput *source) for (int i=0; iparentNode()->OutputsTo(queued_input, true)) { // In which case, no further copy is necessary @@ -300,7 +306,7 @@ void RenderBackend::RunNextJob() worker->SetVideoParams(video_params_); worker->SetAudioParams(audio_params_); - worker->SetVideoDownloadMatrix(video_download_matrix_); + worker->SetVideoDownloadMatrix(video_dwnload_matrix_); worker->SetRenderMode(render_mode_); if (preview_job_time_) { worker->EnablePreviewGeneration(viewer_node_->audio_playback_cache(), preview_job_time_); @@ -311,13 +317,21 @@ void RenderBackend::RunNextJob() render_queue_.pop_front(); switch (ticket->GetType()) { + case RenderTicket::kTypeHash: + QtConcurrent::run(&pool_, + worker, + &RenderWorker::Hash, + ticket, + copied_viewer_node_, + ticket->GetTime().value >()); + break; case RenderTicket::kTypeVideo: QtConcurrent::run(&pool_, worker, &RenderWorker::RenderFrame, ticket, copied_viewer_node_, - ticket->GetTime().in()); + ticket->GetTime().value()); break; case RenderTicket::kTypeAudio: QtConcurrent::run(&pool_, @@ -325,7 +339,7 @@ void RenderBackend::RunNextJob() &RenderWorker::RenderAudio, ticket, copied_viewer_node_, - ticket->GetTime()); + ticket->GetTime().value()); break; } @@ -337,27 +351,25 @@ void RenderBackend::RunNextJob() } } +//#define PRINT_UPDATE_QUEUE_INFO void RenderBackend::ProcessUpdateQueue() { +#ifdef PRINT_UPDATE_QUEUE_INFO + qint64 t = QDateTime::currentMSecsSinceEpoch(); + qDebug() << "Processing update queue of" << graph_update_queue_.size() << "elements:"; +#endif + while (!graph_update_queue_.isEmpty()) { - CopyNodeInputValue(graph_update_queue_.takeFirst()); - } -} - -QByteArray RenderBackend::HashNode(const Node *n, const VideoParams ¶ms, const rational &time) -{ - QCryptographicHash hasher(QCryptographicHash::Sha1); - - // Embed video parameters into this hash - hasher.addData(reinterpret_cast(¶ms.effective_width()), sizeof(int)); - hasher.addData(reinterpret_cast(¶ms.effective_height()), sizeof(int)); - hasher.addData(reinterpret_cast(¶ms.format()), sizeof(PixelFormat::Format)); - - if (n) { - n->Hash(hasher, time); + NodeInput* i = graph_update_queue_.takeFirst(); +#ifdef PRINT_UPDATE_QUEUE_INFO + qDebug() << " " << i->parentNode()->id() << i->id(); +#endif + CopyNodeInputValue(i); } - return hasher.result(); +#ifdef PRINT_UPDATE_QUEUE_INFO + qDebug() << "Update queue took:" << (QDateTime::currentMSecsSinceEpoch() - t); +#endif } void RenderBackend::WorkerFinished() @@ -386,6 +398,7 @@ void RenderBackend::CopyNodeInputValue(NodeInput *input) // Copy the standard/keyframe values between these two inputs NodeInput::CopyValues(input, our_copy, + false, false); // Handle connections diff --git a/app/render/backend/renderbackend.h b/app/render/backend/renderbackend.h index c31632aef..19ab3948f 100644 --- a/app/render/backend/renderbackend.h +++ b/app/render/backend/renderbackend.h @@ -69,12 +69,10 @@ public: void ProcessUpdateQueue(); - static QByteArray HashNode(const Node* n, const VideoParams& params, const rational& time); - /** * @brief Asynchronously generate a hash at a given time */ - QFuture > Hash(const QVector ×); + RenderTicketPtr Hash(const QVector ×); /** * @brief Asynchronously generate a frame at a given time diff --git a/app/render/backend/renderticket.cpp b/app/render/backend/renderticket.cpp index 85d0d46d8..1e719a5f6 100644 --- a/app/render/backend/renderticket.cpp +++ b/app/render/backend/renderticket.cpp @@ -22,7 +22,7 @@ OLIVE_NAMESPACE_ENTER -RenderTicket::RenderTicket(Type type, const TimeRange &time) : +RenderTicket::RenderTicket(Type type, const QVariant &time) : finished_(false), cancelled_(false), time_(time), diff --git a/app/render/backend/renderticket.h b/app/render/backend/renderticket.h index 3432b0df5..22fe46c8d 100644 --- a/app/render/backend/renderticket.h +++ b/app/render/backend/renderticket.h @@ -35,13 +35,14 @@ class RenderTicket : public QObject Q_OBJECT public: enum Type { + kTypeHash, kTypeVideo, kTypeAudio }; - RenderTicket(Type type, const TimeRange& time); + RenderTicket(Type type, const QVariant& time); - const TimeRange& GetTime() const + const QVariant& GetTime() const { return time_; } @@ -82,7 +83,7 @@ private: QWaitCondition wait_; - TimeRange time_; + QVariant time_; Type type_; diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 1ff861658..37de178b4 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -21,13 +21,13 @@ #include "renderworker.h" #include +#include #include "audio/audiovisualwaveform.h" #include "common/functiontimer.h" #include "config/config.h" #include "node/block/clip/clip.h" #include "task/conform/conform.h" -#include "renderbackend.h" OLIVE_NAMESPACE_ENTER @@ -40,6 +40,37 @@ RenderWorker::RenderWorker(RenderBackend* parent) : { } +void RenderWorker::Hash(RenderTicketPtr ticket, ViewerOutput *viewer, const QVector ×) +{ + QVector hashes(times.size()); + + for (int i=0;itexture_input()->get_connected_node(), + video_params_, + times.at(i)); + } + + ticket->Finish(QVariant::fromValue(hashes)); + + emit FinishedJob(); +} + +QByteArray RenderWorker::HashNode(const Node *n, const VideoParams ¶ms, const rational &time) +{ + QCryptographicHash hasher(QCryptographicHash::Sha1); + + // Embed video parameters into this hash + hasher.addData(reinterpret_cast(¶ms.effective_width()), sizeof(int)); + hasher.addData(reinterpret_cast(¶ms.effective_height()), sizeof(int)); + hasher.addData(reinterpret_cast(¶ms.format()), sizeof(PixelFormat::Format)); + + if (n) { + n->Hash(hasher, time); + } + + return hasher.result(); +} + void RenderWorker::RenderFrame(RenderTicketPtr ticket, ViewerOutput* viewer, const rational &time) { NodeValueTable table = ProcessInput(viewer->texture_input(), @@ -237,7 +268,7 @@ QVariant RenderWorker::ProcessFrameGeneration(const Node* node, const GenerateJo QVariant RenderWorker::GetCachedFrame(const Node* node, const rational& time) { if (node->id() == QStringLiteral("org.olivevideoeditor.Olive.videoinput")) { - QByteArray hash = RenderBackend::HashNode(node, video_params(), time); + QByteArray hash = HashNode(node, video_params(), time); QString fn = FrameHashCache::CachePathName(hash); diff --git a/app/render/backend/renderworker.h b/app/render/backend/renderworker.h index 3f5af3f9d..65655a2b2 100644 --- a/app/render/backend/renderworker.h +++ b/app/render/backend/renderworker.h @@ -84,6 +84,8 @@ public: preview_job_time_ = job_time; } + void Hash(RenderTicketPtr ticket, ViewerOutput* viewer, const QVector& times); + /** * @brief Render the frame at this time * @@ -146,6 +148,8 @@ signals: private: DecoderPtr ResolveDecoderFromInput(StreamPtr stream); + static QByteArray HashNode(const Node* n, const VideoParams& params, const rational& time); + RenderBackend* parent_; VideoParams video_params_; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 31da11f17..1b93fe61e 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -107,11 +107,13 @@ void RenderTask::Render(const TimeRangeList& video_range, total_length += video_frame_sz * times.size(); - QFuture > hash_future = backend_->Hash(times); - hashes = hash_future.result(); + RenderTicketPtr hash_future = backend_->Hash(times); + hashes = hash_future->Get().value >(); - for (int i=0;iWasCancelled()) { + for (int i=0;ivideo_frame_cache()->CachePathName(cached_hash); - RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeVideo, TimeRange(t, t)); + RenderTicketPtr ticket = std::make_shared(RenderTicket::kTypeVideo, + QVariant::fromValue(t)); QtConcurrent::run(DecodeCachedImage, ticket, cache_fn, t); return ticket; @@ -875,10 +876,7 @@ void ViewerWidget::StartBackgroundCaching() cache_wait_timer_.start(); } else { - cache_background_task_ = new CacheTask(GetConnectedNode(), - GetConnectedNode()->video_params(), - GetConnectedNode()->audio_params(), - false); + cache_background_task_ = new CacheTask(renderer_, false); our_cache_background_task_ = cache_background_task_; @@ -1223,7 +1221,7 @@ void ViewerWidget::ViewerInvalidatedRange() StopAllBackgroundCacheTasks(false); if (!(qApp->mouseButtons() & Qt::LeftButton)) { - StartBackgroundCaching(); + cache_wait_timer_.start(); } } From c710fd162e332a7ad1e849db9a56545707981e1e Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 18:42:13 +1000 Subject: [PATCH 059/138] richtextdialog: show html tags rather than wysiwyg While good in theory, a WYSIWYG rich text editor for large video frames was unwieldy (and in many cases unhelpful). Instead the titler will show plain text/HTML tags so the user can still write rich text but without the unwieldy UI. For a true WYSIWYG experience, we would probably need to write a true graphical editor (a la Premiere's titler), but that's a later goal. This titler will be sufficient in a good majority of cases and there are plenty of dedicated graphics packages if more complex titling is required for the timebeing. --- app/dialog/richtext/richtext.cpp | 11 ++++++++--- app/dialog/richtext/richtext.h | 9 +++++++-- app/widget/nodeparamview/nodeparamviewrichtext.cpp | 6 ++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/dialog/richtext/richtext.cpp b/app/dialog/richtext/richtext.cpp index 8807156b9..1b49c649a 100644 --- a/app/dialog/richtext/richtext.cpp +++ b/app/dialog/richtext/richtext.cpp @@ -29,7 +29,7 @@ OLIVE_NAMESPACE_ENTER -RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : +RichTextDialog::RichTextDialog(QString start, QWidget* parent) : QDialog(parent) { QVBoxLayout* layout = new QVBoxLayout(this); @@ -70,8 +70,9 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : // Create text edit widget text_edit_ = new QTextEdit(); text_edit_->setWordWrapMode(QTextOption::NoWrap); - connect(text_edit_, &QTextEdit::cursorPositionChanged, this, &RichTextDialog::UpdateButtons); - text_edit_->document()->setHtml(start); + //connect(text_edit_, &QTextEdit::cursorPositionChanged, this, &RichTextDialog::UpdateButtons); + start.replace(QStringLiteral("
"), QStringLiteral("\n")); + text_edit_->document()->setPlainText(start); layout->addWidget(text_edit_); // Create buttons @@ -81,6 +82,7 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : connect(buttons, &QDialogButtonBox::rejected, this, &RichTextDialog::reject); // Connect font buttons + /* connect(bold_btn_, &QPushButton::clicked, this, [this](bool e){ text_edit_->setFontWeight(e ? QFont::Bold : QFont::Normal); }); @@ -113,6 +115,7 @@ RichTextDialog::RichTextDialog(const QString &start, QWidget* parent) : connect(font_combo_, &QFontComboBox::currentTextChanged, this, [this](const QString& s){ text_edit_->setFontFamily(s); }); + */ } QPushButton *RichTextDialog::CreateToolbarButton(const QString& label, const QString& tooltip) @@ -126,6 +129,7 @@ QPushButton *RichTextDialog::CreateToolbarButton(const QString& label, const QSt void RichTextDialog::UpdateButtons() { + /* bold_btn_->setChecked(text_edit_->fontWeight() > QFont::Normal); italic_btn_->setChecked(text_edit_->fontItalic()); underline_btn_->setChecked(text_edit_->fontUnderline()); @@ -142,6 +146,7 @@ void RichTextDialog::UpdateButtons() center_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignCenter); right_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignRight); justify_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignJustify); + */ } OLIVE_NAMESPACE_EXIT diff --git a/app/dialog/richtext/richtext.h b/app/dialog/richtext/richtext.h index 7e2bbc252..aface3d04 100644 --- a/app/dialog/richtext/richtext.h +++ b/app/dialog/richtext/richtext.h @@ -34,11 +34,16 @@ class RichTextDialog : public QDialog { Q_OBJECT public: - RichTextDialog(const QString& start, QWidget* parent = nullptr); + RichTextDialog(QString start, QWidget* parent = nullptr); QString text() const { - return text_edit_->document()->toHtml("utf-8"); + QString s = text_edit_->document()->toPlainText(); + + // Convert linebreaks + s.replace('\n', QStringLiteral("
")); + + return s; } private: diff --git a/app/widget/nodeparamview/nodeparamviewrichtext.cpp b/app/widget/nodeparamview/nodeparamviewrichtext.cpp index de382f0c9..476c7cc76 100644 --- a/app/widget/nodeparamview/nodeparamviewrichtext.cpp +++ b/app/widget/nodeparamview/nodeparamviewrichtext.cpp @@ -48,8 +48,10 @@ void NodeParamViewRichText::ShowRichTextDialog() { RichTextDialog d(line_edit_->text(), this); if (d.exec() == QDialog::Accepted) { - line_edit_->setText(d.text()); - emit textEdited(d.text()); + QString s = d.text(); + + line_edit_->setText(s); + emit textEdited(s); } } From cabf581a2036b86fcbc4dbb8f2d6d3a20e4c8c08 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 18:45:45 +1000 Subject: [PATCH 060/138] code: fixed typo Not sure how this got here but ok --- app/render/backend/renderbackend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index 2dddc700e..5e1a1f018 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -306,7 +306,7 @@ void RenderBackend::RunNextJob() worker->SetVideoParams(video_params_); worker->SetAudioParams(audio_params_); - worker->SetVideoDownloadMatrix(video_dwnload_matrix_); + worker->SetVideoDownloadMatrix(video_download_matrix_); worker->SetRenderMode(render_mode_); if (preview_job_time_) { worker->EnablePreviewGeneration(viewer_node_->audio_playback_cache(), preview_job_time_); From 39d71eb7cde1813492cbdb04b666011263a016ab Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 10 Jul 2020 19:28:42 +1000 Subject: [PATCH 061/138] nodeview: drop nodes on release rather than press Minor UI improvement that also addresses issue where the cache would ignore the invalidation. --- app/widget/nodeview/nodeview.cpp | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index e2a5ed929..ebd5d8c26 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -356,32 +356,6 @@ void NodeView::mousePressEvent(QMouseEvent *event) { if (HandPress(event)) return; - if (!attached_items_.isEmpty()) { - if (attached_items_.size() == 1) { - Node* dropping_node = attached_items_.first().item->GetNode(); - - if (drop_edge_) { - NodeEdgePtr old_edge = drop_edge_->edge(); - - // We have everything we need to place the node in between - QUndoCommand* command = new QUndoCommand(); - - // Remove old edge - new NodeEdgeRemoveCommand(old_edge, command); - - // Place new edges - new NodeEdgeAddCommand(old_edge->output(), drop_input_, command); - new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); - - Core::instance()->undo_stack()->push(command); - } - - drop_edge_ = nullptr; - } - - DetachItemsFromCursor(); - } - super::mousePressEvent(event); } @@ -456,6 +430,32 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) { if (HandRelease(event)) return; + if (!attached_items_.isEmpty()) { + if (attached_items_.size() == 1) { + Node* dropping_node = attached_items_.first().item->GetNode(); + + if (drop_edge_) { + NodeEdgePtr old_edge = drop_edge_->edge(); + + // We have everything we need to place the node in between + QUndoCommand* command = new QUndoCommand(); + + // Remove old edge + new NodeEdgeRemoveCommand(old_edge, command); + + // Place new edges + new NodeEdgeAddCommand(old_edge->output(), drop_input_, command); + new NodeEdgeAddCommand(dropping_node->output(), old_edge->input(), command); + + Core::instance()->undo_stack()->push(command); + } + + drop_edge_ = nullptr; + } + + DetachItemsFromCursor(); + } + super::mouseReleaseEvent(event); } From ceeb4912939c3640d2a476cf6455dc7d295a62d1 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sat, 11 Jul 2020 01:44:48 +1000 Subject: [PATCH 062/138] richtextdialog: implemented basic html tag editing Another step in moving away from the WYSIWYG rich text dialog. --- app/dialog/richtext/richtext.cpp | 235 +++++++++++++++++++++++++++---- app/dialog/richtext/richtext.h | 15 +- 2 files changed, 225 insertions(+), 25 deletions(-) diff --git a/app/dialog/richtext/richtext.cpp b/app/dialog/richtext/richtext.cpp index 1b49c649a..40f2baabd 100644 --- a/app/dialog/richtext/richtext.cpp +++ b/app/dialog/richtext/richtext.cpp @@ -20,6 +20,7 @@ #include "richtext.h" +#include #include #include #include @@ -37,13 +38,13 @@ RichTextDialog::RichTextDialog(QString start, QWidget* parent) : // Create toolbar QHBoxLayout* toolbar_layout = new QHBoxLayout(); - bold_btn_ = CreateToolbarButton(tr("B"), tr("Bold")); + bold_btn_ = CreateToolbarButton(tr("B"), tr("Bold"), {QStringLiteral("b"), QStringLiteral("strong")}); toolbar_layout->addWidget(bold_btn_); - italic_btn_ = CreateToolbarButton(tr("I"), tr("Italic")); + italic_btn_ = CreateToolbarButton(tr("I"), tr("Italic"), {QStringLiteral("i"), QStringLiteral("em")}); toolbar_layout->addWidget(italic_btn_); - underline_btn_ = CreateToolbarButton(tr("U"), tr("Underline")); + underline_btn_ = CreateToolbarButton(tr("U"), tr("Underline"), {QStringLiteral("u")}); toolbar_layout->addWidget(underline_btn_); - strikeout_btn_ = CreateToolbarButton(tr("S"), tr("Strikethrough")); + strikeout_btn_ = CreateToolbarButton(tr("S"), tr("Strikethrough"), {QStringLiteral("strike")}); toolbar_layout->addWidget(strikeout_btn_); font_combo_ = new QFontComboBox(); font_combo_->setToolTip(tr("Font Family")); @@ -56,13 +57,13 @@ RichTextDialog::RichTextDialog(QString start, QWidget* parent) : toolbar_layout->addStretch(); - left_align_btn_ = CreateToolbarButton(tr("L"), tr("Left Align")); + left_align_btn_ = CreateToolbarButton(tr("L"), tr("Left Align"), {}); toolbar_layout->addWidget(left_align_btn_); - center_align_btn_ = CreateToolbarButton(tr("C"), tr("Center Align")); + center_align_btn_ = CreateToolbarButton(tr("C"), tr("Center Align"), {}); toolbar_layout->addWidget(center_align_btn_); - right_align_btn_ = CreateToolbarButton(tr("R"), tr("Right Align")); + right_align_btn_ = CreateToolbarButton(tr("R"), tr("Right Align"), {}); toolbar_layout->addWidget(right_align_btn_); - justify_align_btn_ = CreateToolbarButton(tr("J"), tr("Justify Align")); + justify_align_btn_ = CreateToolbarButton(tr("J"), tr("Justify Align"), {}); toolbar_layout->addWidget(justify_align_btn_); layout->addLayout(toolbar_layout); @@ -70,7 +71,7 @@ RichTextDialog::RichTextDialog(QString start, QWidget* parent) : // Create text edit widget text_edit_ = new QTextEdit(); text_edit_->setWordWrapMode(QTextOption::NoWrap); - //connect(text_edit_, &QTextEdit::cursorPositionChanged, this, &RichTextDialog::UpdateButtons); + connect(text_edit_, &QTextEdit::cursorPositionChanged, this, &RichTextDialog::UpdateButtons); start.replace(QStringLiteral("
"), QStringLiteral("\n")); text_edit_->document()->setPlainText(start); layout->addWidget(text_edit_); @@ -83,16 +84,6 @@ RichTextDialog::RichTextDialog(QString start, QWidget* parent) : // Connect font buttons /* - connect(bold_btn_, &QPushButton::clicked, this, [this](bool e){ - text_edit_->setFontWeight(e ? QFont::Bold : QFont::Normal); - }); - connect(italic_btn_, &QPushButton::clicked, text_edit_, &QTextEdit::setFontItalic); - connect(underline_btn_, &QPushButton::clicked, text_edit_, &QTextEdit::setFontUnderline); - connect(strikeout_btn_, &QPushButton::clicked, this, [this](bool e){ - QFont current_font = text_edit_->currentFont(); - current_font.setStrikeOut(e); - text_edit_->setCurrentFont(current_font); - }); connect(size_slider_, &FloatSlider::ValueChanged, text_edit_, &QTextEdit::setFontPointSize); connect(left_align_btn_, &QPushButton::clicked, this, [this](){ text_edit_->setAlignment(Qt::AlignLeft); @@ -118,22 +109,218 @@ RichTextDialog::RichTextDialog(QString start, QWidget* parent) : */ } -QPushButton *RichTextDialog::CreateToolbarButton(const QString& label, const QString& tooltip) +QPushButton *RichTextDialog::CreateToolbarButton(const QString& label, const QString& tooltip, const QStringList &tags) { QPushButton* btn = new QPushButton(label); btn->setCheckable(true); btn->setToolTip(tooltip); btn->setFixedWidth(btn->sizeHint().height()); + + if (!tags.isEmpty()) { + btn->setProperty("tag", tags); + connect(btn, &QPushButton::clicked, this, &RichTextDialog::TagButtonToggled); + } + return btn; } +int SnapPositionOutsideTags(const QString& text, int pos) +{ + // Look for closest opening bracket before position + int opening_bracket_pos = text.lastIndexOf('<', pos - text.size() -1); + + // Look for closest closing bracket before position + int closing_bracket_pos = text.indexOf('>', opening_bracket_pos); + + if (opening_bracket_pos > -1 && closing_bracket_pos >= pos) { + // Must be inside an angle bracket, snap to closest position outside of bracket + closing_bracket_pos++; + + if (pos - opening_bracket_pos < closing_bracket_pos - pos) { + // Closer to opening bracket pos + return opening_bracket_pos; + } else { + return closing_bracket_pos; + } + } + + return pos; +} + +void RichTextDialog::SetTags(const QStringList &t, bool enabled) +{ + QString s = text_edit_->toPlainText(); + + int selection_start, selection_end; + + { + QTextCursor c = text_edit_->textCursor(); + + if (c.hasSelection()) { + selection_start = SnapPositionOutsideTags(s, c.selectionStart()); + selection_end = SnapPositionOutsideTags(s, c.selectionEnd()); + + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + } else { + selection_start = SnapPositionOutsideTags(s, c.position()); + selection_end = selection_start; + c.setPosition(selection_start, QTextCursor::MoveAnchor); + } + + text_edit_->setTextCursor(c); + } + + QString open_tag = CreateOpeningTag(t.first()); + QString close_tag = CreateClosingTag(t.first()); + + // Insert tags + QString new_text; + + if (!enabled) { + std::swap(open_tag, close_tag); + } + + bool open_tag_cancels_out = !QString::compare(s.mid(selection_start - close_tag.size(), close_tag.size()), close_tag, Qt::CaseInsensitive); + bool close_tag_cancels_out = !QString::compare(s.mid(selection_end, open_tag.size()), open_tag, Qt::CaseInsensitive); + + QString selected_text = text_edit_->textCursor().selectedText(); + + if (open_tag_cancels_out && close_tag_cancels_out) { + + // Both tags cancel each other out, simply remove + selection_start -= close_tag.size(); + + QTextCursor c = text_edit_->textCursor(); + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end + open_tag.size(), QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + + selection_end -= close_tag.size(); + + new_text = selected_text; + + } else if (open_tag_cancels_out) { + + // Open tag cancels out, shift close tag rather than inserting new tags + selection_start -= close_tag.size(); + + QTextCursor c = text_edit_->textCursor(); + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + + selection_end -= close_tag.size(); + + new_text = selected_text; + new_text.append(close_tag); + + } else if (close_tag_cancels_out) { + + // Close tag cancels out, shift open tag rather than inserting new tags + selection_end += open_tag.size(); + + QTextCursor c = text_edit_->textCursor(); + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + + selection_start += open_tag.size(); + + new_text = open_tag; + new_text.append(selected_text); + + } else { + // Nothing is cancelled out, simply insert tags + new_text = QStringLiteral("%1%2%3").arg(open_tag, + selected_text, + close_tag); + + selection_start += open_tag.size(); + selection_end += open_tag.size(); + } + + text_edit_->insertPlainText(new_text); + + text_edit_->setFocus(); + + { + // Re-select text + QTextCursor c = text_edit_->textCursor(); + + c.clearSelection(); + c.setPosition(selection_start, QTextCursor::MoveAnchor); + c.setPosition(selection_end, QTextCursor::KeepAnchor); + text_edit_->setTextCursor(c); + } +} + +QString RichTextDialog::CreateOpeningTag(const QString &s) +{ + return QStringLiteral("<%1>").arg(s); +} + +QString RichTextDialog::CreateClosingTag(const QString &s) +{ + return QStringLiteral("").arg(s); +} + +void RichTextDialog::UpdateTagButton(QPushButton *btn, + const QString &text, + int cursor_pos) +{ + QStringList tags = btn->property("tag").toStringList(); + foreach (const QString& t, tags) { + QString opening = CreateOpeningTag(t); + QString closing = CreateClosingTag(t); + + int opening_index = text.lastIndexOf(opening, + cursor_pos - text.size() - 1, + Qt::CaseInsensitive); + int closing_index = text.indexOf(closing, + opening_index, + Qt::CaseInsensitive); + + if (opening_index > -1 && closing_index + closing.size() > cursor_pos) { + btn->setChecked(true); + btn->setProperty("foundtag", t); + return; + } + } + + btn->setChecked(false); + btn->setProperty("foundtag", QVariant()); +} + +void RichTextDialog::TagButtonToggled(bool checked) +{ + QPushButton* src = static_cast(sender()); + QStringList tags; + + if (src->property("foundtag").isNull()) { + tags = src->property("tag").toStringList(); + } else { + tags = QStringList({src->property("foundtag").toString()}); + } + + SetTags(tags, checked); +} + void RichTextDialog::UpdateButtons() { + QString text = text_edit_->toPlainText(); + int cursor_pos = text_edit_->textCursor().position(); + + UpdateTagButton(bold_btn_, text, cursor_pos); + UpdateTagButton(italic_btn_, text, cursor_pos); + UpdateTagButton(underline_btn_, text, cursor_pos); + UpdateTagButton(strikeout_btn_, text, cursor_pos); + /* - bold_btn_->setChecked(text_edit_->fontWeight() > QFont::Normal); - italic_btn_->setChecked(text_edit_->fontItalic()); - underline_btn_->setChecked(text_edit_->fontUnderline()); - strikeout_btn_->setChecked(text_edit_->currentFont().strikeOut()); // Update font family font_combo_->blockSignals(true); diff --git a/app/dialog/richtext/richtext.h b/app/dialog/richtext/richtext.h index aface3d04..b3514df4b 100644 --- a/app/dialog/richtext/richtext.h +++ b/app/dialog/richtext/richtext.h @@ -47,7 +47,18 @@ public: } private: - QPushButton* CreateToolbarButton(const QString &label, const QString &tooltip); + QPushButton* CreateToolbarButton(const QString &label, + const QString &tooltip, + const QStringList& tags); + + void SetTags(const QStringList& t, bool enabled); + + static QString CreateOpeningTag(const QString& s); + static QString CreateClosingTag(const QString& s); + + static void UpdateTagButton(QPushButton* btn, + const QString &text, + int cursor_pos); QFontDatabase font_db_; @@ -65,6 +76,8 @@ private: QPushButton* justify_align_btn_; private slots: + void TagButtonToggled(bool checked); + void UpdateButtons(); }; From 4d6003bb55f457748a9f5304720a432775afbc7c Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 12 Jul 2020 13:54:31 +1000 Subject: [PATCH 063/138] slider: update label color Dark blue on dark gray was a little unreadable. --- app/ui/style/olive-dark/palette.ini | 2 +- app/widget/slider/sliderlabel.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/style/olive-dark/palette.ini b/app/ui/style/olive-dark/palette.ini index 7f93ffd25..778c3d8fc 100644 --- a/app/ui/style/olive-dark/palette.ini +++ b/app/ui/style/olive-dark/palette.ini @@ -6,7 +6,7 @@ Button=#353535 ButtonText=#FFFFFF Highlight=#2A82DA HighlightedText=#FFFFFF -Link=#2A82DA +Link=#E0B040 Text=#FFFFFF ToolTipBase=#191919 ToolTipText=#FFFFFF diff --git a/app/widget/slider/sliderlabel.cpp b/app/widget/slider/sliderlabel.cpp index 319398a91..d100379a5 100644 --- a/app/widget/slider/sliderlabel.cpp +++ b/app/widget/slider/sliderlabel.cpp @@ -38,7 +38,7 @@ SliderLabel::SliderLabel(QWidget *parent) : setPalette(p); // Use highlight color as font color - setForegroundRole(QPalette::Highlight); + setForegroundRole(QPalette::Link); // Set underlined QFont f = font(); From 11afdf32feb7ed0a16972831a06ef22f051a0454 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 12 Jul 2020 18:25:51 +1000 Subject: [PATCH 064/138] decoder/timeline: improve decoding of still images --- app/codec/ffmpeg/ffmpegdecoder.cpp | 424 ++++++++++-------- app/codec/ffmpeg/ffmpegdecoder.h | 2 + .../videostreamproperties.cpp | 23 +- .../streamproperties/videostreamproperties.h | 8 + app/project/item/footage/imagestream.cpp | 3 +- app/project/item/footage/imagestream.h | 20 + app/widget/timelinewidget/tool/import.cpp | 27 +- app/widget/timelinewidget/tool/pointer.cpp | 3 + 8 files changed, 313 insertions(+), 197 deletions(-) diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 3b54fcdb2..c3aee6965 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -86,12 +86,12 @@ bool FFmpegDecoder::Open() return false; } - if (stream()->type() == Stream::kVideo) { + if (stream()->type() == Stream::kImage || stream()->type() == Stream::kVideo) { // Get an Olive compatible AVPixelFormat src_pix_fmt_ = static_cast(our_instance->stream()->codecpar->format); ideal_pix_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(src_pix_fmt_); - { + if (stream()->type() == Stream::kVideo) { QMutexLocker map_locker(&instance_map_lock_); // FIXME: Test code, this should be changed later @@ -144,165 +144,177 @@ FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode, const int &divid return nullptr; } - if (stream()->type() != Stream::kVideo) { + if (stream()->type() != Stream::kImage && stream()->type() != Stream::kVideo) { return nullptr; } - int64_t target_ts = Timecode::time_to_timestamp(timecode, time_base_) + start_time_; + ImageStreamPtr is = std::static_pointer_cast(stream()); - VideoStreamPtr vs = std::static_pointer_cast(stream()); + if (stream()->type() == Stream::kImage) { - FFmpegDecoderInstance* working_instance = nullptr; - FFmpegFramePool::ElementPtr return_frame = nullptr; + // FIXME: Hacky + FFmpegDecoderInstance i(stream()->footage()->filename().toUtf8(), stream()->index()); - // Find instance - do { - QMutexLocker list_locker(&instance_map_lock_); + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + FramePtr output_frame = nullptr; - QList non_ideal_contenders; + int ret = i.GetFrame(pkt, frame); - QList instances = instance_map_.value(stream().get()); + if (ret >= 0) { + output_frame = BuffersToNativeFrame(divider, + is->width(), + is->height(), + 0, + frame->data, + frame->linesize); + } else { + qWarning() << "Failed to retrieve still image from decoder"; + } - foreach (FFmpegDecoderInstance* i, instances) { + av_frame_free(&frame); + av_packet_free(&pkt); - i->cache_lock()->lock(); + return output_frame; - if (i->CacheContainsTime(target_ts)) { + } else { - // Found our instance, allow others to enter the list + FFmpegFramePool::ElementPtr return_frame = nullptr; - list_locker.unlock(); + int64_t target_ts = Timecode::time_to_timestamp(timecode, time_base_) + start_time_; - // Get the frame from this cache - return_frame = i->GetFrameFromCache(target_ts); + VideoStreamPtr vs = std::static_pointer_cast(stream()); - // Got our frame, allow cache to continue - i->cache_lock()->unlock(); - break; + FFmpegDecoderInstance* working_instance = nullptr; - } else if (i->CacheWillContainTime(target_ts) || i->CacheCouldContainTime(target_ts)) { + // Find instance + do { + QMutexLocker list_locker(&instance_map_lock_); - // Found our instance, allow others to enter the list - list_locker.unlock(); + QList non_ideal_contenders; - // If the instance is currently in use, enter into a loop of seeing from frames come up next in case one is ours - if (i->IsWorking()) { + QList instances = instance_map_.value(stream().get()); - do { - // Allow instance to continue to the next frame - i->cache_wait_cond()->wait(i->cache_lock()); + foreach (FFmpegDecoderInstance* i, instances) { - // See if the cache now contains this frame, if so we'll exit this loop - if (i->CacheContainsTime(target_ts)) { + i->cache_lock()->lock(); - // Grab the frame - return_frame = i->GetFrameFromCache(target_ts); + if (i->CacheContainsTime(target_ts)) { - // We can release this worker now since we don't need it anymore - i->cache_lock()->unlock(); + // Found our instance, allow others to enter the list - } else if (!i->IsWorking()) { + list_locker.unlock(); - // This instance finished and we didn't get our frame, we'll take it and continue it - working_instance = i; - break; + // Get the frame from this cache + return_frame = i->GetFrameFromCache(target_ts); - } - } while (!return_frame); + // Got our frame, allow cache to continue + i->cache_lock()->unlock(); + break; + + } else if (i->CacheWillContainTime(target_ts) || i->CacheCouldContainTime(target_ts)) { + + // Found our instance, allow others to enter the list + list_locker.unlock(); + + // If the instance is currently in use, enter into a loop of seeing from frames come up next in case one is ours + if (i->IsWorking()) { + + do { + // Allow instance to continue to the next frame + i->cache_wait_cond()->wait(i->cache_lock()); + + // See if the cache now contains this frame, if so we'll exit this loop + if (i->CacheContainsTime(target_ts)) { + + // Grab the frame + return_frame = i->GetFrameFromCache(target_ts); + + // We can release this worker now since we don't need it anymore + i->cache_lock()->unlock(); + + } else if (!i->IsWorking()) { + + // This instance finished and we didn't get our frame, we'll take it and continue it + working_instance = i; + break; + + } + } while (!return_frame); + + } else { + // Otherwise, we'll grab this instance and continue it ourselves + working_instance = i; + } + + break; + + } else if (i->IsWorking()) { + + // Ignore currently working instances + i->cache_lock()->unlock(); + + } else if (i->CacheIsEmpty()) { + + // Prioritize this cache over others (leaves this instance LOCKED in case we end up using it later) + non_ideal_contenders.prepend(i); } else { - // Otherwise, we'll grab this instance and continue it ourselves - working_instance = i; + + // De-prioritize this cache (leaves this instance LOCKED in case we end up using it later) + non_ideal_contenders.append(i); + } - - break; - - } else if (i->IsWorking()) { - - // Ignore currently working instances - i->cache_lock()->unlock(); - - } else if (i->CacheIsEmpty()) { - - // Prioritize this cache over others (leaves this instance LOCKED in case we end up using it later) - non_ideal_contenders.prepend(i); - - } else { - - // De-prioritize this cache (leaves this instance LOCKED in case we end up using it later) - non_ideal_contenders.append(i); - } + + // If we didn't find a suitable contender, grab the first non-suitable and roll with that + if (!return_frame && !working_instance && !non_ideal_contenders.isEmpty()) { + working_instance = non_ideal_contenders.takeFirst(); + } + + // For all instances we left locked but didn't end up using, lock them now + foreach (FFmpegDecoderInstance* unsuitable_instance, non_ideal_contenders) { + unsuitable_instance->cache_lock()->unlock(); + } + } while (!return_frame && !working_instance); + + if (!return_frame && working_instance) { + + // This instance SHOULD remain locked from our earlier loop, making this operation safe + working_instance->SetWorking(true); + + // Retrieve frame + return_frame = working_instance->RetrieveFrame(target_ts, true); + + // Set working to false and wake any threads waiting + working_instance->cache_lock()->lock(); + working_instance->SetWorking(false); + working_instance->cache_wait_cond()->wakeAll(); + working_instance->cache_lock()->unlock(); } - // If we didn't find a suitable contender, grab the first non-suitable and roll with that - if (!return_frame && !working_instance && !non_ideal_contenders.isEmpty()) { - working_instance = non_ideal_contenders.takeFirst(); + // We found the frame, we'll return a copy + if (return_frame) { + // Align buffer to data/linesize points that can be passed to sws_scale + uint8_t* input_data[4]; + int input_linesize[4]; + + av_image_fill_arrays(input_data, + input_linesize, + reinterpret_cast(return_frame->data()), + src_pix_fmt_, + vs->width(), + vs->height(), + 1); + + return BuffersToNativeFrame(divider, + vs->width(), + vs->height(), + target_ts, + input_data, + input_linesize); } - // For all instances we left locked but didn't end up using, lock them now - foreach (FFmpegDecoderInstance* unsuitable_instance, non_ideal_contenders) { - unsuitable_instance->cache_lock()->unlock(); - } - } while (!return_frame && !working_instance); - - if (!return_frame && working_instance) { - - // This instance SHOULD remain locked from our earlier loop, making this operation safe - working_instance->SetWorking(true); - - // Retrieve frame - return_frame = working_instance->RetrieveFrame(target_ts, true); - - // Set working to false and wake any threads waiting - working_instance->cache_lock()->lock(); - working_instance->SetWorking(false); - working_instance->cache_wait_cond()->wakeAll(); - working_instance->cache_lock()->unlock(); - } - - // We found the frame, we'll return a copy - if (return_frame) { - if (divider != scale_divider_) { - FreeScaler(); - InitScaler(divider); - } - - // Create frame to return - FramePtr copy = Frame::Create(); - copy->set_video_params(VideoParams(vs->width(), - vs->height(), - native_pix_fmt_, - divider)); - copy->set_timestamp(Timecode::timestamp_to_time(target_ts, time_base_)); - copy->set_sample_aspect_ratio(aspect_ratio_); - copy->allocate(); - - // Align buffer to data/linesize points that can be passed to sws_scale - uint8_t* input_data[4]; - int input_linesize[4]; - - av_image_fill_arrays(input_data, - input_linesize, - reinterpret_cast(return_frame->data()), - src_pix_fmt_, - vs->width(), - vs->height(), - 1); - - // Convert frame to RGB/A for the rest of the pipeline - uint8_t* output_data = reinterpret_cast(copy->data()); - int output_linesize = copy->linesize_bytes(); - - sws_scale(scale_ctx_, - input_data, - input_linesize, - 0, - vs->height(), - &output_data, - &output_linesize); - - return copy; } return nullptr; @@ -436,8 +448,6 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) AVFormatContext* fmt_ctx = nullptr; error_code = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr); - QList streams_that_need_manual_duration; - // Handle format context error if (error_code == 0) { @@ -453,16 +463,72 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) if (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - // Create a video stream object - VideoStreamPtr video_stream = std::make_shared(); + bool image_is_still = false; + ImageStream::Interlacing interlacing = ImageStream::kInterlaceNone; - video_stream->set_width(avstream->codecpar->width); - video_stream->set_height(avstream->codecpar->height); - video_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast(avstream->codecpar->format)))); - video_stream->set_frame_rate(av_guess_frame_rate(fmt_ctx, avstream, nullptr)); - video_stream->set_start_time(avstream->start_time); + { + // Read at least two frames to get more information about this video stream + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); - str = video_stream; + { + FFmpegDecoderInstance instance(filename, i); + + // Read first frame and retrieve some metadata + if (instance.GetFrame(pkt, frame) >= 0) { + // Check if video is interlaced and what field dominance it has if so + if (frame->interlaced_frame) { + if (frame->top_field_first) { + interlacing = ImageStream::kInterlacedTopFirst; + } else { + interlacing = ImageStream::kInterlacedBottomFirst; + } + } + } + + // Read second frame + int ret = instance.GetFrame(pkt, frame); + + if (ret >= 0) { + // Check if we need a manual duration + if (avstream->duration == AV_NOPTS_VALUE) { + int64_t new_dur; + + do { + new_dur = frame->pts; + } while (instance.GetFrame(pkt, frame) >= 0); + + avstream->duration = new_dur; + } + } else if (ret == AVERROR_EOF) { + // Video has only one frame in it, treat it like a still image + image_is_still = true; + } + } + + av_frame_free(&frame); + av_packet_free(&pkt); + } + + ImageStreamPtr image_stream; + + if (image_is_still) { + image_stream = std::make_shared(); + } else { + VideoStreamPtr video_stream = std::make_shared(); + + video_stream->set_frame_rate(av_guess_frame_rate(fmt_ctx, avstream, nullptr)); + video_stream->set_start_time(avstream->start_time); + + image_stream = video_stream; + } + + image_stream->set_width(avstream->codecpar->width); + image_stream->set_height(avstream->codecpar->height); + image_stream->set_format(GetNativePixelFormat(FFmpegCommon::GetCompatiblePixelFormat(static_cast(avstream->codecpar->format)))); + image_stream->set_interlacing(interlacing); + + str = image_stream; } else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { @@ -511,11 +577,6 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) str->set_timebase(avstream->time_base); str->set_duration(avstream->duration); - // The container/stream info may not contain a duration, so we'll need to manually retrieve it - if (avstream->duration == AV_NOPTS_VALUE) { - streams_that_need_manual_duration.append(str.get()); - } - f->add_stream(str); } @@ -523,51 +584,6 @@ bool FFmpegDecoder::Probe(Footage *f, const QAtomicInt* cancelled) result = true; } - // If the metadata did not contain a duration, we'll need to loop through the file to retrieve it - if (!streams_that_need_manual_duration.isEmpty()) { - - AVPacket* pkt = av_packet_alloc(); - - QVector durations(streams_that_need_manual_duration.size()); - durations.fill(0); - - while (true) { - if (cancelled && *cancelled) { - break; - } - - // Ensure previous buffers are cleared - av_packet_unref(pkt); - - // Read packet from file - int ret = av_read_frame(fmt_ctx, pkt); - - if (ret < 0) { - // Handle errors that aren't EOF (which simply means the file is finished) - if (ret != AVERROR_EOF) { - qWarning() << "Error while finding duration"; - } - break; - } else { - for (int i=0;iindex() == pkt->stream_index - && pkt->pts > durations.at(i)) { - durations.replace(i, pkt->pts); - } - } - } - } - - av_packet_free(&pkt); - - if (!cancelled || !*cancelled) { - for (int i=0;iset_duration(durations.at(i)); - } - } - - } - // Free all memory avformat_close_input(&fmt_ctx); @@ -795,6 +811,38 @@ uint64_t FFmpegDecoder::ValidateChannelLayout(AVStream* stream) return av_get_default_channel_layout(stream->codecpar->channels); } +FramePtr FFmpegDecoder::BuffersToNativeFrame(int divider, int width, int height, int64_t ts, uint8_t** input_data, int* input_linesize) +{ + if (divider != scale_divider_) { + FreeScaler(); + InitScaler(divider); + } + + // Create frame to return + FramePtr copy = Frame::Create(); + copy->set_video_params(VideoParams(width, + height, + native_pix_fmt_, + divider)); + copy->set_timestamp(Timecode::timestamp_to_time(ts, time_base_)); + copy->set_sample_aspect_ratio(aspect_ratio_); + copy->allocate(); + + // Convert frame to RGB/A for the rest of the pipeline + uint8_t* output_data = reinterpret_cast(copy->data()); + int output_linesize = copy->linesize_bytes(); + + sws_scale(scale_ctx_, + input_data, + input_linesize, + 0, + height, + &output_data, + &output_linesize); + + return copy; +} + int FFmpegDecoderInstance::GetFrame(AVPacket *pkt, AVFrame *frame) { bool eof = false; diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index 009a9a5ae..0904bf9d1 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -182,6 +182,8 @@ private: static uint64_t ValidateChannelLayout(AVStream *stream); + FramePtr BuffersToNativeFrame(int divider, int width, int height, int64_t ts, uint8_t **input_data, int* input_linesize); + SwsContext* scale_ctx_; int scale_divider_; AVPixelFormat src_pix_fmt_; diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 6f565aa33..d60e21df1 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -41,6 +41,21 @@ VideoStreamProperties::VideoStreamProperties(ImageStreamPtr stream) : int row = 0; + video_layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + + video_interlace_combo_ = new QComboBox(); + + // These must match the Interlacing enum in ImageStream + video_interlace_combo_->addItem(tr("None (Progressive)")); + video_interlace_combo_->addItem(tr("Top-Field First")); + video_interlace_combo_->addItem(tr("Bottom-Field First")); + + video_interlace_combo_->setCurrentIndex(stream->interlacing()); + + video_layout->addWidget(video_interlace_combo_, row, 1); + + row++; + video_layout->addWidget(new QLabel(tr("Color Space:")), row, 0); video_color_space_ = new QComboBox(); @@ -109,6 +124,7 @@ void VideoStreamProperties::Accept(QUndoCommand *parent) new VideoStreamChangeCommand(stream_, video_premultiply_alpha_->isChecked(), set_colorspace, + static_cast(video_interlace_combo_->currentIndex()), parent); } @@ -150,11 +166,13 @@ bool VideoStreamProperties::IsImageSequence(ImageStream *stream) VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(ImageStreamPtr stream, bool premultiplied, QString colorspace, + ImageStream::Interlacing interlacing, QUndoCommand *parent) : UndoCommand(parent), stream_(stream), new_premultiplied_(premultiplied), - new_colorspace_(colorspace) + new_colorspace_(colorspace), + new_interlacing_(interlacing) { } @@ -167,15 +185,18 @@ void VideoStreamProperties::VideoStreamChangeCommand::redo_internal() { old_premultiplied_ = stream_->premultiplied_alpha(); old_colorspace_ = stream_->colorspace(false); + old_interlacing_ = stream_->interlacing(); stream_->set_premultiplied_alpha(new_premultiplied_); stream_->set_colorspace(new_colorspace_); + stream_->set_interlacing(new_interlacing_); } void VideoStreamProperties::VideoStreamChangeCommand::undo_internal() { stream_->set_premultiplied_alpha(old_premultiplied_); stream_->set_colorspace(old_colorspace_); + stream_->set_interlacing(old_interlacing_); } VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand(VideoStreamPtr video_stream, int64_t start_index, int64_t duration, QUndoCommand *parent) : diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index 6bd164ab8..0770d6a07 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -58,6 +58,11 @@ private: */ QComboBox* video_color_space_; + /** + * @brief Setting for video interlacing + */ + QComboBox* video_interlace_combo_; + /** * @brief Sets the start index for image sequences */ @@ -73,6 +78,7 @@ private: VideoStreamChangeCommand(ImageStreamPtr stream, bool premultiplied, QString colorspace, + ImageStream::Interlacing interlacing, QUndoCommand* parent = nullptr); virtual Project* GetRelevantProject() const override; @@ -86,9 +92,11 @@ private: bool new_premultiplied_; QString new_colorspace_; + ImageStream::Interlacing new_interlacing_; bool old_premultiplied_; QString old_colorspace_; + ImageStream::Interlacing old_interlacing_; }; diff --git a/app/project/item/footage/imagestream.cpp b/app/project/item/footage/imagestream.cpp index 41a9d0d23..fc74ac6e3 100644 --- a/app/project/item/footage/imagestream.cpp +++ b/app/project/item/footage/imagestream.cpp @@ -28,7 +28,8 @@ OLIVE_NAMESPACE_ENTER ImageStream::ImageStream() : - premultiplied_alpha_(false) + premultiplied_alpha_(false), + interlacing_(kInterlaceNone) { set_type(kImage); } diff --git a/app/project/item/footage/imagestream.h b/app/project/item/footage/imagestream.h index 30fc712a2..78a700f77 100644 --- a/app/project/item/footage/imagestream.h +++ b/app/project/item/footage/imagestream.h @@ -75,6 +75,24 @@ public: QString get_colorspace_match_string() const; + enum Interlacing { + kInterlaceNone, + kInterlacedTopFirst, + kInterlacedBottomFirst + }; + + Interlacing interlacing() const + { + return interlacing_; + } + + void set_interlacing(Interlacing i) + { + interlacing_ = i; + + emit ParametersChanged(); + } + protected: virtual void FootageSetEvent(Footage*) override; @@ -87,6 +105,7 @@ private: int height_; bool premultiplied_alpha_; QString colorspace_; + Interlacing interlacing_; PixelFormat::Format format_; @@ -94,6 +113,7 @@ private slots: void ColorConfigChanged(); void DefaultColorSpaceChanged(); + }; using ImageStreamPtr = std::shared_ptr; diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 148129721..5d9150388 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -216,7 +216,9 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi QVector track_offsets(Timeline::kTrackTypeCount); track_offsets.fill(track_start); + QVector footage_ghosts; rational footage_duration; + bool contains_image_stream = false; quint64 enabled_streams = footage.streams(); @@ -236,22 +238,21 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); if (stream->type() == Stream::kImage) { - // Stream is essentially length-less - use config's default image length - footage_duration = Config::Current()["DefaultStillLength"].value(); + // Stream is essentially length-less - we may use the default still image length in config, + // or we may use another stream's length depending on the circumstance + contains_image_stream = true; } else { // Rescale stream duration to timeline timebase // Convert to rational time if (footage.footage()->workarea()->enabled()) { - footage_duration = footage.footage()->workarea()->range().length(); + footage_duration = qMax(footage_duration, footage.footage()->workarea()->range().length()); ghost->SetMediaIn(footage.footage()->workarea()->in()); } else { int64_t stream_duration = Timecode::rescale_timestamp_ceil(stream->duration(), stream->timebase(), dest_tb); - footage_duration = Timecode::timestamp_to_time(stream_duration, dest_tb); + footage_duration = qMax(footage_duration, Timecode::timestamp_to_time(stream_duration, dest_tb)); } } - ghost->SetIn(ghost_start); - ghost->SetOut(ghost_start + footage_duration); ghost->SetTrack(TrackReference(track_type, track_offsets.at(track_type))); // Increment track count for this track type @@ -263,10 +264,22 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi ghost->setData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); ghost->SetMode(Timeline::kMove); - parent()->AddGhost(ghost); + footage_ghosts.append(ghost); } + if (contains_image_stream && footage_duration.isNull()) { + // Footage must ONLY be image streams so no duration value was found, use default in config + footage_duration = Config::Current()["DefaultStillLength"].value(); + } + + foreach (TimelineViewGhostItem* ghost, footage_ghosts) { + ghost->SetIn(ghost_start); + ghost->SetOut(ghost_start + footage_duration); + + parent()->AddGhost(ghost); + } + // Stack each ghost one after the other ghost_start += footage_duration; diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 76a2ae157..6afac6c25 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -548,6 +548,9 @@ rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, } prev = prev->previous(); } + + // Limit in point at 0 on the timeline + earliest_in = qMax(rational(), earliest_in); } } From ae62750d9dd994c84b1771935fbd662f5f8986b2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 14 Jul 2020 22:31:11 +1000 Subject: [PATCH 065/138] various: fix broken transition creation It's now possible to create transitions! But they're still broken in a multitude of other ways so plenty more work to do hahahahahahahahaha --- app/core.cpp | 12 +++- app/core.h | 17 ++++- app/node/block/transition/CMakeLists.txt | 3 + .../transition/crossdissolve/CMakeLists.txt | 22 ++++++ .../crossdissolve/crossdissolvetransition.cpp | 62 +++++++++++++++++ .../crossdissolve/crossdissolvetransition.h | 48 +++++++++++++ .../transition/diptocolor/CMakeLists.txt | 22 ++++++ .../diptocolor/diptocolortransition.cpp | 68 +++++++++++++++++++ .../diptocolor/diptocolortransition.h | 52 ++++++++++++++ app/node/block/transition/transition.cpp | 31 ++++++--- app/node/block/transition/transition.h | 5 ++ app/node/factory.cpp | 17 ++++- app/node/factory.h | 9 ++- app/node/node.cpp | 2 + app/node/node.h | 1 + app/panel/tool/tool.cpp | 1 + app/shaders/crossdissolve.frag | 3 +- app/shaders/diptoblack.frag | 24 +++---- app/widget/timelinewidget/tool/transition.cpp | 10 ++- app/widget/toolbar/toolbar.cpp | 22 +++++- app/widget/toolbar/toolbar.h | 17 +++++ 21 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 app/node/block/transition/crossdissolve/CMakeLists.txt create mode 100644 app/node/block/transition/crossdissolve/crossdissolvetransition.cpp create mode 100644 app/node/block/transition/crossdissolve/crossdissolvetransition.h create mode 100644 app/node/block/transition/diptocolor/CMakeLists.txt create mode 100644 app/node/block/transition/diptocolor/diptocolortransition.cpp create mode 100644 app/node/block/transition/diptocolor/diptocolortransition.h diff --git a/app/core.cpp b/app/core.cpp index e8f34fcf4..d7014f408 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -281,16 +281,26 @@ const Tool::Item &Core::tool() const return tool_; } -const Tool::AddableObject &Core::selected_addable_object() const +const Tool::AddableObject &Core::GetSelectedAddableObject() const { return addable_object_; } +const QString &Core::GetSelectedTransition() const +{ + return selected_transition_; +} + void Core::SetSelectedAddableObject(const Tool::AddableObject &obj) { addable_object_ = obj; } +void Core::SetSelectedTransitionObject(const QString &obj) +{ + selected_transition_ = obj; +} + void Core::ClearOpenRecentList() { recent_projects_.clear(); diff --git a/app/core.h b/app/core.h index 9b3d25728..1ead89b5c 100644 --- a/app/core.h +++ b/app/core.h @@ -114,7 +114,12 @@ public: /** * @brief Get the currently selected object that the add tool should make (if the add tool is active) */ - const Tool::AddableObject& selected_addable_object() const; + const Tool::AddableObject& GetSelectedAddableObject() const; + + /** + * @brief Get the currently selected node that the transition tool should make (if the transition tool is active) + */ + const QString& GetSelectedTransition() const; /** * @brief Get current snapping value @@ -341,6 +346,11 @@ public slots: */ void SetSelectedAddableObject(const Tool::AddableObject& obj); + /** + * @brief Set the currently selected object that the add tool should make + */ + void SetSelectedTransitionObject(const QString& obj); + /** * @brief Clears the list of recently opened/saved projects */ @@ -456,6 +466,11 @@ private: */ Tool::AddableObject addable_object_; + /** + * @brief Currently selected transition + */ + QString selected_transition_; + /** * @brief Current snapping setting */ diff --git a/app/node/block/transition/CMakeLists.txt b/app/node/block/transition/CMakeLists.txt index 65f672588..4a380bc81 100644 --- a/app/node/block/transition/CMakeLists.txt +++ b/app/node/block/transition/CMakeLists.txt @@ -14,6 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(crossdissolve) +add_subdirectory(diptocolor) + set(OLIVE_SOURCES ${OLIVE_SOURCES} node/block/transition/transition.h diff --git a/app/node/block/transition/crossdissolve/CMakeLists.txt b/app/node/block/transition/crossdissolve/CMakeLists.txt new file mode 100644 index 000000000..25c90ed8e --- /dev/null +++ b/app/node/block/transition/crossdissolve/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/transition/crossdissolve/crossdissolvetransition.h + node/block/transition/crossdissolve/crossdissolvetransition.cpp + PARENT_SCOPE +) diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp new file mode 100644 index 000000000..c6d471030 --- /dev/null +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "crossdissolvetransition.h" + +OLIVE_NAMESPACE_ENTER + +CrossDissolveTransition::CrossDissolveTransition() +{ + +} + +Node *CrossDissolveTransition::copy() const +{ + return new CrossDissolveTransition(); +} + +QString CrossDissolveTransition::Name() const +{ + return tr("Cross Dissolve"); +} + +QString CrossDissolveTransition::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.crossdissolve"); +} + +QList CrossDissolveTransition::Category() const +{ + return {kCategoryTransition}; +} + +QString CrossDissolveTransition::Description() const +{ + return tr("Smoothly transition between two clips."); +} + +ShaderCode CrossDissolveTransition::GetShaderCode(const QString &shader_id) const +{ + Q_UNUSED(shader_id) + + return ShaderCode(Node::ReadFileAsString(":/shaders/crossdissolve.frag"), QString()); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.h b/app/node/block/transition/crossdissolve/crossdissolvetransition.h new file mode 100644 index 000000000..ccd8877d3 --- /dev/null +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.h @@ -0,0 +1,48 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef CROSSDISSOLVETRANSITION_H +#define CROSSDISSOLVETRANSITION_H + +#include "node/block/transition/transition.h" + +OLIVE_NAMESPACE_ENTER + +class CrossDissolveTransition : public TransitionBlock +{ +public: + CrossDissolveTransition(); + + virtual Node* copy() const override; + + virtual QString Name() const override; + virtual QString id() const override; + virtual QList Category() const override; + virtual QString Description() const override; + + //virtual void Retranslate() override; + + virtual ShaderCode GetShaderCode(const QString& shader_id) const override; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // CROSSDISSOLVETRANSITION_H diff --git a/app/node/block/transition/diptocolor/CMakeLists.txt b/app/node/block/transition/diptocolor/CMakeLists.txt new file mode 100644 index 000000000..7eb37f14d --- /dev/null +++ b/app/node/block/transition/diptocolor/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/block/transition/diptocolor/diptocolortransition.h + node/block/transition/diptocolor/diptocolortransition.cpp + PARENT_SCOPE +) diff --git a/app/node/block/transition/diptocolor/diptocolortransition.cpp b/app/node/block/transition/diptocolor/diptocolortransition.cpp new file mode 100644 index 000000000..816c75011 --- /dev/null +++ b/app/node/block/transition/diptocolor/diptocolortransition.cpp @@ -0,0 +1,68 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "diptocolortransition.h" + +OLIVE_NAMESPACE_ENTER + +DipToColorTransition::DipToColorTransition() +{ + color_input_ = new NodeInput(QStringLiteral("color_in"), NodeParam::kColor, QVariant::fromValue(Color(0, 0, 0))); + AddInput(color_input_); +} + +Node *DipToColorTransition::copy() const +{ + return new DipToColorTransition(); +} + +QString DipToColorTransition::Name() const +{ + return tr("Dip To Color"); +} + +QString DipToColorTransition::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.diptocolor"); +} + +QList DipToColorTransition::Category() const +{ + return {kCategoryTransition}; +} + +QString DipToColorTransition::Description() const +{ + return tr("Transition between clips by dipping to a color."); +} + +ShaderCode DipToColorTransition::GetShaderCode(const QString &shader_id) const +{ + Q_UNUSED(shader_id) + + return ShaderCode(Node::ReadFileAsString(":/shaders/diptoblack.frag"), QString()); +} + +void DipToColorTransition::ShaderJobEvent(NodeValueDatabase &value, ShaderJob &job) const +{ + job.InsertValue(color_input_, value); +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/diptocolor/diptocolortransition.h b/app/node/block/transition/diptocolor/diptocolortransition.h new file mode 100644 index 000000000..2c19443e1 --- /dev/null +++ b/app/node/block/transition/diptocolor/diptocolortransition.h @@ -0,0 +1,52 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef DIPTOCOLORTRANSITION_H +#define DIPTOCOLORTRANSITION_H + +#include "node/block/transition/transition.h" + +OLIVE_NAMESPACE_ENTER + +class DipToColorTransition : public TransitionBlock +{ +public: + DipToColorTransition(); + + virtual Node* copy() const override; + + virtual QString Name() const override; + virtual QString id() const override; + virtual QList Category() const override; + virtual QString Description() const override; + + virtual ShaderCode GetShaderCode(const QString& shader_id) const override; + +protected: + virtual void ShaderJobEvent(NodeValueDatabase &value, ShaderJob& job) const override; + +private: + NodeInput* color_input_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // DIPTOCOLORTRANSITION_H diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index 3f820fd47..aa8a77c0a 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -131,6 +131,8 @@ double TransitionBlock::GetInProgress(const rational &time) const void TransitionBlock::Hash(QCryptographicHash &hash, const rational &time) const { + Node::Hash(hash, time); + double all_prog = GetTotalProgress(time); double in_prog = GetInProgress(time); double out_prog = GetOutProgress(time); @@ -138,14 +140,6 @@ void TransitionBlock::Hash(QCryptographicHash &hash, const rational &time) const hash.addData(reinterpret_cast(&all_prog), sizeof(double)); hash.addData(reinterpret_cast(&in_prog), sizeof(double)); hash.addData(reinterpret_cast(&out_prog), sizeof(double)); - - if (out_block_input_->is_connected()) { - out_block_input_->get_connected_node()->Hash(hash, time); - } - - if (in_block_input_->is_connected()) { - in_block_input_->get_connected_node()->Hash(hash, time); - } } double TransitionBlock::GetInternalTransitionTime(const rational &time) const @@ -177,4 +171,25 @@ void TransitionBlock::BlockDisconnected(NodeEdgePtr edge) } } +NodeValueTable TransitionBlock::Value(NodeValueDatabase &value) const +{ + ShaderJob job; + + job.InsertValue(out_block_input(), value); + job.InsertValue(in_block_input(), value); + job.SetAlphaChannelRequired(true); + + ShaderJobEvent(value, job); + + NodeValueTable table = value.Merge(); + table.Push(NodeParam::kShaderJob, QVariant::fromValue(job), this); + return table; +} + +void TransitionBlock::ShaderJobEvent(NodeValueDatabase &value, ShaderJob &job) const +{ + Q_UNUSED(value) + Q_UNUSED(job) +} + OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/transition.h b/app/node/block/transition/transition.h index 62f4280b5..50d6ac204 100644 --- a/app/node/block/transition/transition.h +++ b/app/node/block/transition/transition.h @@ -49,6 +49,11 @@ public: virtual void Hash(QCryptographicHash& hash, const rational &time) const override; + virtual NodeValueTable Value(NodeValueDatabase &value) const override; + +protected: + virtual void ShaderJobEvent(NodeValueDatabase &value, ShaderJob& job) const; + private: double GetInternalTransitionTime(const rational& time) const; diff --git a/app/node/factory.cpp b/app/node/factory.cpp index ee2fe3892..7c389f443 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -24,6 +24,8 @@ #include "audio/volume/volume.h" #include "block/clip/clip.h" #include "block/gap/gap.h" +#include "block/transition/crossdissolve/crossdissolvetransition.h" +#include "block/transition/diptocolor/diptocolortransition.h" #include "generator/matrix/matrix.h" #include "generator/polygon/polygon.h" #include "generator/solid/solid.h" @@ -48,7 +50,7 @@ void NodeFactory::Initialize() // Add internal types for (int i=0;i(i))); + library_.append(CreateFromFactoryIndex(static_cast(i))); } /* @@ -63,7 +65,7 @@ void NodeFactory::Destroy() library_.clear(); } -Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item) +Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item, Node::CategoryID restrict_to) { Menu* menu = new Menu(parent); menu->setToolTipsVisible(true); @@ -71,6 +73,11 @@ Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item) for (int i=0;iCategory().contains(restrict_to)) { + // Skip this node + continue; + } + // Make sure nodes are up-to-date with the current translation n->Retranslate(); @@ -165,7 +172,7 @@ Node *NodeFactory::CreateFromID(const QString &id) return nullptr; } -Node *NodeFactory::CreateInternal(const NodeFactory::InternalID &id) +Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) { switch (id) { case kClipBlock: @@ -204,6 +211,10 @@ Node *NodeFactory::CreateInternal(const NodeFactory::InternalID &id) return new StrokeFilterNode(); case kTextGenerator: return new TextGenerator(); + case kCrossDissolveTransition: + return new CrossDissolveTransition(); + case kDipToColorTransition: + return new DipToColorTransition(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index b5c1b6f0f..6025070ef 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -50,6 +50,8 @@ public: kMerge, kStrokeFilter, kTextGenerator, + kCrossDissolveTransition, + kDipToColorTransition, // Count value kInternalNodeCount @@ -61,7 +63,7 @@ public: static void Destroy(); - static Menu* CreateMenu(QWidget *parent, bool create_none_item = false); + static Menu* CreateMenu(QWidget *parent, bool create_none_item = false, Node::CategoryID restrict_to = Node::kCategoryUnknown); static Node* CreateFromMenuAction(QAction* action); @@ -71,10 +73,11 @@ public: static Node* CreateFromID(const QString& id); -private: - static Node* CreateInternal(const InternalID& id); + static Node* CreateFromFactoryIndex(const InternalID& id); +private: static QList library_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/node.cpp b/app/node/node.cpp index 840406ed8..46d615b60 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -668,6 +668,8 @@ QString Node::GetCategoryName(const CategoryID &c) return tr("Generator"); case kCategoryChannels: return tr("Channel"); + case kCategoryTransition: + return tr("Transition"); case kCategoryUnknown: case kCategoryCount: break; diff --git a/app/node/node.h b/app/node/node.h index 11a69704f..eb3b272a2 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -70,6 +70,7 @@ public: kCategoryGeneral, kCategoryTimeline, kCategoryChannels, + kCategoryTransition, kCategoryCount }; diff --git a/app/panel/tool/tool.cpp b/app/panel/tool/tool.cpp index ac8b1162d..7822ece90 100644 --- a/app/panel/tool/tool.cpp +++ b/app/panel/tool/tool.cpp @@ -42,6 +42,7 @@ ToolPanel::ToolPanel(QWidget *parent) : connect(Core::instance(), &Core::SnappingChanged, t, &Toolbar::SetSnapping); connect(t, &Toolbar::AddableObjectChanged, Core::instance(), &Core::SetSelectedAddableObject); + connect(t, &Toolbar::SelectedTransitionChanged, Core::instance(), &Core::SetSelectedTransitionObject); Retranslate(); } diff --git a/app/shaders/crossdissolve.frag b/app/shaders/crossdissolve.frag index 22eb8e54f..2af4b17b4 100644 --- a/app/shaders/crossdissolve.frag +++ b/app/shaders/crossdissolve.frag @@ -19,8 +19,7 @@ void main(void) { } if (in_block_in_enabled) { - vec4 in_block_col = texture(in_block_in, ove_texcoord) * ove_tprog_all; - composite += in_block_col; + composite += texture(in_block_in, ove_texcoord) * ove_tprog_all; } fragColor = composite; diff --git a/app/shaders/diptoblack.frag b/app/shaders/diptoblack.frag index 064264506..b6e89dad3 100644 --- a/app/shaders/diptoblack.frag +++ b/app/shaders/diptoblack.frag @@ -4,7 +4,9 @@ uniform sampler2D out_block_in; uniform sampler2D in_block_in; uniform bool out_block_in_enabled; uniform bool in_block_in_enabled; +uniform vec4 color_in; +uniform float ove_tprog_all; uniform float ove_tprog_out; uniform float ove_tprog_in; @@ -13,20 +15,16 @@ in vec2 ove_texcoord; out vec4 fragColor; void main(void) { - vec4 out_block_col; - vec4 in_block_col; + if (out_block_in_enabled && in_block_in_enabled) { + vec4 out_block_col = mix(texture(out_block_in, ove_texcoord), color_in, ove_tprog_out); + vec4 in_block_col = mix(texture(in_block_in, ove_texcoord), color_in, 1.0 - ove_tprog_in); - if (out_block_in_enabled) { - out_block_col = texture(out_block_in, ove_texcoord) * pow(ove_tprog_out, 2.0); + fragColor = out_block_col + in_block_col; + } else if (out_block_in_enabled) { + fragColor = mix(texture(out_block_in, ove_texcoord), color_in, ove_tprog_all); + } else if (in_block_in_enabled) { + fragColor = mix(texture(in_block_in, ove_texcoord), color_in, 1.0 - ove_tprog_all); } else { - out_block_col = vec4(0.0); + fragColor = vec4(0.0); } - - if (in_block_in_enabled) { - in_block_col = texture(in_block_in, ove_texcoord) * pow(ove_tprog_in, 2.0); - } else { - in_block_col = vec4(0.0); - } - - fragColor = out_block_col + in_block_col; } diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index 6592581e9..3b8f0ae40 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -20,6 +20,7 @@ #include "widget/timelinewidget/timelinewidget.h" +#include "node/block/transition/crossdissolve/crossdissolvetransition.h" #include "node/block/transition/transition.h" #include "node/factory.h" #include "widget/nodeview/nodeviewundo.h" @@ -111,7 +112,14 @@ void TimelineWidget::TransitionTool::MouseRelease(TimelineViewMouseEvent *event) if (ghost_) { if (!ghost_->AdjustedLength().isNull()) { - TransitionBlock* transition = static_cast(NodeFactory::CreateFromID("org.olivevideoeditor.Olive.crossdissolve")); + TransitionBlock* transition; + + if (Core::instance()->GetSelectedTransition().isEmpty()) { + // Fallback if the user hasn't selected one yet + transition = new CrossDissolveTransition(); + } else { + transition = static_cast(NodeFactory::CreateFromID(Core::instance()->GetSelectedTransition())); + } QUndoCommand* command = new QUndoCommand(); diff --git a/app/widget/toolbar/toolbar.cpp b/app/widget/toolbar/toolbar.cpp index c04a4661c..62bb3f5ab 100644 --- a/app/widget/toolbar/toolbar.cpp +++ b/app/widget/toolbar/toolbar.cpp @@ -25,8 +25,9 @@ #include #include -#include "widget/menu/menu.h" +#include "node/factory.h" #include "ui/icons/icons.h" +#include "widget/menu/menu.h" OLIVE_NAMESPACE_ENTER @@ -54,6 +55,9 @@ Toolbar::Toolbar(QWidget *parent) : btn_snapping_toggle_ = CreateNonToolButton(); connect(btn_snapping_toggle_, &QPushButton::clicked, this, &Toolbar::SnappingButtonClicked); + // Connect transition button to menu signal + connect(btn_transition_tool_, &QPushButton::clicked, this, &Toolbar::TransitionButtonClicked); + // Connect add button to menu signal connect(btn_add_, &QPushButton::clicked, this, &Toolbar::AddButtonClicked); @@ -181,9 +185,25 @@ void Toolbar::AddButtonClicked() m.exec(QCursor::pos()); } +void Toolbar::TransitionButtonClicked() +{ + Menu* m = NodeFactory::CreateMenu(this, false, Node::kCategoryTransition); + + connect(m, &QMenu::triggered, this, &Toolbar::TransitionMenuItemTriggered); + + m->exec(QCursor::pos()); + + delete m; +} + void Toolbar::AddMenuItemTriggered(QAction* a) { emit AddableObjectChanged(static_cast(a->data().toInt())); } +void Toolbar::TransitionMenuItemTriggered(QAction *a) +{ + emit SelectedTransitionChanged(NodeFactory::GetIDFromMenuAction(a)); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/toolbar/toolbar.h b/app/widget/toolbar/toolbar.h index 2f315e738..74c48167d 100644 --- a/app/widget/toolbar/toolbar.h +++ b/app/widget/toolbar/toolbar.h @@ -113,6 +113,11 @@ signals: */ void AddableObjectChanged(const Tool::AddableObject& obj); + /** + * @brief Emitted when the selected transition is changed from the transition tool menu + */ + void SelectedTransitionChanged(const QString& id); + private: /** * @brief Reset all strings based on the currently selected language @@ -210,11 +215,23 @@ private slots: */ void AddButtonClicked(); + /** + * @brief Receiver for the transition button + * + * The transition button pops up a list for which transition to create. + */ + void TransitionButtonClicked(); + /** * @brief Receiver for the menu created by AddButtonClicked() */ void AddMenuItemTriggered(QAction* a); + /** + * @brief Receiver for the menu created by TransitionButtonClicked() + */ + void TransitionMenuItemTriggered(QAction* a); + }; OLIVE_NAMESPACE_EXIT From fdfadd88589aac6eb949cc5aac60651e4c7109e7 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 14 Jul 2020 22:32:01 +1000 Subject: [PATCH 066/138] add tool: use new class directly rather than text ID Minor code improvement/optimization. --- app/widget/timelinewidget/tool/add.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index a7a34a1f7..58b4e44de 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -22,6 +22,8 @@ #include "core.h" #include "node/factory.h" +#include "node/generator/solid/solid.h" +#include "node/generator/text/text.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER @@ -44,7 +46,7 @@ void TimelineWidget::AddTool::MousePress(TimelineViewMouseEvent *event) Timeline::TrackType add_type = Timeline::kTrackTypeNone; - switch (Core::instance()->selected_addable_object()) { + switch (Core::instance()->GetSelectedAddableObject()) { case OLIVE_NAMESPACE::Tool::kAddableBars: case OLIVE_NAMESPACE::Tool::kAddableSolid: case OLIVE_NAMESPACE::Tool::kAddableTitle: @@ -97,7 +99,7 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) ClipBlock* clip = new ClipBlock(); clip->set_length_and_media_out(ghost_->AdjustedLength()); - clip->SetLabel(OLIVE_NAMESPACE::Tool::GetAddableObjectName(Core::instance()->selected_addable_object())); + clip->SetLabel(OLIVE_NAMESPACE::Tool::GetAddableObjectName(Core::instance()->GetSelectedAddableObject())); NodeGraph* graph = static_cast(parent()->GetConnectedNode()->parent()); @@ -111,13 +113,13 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) ghost_->GetAdjustedIn(), command); - switch (Core::instance()->selected_addable_object()) { + switch (Core::instance()->GetSelectedAddableObject()) { case OLIVE_NAMESPACE::Tool::kAddableEmpty: // Empty, nothing to be done break; case OLIVE_NAMESPACE::Tool::kAddableSolid: { - Node* solid = NodeFactory::CreateFromID(QStringLiteral("org.olivevideoeditor.Olive.solidgenerator")); + Node* solid = new SolidGenerator(); new NodeAddCommand(graph, solid, @@ -128,7 +130,7 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) } case OLIVE_NAMESPACE::Tool::kAddableTitle: { - Node* text = NodeFactory::CreateFromID(QStringLiteral("org.olivevideoeditor.Olive.textgenerator")); + Node* text = new TextGenerator(); new NodeAddCommand(graph, text, @@ -140,7 +142,7 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) case OLIVE_NAMESPACE::Tool::kAddableBars: case OLIVE_NAMESPACE::Tool::kAddableTone: // Not implemented yet - qWarning() << "Unimplemented add object:" << Core::instance()->selected_addable_object(); + qWarning() << "Unimplemented add object:" << Core::instance()->GetSelectedAddableObject(); break; case OLIVE_NAMESPACE::Tool::kAddableCount: // Invalid value, do nothing From e67b801a50afbdf879a089898def61eaf94a778a Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 14 Jul 2020 22:32:27 +1000 Subject: [PATCH 067/138] renderer: treat buffer inputs as texture inputs when appropriate Fixes bug where a null buffer would not set a null texture. --- app/render/backend/opengl/openglproxy.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index 08aa8864c..32c24f92d 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -288,7 +288,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, // This variable is used in the shader, let's set it const QVariant& value = it.value().data(); - const NodeParam::DataType& data_type = (it.value().type() != NodeParam::kNone) + NodeParam::DataType data_type = (it.value().type() != NodeParam::kNone) ? it.value().type() : it.key()->data_type(); @@ -340,6 +340,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, case NodeInput::kBoolean: shader->setUniformValue(variable_location, value.toBool()); break; + case NodeInput::kBuffer: case NodeInput::kTexture: { OpenGLTextureCache::ReferencePtr texture = value.value(); @@ -392,7 +393,6 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, case NodeInput::kSampleJob: case NodeInput::kGenerateJob: case NodeInput::kFootage: - case NodeInput::kBuffer: case NodeInput::kNone: case NodeInput::kAny: break; From de74adeb5e2501e3c3a802358f201a9ae0deeeb5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 16 Jul 2020 00:32:00 +1000 Subject: [PATCH 068/138] nodes: improved changed signal processing Improves stability and cache reliability. Earlier iterations were prone to skipping necessary signals (usually leading to some sort of assert fail), particularly when track optimizations were used. Those optimizations have been moved to the viewer node so there's a higher degree of control over which signals get optimized and in which ways. --- app/node/block/block.cpp | 10 ---- app/node/block/block.h | 2 - app/node/node.cpp | 27 ++++++++-- app/node/node.h | 13 +++++ app/node/output/track/track.cpp | 70 ++++++++++--------------- app/node/output/track/track.h | 11 ---- app/node/output/viewer/viewer.cpp | 41 ++++++++++----- app/node/output/viewer/viewer.h | 6 +++ app/widget/timelinewidget/undo/undo.cpp | 66 +++++++++++------------ 9 files changed, 128 insertions(+), 118 deletions(-) diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index f46179786..222bc042d 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -355,16 +355,6 @@ NodeInput *Block::speed_input() const return speed_input_; } -void Block::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source) -{ - if (range.out() <= in() || range.in() >= out()) { - // Ignore this range - return; - } - - Node::InvalidateCache(TimeRange(qMax(range.in(), in()), qMin(range.out(), out())), from, source); -} - void Block::Hash(QCryptographicHash &, const rational &) const { // A block does nothing by default, so we hash nothing diff --git a/app/node/block/block.h b/app/node/block/block.h index 5694d4391..09182e7ea 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -88,8 +88,6 @@ public: NodeInput* media_in_input() const; NodeInput* speed_input() const; - virtual void InvalidateCache(const TimeRange& range, NodeInput* from, NodeInput* source) override; - virtual void Hash(QCryptographicHash &hash, const rational &time) const override; public slots: diff --git a/app/node/node.cpp b/app/node/node.cpp index 46d615b60..051575c05 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -177,6 +177,28 @@ void Node::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *s SendInvalidateCache(range, source); } +void Node::BeginOperation() +{ + foreach (NodeParam* param, params_) { + if (param->type() == NodeParam::kOutput) { + foreach (NodeEdgePtr edge, param->edges()) { + edge->input()->parentNode()->BeginOperation(); + } + } + } +} + +void Node::EndOperation() +{ + foreach (NodeParam* param, params_) { + if (param->type() == NodeParam::kOutput) { + foreach (NodeEdgePtr edge, param->edges()) { + edge->input()->parentNode()->EndOperation(); + } + } + } +} + TimeRange Node::InputTimeAdjustment(NodeInput *, const TimeRange &input_time) const { // Default behavior is no time adjustment at all @@ -195,10 +217,7 @@ void Node::SendInvalidateCache(const TimeRange &range, NodeInput *source) foreach (NodeParam* param, params_) { // If the Node is an output, relay the signal to any Nodes that are connected to it if (param->type() == NodeParam::kOutput) { - - QVector edges = param->edges(); - - foreach (NodeEdgePtr edge, edges) { + foreach (NodeEdgePtr edge, param->edges()) { NodeInput* connected_input = edge->input(); Node* connected_node = connected_input->parentNode(); diff --git a/app/node/node.h b/app/node/node.h index eb3b272a2..9260ed44f 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -304,6 +304,19 @@ public: */ virtual void InvalidateCache(const TimeRange& range, NodeInput* from, NodeInput* source); + /** + * @brief Limits cache invalidation temporarily + * + * If you intend to do a number of operations in quick succession, you can optimize it by running + * this function with EndOperation(). + */ + virtual void BeginOperation(); + + /** + * @brief Stops limiting cache invalidation and flushes changes + */ + virtual void EndOperation(); + /** * @brief Adjusts time that should be sent to nodes connected to certain inputs. * diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 96c2f6145..daa15748a 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -31,10 +31,8 @@ OLIVE_NAMESPACE_ENTER TrackOutput::TrackOutput() : track_type_(Timeline::kTrackTypeNone), - block_invalidate_cache_stack_(0), index_(-1), - locked_(false), - queued_length_change_(false) + locked_(false) { block_input_ = new NodeInputArray("block_in", NodeParam::kAny); block_input_->set_is_keyframable(false); @@ -252,11 +250,24 @@ const QList &TrackOutput::Blocks() const void TrackOutput::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source) { - if (block_invalidate_cache_stack_ == 0) { - PushLengthChangeSignal(true); + TimeRange limited; - Node::InvalidateCache(TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), track_length())), from, source); + if (block_input_->sub_params().contains(from) + && from->get_connected_node() + && from->get_connected_node()->IsBlock()) { + // Limit the range signal to the corresponding block + Block* b = static_cast(from->get_connected_node()); + + if (range.out() <= b->in() || range.in() >= b->out()) { + return; + } + + limited = TimeRange(qMax(range.in(), b->in()), qMin(range.out(), b->out())); + } else { + limited = TimeRange(qMax(range.in(), rational(0)), qMin(range.out(), track_length())); } + + Node::InvalidateCache(limited, from, source); } void TrackOutput::InsertBlockBefore(Block* block, Block* after) @@ -279,12 +290,12 @@ void TrackOutput::InsertBlockAfter(Block *block, Block *before) void TrackOutput::PrependBlock(Block *block) { - BlockInvalidateCache(); + BeginOperation(); block_input_->Prepend(); NodeParam::ConnectEdge(block->output(), block_input_->First()); - UnblockInvalidateCache(); + EndOperation(); // Everything has shifted at this point InvalidateCache(TimeRange(0, track_length()), block_input_, block_input_); @@ -292,57 +303,47 @@ void TrackOutput::PrependBlock(Block *block) void TrackOutput::InsertBlockAtIndex(Block *block, int index) { - BlockInvalidateCache(); + BeginOperation(); int insert_index = GetInputIndexFromCacheIndex(index); block_input_->InsertAt(insert_index); NodeParam::ConnectEdge(block->output(), block_input_->At(insert_index)); - UnblockInvalidateCache(); + EndOperation(); InvalidateCache(TimeRange(block->in(), track_length()), block_input_, block_input_); } void TrackOutput::AppendBlock(Block *block) { - BlockInvalidateCache(); + BeginOperation(); block_input_->Append(); NodeParam::ConnectEdge(block->output(), block_input_->Last()); - UnblockInvalidateCache(); + EndOperation(); // Invalidate area that block was added to InvalidateCache(TimeRange(block->in(), track_length()), block_input_, block_input_); } -void TrackOutput::BlockInvalidateCache() -{ - block_invalidate_cache_stack_++; -} - -void TrackOutput::UnblockInvalidateCache() -{ - block_invalidate_cache_stack_--; -} - void TrackOutput::RippleRemoveBlock(Block *block) { - BlockInvalidateCache(); + BeginOperation(); rational remove_in = block->in(); block_input_->RemoveAt(GetInputIndexFromCacheIndex(block)); - UnblockInvalidateCache(); + EndOperation(); InvalidateCache(TimeRange(remove_in, track_length()), block_input_, block_input_); } void TrackOutput::ReplaceBlock(Block *old, Block *replace) { - BlockInvalidateCache(); + BeginOperation(); int index_of_old_block = GetInputIndexFromCacheIndex(old); @@ -352,7 +353,7 @@ void TrackOutput::ReplaceBlock(Block *old, Block *replace) NodeParam::ConnectEdge(replace->output(), block_input_->At(index_of_old_block)); - UnblockInvalidateCache(); + EndOperation(); if (old->length() == replace->length()) { InvalidateCache(TimeRange(replace->in(), replace->out()), block_input_, block_input_); @@ -443,14 +444,6 @@ void TrackOutput::Hash(QCryptographicHash &hash, const rational &time) const } } -void TrackOutput::PushLengthChangeSignal(bool invalidate) -{ - if (queued_length_change_) { - queued_length_change_ = false; - SetLengthInternal(queued_length_, invalidate); - } -} - void TrackOutput::SetTrackName(const QString &name) { track_name_ = name; @@ -507,15 +500,6 @@ int TrackOutput::GetInputIndexFromCacheIndex(Block *block) void TrackOutput::SetLengthInternal(const rational &r, bool invalidate) { - if (block_invalidate_cache_stack_ > 0) { - queued_length_change_ = true; - } - - if (queued_length_change_) { - queued_length_ = r; - return; - } - if (r != track_length_) { TimeRange invalidate_range(track_length_, r); diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 35c444a0e..ad658bcd2 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -166,10 +166,6 @@ public: */ void ReplaceBlock(Block* old, Block* replace); - void BlockInvalidateCache(); - - void UnblockInvalidateCache(); - static TrackOutput* TrackFromBlock(const Block *block); const rational& track_length() const; @@ -192,8 +188,6 @@ public: virtual void Hash(QCryptographicHash& hash, const rational &time) const override; - void PushLengthChangeSignal(bool invalidate = false); - AudioVisualWaveform& waveform() { return waveform_; @@ -274,15 +268,10 @@ private: QString track_name_; - int block_invalidate_cache_stack_; - int index_; bool locked_; - bool queued_length_change_; - rational queued_length_; - AudioVisualWaveform waveform_; QMutex waveform_lock_; diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index b5b0f495b..457ce99de 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -26,7 +26,8 @@ OLIVE_NAMESPACE_ENTER ViewerOutput::ViewerOutput() : video_frame_cache_(this), - audio_playback_cache_(this) + audio_playback_cache_(this), + operation_stack_(0) { texture_input_ = new NodeInput("tex_in", NodeInput::kTexture); AddInput(texture_input_); @@ -49,7 +50,7 @@ ViewerOutput::ViewerOutput() : TrackList* list = new TrackList(this, static_cast(i), track_input); track_lists_.replace(i, list); connect(list, &TrackList::TrackListChanged, this, &ViewerOutput::UpdateTrackCache); - connect(list, &TrackList::LengthChanged, this, &ViewerOutput::VerifyLength); + //connect(list, &TrackList::LengthChanged, this, &ViewerOutput::VerifyLength); connect(list, &TrackList::BlockAdded, this, &ViewerOutput::TrackListAddedBlock); connect(list, &TrackList::BlockRemoved, this, &ViewerOutput::BlockRemoved); connect(list, &TrackList::TrackAdded, this, &ViewerOutput::TrackListAddedTrack); @@ -111,20 +112,22 @@ void ViewerOutput::InvalidateCache(const TimeRange &range, NodeInput *from, Node { emit GraphChangedFrom(source); - if (from == texture_input_ || from == samples_input_) { - TimeRange invalidated_range(qMax(rational(), range.in()), - qMin(GetLength(), range.out())); + if (operation_stack_ == 0) { + if (from == texture_input_ || from == samples_input_) { + TimeRange invalidated_range(qMax(rational(), range.in()), + qMin(GetLength(), range.out())); - if (invalidated_range.in() != invalidated_range.out()) { - if (from == texture_input_) { - video_frame_cache_.Invalidate(invalidated_range); - } else { - audio_playback_cache_.Invalidate(invalidated_range); + if (invalidated_range.in() != invalidated_range.out()) { + if (from == texture_input_) { + video_frame_cache_.Invalidate(invalidated_range); + } else { + audio_playback_cache_.Invalidate(invalidated_range); + } } } - } - VerifyLength(); + VerifyLength(); + } Node::InvalidateCache(range, from, source); } @@ -257,6 +260,20 @@ void ViewerOutput::set_media_name(const QString &name) emit MediaNameChanged(media_name_); } +void ViewerOutput::BeginOperation() +{ + operation_stack_++; + + Node::BeginOperation(); +} + +void ViewerOutput::EndOperation() +{ + operation_stack_--; + + Node::EndOperation(); +} + void ViewerOutput::TrackListAddedBlock(Block *block, int index) { Timeline::TrackType type = static_cast(sender())->type(); diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index b2ebb3b34..e57406c46 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -118,6 +118,10 @@ public: return &audio_playback_cache_; } + virtual void BeginOperation() override; + + virtual void EndOperation() override; + signals: void TimebaseChanged(const rational&); @@ -164,6 +168,8 @@ private: AudioPlaybackCache audio_playback_cache_; + int operation_stack_; + private slots: void UpdateTrackCache(); diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 9b03b5bec..ce66be641 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -202,7 +202,7 @@ void TrackRippleRemoveAreaCommand::redo_internal() } } - track_->BlockInvalidateCache(); + track_->BeginOperation(); // If we picked up a block to splice if (splice_) { @@ -273,7 +273,7 @@ void TrackRippleRemoveAreaCommand::redo_internal() } } - track_->UnblockInvalidateCache(); + track_->EndOperation(); track_->InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX), track_->block_input(), @@ -282,7 +282,7 @@ void TrackRippleRemoveAreaCommand::redo_internal() void TrackRippleRemoveAreaCommand::undo_internal() { - track_->BlockInvalidateCache(); + track_->BeginOperation(); // If we were given a block to insert, insert it here if (insert_ != nullptr) { @@ -330,7 +330,7 @@ void TrackRippleRemoveAreaCommand::undo_internal() } - track_->UnblockInvalidateCache(); + track_->EndOperation(); track_->InvalidateCache(TimeRange(in_, insert_ ? out_ : RATIONAL_MAX), track_->block_input(), track_->block_input()); } @@ -432,7 +432,7 @@ Project *BlockSplitCommand::GetRelevantProject() const void BlockSplitCommand::redo_internal() { - track_->BlockInvalidateCache(); + track_->BeginOperation(); static_cast(block_->parent())->AddNode(new_block_); Node::CopyInputs(block_, new_block_); @@ -450,12 +450,12 @@ void BlockSplitCommand::redo_internal() NodeParam::ConnectEdge(new_block_->output(), transition); } - track_->UnblockInvalidateCache(); + track_->EndOperation(); } void BlockSplitCommand::undo_internal() { - track_->BlockInvalidateCache(); + track_->BeginOperation(); block_->set_length_and_media_out(old_length_); track_->RippleRemoveBlock(new_block_); @@ -467,7 +467,7 @@ void BlockSplitCommand::undo_internal() NodeParam::ConnectEdge(block_->output(), transition); } - track_->UnblockInvalidateCache(); + track_->EndOperation(); } Block *BlockSplitCommand::new_block() @@ -856,7 +856,7 @@ Project *BlockTrimCommand::GetRelevantProject() const void BlockTrimCommand::redo_internal() { - track_->BlockInvalidateCache(); + track_->BeginOperation(); // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer rational trim_diff = old_length_ - new_length_; @@ -923,14 +923,14 @@ void BlockTrimCommand::redo_internal() } } - track_->UnblockInvalidateCache(); + track_->EndOperation(); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } void BlockTrimCommand::undo_internal() { - track_->BlockInvalidateCache(); + track_->BeginOperation(); // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer rational trim_diff = old_length_ - new_length_; @@ -979,7 +979,7 @@ void BlockTrimCommand::undo_internal() invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff); } - track_->UnblockInvalidateCache(); + track_->EndOperation(); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } @@ -1003,7 +1003,7 @@ void TrackReplaceBlockWithGapCommand::redo_internal() { TimeRange invalidate_range; - track_->BlockInvalidateCache(); + track_->BeginOperation(); // If the block has no next, it's at the end of the track and there's no need to create a gap if (block_->next()) { @@ -1062,7 +1062,7 @@ void TrackReplaceBlockWithGapCommand::redo_internal() invalidate_range = TimeRange(earliest_change, RATIONAL_MAX); } - track_->UnblockInvalidateCache(); + track_->EndOperation(); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } @@ -1071,7 +1071,7 @@ void TrackReplaceBlockWithGapCommand::undo_internal() { TimeRange invalidate_range; - track_->BlockInvalidateCache(); + track_->BeginOperation(); if (gap_) { @@ -1116,7 +1116,7 @@ void TrackReplaceBlockWithGapCommand::undo_internal() merged_gap_ = nullptr; - track_->UnblockInvalidateCache(); + track_->EndOperation(); track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); } @@ -1156,7 +1156,7 @@ void TrackSlideCommand::slide_internal(bool undo) // Perform trims foreach (const BlockSlideInfo& info, blocks_) { - info.track->BlockInvalidateCache(); + info.track->BeginOperation(); if (info.mode == Timeline::kTrimIn || info.mode == Timeline::kTrimOut) { rational new_len = undo ? info.old_time : info.new_time; @@ -1176,7 +1176,7 @@ void TrackSlideCommand::slide_internal(bool undo) added_gaps_.append(gap); } - info.track->UnblockInvalidateCache(); + info.track->EndOperation(); } if (undo) { @@ -1184,12 +1184,12 @@ void TrackSlideCommand::slide_internal(bool undo) foreach (GapBlock* gap, added_gaps_) { TrackOutput* track = TrackOutput::TrackFromBlock(gap); - track->BlockInvalidateCache(); + track->BeginOperation(); track->RippleRemoveBlock(gap); delete TakeNodeFromParentGraph(gap); - track->UnblockInvalidateCache(); + track->EndOperation(); } added_gaps_.clear(); @@ -1253,7 +1253,7 @@ void TrackListRippleRemoveAreaCommand::redo_internal() } foreach (TrackOutput* track, working_tracks_) { - track->BlockInvalidateCache(); + track->BeginOperation(); } } @@ -1263,8 +1263,7 @@ void TrackListRippleRemoveAreaCommand::redo_internal() if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { - track->UnblockInvalidateCache(); - track->PushLengthChangeSignal(); + track->EndOperation(); } } } @@ -1281,7 +1280,7 @@ void TrackListRippleRemoveAreaCommand::undo_internal() } foreach (TrackOutput* track, working_tracks_) { - track->BlockInvalidateCache(); + track->BeginOperation(); } } @@ -1291,8 +1290,7 @@ void TrackListRippleRemoveAreaCommand::undo_internal() if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { - track->UnblockInvalidateCache(); - track->PushLengthChangeSignal(); + track->EndOperation(); } } } @@ -1338,7 +1336,7 @@ void TrackListRippleToolCommand::redo_internal() if (all_tracks_unlocked_) { // We can do some optimization here foreach (const RippleInfo& info, info_) { - info.track->BlockInvalidateCache(); + info.track->BeginOperation(); } old_latest_pt = RATIONAL_MIN; @@ -1415,15 +1413,13 @@ void TrackListRippleToolCommand::redo_internal() } foreach (const RippleInfo& info, info_) { - info.track->UnblockInvalidateCache(); + info.track->EndOperation(); // FIXME: Untested, is this desirable behavior? if (earliest_pt < new_latest_pt) { info.track->InvalidateCache(TimeRange(earliest_pt, new_latest_pt), info.track->block_input(), info.track->block_input()); - } else { - info.track->PushLengthChangeSignal(); } } } @@ -1505,7 +1501,7 @@ void TrackListInsertGaps::redo_internal() } foreach (TrackOutput* track, working_tracks_) { - track->BlockInvalidateCache(); + track->BeginOperation(); } } @@ -1545,8 +1541,7 @@ void TrackListInsertGaps::redo_internal() if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { - track->UnblockInvalidateCache(); - track->PushLengthChangeSignal(false); + track->EndOperation(); } } } @@ -1562,7 +1557,7 @@ void TrackListInsertGaps::undo_internal() } foreach (TrackOutput* track, working_tracks_) { - track->BlockInvalidateCache(); + track->BeginOperation(); } } @@ -1588,8 +1583,7 @@ void TrackListInsertGaps::undo_internal() if (all_tracks_unlocked_) { foreach (TrackOutput* track, working_tracks_) { - track->UnblockInvalidateCache(); - track->PushLengthChangeSignal(false); + track->EndOperation(); } } } From 662d66ad1021519bbe4507eb48d4eb95dceaabca Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Thu, 16 Jul 2020 00:36:21 +1000 Subject: [PATCH 069/138] transitions: extended transition logic General code and functionality improvements. Moved more code out of the OpenGL backend for portability. Implemented audio transitions. Implemented basic transition animation curve settings. --- .../crossdissolve/crossdissolvetransition.cpp | 27 ++++ .../crossdissolve/crossdissolvetransition.h | 3 + app/node/block/transition/transition.cpp | 127 +++++++++++++++--- app/node/block/transition/transition.h | 22 ++- app/node/traverser.cpp | 15 ++- app/node/traverser.h | 2 + app/render/backend/opengl/openglproxy.cpp | 30 ++--- app/render/backend/renderworker.cpp | 13 +- app/render/shaderinfo.h | 18 ++- app/shaders/crossdissolve.frag | 19 ++- 10 files changed, 224 insertions(+), 52 deletions(-) diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp index c6d471030..2dfe7ace4 100644 --- a/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.cpp @@ -59,4 +59,31 @@ ShaderCode CrossDissolveTransition::GetShaderCode(const QString &shader_id) cons return ShaderCode(Node::ReadFileAsString(":/shaders/crossdissolve.frag"), QString()); } +void CrossDissolveTransition::SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const +{ + for (int i=0; isample_count(); i++) { + double this_sample_time = out_samples->audio_params().samples_to_time(i).toDouble() + time_in; + double progress = GetTotalProgress(this_sample_time); + + for (int j=0; jaudio_params().channel_count(); j++) { + out_samples->data()[j][i] = 0; + + if (from_samples) { + if (i < from_samples->sample_count()) { + out_samples->data()[j][i] += from_samples->data()[j][i] * TransformCurve(1.0 - progress); + } + } + + if (to_samples) { + // Offset input samples from the end + int in_index = i - (out_samples->sample_count() - to_samples->sample_count()); + + if (in_index >= 0) { + out_samples->data()[j][i] += to_samples->data()[j][in_index] * TransformCurve(progress); + } + } + } + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.h b/app/node/block/transition/crossdissolve/crossdissolvetransition.h index ccd8877d3..965267e83 100644 --- a/app/node/block/transition/crossdissolve/crossdissolvetransition.h +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.h @@ -41,6 +41,9 @@ public: virtual ShaderCode GetShaderCode(const QString& shader_id) const override; +protected: + virtual void SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index aa8a77c0a..abf79edc2 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -28,17 +28,22 @@ TransitionBlock::TransitionBlock() : connected_out_block_(nullptr), connected_in_block_(nullptr) { - out_block_input_ = new NodeInput("out_block_in", NodeParam::kBuffer); + out_block_input_ = new NodeInput(QStringLiteral("out_block_in"), NodeParam::kBuffer); out_block_input_->set_is_keyframable(false); connect(out_block_input_, &NodeParam::EdgeAdded, this, &TransitionBlock::BlockConnected); connect(out_block_input_, &NodeParam::EdgeRemoved, this, &TransitionBlock::BlockDisconnected); AddInput(out_block_input_); - in_block_input_ = new NodeInput("in_block_in", NodeParam::kBuffer); + in_block_input_ = new NodeInput(QStringLiteral("in_block_in"), NodeParam::kBuffer); in_block_input_->set_is_keyframable(false); connect(in_block_input_, &NodeParam::EdgeAdded, this, &TransitionBlock::BlockConnected); connect(in_block_input_, &NodeParam::EdgeRemoved, this, &TransitionBlock::BlockDisconnected); AddInput(in_block_input_); + + curve_input_ = new NodeInput(QStringLiteral("curve_in"), NodeParam::kCombo); + curve_input_->set_is_keyframable(false); + curve_input_->set_connectable(false); + AddInput(curve_input_); } Block::Type TransitionBlock::type() const @@ -62,6 +67,10 @@ void TransitionBlock::Retranslate() out_block_input_->set_name(tr("From")); in_block_input_->set_name(tr("To")); + curve_input_->set_name(tr("Curve")); + + // These must correspond to the CurveType enum + curve_input_->set_combobox_strings({ tr("Linear"), tr("Exponential"), tr("Logarithmic") }); } rational TransitionBlock::in_offset() const @@ -106,12 +115,12 @@ Block *TransitionBlock::connected_in_block() const return connected_in_block_; } -double TransitionBlock::GetTotalProgress(const rational &time) const +double TransitionBlock::GetTotalProgress(const double &time) const { return GetInternalTransitionTime(time) / length().toDouble(); } -double TransitionBlock::GetOutProgress(const rational &time) const +double TransitionBlock::GetOutProgress(const double &time) const { if (out_offset() == 0) { return 0; @@ -120,7 +129,7 @@ double TransitionBlock::GetOutProgress(const rational &time) const return clamp(1.0 - (GetInternalTransitionTime(time) / out_offset().toDouble()), 0.0, 1.0); } -double TransitionBlock::GetInProgress(const rational &time) const +double TransitionBlock::GetInProgress(const double &time) const { if (in_offset() == 0) { return 0; @@ -133,18 +142,34 @@ void TransitionBlock::Hash(QCryptographicHash &hash, const rational &time) const { Node::Hash(hash, time); - double all_prog = GetTotalProgress(time); - double in_prog = GetInProgress(time); - double out_prog = GetOutProgress(time); + double time_dbl = time.toDouble(); + double all_prog = GetTotalProgress(time_dbl); + double in_prog = GetInProgress(time_dbl); + double out_prog = GetOutProgress(time_dbl); hash.addData(reinterpret_cast(&all_prog), sizeof(double)); hash.addData(reinterpret_cast(&in_prog), sizeof(double)); hash.addData(reinterpret_cast(&out_prog), sizeof(double)); } -double TransitionBlock::GetInternalTransitionTime(const rational &time) const +double TransitionBlock::GetInternalTransitionTime(const double &time) const { - return time.toDouble() - in().toDouble(); + return time - in().toDouble(); +} + +void TransitionBlock::InsertTransitionTimes(AcceleratedJob *job, const double &time) const +{ + // Provides total transition progress from 0.0 (start) - 1.0 (end) + job->InsertValue(QStringLiteral("ove_tprog_all"), + NodeValue(NodeParam::kFloat, GetTotalProgress(time), this)); + + // Provides progress of out section from 1.0 (start) - 0.0 (end) + job->InsertValue(QStringLiteral("ove_tprog_out"), + NodeValue(NodeParam::kFloat, GetOutProgress(time), this)); + + // Provides progress of in section from 0.0 (start) - 1.0 (end) + job->InsertValue(QStringLiteral("ove_tprog_in"), + NodeValue(NodeParam::kFloat, GetInProgress(time), this)); } void TransitionBlock::BlockConnected(NodeEdgePtr edge) @@ -173,16 +198,62 @@ void TransitionBlock::BlockDisconnected(NodeEdgePtr edge) NodeValueTable TransitionBlock::Value(NodeValueDatabase &value) const { - ShaderJob job; + NodeParam::DataType data_type; - job.InsertValue(out_block_input(), value); - job.InsertValue(in_block_input(), value); - job.SetAlphaChannelRequired(true); + if (out_block_input()->is_connected()) { + data_type = value[out_block_input()].GetWithMeta(NodeParam::kBuffer).type(); + } else if (in_block_input()->is_connected()) { + data_type = value[in_block_input()].GetWithMeta(NodeParam::kBuffer).type(); + } else { + data_type = NodeParam::kNone; + } - ShaderJobEvent(value, job); + NodeParam::DataType job_type; + QVariant push_job; + + if (data_type == NodeParam::kTexture) { + // This must be a visual transition + ShaderJob job; + + job.InsertValue(out_block_input(), value); + job.InsertValue(in_block_input(), value); + job.InsertValue(curve_input_, value); + job.SetAlphaChannelRequired(true); + + double time = value[QStringLiteral("global")].Get(NodeParam::kFloat, QStringLiteral("time_in")).toDouble(); + InsertTransitionTimes(&job, time); + + ShaderJobEvent(value, job); + + job_type = NodeParam::kShaderJob; + push_job = QVariant::fromValue(job); + } else if (data_type == NodeParam::kSamples) { + // This must be an audio transition + SampleBufferPtr from_samples = value[out_block_input()].Take(NodeParam::kBuffer).value(); + SampleBufferPtr to_samples = value[in_block_input()].Take(NodeParam::kBuffer).value(); + + if (from_samples || to_samples) { + double time_in = value[QStringLiteral("global")].Get(NodeParam::kFloat, QStringLiteral("time_in")).toDouble(); + double time_out = value[QStringLiteral("global")].Get(NodeParam::kFloat, QStringLiteral("time_out")).toDouble(); + + const AudioParams& params = (from_samples) ? from_samples->audio_params() : to_samples->audio_params(); + + int nb_samples = params.time_to_samples(time_out - time_in); + + SampleBufferPtr out_samples = SampleBuffer::CreateAllocated(params, nb_samples); + SampleJobEvent(from_samples, to_samples, out_samples, time_in); + + job_type = NodeParam::kSamples; + push_job = QVariant::fromValue(out_samples); + } + } NodeValueTable table = value.Merge(); - table.Push(NodeParam::kShaderJob, QVariant::fromValue(job), this); + + if (!push_job.isNull()) { + table.Push(job_type, push_job, this); + } + return table; } @@ -192,4 +263,28 @@ void TransitionBlock::ShaderJobEvent(NodeValueDatabase &value, ShaderJob &job) c Q_UNUSED(job) } +void TransitionBlock::SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const +{ + Q_UNUSED(from_samples) + Q_UNUSED(to_samples) + Q_UNUSED(out_samples) + Q_UNUSED(time_in) +} + +double TransitionBlock::TransformCurve(double linear) const +{ + switch (static_cast(curve_input_->get_standard_value().toInt())) { + case kLinear: + break; + case kExponential: + linear *= linear; + break; + case kLogarithmic: + linear = qSqrt(linear); + break; + } + + return linear; +} + OLIVE_NAMESPACE_EXIT diff --git a/app/node/block/transition/transition.h b/app/node/block/transition/transition.h index 50d6ac204..34a6f074d 100644 --- a/app/node/block/transition/transition.h +++ b/app/node/block/transition/transition.h @@ -43,9 +43,9 @@ public: Block* connected_out_block() const; Block* connected_in_block() const; - double GetTotalProgress(const rational& time) const; - double GetOutProgress(const rational& time) const; - double GetInProgress(const rational& time) const; + double GetTotalProgress(const double &time) const; + double GetOutProgress(const double &time) const; + double GetInProgress(const double &time) const; virtual void Hash(QCryptographicHash& hash, const rational &time) const override; @@ -54,13 +54,27 @@ public: protected: virtual void ShaderJobEvent(NodeValueDatabase &value, ShaderJob& job) const; + virtual void SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const; + + double TransformCurve(double linear) const; + private: - double GetInternalTransitionTime(const rational& time) const; + enum CurveType { + kLinear, + kExponential, + kLogarithmic + }; + + double GetInternalTransitionTime(const double &time) const; + + void InsertTransitionTimes(AcceleratedJob* job, const double& time) const; NodeInput* out_block_input_; NodeInput* in_block_input_; + NodeInput* curve_input_; + Block* connected_out_block_; Block* connected_in_block_; diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index d613060c8..5ba40748f 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -41,11 +41,7 @@ NodeValueDatabase NodeTraverser::GenerateDatabase(const Node* node, const TimeRa database.Insert(input, ProcessInput(input, input_time)); } - // Insert global variables - NodeValueTable global; - global.Push(NodeParam::kFloat, range.in().toDouble(), nullptr, QStringLiteral("time_in")); - global.Push(NodeParam::kFloat, range.out().toDouble(), nullptr, QStringLiteral("time_out")); - database.Insert(QStringLiteral("global"), global); + AddGlobalsToDatabase(database, range); return database; } @@ -156,6 +152,15 @@ QVariant NodeTraverser::GetCachedFrame(const Node *node, const rational &time) return QVariant(); } +void NodeTraverser::AddGlobalsToDatabase(NodeValueDatabase &db, const TimeRange& range) +{ + // Insert global variables + NodeValueTable global; + global.Push(NodeParam::kFloat, range.in().toDouble(), nullptr, QStringLiteral("time_in")); + global.Push(NodeParam::kFloat, range.out().toDouble(), nullptr, QStringLiteral("time_out")); + db.Insert(QStringLiteral("global"), global); +} + void NodeTraverser::PostProcessTable(const Node *node, const TimeRange &range, NodeValueTable &output_params) { bool got_cached_frame = false; diff --git a/app/node/traverser.h b/app/node/traverser.h index e9e1523fc..e9a377ae4 100644 --- a/app/node/traverser.h +++ b/app/node/traverser.h @@ -56,6 +56,8 @@ protected: virtual QVariant GetCachedFrame(const Node *node, const rational &time); + static void AddGlobalsToDatabase(NodeValueDatabase& db, const TimeRange &range); + private: void PostProcessTable(const Node *node, const TimeRange &range, NodeValueTable &output_params); diff --git a/app/render/backend/opengl/openglproxy.cpp b/app/render/backend/opengl/openglproxy.cpp index 32c24f92d..fff23b55d 100644 --- a/app/render/backend/opengl/openglproxy.cpp +++ b/app/render/backend/opengl/openglproxy.cpp @@ -279,18 +279,21 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, NodeValueMap::const_iterator it; for (it=job.GetValues().constBegin(); it!=job.GetValues().constEnd(); it++) { // See if the shader has takes this parameter as an input - int variable_location = shader->uniformLocation(it.key()->id()); + int variable_location = shader->uniformLocation(it.key()); if (variable_location == -1) { continue; } + // See if this value corresponds to an input (NOTE: it may not and this may be null) + NodeInput* corresponding_input = node->GetInputWithID(it.key()); + // This variable is used in the shader, let's set it const QVariant& value = it.value().data(); NodeParam::DataType data_type = (it.value().type() != NodeParam::kNone) ? it.value().type() - : it.key()->data_type(); + : corresponding_input->data_type(); switch (data_type) { case NodeInput::kInt: @@ -300,7 +303,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValue(variable_location, value.toFloat()); break; case NodeInput::kVec2: - if (it.key()->IsArray()) { + if (corresponding_input && corresponding_input->IsArray()) { QVector nv = value.value< QVector >(); QVector a(nv.size()); @@ -310,7 +313,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValueArray(variable_location, a.constData(), a.size()); - int count_location = shader->uniformLocation(QStringLiteral("%1_count").arg(it.key()->id())); + int count_location = shader->uniformLocation(QStringLiteral("%1_count").arg(it.key())); if (count_location > -1) { shader->setUniformValue(count_location, a.size()); } @@ -355,7 +358,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, shader->setUniformValue(variable_location, textures_to_bind.size()); // If this texture binding is the iterative input, set it here - if (it.key() == job.GetIterativeInput()) { + if (corresponding_input && corresponding_input == job.GetIterativeInput()) { iterative_input = textures_to_bind.size(); } @@ -363,7 +366,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, textures_to_bind.append(tex_id); // Set enable flag if shader wants it - int enable_param_location = shader->uniformLocation(QStringLiteral("%1_enabled").arg(it.key()->id())); + int enable_param_location = shader->uniformLocation(QStringLiteral("%1_enabled").arg(it.key())); if (enable_param_location > -1) { shader->setUniformValue(enable_param_location, tex_id > 0); @@ -371,7 +374,7 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, if (tex_id > 0) { // Set texture resolution if shader wants it - int res_param_location = shader->uniformLocation(QStringLiteral("%1_resolution").arg(it.key()->id())); + int res_param_location = shader->uniformLocation(QStringLiteral("%1_resolution").arg(it.key())); if (res_param_location > -1) { shader->setUniformValue(res_param_location, static_cast(texture->texture()->width() * texture->texture()->divider()), @@ -404,19 +407,6 @@ QVariant OpenGLProxy::RunNodeAccelerated(const Node *node, static_cast(params.width()), static_cast(params.height())); - if (node->IsBlock() && static_cast(node)->type() == Block::kTransition) { - const TransitionBlock* transition_node = static_cast(node); - - // Provides total transition progress from 0.0 (start) - 1.0 (end) - shader->setUniformValue("ove_tprog_all", static_cast(transition_node->GetTotalProgress(range.in()))); - - // Provides progress of out section from 1.0 (start) - 0.0 (end) - shader->setUniformValue("ove_tprog_out", static_cast(transition_node->GetOutProgress(range.in()))); - - // Provides progress of in section from 0.0 (start) - 1.0 (end) - shader->setUniformValue("ove_tprog_in", static_cast(transition_node->GetInProgress(range.in()))); - } - shader->release(); // Create the output textures diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index 37de178b4..31194a105 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -230,9 +230,20 @@ QVariant RenderWorker::ProcessSamples(const Node *node, const TimeRange &range, // Update all non-sample and non-footage inputs NodeValueMap::const_iterator j; for (j=job.GetValues().constBegin(); j!=job.GetValues().constEnd(); j++) { - value_db.Insert(j.key(), ProcessInput(j.key(), TimeRange(this_sample_time, this_sample_time))); + NodeValueTable value; + NodeInput* corresponding_input = node->GetInputWithID(j.key()); + + if (corresponding_input) { + value = ProcessInput(corresponding_input, TimeRange(this_sample_time, this_sample_time)); + } else { + value.Push(j.value()); + } + + value_db.Insert(j.key(), value); } + AddGlobalsToDatabase(value_db, TimeRange(this_sample_time, this_sample_time)); + node->ProcessSamples(value_db, job.samples(), output_buffer, diff --git a/app/render/shaderinfo.h b/app/render/shaderinfo.h index 9db03d9e0..1fa685e2b 100644 --- a/app/render/shaderinfo.h +++ b/app/render/shaderinfo.h @@ -8,13 +8,18 @@ OLIVE_NAMESPACE_ENTER -using NodeValueMap = QHash; +using NodeValueMap = QHash; class AcceleratedJob { public: AcceleratedJob() = default; NodeValue GetValue(NodeInput* input) const + { + return value_map_.value(input->id()); + } + + NodeValue GetValue(const QString& input) const { return value_map_.value(input); } @@ -31,15 +36,20 @@ public: values[j] = value[subparam].TakeWithMeta(subparam->data_type()); } - value_map_.insert(input, NodeValue(NodeParam::kVec2, QVariant::fromValue(values), input->parentNode())); + InsertValue(input->id(), NodeValue(NodeParam::kVec2, QVariant::fromValue(values), input->parentNode())); } else { - value_map_.insert(input, value[input].TakeWithMeta(input->data_type())); + InsertValue(input->id(), value[input].TakeWithMeta(input->data_type())); } } + void InsertValue(const QString& input, const NodeValue& value) + { + value_map_.insert(input, value); + } + void InsertValue(NodeInput* input, const NodeValue& value) { - value_map_.insert(input, value); + value_map_.insert(input->id(), value); } const NodeValueMap &GetValues() const diff --git a/app/shaders/crossdissolve.frag b/app/shaders/crossdissolve.frag index 2af4b17b4..e9461244c 100644 --- a/app/shaders/crossdissolve.frag +++ b/app/shaders/crossdissolve.frag @@ -1,9 +1,14 @@ #version 150 +#define LINEAR_CURVE 0 +#define EXPONENTIAL_CURVE 1 +#define LOGARITHMIC_CURVE 2 + uniform sampler2D out_block_in; uniform sampler2D in_block_in; uniform bool out_block_in_enabled; uniform bool in_block_in_enabled; +uniform int curve_in; uniform float ove_tprog_all; @@ -11,15 +16,25 @@ in vec2 ove_texcoord; out vec4 fragColor; +float TransformCurve(float linear) { + if (curve_in == EXPONENTIAL_CURVE) { + return linear * linear; + } else if (curve_in == LOGARITHMIC_CURVE) { + return sqrt(linear); + } else { + return linear; + } +} + void main(void) { vec4 composite = vec4(0.0); if (out_block_in_enabled) { - composite += texture(out_block_in, ove_texcoord) * (1.0 - ove_tprog_all); + composite += texture(out_block_in, ove_texcoord) * TransformCurve(1.0 - ove_tprog_all); } if (in_block_in_enabled) { - composite += texture(in_block_in, ove_texcoord) * ove_tprog_all; + composite += texture(in_block_in, ove_texcoord) * TransformCurve(ove_tprog_all); } fragColor = composite; From 6309b8fbb7637d9ef38cfa0462f0fe685476eccc Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 17 Jul 2020 12:33:28 +1000 Subject: [PATCH 070/138] math: fixed power function Corrected shader code and fixed bug in widget bridge that reported an incorrect index. --- app/node/math/math/mathbase.cpp | 11 ++++++++++- .../nodeparamview/nodeparamviewwidgetbridge.cpp | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/node/math/math/mathbase.cpp b/app/node/math/math/mathbase.cpp index b5ee9d76d..4a793af93 100644 --- a/app/node/math/math/mathbase.cpp +++ b/app/node/math/math/mathbase.cpp @@ -65,7 +65,16 @@ ShaderCode MathNodeBase::GetShaderCodeInternal(const QString &shader_id, NodeInp operation = QStringLiteral("%1 / %2"); break; case kOpPower: - operation = QStringLiteral("pow(%1, %2)"); + if (pairing == kPairTextureNumber) { + // The "number" in this operation has to be declared a vec4 + if (type_a & NodeParam::kNumber) { + operation = QStringLiteral("pow(%2, vec4(%1))"); + } else { + operation = QStringLiteral("pow(%1, vec4(%2))"); + } + } else { + operation = QStringLiteral("pow(%1, %2)"); + } break; } diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 9950864d7..456323238 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -363,7 +363,17 @@ void NodeParamViewWidgetBridge::WidgetCallback() case NodeParam::kCombo: { // Widget is a QComboBox - SetInputValue(static_cast(widgets_.first())->currentIndex(), 0); + QComboBox* cb = static_cast(widgets_.first()); + int index = cb->currentIndex(); + + // Subtract any splitters up until this point + for (int i=index-1; i>=0; i--) { + if (cb->itemData(i, Qt::AccessibleDescriptionRole).toString() == QStringLiteral("separator")) { + index--; + } + } + + SetInputValue(index, 0); break; } } From f4da255b35a40ae0f3ca0d13f65408cd8b97a951 Mon Sep 17 00:00:00 2001 From: Thomas Wilshaw Date: Fri, 17 Jul 2020 17:35:29 +0100 Subject: [PATCH 071/138] Attempt to fix CI build errors. --- .../block/transition/crossdissolve/crossdissolvetransition.h | 2 +- app/window/mainwindow/mainwindow.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/node/block/transition/crossdissolve/crossdissolvetransition.h b/app/node/block/transition/crossdissolve/crossdissolvetransition.h index 965267e83..e45d1de67 100644 --- a/app/node/block/transition/crossdissolve/crossdissolvetransition.h +++ b/app/node/block/transition/crossdissolve/crossdissolvetransition.h @@ -42,7 +42,7 @@ public: virtual ShaderCode GetShaderCode(const QString& shader_id) const override; protected: - virtual void SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const; + virtual void SampleJobEvent(SampleBufferPtr from_samples, SampleBufferPtr to_samples, SampleBufferPtr out_samples, double time_in) const override; }; diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index c34ee5764..4825c9b61 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -358,7 +358,7 @@ void MainWindow::SetApplicationProgressStatus(ProgressStatus status) taskbar_interface_->SetProgressState(reinterpret_cast(this->winId()), TBPF_NORMAL); break; case kProgressNone: - taskbar_interface_->SetProgressState(reinterpret_ cast(this->winId()), TBPF_NOPROGRESS); + taskbar_interface_->SetProgressState(reinterpret_cast(this->winId()), TBPF_NOPROGRESS); break; case kProgressError: taskbar_interface_->SetProgressState(reinterpret_cast(this->winId()), TBPF_ERROR); From d54d59fac6c63361f15d16f2688d8d5abba5568b Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 19 Jul 2020 11:33:51 +1000 Subject: [PATCH 072/138] transition: remove alpha channel requirement --- app/node/block/transition/transition.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index abf79edc2..0fb644e94 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -218,7 +218,6 @@ NodeValueTable TransitionBlock::Value(NodeValueDatabase &value) const job.InsertValue(out_block_input(), value); job.InsertValue(in_block_input(), value); job.InsertValue(curve_input_, value); - job.SetAlphaChannelRequired(true); double time = value[QStringLiteral("global")].Get(NodeParam::kFloat, QStringLiteral("time_in")).toDouble(); InsertTransitionTimes(&job, time); From 70b21d6c2a16787e175b82f620339ebddbccd0af Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Sun, 19 Jul 2020 12:03:31 +1000 Subject: [PATCH 073/138] renderer: always prioritize array parent over children Fixes common segfault. --- app/render/backend/renderbackend.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/render/backend/renderbackend.cpp b/app/render/backend/renderbackend.cpp index 5e1a1f018..e8e3a3f6b 100644 --- a/app/render/backend/renderbackend.cpp +++ b/app/render/backend/renderbackend.cpp @@ -227,8 +227,15 @@ void RenderBackend::NodeGraphChanged(NodeInput *source) return; } + // Check if the source is a member of this array, in which case it'll be copied eventually anyway + if (queued_input->IsArray() + && static_cast(queued_input)->sub_params().contains(source)) { + return; + } + // Check if this input supersedes an already queued input - if (queued_input->parentNode()->OutputsTo(source, true)) { + if (queued_input->parentNode()->OutputsTo(source, true) + || (source->IsArray() && static_cast(source)->sub_params().contains(queued_input))) { // In which case, we don't need to queue it and can queue our own graph_update_queue_.removeAt(i); i--; From 4268ccd75e384d34353161b695b49fc23c2f9421 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 22 Jul 2020 10:32:04 +1000 Subject: [PATCH 074/138] importtool: fixed broken snapping Snap points were made before the ghost's in/outs were set resulting in incorrect points. --- app/widget/timelinewidget/tool/import.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 5d9150388..8a409f0ab 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -258,9 +258,6 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi // Increment track count for this track type track_offsets[track_type]++; - snap_points_.append(ghost->In()); - snap_points_.append(ghost->Out()); - ghost->setData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); ghost->SetMode(Timeline::kMove); @@ -277,6 +274,9 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi ghost->SetIn(ghost_start); ghost->SetOut(ghost_start + footage_duration); + snap_points_.append(ghost->In()); + snap_points_.append(ghost->Out()); + parent()->AddGhost(ghost); } From 73769d9e8520f6b7e79954939d7fb3c8ee3dd0a2 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 22 Jul 2020 10:33:06 +1000 Subject: [PATCH 075/138] mainmenu: set snapping menu item's checked value on startup Since this action handles the key press, if it's checked value is not equal to the real value, it will be a no-op first before it "syncs" with the real value. --- app/window/mainwindow/mainmenu.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 004b03877..07db2641b 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -262,6 +262,7 @@ MainMenu::MainMenu(MainWindow *parent) : tools_snapping_item_ = tools_menu_->AddItem("snapping", Core::instance(), &Core::SetSnapping, "S"); tools_snapping_item_->setCheckable(true); + tools_snapping_item_->setChecked(Core::instance()->snapping()); tools_menu_->addSeparator(); From ce353ccbedc03b7159c01651bd644c9dcaf1ee04 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Wed, 22 Jul 2020 18:09:57 +1000 Subject: [PATCH 076/138] timeline: various work to improve timeline behavior, particularly when dealing with transitions --- app/node/block/transition/transition.cpp | 33 + app/node/block/transition/transition.h | 4 + app/node/node.cpp | 7 +- app/node/node.h | 2 +- app/widget/timelinewidget/timelinewidget.cpp | 95 +-- app/widget/timelinewidget/timelinewidget.h | 47 +- app/widget/timelinewidget/tool/import.cpp | 6 +- app/widget/timelinewidget/tool/pointer.cpp | 642 ++++++++++++------ app/widget/timelinewidget/tool/ripple.cpp | 9 +- app/widget/timelinewidget/tool/rolling.cpp | 36 +- app/widget/timelinewidget/tool/slide.cpp | 94 +-- app/widget/timelinewidget/tool/tool.cpp | 14 +- app/widget/timelinewidget/undo/undo.cpp | 177 ++++- app/widget/timelinewidget/undo/undo.h | 24 + .../view/timelineviewghostitem.cpp | 10 +- .../view/timelineviewghostitem.h | 4 +- 16 files changed, 737 insertions(+), 467 deletions(-) diff --git a/app/node/block/transition/transition.cpp b/app/node/block/transition/transition.cpp index 0fb644e94..3b339f536 100644 --- a/app/node/block/transition/transition.cpp +++ b/app/node/block/transition/transition.cpp @@ -256,6 +256,39 @@ NodeValueTable TransitionBlock::Value(NodeValueDatabase &value) const return table; } +TransitionBlock *GetBlockTransitionInternal(Block *block, Timeline::MovementMode mode) +{ + // See if this block outputs to a transition + foreach (NodeEdgePtr edge, block->output()->edges()) { + Node* connected_node = edge->input()->parentNode(); + + if (connected_node->IsBlock()) { + Block* connected_block = static_cast(connected_node); + + if (connected_block->type() == Block::kTransition) { + TransitionBlock* connected_transition = static_cast(connected_block); + + if ((mode == Timeline::kTrimIn && edge->input() == connected_transition->in_block_input()) + || (mode == Timeline::kTrimOut && edge->input() == connected_transition->out_block_input())) { + return connected_transition; + } + } + } + } + + return nullptr; +} + +TransitionBlock *TransitionBlock::GetBlockInTransition(Block *block) +{ + return GetBlockTransitionInternal(block, Timeline::kTrimIn); +} + +TransitionBlock *TransitionBlock::GetBlockOutTransition(Block *block) +{ + return GetBlockTransitionInternal(block, Timeline::kTrimOut); +} + void TransitionBlock::ShaderJobEvent(NodeValueDatabase &value, ShaderJob &job) const { Q_UNUSED(value) diff --git a/app/node/block/transition/transition.h b/app/node/block/transition/transition.h index 34a6f074d..d8c5c6a39 100644 --- a/app/node/block/transition/transition.h +++ b/app/node/block/transition/transition.h @@ -51,6 +51,10 @@ public: virtual NodeValueTable Value(NodeValueDatabase &value) const override; + static TransitionBlock* GetBlockInTransition(Block* block); + + static TransitionBlock* GetBlockOutTransition(Block* block); + protected: virtual void ShaderJobEvent(NodeValueDatabase &value, ShaderJob& job) const; diff --git a/app/node/node.cpp b/app/node/node.cpp index 051575c05..ec38c7fdb 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -556,7 +556,7 @@ bool Node::OutputsTo(const QString &id, bool recursively) const return false; } -bool Node::OutputsTo(NodeInput *input, bool recursively) const +bool Node::OutputsTo(NodeInput *input, bool recursively, bool include_arrays) const { QList outputs = GetOutputs(); @@ -566,7 +566,10 @@ bool Node::OutputsTo(NodeInput *input, bool recursively) const if (connected == input) { return true; - } else if (recursively && connected->parentNode()->OutputsTo(input, recursively)) { + } else if (include_arrays && input->IsArray() + && static_cast(input)->sub_params().contains(connected)) { + return true; + } else if (recursively && connected->parentNode()->OutputsTo(input, recursively, include_arrays)) { return true; } } diff --git a/app/node/node.h b/app/node/node.h index 9260ed44f..ba05359ce 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -224,7 +224,7 @@ public: /** * @brief Same as OutputsTo(Node*), but for a specific node input rather than just a node. */ - bool OutputsTo(NodeInput* input, bool recursively) const; + bool OutputsTo(NodeInput* input, bool recursively, bool include_arrays) const; /** * @brief Returns whether this node ever receives an input from a particular node instance diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index e8ac028bd..63163da0e 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -472,67 +472,13 @@ void TimelineWidget::SplitAtPlayhead() } } -void TimelineWidget::DeleteSelectedInternal(const QList &blocks, - bool transition_aware, +void TimelineWidget::ReplaceBlocksWithGaps(const QList &blocks, bool remove_from_graph, QUndoCommand *command) { foreach (Block* b, blocks) { TrackOutput* original_track = TrackOutput::TrackFromBlock(b); - /*if (transition_aware && b->type() == Block::kTransition) { - // Deleting transitions restores their in/out offsets to their attached blocks - TransitionBlock* transition = static_cast(b); - - // Ripple remove transition - new TrackRippleRemoveBlockCommand(original_track, - transition, - command); - - // Resize attached blocks to make up length - if (transition->connected_in_block()) { - new BlockResizeWithMediaInCommand(transition->connected_in_block(), - transition->connected_in_block()->length() + transition->in_offset(), - command); - } - - if (transition->connected_out_block()) { - new BlockResizeCommand(transition->connected_out_block(), - transition->connected_out_block()->length() + transition->out_offset(), - command); - } - } else */ - - - /* - if (b->next()) { - - new TrackRippleRemoveBlockCommand(original_track, b, command); - - if (b->previous() && b->previous()->type() == Block::kGap - && b->next() && b->next()->type() == Block::kGap) { - - // Both previous AND next are blocks. We'll want to merge them together. - new TrackRippleRemoveBlockCommand(original_track, b->next(), command); - - } else { - - // Make new gap and replace old Block with it for now - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(b->length()); - - new NodeAddCommand(static_cast(b->parent()), - gap, - command); - - new TrackReplaceBlockCommand(original_track, - b, - gap, - command); - } - } - */ - new TrackReplaceBlockWithGapCommand(original_track, b, command); if (remove_from_graph) { @@ -566,17 +512,30 @@ void TimelineWidget::DeleteSelected(bool ripple) QUndoCommand* command = new QUndoCommand(); - // Replace blocks with gaps (effectively deleting them) - DeleteSelectedInternal(blocks_to_delete, true, true, command); + QList clips_to_delete; + QList transitions_to_delete; - /* - // Clean each track - foreach (const TrackReference& track, tracks_affected) { - new TrackCleanGapsCommand(GetConnectedNode()->track_list(track.type()), - track.index(), - command); + foreach (Block* b, blocks_to_delete) { + if (b->type() == Block::kClip) { + clips_to_delete.append(b); + } else if (b->type() == Block::kTransition) { + transitions_to_delete.append(static_cast(b)); + } + } + + // Replace clips with gaps (effectively deleting them) + ReplaceBlocksWithGaps(clips_to_delete, true, command); + + // For transitions, remove them but extend their attached blocks to fill their place + foreach (TransitionBlock* transition, transitions_to_delete) { + new TransitionRemoveCommand(TrackOutput::TrackFromBlock(transition), + transition, + command); + + new NodeRemoveWithExclusiveDeps(static_cast(GetConnectedNode()->parent()), + transition, + command); } - */ // Insert ripple command now that it's all cleaned up gaps if (ripple) { @@ -634,12 +593,16 @@ void TimelineWidget::ToggleLinksOnSelected() { QList sel = GetSelectedBlocks(); - // Prioritize unlinking - QList blocks; bool link = true; foreach (TimelineViewBlockItem* item, sel) { + // Only clips can be linked + if (item->block()->type() != Block::kClip) { + continue; + } + + // Prioritize unlinking, if any block has links, assume we're unlinking if (link && item->block()->HasLinks()) { link = false; } diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index fbd76a47f..c3d6d5912 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -26,6 +26,7 @@ #include #include "core.h" +#include "node/block/transition/transition.h" #include "node/output/viewer/viewer.h" #include "snapservice.h" #include "timeline/timelinecommon.h" @@ -189,18 +190,18 @@ private: * Validation is the process of ensuring that whatever movements the user is making are "valid" and "legal". This * function's validation ensures that no Ghost's in point ends up in a negative timecode. */ - rational ValidateTimeMovement(rational movement, const QVector ghosts); + rational ValidateTimeMovement(rational movement); /** * @brief Validates Ghosts that are moving vertically (track-based) * * This function's validation ensures that no Ghost's track ends up in a negative (non-existent) track. */ - int ValidateTrackMovement(int movement, const QVector ghosts); + int ValidateTrackMovement(int movement, const QVector &ghosts); - void GetGhostData(const QVector& ghosts, rational *earliest_point, rational *latest_point); + void GetGhostData(rational *earliest_point, rational *latest_point); - void InsertGapsAtGhostDestination(const QVector& ghosts, QUndoCommand* command); + void InsertGapsAtGhostDestination(QUndoCommand* command); QList snap_points_; @@ -242,9 +243,9 @@ private: virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode); - TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode); + TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed); - TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, const TrackReference& track, Timeline::MovementMode mode); + TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed); /** * @brief Validates Ghosts that are getting their in points trimmed @@ -252,7 +253,7 @@ private: * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no * Ghost's length becomes 0 or negative. */ - rational ValidateInTrimming(rational movement, const QVector ghosts, bool prevent_overwriting); + rational ValidateInTrimming(rational movement); /** * @brief Validates Ghosts that are getting their out points trimmed @@ -260,10 +261,21 @@ private: * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no * Ghost's length becomes 0 or negative. */ - rational ValidateOutTrimming(rational movement, const QVector ghosts, bool prevent_overwriting); + rational ValidateOutTrimming(rational movement); virtual void ProcessDrag(const TimelineCoordinate &mouse_pos); + enum GhostMode { + kPointer, + kRolling, + kSlide + }; + + void InitiateDragInternal(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + GhostMode pointer_mode, + bool trim_overwrite_allowed); + const Timeline::MovementMode& drag_movement_mode() const { return drag_movement_mode_; @@ -284,11 +296,6 @@ private: track_movement_allowed_ = e; } - void SetTrimOverwriteAllowed(bool e) - { - trim_overwrite_allowed_ = e; - } - void SetGapTrimmingAllowed(bool e) { gap_trimming_allowed_ = e; @@ -297,16 +304,21 @@ private: private: Timeline::MovementMode IsCursorInTrimHandle(TimelineViewBlockItem* block, qreal cursor_x); - void AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode); + void AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode, bool trim_overwrite_allowed); bool IsClipTrimmable(TimelineViewBlockItem* clip, const QList& items, const Timeline::MovementMode& mode); + void ProcessGhostsForSliding(); + + void ProcessGhostsForRolling(); + + bool AddMovingTransitionsToClipGhost(Block *block, const TrackReference &track, Timeline::MovementMode movement, const QList &selected_items); + bool movement_allowed_; bool trimming_allowed_; bool track_movement_allowed_; - bool trim_overwrite_allowed_; bool gap_trimming_allowed_; bool rubberband_selecting_; @@ -383,8 +395,6 @@ private: RollingTool(TimelineWidget* parent); protected: - virtual void FinishDrag(TimelineViewMouseEvent *event) override; - virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) override; }; @@ -395,7 +405,6 @@ private: SlideTool(TimelineWidget* parent); protected: - virtual void FinishDrag(TimelineViewMouseEvent *event) override; virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) override; @@ -455,7 +464,7 @@ private: void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command); - void DeleteSelectedInternal(const QList& blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command); + void ReplaceBlocksWithGaps(const QList& blocks, bool remove_from_graph, QUndoCommand* command); void SetBlockLinksSelected(Block *block, bool selected); diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 8a409f0ab..9850a6eab 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -123,14 +123,14 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) rational time_movement = event->GetFrame() - drag_start_.GetFrame(); int track_movement = event->GetTrack().index() - drag_start_.GetTrack().index(); - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); + time_movement = ValidateTimeMovement(time_movement); track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); // If snapping is enabled, check for snap points if (Core::instance()->snapping()) { parent()->SnapPoint(snap_points_, &time_movement); - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); + time_movement = ValidateTimeMovement(time_movement); track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); } @@ -398,7 +398,7 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert) // Check if we're inserting if (insert) { - InsertGapsAtGhostDestination(parent()->ghost_items_, command); + InsertGapsAtGhostDestination(command); } for (int i=0;ighost_items_.size();i++) { diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 6afac6c25..9c17f4e8d 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -41,7 +41,6 @@ TimelineWidget::PointerTool::PointerTool(TimelineWidget *parent) : movement_allowed_(true), trimming_allowed_(true), track_movement_allowed_(true), - trim_overwrite_allowed_(false), gap_trimming_allowed_(false), rubberband_selecting_(false) { @@ -119,31 +118,31 @@ void TimelineWidget::PointerTool::MouseMove(TimelineViewMouseEvent *event) if (rubberband_selecting_) { // Process rubberband select parent()->MoveRubberBandSelect(true, !(event->GetModifiers() & Qt::AltModifier)); - return; - } + } else { + // Process drag + if (!dragging_) { - if (!dragging_) { + // Now that the cursor has moved, we will assume the intention is to drag - // Now that the cursor has moved, we will assume the intention is to drag + // Clear snap points + snap_points_.clear(); - // Clear snap points - snap_points_.clear(); + // If we're performing an action, we can initiate ghosts + if (drag_movement_mode_ != Timeline::kNone) { + InitiateDrag(clicked_item_, drag_movement_mode_); + } + + // Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed + dragging_ = true; - // If we're performing an action, we can initiate ghosts - if (drag_movement_mode_ != Timeline::kNone) { - InitiateDrag(clicked_item_, drag_movement_mode_); } - // Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed - dragging_ = true; + if (dragging_ && !parent()->ghost_items_.isEmpty()) { - } - - if (dragging_ && !parent()->ghost_items_.isEmpty()) { - - // We're already dragging AND we have ghosts to work with - ProcessDrag(event->GetCoordinates()); + // We're already dragging AND we have ghosts to work with + ProcessDrag(event->GetCoordinates()); + } } } @@ -157,10 +156,12 @@ void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event) } if (dragging_) { + // If we were dragging, process the end of the drag if (!parent()->ghost_items_.isEmpty()) { FinishDrag(event); } + // Clean up parent()->ClearGhosts(); snap_points_.clear(); @@ -193,92 +194,141 @@ void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event) } } -void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) +void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *clicked_item, + Timeline::MovementMode trim_mode, + GhostMode pointer_mode, + bool trim_overwrite_allowed) { - QList ghosts_moving; - QList blocks_moving; - QList ghosts_trimming; - QList blocks_trimming; + // Get list of selected blocks + QList clips = parent()->GetSelectedBlocks(); - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (!ghost->HasBeenAdjusted()) { - continue; + if (trim_mode == Timeline::kMove) { + + // Each block type has different behavior, so we determine the type of the block that was + // clicked and filter out any others. + Block::Type clicked_block_type = clicked_item->block()->type(); + + // Gaps are not allowed to move, and since we only allow moving one block type at a time, + // dragging a gap is a no-op + if (clicked_block_type == Block::kGap) { + return; } - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + // Create ghosts for moving + foreach (TimelineViewBlockItem* clip_item, clips) { + Block* block = clip_item->block(); - if (ghost->mode() == Timeline::kMove) { - ghosts_moving.append(ghost); - blocks_moving.append(b); - } else if (Timeline::IsATrimMode(ghost->mode())) { - ghosts_trimming.append(ghost); - blocks_trimming.append(b); - } - } - - if (blocks_moving.isEmpty() && blocks_trimming.isEmpty()) { - // Likely means no block was adjusted, so we can skip the rest of the processing - return; - } - - // See if we're duplicated because ALT is held (only moved blocks can duplicate) - bool duplicate_clips = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::AltModifier); - bool inserting = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::ControlModifier); - - QUndoCommand* command = new QUndoCommand(); - - for (int i=0;iGetTrackFromReference(ghost->GetAdjustedTrack()), - blocks_trimming.at(i), - ghost->AdjustedLength(), - ghost->mode(), - command); - } - - if (!blocks_moving.isEmpty()) { - // If we're not duplicating, "remove" the clips and replace them with gaps - if (!duplicate_clips) { - parent()->DeleteSelectedInternal(blocks_moving, false, false, command); - } - - if (inserting) { - // If we're inserting, ripple everything at the destination with gaps - InsertGapsAtGhostDestination(parent()->ghost_items_, command); - } - - // Now we can re-add each clip - for (int i=0;icopy(); - - new NodeAddCommand(static_cast(block->parent()), - copy, - command); - - new NodeCopyInputsCommand(block, copy, true, command); - - // Place the copy instead of the original block - block = static_cast(copy); + if (block->type() == Block::kGap) { + // Gaps cannot move, ignore this block + continue; } - const TrackReference& track_ref = ghost->GetAdjustedTrack(); - new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(track_ref.type()), - track_ref.index(), - block, - ghost->GetAdjustedIn(), - command); + if (clicked_block_type == Block::kTransition && block->type() != Block::kTransition) { + // Transitions always slide rather than move, so if we clicked a transition, ignore any + // non-transitions + continue; + } + + // Create ghost + TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), + trim_mode, trim_overwrite_allowed); + + if (clicked_block_type == Block::kTransition) { + // Transition moves are always a slide + ghost->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); + } else { + // Set to default behavior + ghost->setData(TimelineViewGhostItem::kPointerToolMode, pointer_mode); + + // Include transitions (if any) + AddMovingTransitionsToClipGhost(block, clip_item->Track(), trim_mode, clips); + } } - // FIXME: Heavy optimization since MOST of the timeline does NOT change in this time - } + // If we slid the blocks, we must process them as such + if (clicked_block_type == Block::kTransition || pointer_mode == kSlide) { + ProcessGhostsForSliding(); + } - Core::instance()->undo_stack()->pushIfHasChildren(command); + } else { + + // "Multi-trim" is trimming a clip on more than one track. Only the earliest (for in trimming) + // or latest (for out trimming) clip on each track can be trimmed. Therefore, it's only enabled + // if the clicked item is the earliest/latest on its track. + bool multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); + + // Create ghosts for trimming + foreach (TimelineViewBlockItem* clip_item, clips) { + if (clip_item != clicked_item + && (!multitrim_enabled || !IsClipTrimmable(clip_item, clips, trim_mode))) { + // Either multitrim is disabled or this clip is NOT the earliest/latest in its track. We + // won't include it. + continue; + } + + Block* block = clip_item->block(); + Timeline::MovementMode block_mode = trim_mode; + bool block_trim_overwrite_allowed = trim_overwrite_allowed; + + // Some tools interpret "gap trimming" as equivalent to resizing the adjacent block. In that + // scenario, we include the adjacent block instead. + if (block->type() == Block::kGap && !gap_trimming_allowed_) { + block = (trim_mode == Timeline::kTrimIn) ? block->previous() : block->next(); + block_mode = FlipTrimMode(trim_mode); + + // If there's no adjacent block, do nothing here + if (!block) { + continue; + } + } + + // For transitions, we create a rolling edit with the attached clip + if (block->type() == Block::kTransition) { + TransitionBlock* transition = static_cast(block); + TimelineViewGhostItem* g = nullptr; + + Block* previous = transition->previous(); + Block* next = transition->next(); + + if (block_mode == Timeline::kTrimIn + && previous + && (previous == transition->connected_out_block() || previous->type() == Block::kGap)) { + g = AddGhostFromBlock(previous, clip_item->Track(), Timeline::kTrimOut, true); + } else if (block_mode == Timeline::kTrimOut + && next + && (next == transition->connected_in_block() || next->type() == Block::kGap)) { + g = AddGhostFromBlock(next, clip_item->Track(), Timeline::kTrimIn, true); + } + + if (g) { + g->setData(TimelineViewGhostItem::kPointerToolMode, kRolling); + block_trim_overwrite_allowed = true; + } + } + + // Create ghost for this block + TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), block_mode, block_trim_overwrite_allowed); + + if (block->type() == Block::kTransition) { + + // If this is a transition, set to rolling as above + ghost->setData(TimelineViewGhostItem::kPointerToolMode, kRolling); + + } else { + + // For trimmed clips, we also "move" the transitions if any are attached + if (AddMovingTransitionsToClipGhost(block, clip_item->Track(), trim_mode, clips)) { + ghost->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); + } else { + ghost->setData(TimelineViewGhostItem::kPointerToolMode, kPointer); + } + } + } + + if (pointer_mode == kRolling) { + ProcessGhostsForRolling(); + } + } } void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_pos) @@ -292,17 +342,17 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po rational time_movement = mouse_pos.GetFrame() - drag_start_.GetFrame(); // Validate movement (enforce all ghosts moving in legal ways) - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); - time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); - time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); + time_movement = ValidateTimeMovement(time_movement); + time_movement = ValidateInTrimming(time_movement); + time_movement = ValidateOutTrimming(time_movement); // Perform snapping if enabled (adjusts time_movement if it's close to any potential snap points) if (Core::instance()->snapping()) { parent()->SnapPoint(snap_points_, &time_movement); - time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); - time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); - time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); + time_movement = ValidateTimeMovement(time_movement); + time_movement = ValidateInTrimming(time_movement); + time_movement = ValidateOutTrimming(time_movement); } // Validate ghosts that are being moved (clips from other track types do NOT get moved) @@ -357,6 +407,140 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po parent()); } +struct GhostBlockPair { + TimelineViewGhostItem* ghost; + Block* block; +}; + +void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) +{ + QList blocks_moving; + QList blocks_sliding; + QList blocks_trimming; + + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + if (ghost->HasBeenAdjusted()) { + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (ghost->mode() == Timeline::kMove) { + if (ghost->data(TimelineViewGhostItem::kPointerToolMode) == kSlide) { + blocks_sliding.append({ghost, b}); + } else { + blocks_moving.append({ghost, b}); + } + } else if (Timeline::IsATrimMode(ghost->mode())) { + blocks_trimming.append({ghost, b}); + } + } + } + + if (blocks_moving.isEmpty() + && blocks_trimming.isEmpty() + && blocks_sliding.isEmpty()) { + // No blocks were adjusted, so nothing to do + return; + } + + // See if we're duplicated because ALT is held (only moved blocks can duplicate) + bool duplicate_clips = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::AltModifier); + bool inserting = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::ControlModifier); + + // Slide info + QVector slide_info; + + QUndoCommand* command = new QUndoCommand(); + + foreach (const GhostBlockPair& p, blocks_trimming) { + TimelineViewGhostItem* ghost = p.ghost; + + GhostMode m = static_cast(ghost->data(TimelineViewGhostItem::kPointerToolMode).toInt()); + + switch (m) { + case kPointer: + case kRolling: + if (m != kRolling || ghost->mode() == drag_movement_mode()) { + BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->GetAdjustedTrack()), + p.block, + ghost->AdjustedLength(), + ghost->mode(), + command); + + if (m == kRolling) { + c->SetAllowNonGapTrimming(true); + } + } + break; + case kSlide: + slide_info.append({parent()->GetTrackFromReference(ghost->Track()), + p.block, + ghost->mode(), + ghost->AdjustedLength(), + ghost->Length()}); + break; + } + } + + if (!blocks_moving.isEmpty()) { + // If we're not duplicating, "remove" the clips and replace them with gaps + if (!duplicate_clips) { + QList blocks_to_delete; + + foreach (const GhostBlockPair& p, blocks_moving) { + blocks_to_delete.append(p.block); + } + + parent()->ReplaceBlocksWithGaps(blocks_to_delete, false, command); + } + + if (inserting) { + // If we're inserting, ripple everything at the destination with gaps + InsertGapsAtGhostDestination(command); + } + + // Now we can re-add each clip + foreach (const GhostBlockPair& p, blocks_moving) { + Block* block = p.block; + + if (duplicate_clips) { + // Duplicate rather than move + Node* copy = block->copy(); + + new NodeAddCommand(static_cast(block->parent()), + copy, + command); + + new NodeCopyInputsCommand(block, copy, true, command); + + // Place the copy instead of the original block + block = static_cast(copy); + } + + const TrackReference& track_ref = p.ghost->GetAdjustedTrack(); + new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(track_ref.type()), + track_ref.index(), + block, + p.ghost->GetAdjustedIn(), + command); + } + } + + if (!blocks_sliding.isEmpty()) { + foreach (const GhostBlockPair& p, blocks_sliding) { + slide_info.append({parent()->GetTrackFromReference(p.ghost->Track()), + p.block, + p.ghost->mode(), + p.ghost->GetAdjustedIn(), + p.ghost->In()}); + } + } + + if (!slide_info.isEmpty()) { + new TrackSlideCommand(slide_info, command); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(TimelineViewBlockItem *block, qreal cursor_x) { double kTrimHandle = QFontMetricsWidth(parent()->fontMetrics(), "H"); @@ -378,73 +562,22 @@ Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(Timelin void TimelineWidget::PointerTool::InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) { - // Get list of selected blocks - QList clips = parent()->GetSelectedBlocks(); - - if (trim_mode == Timeline::kMove) { - - // Create ghosts for moving - foreach (TimelineViewBlockItem* clip_item, clips) { - - // Gaps are not allowed to move, so we ignore those here - if (clip_item->block()->type() == Block::kGap) { - continue; - } - - AddGhostFromBlock(clip_item->block(), clip_item->Track(), trim_mode); - } - - } else { - - // "Multi-trim" is trimming a clip on more than one track. Only the earliest (for in trimming) - // or latest (for out trimming) clip on each track can be trimmed. Therefore, it's only enabled - // if the clicked item is the earliest/latest on its track. - bool multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); - - // Create ghosts for trimming - foreach (TimelineViewBlockItem* clip_item, clips) { - if (clip_item != clicked_item - && (!multitrim_enabled || !IsClipTrimmable(clip_item, clips, trim_mode))) { - // Either multitrim is disabled or this clip is NOT the earliest/latest in its track. We - // won't include it. - continue; - } - - Block* block = clip_item->block(); - Timeline::MovementMode block_mode = trim_mode; - - // Some tools interpret "gap trimming" as equivalent to resizing the adjacent block. In that - // scenario, we include the adjacent block instead. - if (block->type() == Block::kGap && !gap_trimming_allowed_) { - block = (trim_mode == Timeline::kTrimIn) ? block->previous() : block->next(); - block_mode = FlipTrimMode(trim_mode); - - // If there's no adjacent block, do nothing here - if (!block) { - continue; - } - } - - // Create ghost for this block - AddGhostFromBlock(block, clip_item->Track(), block_mode); - } - - } + InitiateDragInternal(clicked_item, trim_mode, kPointer, false); } -TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode) +TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed) { TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromBlock(block, track, parent()->GetTrackY(track), parent()->GetTrackHeight(track)); - AddGhostInternal(ghost, mode); + AddGhostInternal(ghost, mode, trim_overwrite_allowed); return ghost; } -TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const rational &in, const rational &out, const TrackReference& track, Timeline::MovementMode mode) +TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const rational &in, const rational &out, const TrackReference& track, Timeline::MovementMode mode, bool trim_overwrite_allowed) { TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); @@ -453,14 +586,15 @@ TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const ratio ghost->SetTrack(track); ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); - AddGhostInternal(ghost, mode); + AddGhostInternal(ghost, mode, trim_overwrite_allowed); return ghost; } -void TimelineWidget::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode) +void TimelineWidget::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode, bool trim_overwrite_allowed) { ghost->SetMode(mode); + ghost->setData(TimelineViewGhostItem::kTrimOverwriteAllowed, trim_overwrite_allowed); // Prepare snap points (optimizes snapping for later) switch (mode) { @@ -497,11 +631,145 @@ bool TimelineWidget::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, return true; } -rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, - const QVector ghosts, - bool prevent_overwriting) +struct TrackBlockListPair { + TrackReference track; + QList blocks; +}; + +void TimelineWidget::PointerTool::ProcessGhostsForSliding() { - foreach (TimelineViewGhostItem* ghost, ghosts) { + // Sort blocks into tracks + QList blocks_per_track; + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + bool found = false; + + for (int i=0;iTrack()) { + blocks_per_track[i].blocks.append(b); + found = true; + break; + } + } + + if (!found) { + blocks_per_track.append({ghost->Track(), {b}}); + } + } + + // Make contiguous runs of blocks per each track + foreach (const TrackBlockListPair& p, blocks_per_track) { + // Blocks must be merged if any are non-adjacent + const TrackReference& track = p.track; + const QList& blocks = p.blocks; + + Block* earliest_block = blocks.first(); + Block* latest_block = blocks.first(); + + // Find the earliest and latest selected blocks + for (int j=1;jin() < earliest_block->in()) { + earliest_block = compare; + } + + if (compare->in() > latest_block->in()) { + latest_block = compare; + } + } + + // Add any blocks between these blocks that aren't already in the list + if (earliest_block != latest_block) { + Block* b = earliest_block; + while ((b = b->next()) != latest_block) { + if (!blocks.contains(b)) { + TimelineViewGhostItem* g = AddGhostFromBlock(b, track, Timeline::kMove, true); + g->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); + } + } + } + + // Add surrounding blocks that will be trimming instead of moving + if (earliest_block->previous()) { + TimelineViewGhostItem* g = AddGhostFromBlock(earliest_block->previous(), track, Timeline::kTrimOut, true); + g->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); + } + + if (latest_block->next()) { + TimelineViewGhostItem* g = AddGhostFromBlock(latest_block->next(), track, Timeline::kTrimIn, true); + g->setData(TimelineViewGhostItem::kPointerToolMode, kSlide); + } + } +} + +void TimelineWidget::PointerTool::ProcessGhostsForRolling() +{ + // For each ghost, we make an equivalent Ghost on the next/previous block + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (ghost->mode() == Timeline::kTrimIn && ghost_block->previous()) { + // Add an extra Ghost for the previous block + AddGhostFromBlock(ghost_block->previous(), ghost->Track(), Timeline::kTrimOut, true); + } else if (ghost->mode() == Timeline::kTrimOut && ghost_block->next()) { + AddGhostFromBlock(ghost_block->next(), ghost->Track(), Timeline::kTrimIn, true); + } + } +} + +bool TimelineWidget::PointerTool::AddMovingTransitionsToClipGhost(Block* block, + const TrackReference& track, + Timeline::MovementMode movement, + const QList& selected_items) +{ + // Assume block is a clip and see if it has any transitions + TransitionBlock* transitions[2]; + + if (movement == Timeline::kMove || movement == Timeline::kTrimOut) { + transitions[0] = TransitionBlock::GetBlockOutTransition(block); + } else { + transitions[0] = nullptr; + } + + if (movement == Timeline::kMove || movement == Timeline::kTrimIn) { + transitions[1] = TransitionBlock::GetBlockInTransition(block); + } else { + transitions[1] = nullptr; + } + + bool ret = false; + + for (int i=0;i<2;i++) { + if (!transitions[i]) { + continue; + } + + bool found = false; + + foreach (TimelineViewBlockItem* item, selected_items) { + if (item->block() == transitions[i]) { + // Do nothing + found = true; + break; + } + } + + if (!found) { + TimelineViewGhostItem* transition_ghost = AddGhostFromBlock(transitions[i], track, + Timeline::kMove, false); + transition_ghost->setData(TimelineViewGhostItem::kPointerToolMode, kPointer); + + ret = true; + } + } + + return ret; +} + +rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement) +{ + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { if (ghost->mode() != Timeline::kTrimIn) { continue; } @@ -516,29 +784,7 @@ rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, } if (block) { - /* FIXME: Rewrite transition logic - if (block->type() == Block::kTransition) { - // For transitions, validate with the attached block - TransitionBlock* transition = static_cast(block); - - if (transition->connected_in_block() && transition->connected_out_block()) { - // Here, we try to get the latest earliest point for both the in and out blocks, we do in here and out will - // be calculated later - earliest_in = GetEarliestPointForClip(transition->connected_in_block()); - - // We set the block to the out block since that will be before the in block and will be the one we use to - // prevent overwriting since we're trimming the in side of this transition - block = transition->connected_out_block(); - - latest_in = transition->in() + transition->out_offset(); - } else { - // Use whatever block is attached - block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); - } - } - */ - - if (prevent_overwriting) { + if (!ghost->data(TimelineViewGhostItem::kTrimOverwriteAllowed).toBool()) { // Look for a Block in the way Block* prev = block->previous(); while (prev != nullptr) { @@ -566,11 +812,9 @@ rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement, return movement; } -rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement, - const QVector ghosts, - bool prevent_overwriting) +rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement) { - foreach (TimelineViewGhostItem* ghost, ghosts) { + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { if (ghost->mode() != Timeline::kTrimOut) { continue; } @@ -588,27 +832,7 @@ rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement, // Ripple tool creates block-less ghosts and creates gaps with them later if (block) { - /* FIXME: Rewrite transition logic - if (block->type() == Block::kTransition) { - // For transitions, validate with the attached block - TransitionBlock* transition = static_cast(block); - - if (transition->connected_in_block() && transition->connected_out_block()) { - // We set the block to the out block since that will be before the in block and will be the one we use to - // prevent overwriting since we're trimming the in side of this transition - - // FIXME: At some point we may add some better logic to `latest_out` akin to the logic in ValidateInTrimming - // which is why this hasn't yet been collapsed into the ternary below. - block = transition->connected_in_block(); - - earliest_out = transition->out() - transition->in_offset(); - } else { - block = transition->connected_in_block() ? transition->connected_in_block() : transition->connected_out_block(); - } - } - */ - - if (prevent_overwriting) { + if (!ghost->data(TimelineViewGhostItem::kTrimOverwriteAllowed).toBool()) { // Determine if there's a block in the way Block* next = block->next(); while (next != nullptr) { diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index 9fb88ff40..a03303b1a 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -29,14 +29,13 @@ TimelineWidget::RippleTool::RippleTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); - SetTrimOverwriteAllowed(true); SetGapTrimmingAllowed(true); } void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - PointerTool::InitiateDrag(clicked_item, trim_mode); + InitiateDragInternal(clicked_item, trim_mode, kPointer, true); if (parent()->ghost_items_.isEmpty()) { return; @@ -86,16 +85,16 @@ void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_ite if (block_before_ripple->type() == Block::kGap) { // If this Block is already a Gap, ghost it now - ghost = AddGhostFromBlock(block_before_ripple, track_ref, trim_mode); + ghost = AddGhostFromBlock(block_before_ripple, track_ref, trim_mode, true); } else if (block_before_ripple->next()) { // Assuming this block is NOT at the end of the track (i.e. next != null) // We're going to create a gap after it. If next is a gap, we can just use that if (block_before_ripple->next()->type() == Block::kGap) { - ghost = AddGhostFromBlock(block_before_ripple->next(), track_ref, trim_mode); + ghost = AddGhostFromBlock(block_before_ripple->next(), track_ref, trim_mode, true); } else { // If next is NOT a gap, we'll need to create one, for which we'll use a null ghost - ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode); + ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode, true); ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); } } diff --git a/app/widget/timelinewidget/tool/rolling.cpp b/app/widget/timelinewidget/tool/rolling.cpp index 6baf25316..8f9f132c9 100644 --- a/app/widget/timelinewidget/tool/rolling.cpp +++ b/app/widget/timelinewidget/tool/rolling.cpp @@ -29,47 +29,13 @@ TimelineWidget::RollingTool::RollingTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); - SetTrimOverwriteAllowed(true); SetGapTrimmingAllowed(true); } void TimelineWidget::RollingTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - PointerTool::InitiateDrag(clicked_item, trim_mode); - - // For each ghost, we make an equivalent Ghost on the next/previous block - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (ghost->mode() == Timeline::kTrimIn && ghost_block->previous()) { - // Add an extra Ghost for the previous block - AddGhostFromBlock(ghost_block->previous(), ghost->Track(), Timeline::kTrimOut); - } else if (ghost->mode() == Timeline::kTrimOut && ghost_block->next()) { - AddGhostFromBlock(ghost_block->next(), ghost->Track(), Timeline::kTrimIn); - } - } -} - -void TimelineWidget::RollingTool::FinishDrag(TimelineViewMouseEvent *event) -{ - QUndoCommand* command = new QUndoCommand(); - - // Find earliest point to ripple around - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (ghost->mode() == drag_movement_mode()) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->Track()), - b, - ghost->AdjustedLength(), - drag_movement_mode(), - command); - c->SetAllowNonGapTrimming(true); - } - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); + InitiateDragInternal(clicked_item, trim_mode, kRolling, true); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/slide.cpp b/app/widget/timelinewidget/tool/slide.cpp index 26423f335..f9bd8f357 100644 --- a/app/widget/timelinewidget/tool/slide.cpp +++ b/app/widget/timelinewidget/tool/slide.cpp @@ -30,105 +30,13 @@ TimelineWidget::SlideTool::SlideTool(TimelineWidget* parent) : { SetTrimmingAllowed(false); SetTrackMovementAllowed(false); - SetTrimOverwriteAllowed(true); SetGapTrimmingAllowed(true); } -struct TrackBlockListPair { - TrackReference track; - QList blocks; -}; - void TimelineWidget::SlideTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { - PointerTool::InitiateDrag(clicked_item, trim_mode); - - // Sort blocks into tracks - QList blocks_per_track; - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - bool found = false; - - for (int i=0;iTrack()) { - blocks_per_track[i].blocks.append(b); - found = true; - break; - } - } - - if (!found) { - blocks_per_track.append({ghost->Track(), {b}}); - } - } - - // Make contiguous runs of blocks per each track - foreach (const TrackBlockListPair& p, blocks_per_track) { - // Blocks must be merged if any are non-adjacent - const TrackReference& track = p.track; - const QList& blocks = p.blocks; - - Block* earliest_block = blocks.first(); - Block* latest_block = blocks.first(); - - // Find the earliest and latest selected blocks - for (int j=1;jin() < earliest_block->in()) { - earliest_block = compare; - } - - if (compare->in() > latest_block->in()) { - latest_block = compare; - } - } - - // Add any blocks between these blocks that aren't already in the list - if (earliest_block != latest_block) { - Block* b = earliest_block; - while ((b = b->next()) != latest_block) { - if (!blocks.contains(b)) { - AddGhostFromBlock(b, track, Timeline::kMove); - } - } - } - - // Add surrounding blocks that will be trimming instead of moving - if (earliest_block->previous()) { - AddGhostFromBlock(earliest_block->previous(), track, Timeline::kTrimOut); - } - - if (latest_block->next()) { - AddGhostFromBlock(latest_block->next(), track, Timeline::kTrimIn); - } - } -} - -void TimelineWidget::SlideTool::FinishDrag(TimelineViewMouseEvent *event) -{ - Q_UNUSED(event) - - QVector info; - - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (!ghost->HasBeenAdjusted()) { - continue; - } - - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - info.append({parent()->GetTrackFromReference(ghost->Track()), - b, - ghost->mode(), - ghost->mode() == Timeline::kMove ? ghost->GetAdjustedIn() : ghost->AdjustedLength(), - ghost->mode() == Timeline::kMove ? ghost->In() : ghost->Length()}); - } - - if (!info.isEmpty()) { - Core::instance()->undo_stack()->push(new TrackSlideCommand(info)); - } + InitiateDragInternal(clicked_item, trim_mode, kSlide, true); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 51f9425ce..502755acb 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -73,9 +73,9 @@ TimelineViewBlockItem *TimelineWidget::Tool::GetItemAtScenePos(const TimelineCoo return nullptr; } -rational TimelineWidget::Tool::ValidateTimeMovement(rational movement, const QVector ghosts) +rational TimelineWidget::Tool::ValidateTimeMovement(rational movement) { - foreach (TimelineViewGhostItem* ghost, ghosts) { + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { if (ghost->mode() != Timeline::kMove) { continue; } @@ -106,7 +106,7 @@ rational TimelineWidget::Tool::ValidateTimeMovement(rational movement, const QVe return movement; } -int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector ghosts) +int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector& ghosts) { foreach (TimelineViewGhostItem* ghost, ghosts) { if (ghost->mode() != Timeline::kMove) { @@ -115,7 +115,7 @@ int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector