15.1.151. tablet_qt/graphics/graphicsfunc.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_CSS
// #define DEBUG_COORDS
// #define DEBUG_SVG

#include "graphicsfunc.h"
#include <QBrush>
#include <QColor>
#include <QDebug>
#include <QGraphicsPixmapItem>
#include <QGraphicsProxyWidget>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QGraphicsTextItem>
#include <QLabel>
#include <QMetaMethod>
#include <QPainter>
#include <QPen>
#include <QPushButton>
#include <QRectF>
#include <QSvgRenderer>
#include <QtGlobal>
#include <QVBoxLayout>
#include "graphics/geometry.h"
#include "graphics/graphicspixmapitemwithopacity.h"
#include "lib/css.h"
#include "maths/mathfunc.h"
// #include "qobjects/stylenofocusrect.h"
#include "widgets/adjustablepie.h"
#include "widgets/svgwidgetclickable.h"
using css::colourCss;
using css::labelCss;
using css::penCss;
using css::pixelCss;
using geometry::clockwiseToAnticlockwise;
using geometry::sixteenthsOfADegree;


namespace graphicsfunc
{

// ============================================================================
// Constants
// ============================================================================

const QString TEST_SVG(
"<svg height=\"210\" width=\"210\">"
"    <polygon points=\"100,10 40,198 190,78 10,78 160,198\""
"     style=\"fill:lime;stroke:purple;stroke-width:5;fill-rule:evenodd;\"/>"
"</svg>"
);


// ============================================================================
// SvgTransform
// ============================================================================

SvgTransform::SvgTransform()
{
}


SvgTransform& SvgTransform::matrix(const qreal a, const qreal b, const qreal c,
                                   const qreal d, const qreal e, const qreal f)
{
    transformations.append(QString("matrix(%1 %2 %3 %4 %5 %6")
                           .arg(a)
                           .arg(b)
                           .arg(c)
                           .arg(d)
                           .arg(e)
                           .arg(f));
    return *this;
}


SvgTransform& SvgTransform::translate(const qreal x, const qreal y)
{
    transformations.append(QString("translate(%1 %2)")
                           .arg(x)
                           .arg(y));
    return *this;
}


SvgTransform& SvgTransform::scale(const qreal xy)
{
    transformations.append(QString("scale(%1)").arg(xy));
    return *this;
}


SvgTransform& SvgTransform::scale(const qreal x, const qreal y)
{
    transformations.append(QString("scale(%1 %2)")
                           .arg(x)
                           .arg(y));
    return *this;
}


SvgTransform& SvgTransform::rotate(const qreal a)
{
    transformations.append(QString("rotate(%1)").arg(a));
    return *this;
}


SvgTransform& SvgTransform::rotate(const qreal a, const qreal x, const qreal y)
{
    transformations.append(QString("rotate(%1 %2 %e)")
                           .arg(a)
                           .arg(x)
                           .arg(y));
    return *this;
}


SvgTransform& SvgTransform::skewX(const qreal a)
{
    transformations.append(QString("skewX(%1)").arg(a));
    return *this;
}


SvgTransform& SvgTransform::skewY(const qreal a)
{
    transformations.append(QString("skewY(%1)").arg(a));
    return *this;
}


QString SvgTransform::string() const
{
    return transformations.join(" ");
}


bool SvgTransform::active() const
{
    return !transformations.isEmpty();
}


// ============================================================================
// SVG
// ============================================================================

QString xmlElement(const QString& tag, const QString& contents,
                   const QMap<QString, QString>& attributes)
{
    const QString attr = xmlAttributes(attributes);
    if (contents.isEmpty()) {
        return QString("<%1%2 />").arg(tag, attr);
    }
    return QString("<%1%2>%3</%4>").arg(tag, attr, contents, tag);
}


QString xmlAttributes(const QMap<QString, QString>& attributes)
{
    if (attributes.isEmpty()) {
        return "";
    }
    QStringList attrlist;
    QMapIterator<QString, QString> i(attributes);
    while (i.hasNext()) {
        i.next();
        attrlist.append(QString("%1=\"%2\"").arg(i.key(),
                                                 i.value().toHtmlEscaped()));
    }
    return " " + attrlist.join(" ");
}


QString svg(const QStringList& elements)
{
    // https://www.w3schools.com/graphics/svg_intro.asp
    return xmlElement("svg", elements.join(""));
}


QString svgPath(const QString& contents,
                const QColor& stroke, const int stroke_width,
                const QColor& fill,
                const SvgTransform& transform,
                const QString& element_id)
{
    // https://www.w3schools.com/graphics/svg_path.asp
    // https://www.w3.org/TR/SVG/paths.html#PathElement
    // https://stackoverflow.com/questions/6042550/svg-fill-color-transparency-alpha
    QMap<QString, QString> attributes{
        {"d", contents},
        {"stroke", stroke.name(QColor::HexRgb)},
        {"stroke-width", QString::number(stroke_width)},
        {"stroke-opacity", opacity(stroke)},
        {"fill", fill.name(QColor::HexRgb)},
        {"fill-opacity", opacity(fill)},
    };
    if (!element_id.isEmpty()) {
        attributes["id"] = element_id;
    }
    if (transform.active()) {
        attributes["transform"] = transform.string();
    }
    return xmlElement("path", "", attributes);
}


QString svgFromPathContents(const QString& path_contents,
                            const QColor& stroke, int stroke_width,
                            const QColor& fill,
                            const SvgTransform& transform,
                            const QString& element_id)
{
#ifdef DEBUG_SVG
    Q_UNUSED(path_contents)
    Q_UNUSED(stroke)
    Q_UNUSED(stroke_width)
    Q_UNUSED(fill)
    Q_UNUSED(element_id)
    return TEST_SVG;
#else
    return svg({
        svgPath(path_contents, stroke, stroke_width, fill, transform,
                   element_id)
    });
#endif
}


QString opacity(const QColor& colour)
{
    return QString::number(mathfunc::byteToProportion(colour.alpha()));
}


int alpha(qreal opacity)
{
    return mathfunc::proportionToByte(opacity);
}


// ============================================================================
// Graphics calculations and painting
// ============================================================================

void alignRect(QRectF& rect, const Qt::Alignment alignment)
{
    // Horizontal
    qreal dx = 0;
    if (alignment & Qt::AlignLeft ||
            alignment & Qt::AlignJustify ||
            alignment & Qt::AlignAbsolute) {
        dx = 0;
    } else if (alignment & Qt::AlignHCenter) {
        dx = -rect.width() / 2;
    } else if (alignment & Qt::AlignRight) {
        dx = -rect.width();
    } else {
        qWarning() << Q_FUNC_INFO << "Unknown horizontal alignment";
    }

    // Vertical
    qreal dy = 0;
    if (alignment & Qt::AlignTop) {
        dy = 0;
    } else if (alignment & Qt::AlignVCenter) {
        dy = -rect.height() / 2;
    } else if (alignment & Qt::AlignBottom ||
               alignment & Qt::AlignBaseline) {
        dy = -rect.height();
    } else {
        qWarning() << Q_FUNC_INFO << "Unknown horizontal alignment";
    }

    rect.translate(dx, dy);
}


QRectF centredRect(const QPointF& centre, const qreal w, const qreal h)
{
    return QRectF(centre.x() - w / 2.0, centre.y() - h / 2.0, w, h);
}


void drawSector(QPainter& painter,
                const QPointF& tip,
                const qreal radius,
                qreal start_angle_deg,
                qreal end_angle_deg,
                const bool move_clockwise_from_start_to_end,
                const QPen& pen,
                const QBrush& brush)
{
#ifdef DEBUG_COORDS
    qDebug() << "drawSector:"
             << "tip" << tip
             << "radius" << radius
             << "start_angle_deg (polar)" << start_angle_deg
             << "end_angle_deg (polar)" << end_angle_deg
             << "move_clockwise_from_start_to_end" << move_clockwise_from_start_to_end;
#endif
    painter.setPen(pen);
    painter.setBrush(brush);
    const qreal diameter = radius * 2;
    const QRectF rect(tip - QPointF(radius, radius), QSizeF(diameter, diameter));
    if (!move_clockwise_from_start_to_end) {
        std::swap(start_angle_deg, end_angle_deg);
    }
    start_angle_deg = clockwiseToAnticlockwise(start_angle_deg);
    end_angle_deg = clockwiseToAnticlockwise(end_angle_deg);
    const qreal span_angle_deg = end_angle_deg - start_angle_deg;
#ifdef DEBUG_COORDS
    qDebug() << "... "
             << "tip" << tip
             << "rect" << rect
             << "start_angle_deg (for QPainter::drawPie)" << start_angle_deg
             << "span_angle_deg (for QPainter::drawPie)" << span_angle_deg;
#endif
    painter.drawPie(rect,
                    sixteenthsOfADegree(start_angle_deg),
                    sixteenthsOfADegree(span_angle_deg));
}


QRectF textRectF(const QString& text, const QFont& font)
{
    const QFontMetrics fm(font);
    // return fm.boundingRect(text);
    return fm.tightBoundingRect(text);
}


void drawText(QPainter& painter, const QPointF& point, const QString& text,
              const QFont& font, const Qt::Alignment align)
{
    const QRectF textrect = textRectF(text, font);

    qreal x = point.x();
    if (align & Qt::AlignRight) {
        x -= textrect.width();
    } else if (align & Qt::AlignHCenter) {
        x -= textrect.width() / 2.0;
    }

    qreal y = point.y();
    if (align & Qt::AlignTop) {
        y += textrect.height();
    } else if (align & Qt::AlignVCenter) {
        y += textrect.height() / 2.0;
    }

    painter.setFont(font);
    painter.drawText(static_cast<int>(x), static_cast<int>(y), text);
}


void drawText(QPainter& painter,
              const qreal x, const qreal y, Qt::Alignment flags,
              const QString& text, QRectF* bounding_rect)
{
    // http://stackoverflow.com/questions/24831484
    const qreal size = 32767.0;
    QPointF corner(x, y - size);

    if (flags & Qt::AlignHCenter) {
       corner.rx() -= size / 2.0;
    }
    else if (flags & Qt::AlignRight) {
       corner.rx() -= size;
    }

    if (flags & Qt::AlignVCenter) {
       corner.ry() += size / 2.0;
    }
    else if (flags & Qt::AlignTop) {
       corner.ry() += size;
    }
    else {
       flags |= Qt::AlignBottom;
    }

    const QRectF rect(corner, QSizeF(size, size));
    painter.drawText(rect, flags, text, bounding_rect);
    // https://doc.qt.io/qt-6.5/qpainter.html#drawText-4
}


void drawText(QPainter& painter, const QPointF& point, Qt::Alignment flags,
              const QString& text, QRectF* bounding_rect)
{
    // http://stackoverflow.com/questions/24831484
    drawText(painter, point.x(), point.y(), flags, text, bounding_rect);
}


void paintPixmapKeepingAspectRatio(QPainter& painter,
                                   const QPixmap& pixmap,
                                   const QRect& destination,
                                   QPaintEvent* paint_event)
{

    if (destination.size() != pixmap.size()) {
        // Scale
        QSize displaysize = pixmap.size();
        displaysize.scale(destination.size(), Qt::KeepAspectRatio);
        const QRect dest_active_rect = QRect(destination.topLeft(), displaysize);
        const QRect source_all_image(QPoint(0, 0), pixmap.size());
#ifdef DEBUG_COORDS
        qDebug().nospace()
                << Q_FUNC_INFO
                << " - Asked to draw to rect of size " << destination.size()
                << "; drawing to size " << displaysize;
#endif
        painter.drawPixmap(dest_active_rect, pixmap, source_all_image);
    } else {
        // No need to scale
        painter.drawPixmap(destination.left(), destination.top(), pixmap);
    }

    Q_UNUSED(paint_event);
    // 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.
    // One would read paint_event->rect(), and paint only the relevant part.
    // See also CanvasWidget.
}


#if 0
QRegion scaleRegion(const QRegion& region, qreal factor)
{
    QRegion result;
    for (auto prect : region) {
        QRect scaled_rect(  // left, top, width, height
                    prect.left() * factor,
                    prect.top() * factor,
                    prect.width() * factor,
                    prect.height() * factor
        );
        result += scaled_rect;
    }
    return result;
}
#endif


// ============================================================================
// Creating QGraphicsScene objects
// ============================================================================

// ROUNDED BUTTONS
// Method 1:
// http://stackoverflow.com/questions/17295329/qt-add-a-round-rect-to-a-graphics-item-group

// http://falsinsoft.blogspot.co.uk/2015/11/qt-snippet-rounded-corners-qpushbutton.html
/*
auto pButtonWidget = new QPushButton();
pButtonWidget->setGeometry(QRect(0, 0, 150, 100));
pButtonWidget->setText("Test");
pButtonWidget->setFlat(true);
pButtonWidget->setAttribute(Qt::WA_TranslucentBackground);
pButtonWidget->setStyleSheet(
    "background-color: darkRed;"
    "border: 1px solid black;"
    "border-radius: 15px;"
    "color: lightGray; "
    "font-size: 25px;"
    );
QGraphicsProxyWidget* pButtonProxyWidget = scene()->addWidget(pButtonWidget);
*/

// https://dzone.com/articles/returning-multiple-values-from-functions-in-c


/*
// Doesn't work:
QMetaObject::Connection ButtonAndProxy::connect(const QObject* receiver,
                                                const QMetaMethod& method,
                                                Qt::ConnectionType type)
{
    return QObject::connect(button, &QPushButton::clicked,
                            receiver, method, type);
}
*/


ButtonAndProxy makeTextButton(QGraphicsScene* scene,  // button is added to scene
                              const QRectF& rect,
                              const ButtonConfig& config,
                              const QString& text,
                              QFont font,
                              QWidget* parent)
{
    Q_ASSERT(scene);
    // We want a button that can take word-wrapping text, but not with the more
    // sophisticated width-adjusting word wrap used by
    // ClickableLabelWordWrapWide.
    // So we add a QLabel, as per
    // - http://stackoverflow.com/questions/8960233/subclassing-qlabel-to-show-native-mouse-hover-button-indicator/8960548#8960548

    // We can't have a stylesheet with both plain "attribute: value;"
    // and "QPushButton:pressed { attribute: value; }"; we get an error
    // "Could not parse stylesheet of object 0x...".
    // So we probably need a full stylesheet, and note that the text is in
    // a QLabel, not a QPushButton. We could generalize with a QWidget or
    // specify them exactly ("QPushButton, QLabel"). But "QWidget:pressed"
    // doesn't work.
    // Also, blending the QPushButton and the QLabel stuff and installing it
    // on the button screws things up w.r.t. the "pressed" bit.
    // A QLabel can't have the "pressed" attribute, but it screws up the button
    // press.
    // Also, the QLabel also needs to have the "pressed" background.
    // Re padding etc., see https://www.w3schools.com/css/css_boxmodel.asp
    QString button_css = QString(
                "QPushButton {"
                " background-color: %1;"
                " border: %2;"
                " border-radius: %3;"
                " font-size: %4;"
                " margin: 0;"
                " outline: none; "
                // ... METHOD 1 of switching off the inner (dotted) focus rect
                " padding: %5; "
                "} "
                "QPushButton:pressed {"
                " background-color: %6;"
                "}")
            .arg(colourCss(config.background_colour),  // 1
                 penCss(config.border_pen),  // 2
                 pixelCss(config.corner_radius_px),  // 3
                 pixelCss(config.font_size_px),  // 4
                 pixelCss(config.padding_px),  // 5
                 colourCss(config.pressed_background_colour));  // 6
    // note CSS specifiers:
    // :checked
    // :focus
    // :hover
    // :pressed
    const QString label_css = labelCss(config.text_colour);
#ifdef DEBUG_CSS
    qDebug() << "makeGraphicsTextButton: button CSS:" << button_css;
    qDebug() << "makeGraphicsTextButton: label CSS:" << label_css;
#endif

    ButtonAndProxy result;

    result.button = new QPushButton(parent);
    result.button->setFlat(true);
    result.button->setAttribute(Qt::WA_TranslucentBackground);
    result.button->setStyleSheet(button_css);
    // result.button->setStyle(new StyleNoFocusRect());
    // ... METHOD 2 of switching off the inner (dotted) focus rectangle

    if (!text.isEmpty()) {
        auto label = new QLabel(result.button);
        label->setStyleSheet(label_css);
        font.setPixelSize(config.font_size_px);
        label->setFont(font);
        label->setText(text);
        label->setWordWrap(true);
        label->setAlignment(config.text_alignment);
        label->setMouseTracking(false);
        label->setTextInteractionFlags(Qt::NoTextInteraction);

        auto layout = new QVBoxLayout();
        layout->setContentsMargins(0, 0, 0, 0);
        layout->addWidget(label);

        result.button->setLayout(layout);
    }

    result.proxy = scene->addWidget(result.button);
    result.proxy->setGeometry(rect);

    return result;
}


LabelAndProxy makeText(QGraphicsScene* scene,  // text is added to scene
                       const QPointF& pos,
                       const TextConfig& config,
                       const QString& text,
                       QFont font,
                       QWidget* parent)
{
    Q_ASSERT(scene);
    // QGraphicsTextItem does not support alignment.
    // http://stackoverflow.com/questions/29483125/does-qgraphicstextitem-support-vertical-center-alignment
    QString css = labelCss(config.colour);
#ifdef DEBUG_CSS
    qDebug() << "makeText: CSS:" << css;
#endif

    LabelAndProxy result;
    result.label = new QLabel(text, parent);
    result.label->setStyleSheet(css);
    font.setPixelSize(config.font_size_px);
    result.label->setFont(font);
    result.label->setOpenExternalLinks(false);
    result.label->setTextInteractionFlags(Qt::NoTextInteraction);
    result.label->setAlignment(config.alignment);  // alignment WITHIN label

    QRectF rect(pos, QSizeF());
    if (config.width == -1) {
        result.label->setWordWrap(false);
        rect.setSize(result.label->size());
    } else {
        // word wrap
        result.label->setWordWrap(true);
        rect.setSize(QSizeF(config.width,
                            result.label->heightForWidth(config.width)));
    }

    // Now fix alignment of WHOLE object
    alignRect(rect, config.alignment);

    result.proxy = scene->addWidget(result.label);
    result.proxy->setGeometry(rect);

    return result;
}


AdjustablePieAndProxy makeAdjustablePie(QGraphicsScene* scene,
                                        const QPointF& centre,
                                        const int n_sectors,
                                        const qreal diameter,
                                        QWidget* parent)
{
    const qreal radius = diameter / 2.0;
    const QPointF top_left(centre - QPointF(radius, radius));
    AdjustablePieAndProxy result;
    result.pie = new AdjustablePie(n_sectors, parent);
    result.pie->setOverallRadius(radius);
    const QRectF rect(top_left, QSizeF(diameter, diameter));
    result.proxy = scene->addWidget(result.pie);
    result.proxy->setGeometry(rect);
    return result;
}


SvgWidgetAndProxy makeSvg(
        QGraphicsScene* scene,  // SVG is added to scene
        const QPointF& centre,
        const QString& svg,
        const QColor& pressed_background_colour,
        const QColor& background_colour,
        const bool transparent_for_mouse,
        QWidget* parent)
{
    SvgWidgetAndProxy result;
    const QByteArray contents = svg.toUtf8();

    result.widget = new SvgWidgetClickable(parent);
    result.widget->load(contents);
    result.widget->setBackgroundColour(background_colour);
    result.widget->setPressedBackgroundColour(pressed_background_colour);
    result.widget->setTransparentForMouseEvents(transparent_for_mouse);  // irrelevant!

    const QSizeF size = result.widget->sizeHint();
    const QPointF top_left(centre.x() - size.width() / 2,
                           centre.y() - size.height() / 2);
    const QRectF rect(top_left, size);

    result.proxy = scene->addWidget(result.widget);
    result.proxy->setGeometry(rect);
    result.proxy->setAcceptedMouseButtons(transparent_for_mouse
                                          ? Qt::NoButton
                                          : Qt::LeftButton);

    return result;
}


QGraphicsRectItem* makeObscuringRect(QGraphicsScene* scene,
                                     const QRectF& rect, const qreal opacity,
                                     const QColor& colour_ignoring_opacity)
{
    const QPen pen(Qt::NoPen);
    QColor colour(colour_ignoring_opacity);
    colour.setAlpha(alpha(opacity));
    const QBrush brush(colour);
    return scene->addRect(rect, pen, brush);
}


QGraphicsPixmapItem* makeImage(
        QGraphicsScene* scene,
        const QRectF& rect,
        const QString& filename,
        const qreal opacity,
        const Qt::AspectRatioMode aspect_ratio_mode,
        const Qt::TransformationMode transformation_mode_1,
        const Qt::TransformationMode transformation_mode_2)
{
    // https://stackoverflow.com/questions/5960074/qimage-in-a-qgraphics-scene
    const QPointF top_left = rect.topLeft();
    const QSize size = QSize(qRound(rect.width()), qRound(rect.height()));  // convert float to int
    const QPixmap pixmap_raw = QPixmap(filename);
    const QPixmap pixmap_scaled = pixmap_raw.scaled(size, aspect_ratio_mode,
                                                    transformation_mode_1);
    QGraphicsPixmapItem* img;
    if (opacity < 1.0) {
        auto opacity_img = new GraphicsPixmapItemWithOpacity(pixmap_scaled);
        opacity_img->setOpacity(opacity);
        img = opacity_img;
        scene->addItem(img);  // the scene takes ownership: https://doc.qt.io/qt-6.5/qgraphicsscene.html#addItem
    } else {
        img = scene->addPixmap(pixmap_scaled);
    }
    img->setOffset(top_left);
    img->setTransformationMode(transformation_mode_2);
    return img;
}


}  // namespace graphicsfunc