diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index d5736155f..c271862f1 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -75,7 +75,8 @@ ViewerWidget::ViewerWidget(QWidget *parent) : connect(display_widget_, &ViewerDisplayWidget::CursorColor, this, &ViewerWidget::CursorColor); connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, this, &ViewerWidget::ColorProcessorChanged); connect(display_widget_, &ViewerDisplayWidget::ColorManagerChanged, this, &ViewerWidget::ColorManagerChanged); - connect(sizer_, &ViewerSizer::RequestMatrix, display_widget_, &ViewerDisplayWidget::SetMatrix); + connect(sizer_, &ViewerSizer::RequestMatrix, display_widget_, &ViewerDisplayWidget::SetMatrixZoom); + connect(sizer_, &ViewerSizer::IsZoomed, display_widget_, &ViewerDisplayWidget::IsZoomed); sizer_->SetWidget(display_widget_); // Create waveform view when audio is connected and video isn't @@ -289,9 +290,9 @@ void ViewerWidget::SetColorMenuEnabled(bool enabled) void ViewerWidget::SetMatrix(const QMatrix4x4 &mat) { - display_widget_->SetMatrix(mat); + display_widget_->SetMatrixZoom(mat); foreach (ViewerWindow* vw, windows_) { - vw->display_widget()->SetMatrix(mat); + vw->display_widget()->SetMatrixZoom(mat); } } diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 7aa6db6fe..8f243503a 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -35,6 +35,7 @@ #include "render/backend/opengl/openglrenderfunctions.h" #include "render/backend/opengl/openglshader.h" #include "render/pixelformat.h" +#include "core.h" OLIVE_NAMESPACE_ENTER @@ -44,8 +45,13 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) : gizmos_(nullptr), gizmo_click_(false), last_loaded_buffer_(nullptr), + zoomed_(false), + hand_tool_clicked_(false), deinterlace_(false) { + connect(Core::instance(), &Core::ToolChanged, this, &ViewerDisplayWidget::ToolChanged); + // Initilises hand_tool_ based on currently selected tool. + ToolChanged(Core::instance()->tool()); } ViewerDisplayWidget::~ViewerDisplayWidget() @@ -53,12 +59,55 @@ ViewerDisplayWidget::~ViewerDisplayWidget() ContextCleanup(); } -void ViewerDisplayWidget::SetMatrix(const QMatrix4x4 &mat) +void ViewerDisplayWidget::SetMatrixTranslate(const QMatrix4x4 &mat) { - matrix_ = mat; + translate_matrix_ = mat; update(); } +void ViewerDisplayWidget::SetMatrixZoom(const QMatrix4x4 &mat) +{ + scale_matrix_ = mat; + update(); +} + +void ViewerDisplayWidget::IsZoomed(bool flag) +{ + zoomed_ = flag; + // If the image is smaller than the container widget we reset the translation. + if (!flag) { + QMatrix4x4 mat; // defaults to identity matrix. + SetMatrixTranslate(mat); + } +} + +void ViewerDisplayWidget::ToolChanged(Tool::Item tool) +{ + if (tool == Tool::kHand) { + hand_tool_ = true; + setCursor(Qt::OpenHandCursor); + } else { + hand_tool_ = false; + unsetCursor(); + } +} + +QMatrix4x4 ViewerDisplayWidget::GetCompleteMatrix() +{ + QMatrix4x4 mat; + // Images is scaled, then translated. + return translate_matrix_ * scale_matrix_ * mat; +} + +QMatrix4x4 ViewerDisplayWidget::GetCompleteMatrixFlippedYTranslation() { + QMatrix4x4 mat; + // Image is scaled, then translated. + mat = translate_matrix_ * scale_matrix_ * mat; + // Y translation is flipped for OpenGL usage + *(mat.data() + 13) = *(mat.data() + 13) * -1.0f; + return mat; +} + void ViewerDisplayWidget::SetSignalCursorColorEnabled(bool e) { signal_cursor_color_ = e; @@ -139,15 +188,61 @@ FramePtr ViewerDisplayWidget::last_loaded_buffer() const return last_loaded_buffer_; } +QTransform ViewerDisplayWidget::GenerateWorldTransform() +{ + /* + * Get matrix elements (roughly) as below in column major order + * + * | Sx 0 0 Tx | + * | 0 Sy 0 Ty | + * | 0 0 Sz Tz | + * | 0 0 0 1 | + */ + float *data = GetCompleteMatrix().data(); + QTransform world; + // Move corner of canvas to correct point + world.translate(width() * 0.5 - width() * *(data)*0.5, height() * 0.5 - height() * *(data + 5) * 0.5); + // Scale + world.scale(*(data), *(data + 5)); + // Translate for mouse movement + world.translate(*(data + 12) * width() * 0.5 / *(data), *(data + 13) * height() * 0.5 / *(data + 5)); + + return world; +} + +QPoint ViewerDisplayWidget::TransformViewerSpaceToBufferSpace(QPoint pos) +{ + /* + * Inversion will only fail if the viewer has been scaled by 0 in any direction + * which I think should never happen. + */ + return pos * GenerateWorldTransform().inverted(); +} + void ViewerDisplayWidget::mousePressEvent(QMouseEvent *event) { - if (gizmos_ - && gizmos_->GizmoPress(gizmo_db_, event->pos(), QVector2D(GetTexturePosition(size())), size())) { + if (event->button() == Qt::LeftButton && gizmos_ + && gizmos_->GizmoPress(gizmo_db_, TransformViewerSpaceToBufferSpace(event->pos()), + QVector2D(GetTexturePosition(size())), size())) { gizmo_click_ = true; gizmo_drag_time_ = GetGizmoTime(); return; } + // Reset translation. + if (event->button() == Qt::MiddleButton && event->modifiers() & Qt::ControlModifier) { + QMatrix4x4 mat; // Identity matrix + SetMatrixTranslate(mat); + return; + } + + // If translation is enabled get current position in preperation for move event. + if ((event->button() == Qt::MiddleButton || hand_tool_) && zoomed_) { + position_ = event->pos(); + if (hand_tool_) hand_tool_clicked_ = true; + return; + } + QOpenGLWidget::mousePressEvent(event); if (event->button() == Qt::LeftButton) { @@ -157,8 +252,28 @@ void ViewerDisplayWidget::mousePressEvent(QMouseEvent *event) void ViewerDisplayWidget::mouseMoveEvent(QMouseEvent *event) { + // Only allow translation if the image is larger than the container widget + if ((event->buttons() & Qt::MiddleButton || hand_tool_clicked_) && zoomed_) { + QPointF delta = event->pos() - position_; + // Scale delta to widget size + delta.setX(2 * delta.x() / width()); + delta.setY(2 * delta.y() / height()); + + QMatrix4x4 mat; + mat = GetMatrixTranslate(); + + mat.translate(delta.x(), delta.y()); + SetMatrixTranslate(mat); + + // Get new start position + position_ = event-> pos(); + return; + } + if (gizmo_click_) { - gizmos_->GizmoMove(event->pos(), QVector2D(GetTexturePosition(size())), gizmo_drag_time_); + gizmos_->GizmoMove(TransformViewerSpaceToBufferSpace(event->pos()), + QVector2D(GetTexturePosition(size())), gizmo_drag_time_); + update(); return; } @@ -172,7 +287,7 @@ void ViewerDisplayWidget::mouseMoveEvent(QMouseEvent *event) static_cast(event->y()) / static_cast(height()) * 2.0f - 1.0f, 0); - pixel_pos = pixel_pos * matrix_.inverted(); + pixel_pos = GetCompleteMatrix().inverted() * pixel_pos; int frame_x = qRound((pixel_pos.x() + 1.0f) * 0.5f * last_loaded_buffer_->width()); int frame_y = qRound((pixel_pos.y() + 1.0f) * 0.5f * last_loaded_buffer_->height()); @@ -194,9 +309,16 @@ void ViewerDisplayWidget::mouseReleaseEvent(QMouseEvent *event) return; } + hand_tool_clicked_ = false; + QOpenGLWidget::mouseReleaseEvent(event); } +QMatrix4x4 ViewerDisplayWidget::GetMatrixTranslate() +{ + return translate_matrix_; +} + void ViewerDisplayWidget::initializeGL() { ManagedDisplayWidget::initializeGL(); @@ -226,13 +348,15 @@ void ViewerDisplayWidget::paintGL() color_service()->pipeline()->release(); // Blit using the color service - color_service()->ProcessOpenGL(true, matrix_); + color_service()->ProcessOpenGL(true, GetCompleteMatrixFlippedYTranslation()); // Release retrieved texture f->glBindTexture(GL_TEXTURE_2D, 0); } + QTransform world = GenerateWorldTransform(); + // Draw gizmos if we have any if (gizmos_) { GizmoTraverser gt(QSize(gizmo_params_.width(), gizmo_params_.height())); @@ -242,12 +366,15 @@ void ViewerDisplayWidget::paintGL() gizmo_db_ = gt.GenerateDatabase(gizmos_, TimeRange(node_time, node_time)); QPainter p(this); + p.setWorldTransform(world); gizmos_->DrawGizmos(gizmo_db_, &p, QVector2D(GetTexturePosition(size())), size()); } // Draw action/title safe areas if (safe_margin_.is_enabled()) { QPainter p(this); + p.setWorldTransform(world); + p.setPen(Qt::lightGray); p.setBrush(Qt::NoBrush); diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index 7a240beff..bbaf4f45f 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -30,6 +30,7 @@ #include "render/backend/opengl/opengltexture.h" #include "render/color.h" #include "render/colormanager.h" +#include "tool/tool.h" #include "viewersafemargininfo.h" #include "widget/manageddisplay/manageddisplay.h" #include "widget/timetarget/timetarget.h" @@ -66,7 +67,22 @@ public: virtual ~ViewerDisplayWidget() override; - const QMatrix4x4& GetMatrix(); + /** + * @brief Return the translation only matrix. + */ + QMatrix4x4 GetMatrixTranslate(); + + /** + * @brief Return the complete translation and scale matrix + * This must be used if you want the entire transformation pipeline (scale and translate). + */ + QMatrix4x4 GetCompleteMatrix(); + + /** + * @brief Return the complete translation and scale matrix but with the Y translation flipped + * as OpenGL stores textures "upside down". + */ + QMatrix4x4 GetCompleteMatrixFlippedYTranslation(); const ViewerSafeMarginInfo& GetSafeMargin() const; void SetSafeMargins(const ViewerSafeMarginInfo& safe_margin); @@ -77,6 +93,18 @@ public: FramePtr last_loaded_buffer() const; + /** + * @brief Return a QTransform that contains all the scale and translation data (inc. mouse drags) + * that can be used to set the world transform for a QPainter. + */ + QTransform GenerateWorldTransform(); + + /** + * @brief Transform a point from viewer space to the buffer space. + * Multiplies by the inverted transform matrix to undo the scaling and translation. + */ + QPoint TransformViewerSpaceToBufferSpace(QPoint pos); + bool IsDeinterlacing() const { return deinterlace_; @@ -88,7 +116,12 @@ public slots: * * Set this if you want the drawing to pass through some sort of transform (most of the time you won't want this). */ - void SetMatrix(const QMatrix4x4& mat); + void SetMatrixTranslate(const QMatrix4x4& mat); + + /** + * @brief Set the scale matrix. + */ + void SetMatrixZoom(const QMatrix4x4& mat); /** * @brief Enables or disables whether this color at the cursor should be emitted @@ -107,6 +140,19 @@ public slots: */ void SetImage(FramePtr in_buffer); + /** + * @brief Set zoomed_ flag if the viewersizer has zoomed the image to be larger than the widget. + * + * If the image is smaller than the widget the translation is reset so the image is centered. + */ + void IsZoomed(bool flag); + + /** + * @brief Changes the pointer type if the tool is changed to the hand tool. Otherwise resets the pointer to it's + * normal type. + */ + void ToolChanged(Tool::Item tool); + /** * @brief Enables/disables a basic deinterlace on the viewer */ @@ -166,9 +212,16 @@ private: OpenGLTexture texture_; /** - * @brief Drawing matrix (defaults to identity) + * @brief Translation only matrix (defaults to identity). */ - QMatrix4x4 matrix_; + QMatrix4x4 translate_matrix_; + + /** + * @breif Scale only matrix. + */ + QMatrix4x4 scale_matrix_; + + bool signal_cursor_color_; @@ -184,6 +237,26 @@ private: FramePtr last_loaded_buffer_; + /** + * @brief Tells us if the image is zoomed in to be larger than the container widget. + */ + bool zoomed_; + + /** + * @brief Position of mouse to calculate delta from. + */ + QPoint position_; + + /** + * @brief Set if the hand tool is selected. + */ + bool hand_tool_; + + /** + * @brief Set if the hand tool is selected and viewer is clicked. + */ + bool hand_tool_clicked_; + bool deinterlace_; private slots: diff --git a/app/widget/viewer/viewersizer.cpp b/app/widget/viewer/viewersizer.cpp index 75f85fd85..5665ff6b6 100644 --- a/app/widget/viewer/viewersizer.cpp +++ b/app/widget/viewer/viewersizer.cpp @@ -110,11 +110,15 @@ void ViewerSizer::UpdateSize() // This container is taller than the image, scale by width child_size = QSize(width(), qRound(child_size.width() / sequence_aspect_ratio)); } + emit IsZoomed(false); } else { float x_scale = 1.0f; float y_scale = 1.0f; + // Translation is only active if the image is bigger then the widget + // See ViewerDisplayWidget::SetZoomData + bool zoomed_in = false; int zoomed_width = qRound(width_ * static_cast(zoom_) * 0.01); int zoomed_height = qRound(height_ * static_cast(zoom_) * 0.01); @@ -122,18 +126,20 @@ void ViewerSizer::UpdateSize() if (zoomed_width > width()) { x_scale = static_cast(zoomed_width) / static_cast(width()); zoomed_width = width(); + zoomed_in = true; } if (zoomed_height > height()) { y_scale = static_cast(zoomed_height) / static_cast(height()); zoomed_height = height(); + zoomed_in = true; } // Rather than make a huge surface, we still crop at our width/height and then signal a matrix child_matrix.scale(x_scale, y_scale, 1.0F); child_size = QSize(zoomed_width, zoomed_height); - + emit IsZoomed(zoomed_in); } widget_->resize(child_size); diff --git a/app/widget/viewer/viewersizer.h b/app/widget/viewer/viewersizer.h index a6dbfc34f..efe6803e3 100644 --- a/app/widget/viewer/viewersizer.h +++ b/app/widget/viewer/viewersizer.h @@ -72,6 +72,11 @@ public: signals: void RequestMatrix(const QMatrix4x4& matrix); + /** + * @brief Tells the viewerdisplay widget if the image is enlarged to be bigger than the widget or not. + */ + void IsZoomed(bool flag); + protected: /** * @brief Listen for resize events to ensure the child widget remains correctly sized diff --git a/app/widget/viewer/viewerwindow.cpp b/app/widget/viewer/viewerwindow.cpp index 525e992be..d9a331676 100644 --- a/app/widget/viewer/viewerwindow.cpp +++ b/app/widget/viewer/viewerwindow.cpp @@ -137,7 +137,7 @@ void ViewerWindow::UpdateMatrix() mat.scale(1.0f, window_ar / image_ar, 1.0f); } - display_widget_->SetMatrix(mat); + display_widget_->SetMatrixZoom(mat); } OLIVE_NAMESPACE_EXIT