15.1.968. tablet_qt/widgets/canvaswidget.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS 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.

    CamCOPS 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 CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/

// #define DEBUG_TRANSLATIONS

#include "canvaswidget.h"
#include <QColor>
#include <QDebug>
#include <QMouseEvent>
#include <QPainter>
#include <QPaintEvent>
#include <QRegion>
#include <QStyle>
#include <QStyleOption>
#include "common/colourdefs.h"
#include "lib/convert.h"
#include "lib/margins.h"
#include "lib/sizehelpers.h"

const QPoint INVALID_POINT(-1, -1);

const int DEFAULT_MIN_SHRINK_HEIGHT = 200;
const QColor DEFAULT_BORDER_COLOR(QCOLOR_SILVER);
const QColor DEFAULT_UNUSED_SPACE_COLOR(QCOLOR_SILVER);


CanvasWidget::CanvasWidget(const QImage::Format format, QWidget* parent) :
    CanvasWidget(QSize(0, 0), format, parent)
{
}


CanvasWidget::CanvasWidget(const QSize& size,
                           const QImage::Format format,
                           QWidget* parent) :
    QFrame(parent)
{
    m_format = format;
    setAllowShrink(false);
    m_minimum_shrink_height = DEFAULT_MIN_SHRINK_HEIGHT;
    m_adjust_display_for_dpi = true;

    m_border_width_px = 2;
    m_border_colour = DEFAULT_BORDER_COLOR;
    m_unused_space_colour = DEFAULT_UNUSED_SPACE_COLOR;
    // Default pen:
    m_pen.setColor(Qt::blue);
    m_pen.setWidth(2);

    m_point = INVALID_POINT;
    m_image_to_display_ratio = 1;

    setBorderCss();
    setImageSizeAndClearImage(size);
}


CanvasWidget::~CanvasWidget()
{
}


void CanvasWidget::setImageSizeAndClearImage(const QSize& size)
{
    // qDebug() << Q_FUNC_INFO;
    m_image = QImage(size, m_format);
    update();
}


void CanvasWidget::setAllowShrink(const bool allow_shrink)
{
    m_allow_shrink = allow_shrink;
    if (m_allow_shrink) {
        setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
        // Can be shrunk in either direction.
        // We can't have a width-for-height constraint as well as a HFW
        // constraint; see https://doc.qt.io/qt-6.5/qsizepolicy.html#setWidthForHeight
        // Instead, we can draw according to our *actual* height...
        // Similarly, we don't need a HFW constraint, which will (in many of
        // our layouts) make the effective height *fixed* once the width is
        // determined; we do this as a widget that accepts any size up to its
        // maximum, and then just draws in a subset.
    } else {
        setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    }
}


void CanvasWidget::setMinimumShrinkHeight(const int height)
{
    m_minimum_shrink_height = height;
}


void CanvasWidget::setBorderWidth(const int width)
{
    m_border_width_px = width;
    setBorderCss();
}


void CanvasWidget::setBorderColour(const QColor& colour)
{
    m_border_colour = colour;
    setBorderCss();
}


void CanvasWidget::setBorder(const int width, const QColor& colour)
{
    m_border_width_px = width;
    m_border_colour = colour;
    setBorderCss();
}


void CanvasWidget::setUnusedSpaceColour(const QColor& colour)
{
    m_unused_space_colour = colour;
}


QSize CanvasWidget::sizeHint() const
{
    // Size of m_image (which is m_size), plus size of borders.
    // To do this, we have to derive from QFrame rather than QWidget, I think.

    // Several ways don't work.
    // - QWidget::sizeHint() returns QSize(-1, -1) despite stylesheet borders,
    //   even after ensurePolished().
    // - getContentsMargins() returns 0, 0, 0, 0 despite stylesheet borders, if
    //   you inherit from a QWidget. But if you inherit from a QFrame... yup,
    //   it works!

    const Margins m = Margins::getContentsMargins(this);
    return m.addMarginsTo(desiredDisplaySize());
}


QSize CanvasWidget::minimumSizeHint() const
{
    if (!m_allow_shrink) {
        return desiredDisplaySize();
    }
    QSize minsize = imageSize();
    minsize.scale(QSize(minsize.width(), m_minimum_shrink_height),
                  Qt::KeepAspectRatio);
    return minsize;
}


void CanvasWidget::setPen(const QPen& pen)
{
    m_pen = pen;
}


void CanvasWidget::clear(const QColor& background)
{
    // qDebug() << Q_FUNC_INFO;
    m_image.fill(background);
    update();
}


void CanvasWidget::setImage(const QImage& image)
{
    // qDebug() << Q_FUNC_INFO;
    if (image.isNull()) {
        qWarning() << Q_FUNC_INFO << "Asked to set null image!";
    }
    m_image = image;
    update();
}


void CanvasWidget::setAdjustDisplayForDpi(const bool adjust_display_for_dpi)
{
    m_adjust_display_for_dpi = adjust_display_for_dpi;
    update();
}


void CanvasWidget::resizeEvent(QResizeEvent* event)
{
    QSize displaysize = imageSize();
    displaysize.scale(contentsRect().size(), Qt::KeepAspectRatio);
    // Store the ratio in a format that allows the most common operations to
    // use multiplication, not division:
    // http://stackoverflow.com/questions/4125033/floating-point-division-vs-floating-point-multiplication
    m_image_to_display_ratio = static_cast<double>(imageSize().width()) /
                               static_cast<double>(displaysize.width());

#ifdef DEBUG_TRANSLATIONS
    qDebug().nospace()
            << Q_FUNC_INFO
            << "- widget size " << event->size()
            << "; contents rect " << contentsRect()
            << "; m_image_to_display_ratio " << m_image_to_display_ratio;
#else
    Q_UNUSED(event)
#endif
}


void CanvasWidget::paintEvent(QPaintEvent* event)
{
    Q_UNUSED(event)
    // If you derive from a QWidget, you can't find out how big the stylesheet
    // borders are, so you can't help overwriting them. So, derive from a
    // QFrame, and draw inside its contentsRect().
    // - https://forum.qt.io/topic/18325
    // - http://stackoverflow.com/questions/22415057

    // 1. The standard bits: background via stylesheet, etc.
    // - http://stackoverflow.com/questions/18344135

    QStyleOption o;
    o.initFrom(this);
    QPainter painter(this);
    style()->drawPrimitive(QStyle::PE_Widget, &o, &painter, this);

    // 2. Our bits
    const QRect cr = contentsRect();
    const QSize imagesize = imageSize();
    if (m_allow_shrink && cr.size() != imagesize) {
        // Scale
        QSize displaysize = imagesize;
        displaysize.scale(cr.size(), Qt::KeepAspectRatio);
        const QRect dest_active_rect = QRect(cr.topLeft(), displaysize);
        const QRect source_all_image(QPoint(0, 0), imagesize);
        painter.drawImage(dest_active_rect, m_image, source_all_image);

        // Optimizations are possible: we don't have to draw all of it...
        // http://blog.qt.io/blog/2006/05/13/fast-transformed-pixmapimage-drawing/
        // ... but I haven't implemented those optimizations.

//#ifdef DEBUG_TRANSLATIONS
//        QRect exposed_rect = painter.matrix().inverted()
//                             .mapRect(event->rect())
//                             .adjusted(-1, -1, 1, 1);
//        qDebug().nospace()
//                << Q_FUNC_INFO << " - contentsRect = " << cr
//                << ", exposed = " << exposed_rect;
//#endif

        // Paint unused space:
        QRegion unused(cr);
        unused -= QRegion(dest_active_rect);
        painter.setClipRegion(unused);
        const QBrush brush_unused(m_unused_space_colour);
        painter.fillRect(cr, brush_unused);

    } else {
        // No need to scale
        painter.drawImage(cr.left(), cr.top(), m_image);
    }
}


QPoint CanvasWidget::transformDisplayToImageCoords(QPoint point) const
{
    // Convert from widget coordinates (NB there's a frame) to contentsRect
    // coordinates:
    auto margins = contentsMargins();
    point.rx() -= margins.left();
    point.ry() -= margins.top();

    // Now transform, if required, to account for any scaling that we're
    // doing:
    if (!m_allow_shrink) {
        return point;
    }
    QPoint result = QPoint(
        qRound(static_cast<double>(point.x()) * m_image_to_display_ratio),
        qRound(static_cast<double>(point.y()) * m_image_to_display_ratio)
    );
#ifdef DEBUG_TRANSLATIONS
    qDebug() << Q_FUNC_INFO << point << "->" << result;
#endif
    return result;
}


void CanvasWidget::mousePressEvent(QMouseEvent* event)
{
    if (event->buttons() & Qt::LeftButton) {
        m_point = INVALID_POINT;
        drawTo(transformDisplayToImageCoords(event->pos()));
        update();
    }
}


void CanvasWidget::mouseMoveEvent(QMouseEvent* event)
{
    if (event->buttons() & Qt::LeftButton) {
        drawTo(transformDisplayToImageCoords(event->pos()));
        update();
    }
}


void CanvasWidget::drawTo(const QPoint& pt)
{
    // The coordinates are IMAGE coordinates.
    if (m_image.isNull()) {
        qWarning() << Q_FUNC_INFO << "null image";
        return;
    }

    // Draw
    QPainter p(&m_image);
    p.setPen(m_pen);
    QPoint from = m_point;
    if (from == INVALID_POINT) {
        from = pt;
    }
    p.drawLine(from, pt);
    m_point = pt;

    emit imageChanged();
}


QImage CanvasWidget::image() const
{
    return m_image;
}


QSize CanvasWidget::imageSize() const
{
    return m_image.size();
}


QSize CanvasWidget::desiredDisplaySize() const
{
    if (m_adjust_display_for_dpi) {
        return convert::convertSizeByLogicalDpi(imageSize());
    }
    return imageSize();
}


void CanvasWidget::setBorderCss()
{
    QString css = QString("border: %1px solid rgba(%2,%3,%4,%5);")
            .arg(m_border_width_px)
            .arg(m_border_colour.red())
            .arg(m_border_colour.green())
            .arg(m_border_colour.blue())
            .arg(m_border_colour.alpha());
    setStyleSheet(css);
}