15.1.153. 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 <QGraphicsRectItem>
#include <QGraphicsScene>
#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,
    const QRectF& rect,
    const ButtonConfig& config,
    const QString& text,
    QFont font,
    QWidget* parent
)
{
    // The button is added to the scene.

    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