diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2021-01-12 15:20:05 +0100 |
---|---|---|
committer | bstoeger <32835590+bstoeger@users.noreply.github.com> | 2021-01-20 08:47:18 +0100 |
commit | 2b961414d7a52d442f5ecd8c2e42e43d044d0d5e (patch) | |
tree | b1d6c47b94dfc80481aaad2e3cfde35335646576 | |
parent | 9be23b5f3f8373ba115f4e66e77c0537abe982b2 (diff) | |
download | subsurface-2b961414d7a52d442f5ecd8c2e42e43d044d0d5e.tar.gz |
statistics: draw legend as a QSGNode
In order not to waste CPU by constantly rerendering the chart,
we must use these weird OpenGL QSGNode things. The interface
is appallingly low-level and unfriendly.
As a first test, try to convert the legend. Create a wrapper
class that represents a rectangular item with a texture
and that will certainly need some (lots of) optimization.
Make sure that all low-level QSG-objects are only accessed
in the rendering thread. This means that the wrapper has
to maintain a notion of "dirtiness" of the state. I.e.
which part of the QSG-objects have to be modified.
From the low-level wrapper derive a class that draws a rounded
rectangle for every resize. The child class of that must then
paint on the rectangle after every resize.
That looks all not very fortunate, but it displays a
legend and will make it possible to move the legend
without and drawing operations, only shifting around
an OpenGL surface.
The render thread goes through all chart-items and
rerenders them if dirty. Currently, on deletion
of these items, this list is not reset. I.e. currently
it is not supported to remove individual items.
Only the full scene can be cleared!
Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
-rw-r--r-- | Subsurface-mobile.pro | 2 | ||||
-rw-r--r-- | stats/CMakeLists.txt | 2 | ||||
-rw-r--r-- | stats/chartitem.cpp | 107 | ||||
-rw-r--r-- | stats/chartitem.h | 51 | ||||
-rw-r--r-- | stats/legend.cpp | 90 | ||||
-rw-r--r-- | stats/legend.h | 18 | ||||
-rw-r--r-- | stats/statsview.cpp | 87 | ||||
-rw-r--r-- | stats/statsview.h | 12 |
8 files changed, 281 insertions, 88 deletions
diff --git a/Subsurface-mobile.pro b/Subsurface-mobile.pro index ea95cf551..e2a724633 100644 --- a/Subsurface-mobile.pro +++ b/Subsurface-mobile.pro @@ -131,6 +131,7 @@ SOURCES += subsurface-mobile-main.cpp \ stats/statsview.cpp \ stats/barseries.cpp \ stats/boxseries.cpp \ + stats/chartitem.cpp \ stats/chartlistmodel.cpp \ stats/informationbox.cpp \ stats/legend.cpp \ @@ -279,6 +280,7 @@ HEADERS += \ backend-shared/roundrectitem.h \ stats/barseries.h \ stats/boxseries.h \ + stats/chartitem.h \ stats/chartlistmodel.h \ stats/informationbox.h \ stats/legend.h \ diff --git a/stats/CMakeLists.txt b/stats/CMakeLists.txt index a084dd0b5..c0fbe3c51 100644 --- a/stats/CMakeLists.txt +++ b/stats/CMakeLists.txt @@ -9,6 +9,8 @@ set(SUBSURFACE_STATS_SRCS barseries.cpp boxseries.h boxseries.cpp + chartitem.h + chartitem.cpp chartlistmodel.h chartlistmodel.cpp informationbox.h diff --git a/stats/chartitem.cpp b/stats/chartitem.cpp new file mode 100644 index 000000000..c0fc50841 --- /dev/null +++ b/stats/chartitem.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "chartitem.h" +#include "statsview.h" + +#include <cmath> +#include <QQuickWindow> +#include <QSGImageNode> +#include <QSGTexture> + +static int round_up(double f) +{ + return static_cast<int>(ceil(f)); +} + +ChartItem::ChartItem(StatsView &v) : + dirty(false), view(v), positionDirty(false), textureDirty(false) +{ +} + +ChartItem::~ChartItem() +{ + painter.reset(); // Make sure to destroy painter before image that is painted on + view.unregisterChartItem(this); +} + +QSizeF ChartItem::sceneSize() const +{ + return view.size(); +} + +void ChartItem::setTextureDirty() +{ + textureDirty = true; + dirty = true; +} + +void ChartItem::setPositionDirty() +{ + positionDirty = true; + dirty = true; +} + +void ChartItem::render() +{ + if (!dirty) + return; + if (!node) { + node.reset(view.w()->createImageNode()); + view.addQSGNode(node.get(), 0); + } + if (!img) { + resize(QSizeF(1,1)); + img->fill(Qt::transparent); + } + if (textureDirty) { + texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel)); + node->setTexture(texture.get()); + textureDirty = false; + } + if (positionDirty) { + node->setRect(rect); + positionDirty = false; + } + dirty = false; +} + +void ChartItem::resize(QSizeF size) +{ + painter.reset(); + img.reset(new QImage(round_up(size.width()), round_up(size.height()), QImage::Format_ARGB32)); + painter.reset(new QPainter(img.get())); + painter->setRenderHint(QPainter::Antialiasing); + rect.setSize(size); + setTextureDirty(); +} + +void ChartItem::setPos(QPointF pos) +{ + rect.moveTopLeft(pos); + setPositionDirty(); +} + +QRectF ChartItem::getRect() const +{ + return rect; +} + +ChartRectItem::ChartRectItem(StatsView &v, const QPen &pen, const QBrush &brush, double radius) : ChartItem(v), + pen(pen), brush(brush), radius(radius) +{ +} + +ChartRectItem::~ChartRectItem() +{ +} + +void ChartRectItem::resize(QSizeF size) +{ + ChartItem::resize(size); + img->fill(Qt::transparent); + painter->setPen(pen); + painter->setBrush(brush); + QSize imgSize = img->size(); + int width = pen.width(); + QRect rect(width / 2, width / 2, imgSize.width() - width, imgSize.height() - width); + painter->drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize); +} diff --git a/stats/chartitem.h b/stats/chartitem.h new file mode 100644 index 000000000..fb4b67dff --- /dev/null +++ b/stats/chartitem.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0 +// Wrappers around QSGImageNode that allow painting onto an image +// and then turning that into a texture to be displayed in a QQuickItem. +#ifndef CHART_ITEM_H +#define CHART_ITEM_H + +#include <memory> +#include <QPainter> + +class QSGImageNode; +class QSGTexture; +class StatsView; + +class ChartItem { +public: + ChartItem(StatsView &v); + ~ChartItem(); + // Attention: The children are responsible for updating the item. None of these calls will. + void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*. + void setPos(QPointF pos); + void render(); // Only call on render thread! + QRectF getRect() const; + bool dirty; // If true, call render() when rebuilding the scene +protected: + std::unique_ptr<QPainter> painter; + std::unique_ptr<QImage> img; + QSizeF sceneSize() const; + void setTextureDirty(); + void setPositionDirty(); +private: + StatsView &view; + QRectF rect; + bool positionDirty; + bool textureDirty; + std::unique_ptr<QSGImageNode> node; + std::unique_ptr<QSGTexture> texture; +}; + +// Draw a rectangular background after resize. Children are responsible for calling update(). +class ChartRectItem : public ChartItem { +public: + ChartRectItem(StatsView &v, const QPen &pen, const QBrush &brush, double radius); + ~ChartRectItem(); + void resize(QSizeF size); +private: + QPen pen; + QBrush brush; + double radius; +}; + +#endif diff --git a/stats/legend.cpp b/stats/legend.cpp index 27607fb51..2cbd883db 100644 --- a/stats/legend.cpp +++ b/stats/legend.cpp @@ -4,8 +4,6 @@ #include "zvalues.h" #include <QFontMetrics> -#include <QGraphicsScene> -#include <QGraphicsSceneMouseEvent> #include <QPen> static const double legendBorderSize = 2.0; @@ -16,51 +14,30 @@ static const double legendInternalBorderSize = 2.0; static const QColor legendColor(0x00, 0x8e, 0xcc, 192); // Note: fourth argument is opacity static const QColor legendBorderColor(Qt::black); -Legend::Legend(const std::vector<QString> &names) : - RoundRectItem(legendBoxBorderRadius), - displayedItems(0), width(0.0), height(0.0) +Legend::Legend(StatsView &view, const std::vector<QString> &names) : + ChartRectItem(view, QPen(legendBorderColor, legendBorderSize), QBrush(legendColor), legendBoxBorderRadius), + displayedItems(0), width(0.0), height(0.0), + font(QFont()) // Make configurable { - setZValue(ZValues::legend); entries.reserve(names.size()); + QFontMetrics fm(font); + fontHeight = fm.height(); int idx = 0; for (const QString &name: names) - entries.emplace_back(name, idx++, (int)names.size(), this); - - // Calculate the height and width of the elements - if (!entries.empty()) { - QFontMetrics fm(entries[0].text->font()); - fontHeight = fm.height(); - for (Entry &e: entries) - e.width = fontHeight + 2.0 * legendBoxBorderSize + - fm.size(Qt::TextSingleLine, e.text->text()).width(); - } else { - // Set to an arbitrary non-zero value, because Coverity doesn't understand - // that we don't use the value as divisor below if entries is empty. - fontHeight = 10.0; - } - setPen(QPen(legendBorderColor, legendBorderSize)); - setBrush(QBrush(legendColor)); + entries.emplace_back(name, idx++, (int)names.size(), fm); } -Legend::Entry::Entry(const QString &name, int idx, int numBins, QGraphicsItem *parent) : - rect(new QGraphicsRectItem(parent)), - text(new QGraphicsSimpleTextItem(name, parent)), - width(0) +Legend::Entry::Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm) : + name(name), + rectBrush(QBrush(binColor(idx, numBins))) { - rect->setZValue(ZValues::legend); - rect->setPen(QPen(legendBorderColor, legendBoxBorderSize)); - rect->setBrush(QBrush(binColor(idx, numBins))); - text->setZValue(ZValues::legend); - text->setBrush(QBrush(darkLabelColor)); + width = fm.height() + 2.0 * legendBoxBorderSize + fm.size(Qt::TextSingleLine, name).width(); } void Legend::hide() { - for (Entry &e: entries) { - e.rect->hide(); - e.text->hide(); - } - QGraphicsRectItem::hide(); + ChartRectItem::resize(QSizeF(1,1)); + img->fill(Qt::transparent); } void Legend::resize() @@ -68,7 +45,7 @@ void Legend::resize() if (entries.empty()) return hide(); - QSizeF size = scene()->sceneRect().size(); + QSizeF size = sceneSize(); // Silly heuristics: make the legend at most half as high and half as wide as the chart. // Not sure if that makes sense - this might need some optimization. @@ -100,31 +77,32 @@ void Legend::resize() } width += legendInternalBorderSize; height = 2 * legendInternalBorderSize + numRows * fontHeight; - updatePosition(); -} -void Legend::updatePosition() -{ - if (displayedItems <= 0) - return hide(); - // For now, place the legend in the top right corner. - QPointF pos(scene()->sceneRect().width() - width - 10.0, 10.0); - setRect(QRectF(pos, QSizeF(width, height))); + ChartRectItem::resize(QSizeF(width, height)); + + // Paint rectangles + painter->setPen(QPen(legendBorderColor, legendBoxBorderSize)); for (int i = 0; i < displayedItems; ++i) { - QPointF itemPos = pos + entries[i].pos; + QPointF itemPos = entries[i].pos; + painter->setBrush(entries[i].rectBrush); QRectF rect(itemPos, QSizeF(fontHeight, fontHeight)); // Decrease box size by legendBoxScale factor double delta = fontHeight * (1.0 - legendBoxScale) / 2.0; rect = rect.adjusted(delta, delta, -delta, -delta); - entries[i].rect->setRect(rect); - itemPos.rx() += fontHeight + 2.0 * legendBoxBorderSize; - entries[i].text->setPos(itemPos); - entries[i].rect->show(); - entries[i].text->show(); + painter->drawRect(rect); } - for (int i = displayedItems; i < (int)entries.size(); ++i) { - entries[i].rect->hide(); - entries[i].text->hide(); + + // Paint labels + painter->setPen(darkLabelColor); // QPainter uses pen not brush for text! + painter->setFont(font); + for (int i = 0; i < displayedItems; ++i) { + QPointF itemPos = entries[i].pos; + itemPos.rx() += fontHeight + 2.0 * legendBoxBorderSize; + QRectF rect(itemPos, QSizeF(entries[i].width, fontHeight)); + painter->drawText(rect, entries[i].name); } - show(); + + // For now, place the legend in the top right corner. + QPointF pos(size.width() - width - 10.0, 10.0); + setPos(pos); } diff --git a/stats/legend.h b/stats/legend.h index c643a41f3..a9d42bf39 100644 --- a/stats/legend.h +++ b/stats/legend.h @@ -3,34 +3,34 @@ #ifndef STATS_LEGEND_H #define STATS_LEGEND_H -#include "backend-shared/roundrectitem.h" +#include "chartitem.h" #include <memory> #include <vector> +#include <QFont> -class QGraphicsScene; -class QGraphicsSceneMouseEvent; +class QFontMetrics; -class Legend : public RoundRectItem { +class Legend : public ChartRectItem { public: - Legend(const std::vector<QString> &names); + Legend(StatsView &view, const std::vector<QString> &names); void hover(QPointF pos); void resize(); // called when the chart size changes. private: // Each entry is a text besides a rectangle showing the color struct Entry { - std::unique_ptr<QGraphicsRectItem> rect; - std::unique_ptr<QGraphicsSimpleTextItem> text; + QString name; + QBrush rectBrush; QPointF pos; double width; - Entry(const QString &name, int idx, int numBins, QGraphicsItem *parent); + Entry(const QString &name, int idx, int numBins, const QFontMetrics &fm); }; int displayedItems; double width; double height; + QFont font; int fontHeight; std::vector<Entry> entries; - void updatePosition(); void hide(); }; diff --git a/stats/statsview.cpp b/stats/statsview.cpp index e5e6b0b0d..61510d60c 100644 --- a/stats/statsview.cpp +++ b/stats/statsview.cpp @@ -25,13 +25,48 @@ #include <QSGImageNode> #include <QSGTexture> +// Constants that control the graph layouts +static const QColor quartileMarkerColor(Qt::red); +static const double quartileMarkerSize = 15.0; +static const double sceneBorder = 5.0; // Border between scene edges and statitistics view +static const double titleBorder = 2.0; // Border between title and chart + +StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent), + highlightedSeries(nullptr), + xAxis(nullptr), + yAxis(nullptr), + rootNode(nullptr) +{ + setFlag(ItemHasContents, true); + + connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible); + + setAcceptHoverEvents(true); + + QFont font; + titleFont = QFont(font.family(), font.pointSize(), QFont::Light); // Make configurable +} + +StatsView::StatsView() : StatsView(nullptr) +{ +} + +StatsView::~StatsView() +{ +} + QSGNode *StatsView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) { // The QtQuick drawing interface is utterly bizzare with a distinct 1980ies-style memory management. // This is just a copy of what is found in Qt's documentation. QSGImageNode *n = static_cast<QSGImageNode *>(oldNode); if (!n) - n = window()->createImageNode(); + n = rootNode = window()->createImageNode(); + + for (ChartItem *item: items) { + if (item->dirty) + item->render(); + } QRectF rect = boundingRect(); if (plotRect != rect) { @@ -47,33 +82,30 @@ QSGNode *StatsView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNod return n; } -// Constants that control the graph layouts -static const QColor quartileMarkerColor(Qt::red); -static const double quartileMarkerSize = 15.0; -static const double sceneBorder = 5.0; // Border between scene edges and statitistics view -static const double titleBorder = 2.0; // Border between title and chart - -StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent), - highlightedSeries(nullptr), - xAxis(nullptr), - yAxis(nullptr) +void StatsView::addQSGNode(QSGNode *node, int) { - setFlag(ItemHasContents, true); - - connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible); - - setAcceptHoverEvents(true); + rootNode->appendChildNode(node); +} - QFont font; - titleFont = QFont(font.family(), font.pointSize(), QFont::Light); // Make configurable +// Currently this does an inefficient linear search in the chart-item vector. +// The reason is that removing individual chart items is very rare: for now, +// it is only done when hiding an InfoBox. In the future, this might have to +// be improved. +void StatsView::unregisterChartItem(const ChartItem *item) +{ + auto it = std::find(items.begin(), items.end(), item); + if (it != items.end()) + items.erase(it); } -StatsView::StatsView() : StatsView(nullptr) +QQuickWindow *StatsView::w() const { + return window(); } -StatsView::~StatsView() +QSizeF StatsView::size() const { + return boundingRect().size(); } void StatsView::plotAreaChanged(const QSizeF &s) @@ -202,6 +234,14 @@ T *StatsView::createAxis(const QString &title, Args&&... args) return res; } +template <typename T, class... Args> +std::unique_ptr<T> StatsView::createChartItem(Args&&... args) +{ + std::unique_ptr<T> res(new T(*this, std::forward<Args>(args)...)); + items.push_back(res.get()); + return res; +} + void StatsView::setAxes(StatsAxis *x, StatsAxis *y) { xAxis = x; @@ -214,6 +254,7 @@ void StatsView::reset() { highlightedSeries = nullptr; xAxis = yAxis = nullptr; + items.clear(); // non-owning pointers legend.reset(); series.clear(); quartileMarkers.clear(); @@ -406,7 +447,7 @@ void StatsView::plotBarChart(const std::vector<dive *> &dives, // Paint legend first, because the bin-names will be moved away from. if (showLegend) - legend = createItemPtr<Legend>(&scene, data.vbinNames); + legend = createChartItem<Legend>(data.vbinNames); std::vector<BarSeries::MultiItem> items; items.reserve(data.hbin_counts.size()); @@ -623,7 +664,7 @@ void StatsView::plotPieChart(const std::vector<dive *> &dives, PieSeries *series = createSeries<PieSeries>(categoryVariable->name(), data, keepOrder, labels); if (showLegend) - legend = createItemPtr<Legend>(&scene, series->binNames()); + legend = createChartItem<Legend>(series->binNames()); } void StatsView::plotDiscreteBoxChart(const std::vector<dive *> &dives, @@ -959,7 +1000,7 @@ void StatsView::plotHistogramStackedChart(const std::vector<dive *> &dives, BarPlotData data(categoryBins, *valueBinner); if (showLegend) - legend = createItemPtr<Legend>(&scene, data.vbinNames); + legend = createChartItem<Legend>(data.vbinNames); CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal); diff --git a/stats/statsview.h b/stats/statsview.h index b1e178565..385a591f4 100644 --- a/stats/statsview.h +++ b/stats/statsview.h @@ -19,8 +19,10 @@ struct StatsVariable; class QGraphicsLineItem; class QGraphicsSimpleTextItem; +class QSGImageNode; class StatsSeries; class CategoryAxis; +class ChartItem; class CountAxis; class HistogramAxis; class StatsAxis; @@ -46,6 +48,10 @@ public: ~StatsView(); void plot(const StatsState &state); + QQuickWindow *w() const; // Make window available to items + QSizeF size() const; + void addQSGNode(QSGNode *node, int z); // Must only be called in render thread! + void unregisterChartItem(const ChartItem *item); private slots: void replotIfVisible(); private: @@ -103,6 +109,9 @@ private: template <typename T, class... Args> T *createAxis(const QString &title, Args&&... args); + template <typename T, class... Args> + std::unique_ptr<T> createChartItem(Args&&... args); + template<typename T> CategoryAxis *createCategoryAxis(const QString &title, const StatsBinner &binner, const std::vector<T> &bins, bool isHorizontal); @@ -156,11 +165,14 @@ private: std::vector<RegressionLine> regressionLines; std::vector<HistogramMarker> histogramMarkers; std::unique_ptr<QGraphicsSimpleTextItem> title; + std::vector<ChartItem *> items; // Attention: currently, items are not automatically removed on destruction! StatsSeries *highlightedSeries; StatsAxis *xAxis, *yAxis; void hoverEnterEvent(QHoverEvent *event) override; void hoverMoveEvent(QHoverEvent *event) override; + + QSGImageNode *rootNode; }; #endif |