From 2eebae13dd8af0060b44c80fcc642a6f88d0c319 Mon Sep 17 00:00:00 2001 From: Berthold Stoeger Date: Sun, 16 Apr 2023 20:05:36 +0200 Subject: [PATCH] stats: break out common QtQuick part of the code Move most of the QtQuick code to its own directory, so that it can be reused in the future for the chart. Signed-off-by: Berthold Stoeger --- CMakeLists.txt | 3 + Subsurface-mobile.pro | 6 + qt-quick/CMakeLists.txt | 19 +++ qt-quick/chartitem.cpp | 246 ++++++++++++++++++++++++++++++++++ qt-quick/chartitem.h | 169 ++++++++++++++++++++++++ qt-quick/chartitem_ptr.h | 51 ++++++++ qt-quick/chartitemhelper.h | 100 ++++++++++++++ qt-quick/chartview.cpp | 243 ++++++++++++++++++++++++++++++++++ qt-quick/chartview.h | 97 ++++++++++++++ stats/barseries.cpp | 2 +- stats/barseries.h | 2 +- stats/boxseries.cpp | 2 +- stats/chartitem.cpp | 262 +------------------------------------ stats/chartitem.h | 176 ++----------------------- stats/histogrammarker.cpp | 2 +- stats/histogrammarker.h | 3 +- stats/informationbox.cpp | 9 +- stats/informationbox.h | 5 +- stats/legend.cpp | 8 +- stats/legend.h | 4 +- stats/pieseries.cpp | 2 +- stats/pieseries.h | 2 +- stats/quartilemarker.cpp | 4 +- stats/quartilemarker.h | 3 +- stats/regressionitem.cpp | 6 +- stats/regressionitem.h | 3 +- stats/scatterseries.cpp | 3 +- stats/scatterseries.h | 1 + stats/statsaxis.cpp | 28 ++-- stats/statsaxis.h | 17 +-- stats/statsgrid.h | 2 +- stats/statshelper.h | 132 +------------------ stats/statsview.cpp | 257 +++--------------------------------- stats/statsview.h | 77 +---------- 34 files changed, 1031 insertions(+), 915 deletions(-) create mode 100644 qt-quick/CMakeLists.txt create mode 100644 qt-quick/chartitem.cpp create mode 100644 qt-quick/chartitem.h create mode 100644 qt-quick/chartitem_ptr.h create mode 100644 qt-quick/chartitemhelper.h create mode 100644 qt-quick/chartview.cpp create mode 100644 qt-quick/chartview.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dfbf4e233..a5c1e5b56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -420,6 +420,7 @@ if(MAPSUPPORT) add_subdirectory(map-widget) endif() add_subdirectory(mobile-widgets) +add_subdirectory(qt-quick) add_subdirectory(stats) endif() add_subdirectory(backend-shared) @@ -473,6 +474,7 @@ if (SUBSURFACE_TARGET_EXECUTABLE MATCHES "MobileExecutable") subsurface_commands subsurface_corelib subsurface_stats + subsurface_qtquick kirigamiplugin ${SUBSURFACE_LINK_LIBRARIES} ) @@ -499,6 +501,7 @@ elseif (SUBSURFACE_TARGET_EXECUTABLE MATCHES "DesktopExecutable") subsurface_commands subsurface_corelib subsurface_stats + subsurface_qtquick ${SUBSURFACE_LINK_LIBRARIES} ) add_dependencies(subsurface_desktop_preferences subsurface_generated_ui) diff --git a/Subsurface-mobile.pro b/Subsurface-mobile.pro index e09a7a61d..efd8b56a9 100644 --- a/Subsurface-mobile.pro +++ b/Subsurface-mobile.pro @@ -129,6 +129,8 @@ SOURCES += subsurface-mobile-main.cpp \ backend-shared/exportfuncs.cpp \ backend-shared/plannershared.cpp \ backend-shared/roundrectitem.cpp \ + qt-quick/chartitem.cpp \ + qt-quick/chartview.cpp \ stats/statsvariables.cpp \ stats/statsview.cpp \ stats/barseries.cpp \ @@ -286,6 +288,10 @@ HEADERS += \ backend-shared/exportfuncs.h \ backend-shared/plannershared.h \ backend-shared/roundrectitem.h \ + qt-quick/chartitem.h \ + qt-quick/chartitemhelper.h \ + qt-quick/chartitem_ptr.h \ + qt-quick/chartview.h \ stats/barseries.h \ stats/boxseries.h \ stats/chartitem.h \ diff --git a/qt-quick/CMakeLists.txt b/qt-quick/CMakeLists.txt new file mode 100644 index 000000000..87e1323e4 --- /dev/null +++ b/qt-quick/CMakeLists.txt @@ -0,0 +1,19 @@ +# code for qt-quick based charts +include_directories(. + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_BINARY_DIR} +) + +set(SUBSURFACE_QTQUICK_SRCS + chartitem.cpp + chartitem.h + chartitemhelper.h + chartitem_ptr.h + chartview.cpp + chartview.h +) + +source_group("Subsurface qtquick sourcecode" FILES ${SUBSURFACE_QTQUICK_SRCS}) + +add_library(subsurface_qtquick STATIC ${SUBSURFACE_QTQUICK_SRCS}) +target_link_libraries(subsurface_qtquick ${QT_LIBRARIES}) diff --git a/qt-quick/chartitem.cpp b/qt-quick/chartitem.cpp new file mode 100644 index 000000000..90f83c545 --- /dev/null +++ b/qt-quick/chartitem.cpp @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "chartitem.h" +#include "chartitemhelper.h" +#include "chartview.h" + +#include +#include +#include +#include + +ChartItem::ChartItem(ChartView &v, size_t z) : + dirty(false), prev(nullptr), next(nullptr), + zValue(z), view(v) +{ + // Register before the derived constructors run, so that the + // derived classes can mark the item as dirty in the constructor. + v.registerChartItem(*this); +} + +ChartItem::~ChartItem() +{ +} + +QSizeF ChartItem::sceneSize() const +{ + return view.size(); +} + +void ChartItem::markDirty() +{ + view.registerDirtyChartItem(*this); +} + +static int round_up(double f) +{ + return static_cast(ceil(f)); +} + +ChartPixmapItem::ChartPixmapItem(ChartView &v, size_t z) : HideableChartItem(v, z), + positionDirty(false), textureDirty(false) +{ +} + +ChartPixmapItem::~ChartPixmapItem() +{ + painter.reset(); // Make sure to destroy painter before image that is painted on +} + +void ChartPixmapItem::setTextureDirty() +{ + textureDirty = true; + markDirty(); +} + +void ChartPixmapItem::setPositionDirty() +{ + positionDirty = true; + markDirty(); +} + +void ChartPixmapItem::render() +{ + if (!node) { + createNode(view.w()->createImageNode()); + view.addQSGNode(node.get(), zValue); + } + updateVisible(); + + if (!img) { + resize(QSizeF(1,1)); + img->fill(Qt::transparent); + } + if (textureDirty) { + texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel)); + node->node->setTexture(texture.get()); + textureDirty = false; + } + if (positionDirty) { + node->node->setRect(rect); + positionDirty = false; + } +} + +void ChartPixmapItem::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 ChartPixmapItem::setPos(QPointF pos) +{ + rect.moveTopLeft(pos); + setPositionDirty(); +} + +QRectF ChartPixmapItem::getRect() const +{ + return rect; +} + +ChartRectItem::ChartRectItem(ChartView &v, size_t z, + const QPen &pen, const QBrush &brush, double radius) : ChartPixmapItem(v, z), + pen(pen), brush(brush), radius(radius) +{ +} + +ChartRectItem::~ChartRectItem() +{ +} + +void ChartRectItem::resize(QSizeF size) +{ + ChartPixmapItem::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); +} + +ChartTextItem::ChartTextItem(ChartView &v, size_t z, const QFont &f, const std::vector &text, bool center) : + ChartPixmapItem(v, z), f(f), center(center) +{ + QFontMetrics fm(f); + double totalWidth = 1.0; + fontHeight = static_cast(fm.height()); + double totalHeight = std::max(1.0, static_cast(text.size()) * fontHeight); + + items.reserve(text.size()); + for (const QString &s: text) { + double w = fm.size(Qt::TextSingleLine, s).width(); + items.push_back({ s, w }); + if (w > totalWidth) + totalWidth = w; + } + resize(QSizeF(totalWidth, totalHeight)); +} + +ChartTextItem::ChartTextItem(ChartView &v, size_t z, const QFont &f, const QString &text) : + ChartTextItem(v, z, f, std::vector({ text }), true) +{ +} + +void ChartTextItem::setColor(const QColor &c) +{ + setColor(c, Qt::transparent); +} + +void ChartTextItem::setColor(const QColor &c, const QColor &background) +{ + img->fill(background); + double y = 0.0; + painter->setPen(QPen(c)); + painter->setFont(f); + double totalWidth = getRect().width(); + for (const auto &[s, w]: items) { + double x = center ? round((totalWidth - w) / 2.0) : 0.0; + QRectF rect(x, y, w, fontHeight); + painter->drawText(rect, s); + y += fontHeight; + } + setTextureDirty(); +} + +ChartLineItemBase::~ChartLineItemBase() +{ +} + +void ChartLineItemBase::setLine(QPointF fromIn, QPointF toIn) +{ + from = fromIn; + to = toIn; + positionDirty = true; + markDirty(); +} + +void ChartLineItem::render() +{ + if (!node) { + geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2)); + geometry->setDrawingMode(QSGGeometry::DrawLines); + material.reset(new QSGFlatColorMaterial); + createNode(); + node->setGeometry(geometry.get()); + node->setMaterial(material.get()); + view.addQSGNode(node.get(), zValue); + positionDirty = materialDirty = true; + } + updateVisible(); + + if (positionDirty) { + // Attention: width is a geometry property and therefore handled by position dirty! + geometry->setLineWidth(static_cast(width)); + auto vertices = geometry->vertexDataAsPoint2D(); + setPoint(vertices[0], from); + setPoint(vertices[1], to); + node->markDirty(QSGNode::DirtyGeometry); + } + + if (materialDirty) { + material->setColor(color); + node->markDirty(QSGNode::DirtyMaterial); + } + + positionDirty = materialDirty = false; +} + +void ChartRectLineItem::render() +{ + if (!node) { + geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 5)); + geometry->setDrawingMode(QSGGeometry::DrawLineStrip); + material.reset(new QSGFlatColorMaterial); + createNode(); + node->setGeometry(geometry.get()); + node->setMaterial(material.get()); + view.addQSGNode(node.get(), zValue); + positionDirty = materialDirty = true; + } + updateVisible(); + + if (positionDirty) { + // Attention: width is a geometry property and therefore handled by position dirty! + geometry->setLineWidth(static_cast(width)); + auto vertices = geometry->vertexDataAsPoint2D(); + setPoint(vertices[0], from); + setPoint(vertices[1], QPointF(from.x(), to.y())); + setPoint(vertices[2], to); + setPoint(vertices[3], QPointF(to.x(), from.y())); + setPoint(vertices[4], from); + node->markDirty(QSGNode::DirtyGeometry); + } + + if (materialDirty) { + material->setColor(color); + node->markDirty(QSGNode::DirtyMaterial); + } + + positionDirty = materialDirty = false; +} diff --git a/qt-quick/chartitem.h b/qt-quick/chartitem.h new file mode 100644 index 000000000..8313c1609 --- /dev/null +++ b/qt-quick/chartitem.h @@ -0,0 +1,169 @@ +// 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 "chartitem_ptr.h" +#include "chartitemhelper.h" + +#include +#include + +class ChartView; +class QSGGeometry; +class QSGGeometryNode; +class QSGFlatColorMaterial; +class QSGImageNode; +class QSGRectangleNode; +class QSGTexture; +class QSGTextureMaterial; + +class ChartItem { +public: + // Only call on render thread! + virtual void render() = 0; + bool dirty; // If true, call render() when rebuilding the scene + ChartItem *prev, *next; // Double linked list of items + const size_t zValue; + virtual ~ChartItem(); // Attention: must only be called by render thread. +protected: + ChartItem(ChartView &v, size_t z); + QSizeF sceneSize() const; + ChartView &view; + void markDirty(); +}; + +template +class HideableChartItem : public ChartItem { +protected: + HideableChartItem(ChartView &v, size_t z); + std::unique_ptr node; + bool visible; + bool visibleChanged; + template + void createNode(Args&&... args); // Call to create node with visibility flag. + void updateVisible(); // Must be called by child class to update visibility flag! +public: + void setVisible(bool visible); +}; + +// A shortcut for ChartItems based on a hideable proxy item +template +using HideableChartProxyItem = HideableChartItem>>; + +// A chart item that blits a precalculated pixmap onto the scene. +class ChartPixmapItem : public HideableChartProxyItem { +public: + ChartPixmapItem(ChartView &v, size_t z); + ~ChartPixmapItem(); + + void setPos(QPointF pos); + void render() override; + QRectF getRect() const; +protected: + void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*. + std::unique_ptr painter; + std::unique_ptr img; + void setTextureDirty(); + void setPositionDirty(); + QRectF rect; +private: + bool positionDirty; // true if the position changed since last render + bool textureDirty; // true if the pixmap changed since last render + std::unique_ptr texture; +}; + +// Draw a rectangular background after resize. Children are responsible for calling update(). +class ChartRectItem : public ChartPixmapItem { +public: + ChartRectItem(ChartView &v, size_t z, const QPen &pen, const QBrush &brush, double radius); + ~ChartRectItem(); + void resize(QSizeF size); +private: + QPen pen; + QBrush brush; + double radius; +}; + +// Attention: text is only drawn after calling setColor()! +class ChartTextItem : public ChartPixmapItem { +public: + ChartTextItem(ChartView &v, size_t z, const QFont &f, const std::vector &text, bool center); + ChartTextItem(ChartView &v, size_t z, const QFont &f, const QString &text); + void setColor(const QColor &color); // Draw on transparent background + void setColor(const QColor &color, const QColor &background); // Fill rectangle with given background color +private: + const QFont &f; + double fontHeight; + bool center; + struct Item { + QString s; + double width; + }; + std::vector items; +}; + +// Common data for line and rect items. Both are represented by two points. +class ChartLineItemBase : public HideableChartItem> { +public: + ChartLineItemBase(ChartView &v, size_t z, QColor color, double width); + ~ChartLineItemBase(); + void setLine(QPointF from, QPointF to); +protected: + QPointF from, to; + QColor color; + double width; + bool positionDirty; + bool materialDirty; + std::unique_ptr material; + std::unique_ptr geometry; +}; + +class ChartLineItem : public ChartLineItemBase { +public: + using ChartLineItemBase::ChartLineItemBase; + void render() override; +}; + +// A simple rectangle without fill. Specified by any two opposing vertices. +class ChartRectLineItem : public ChartLineItemBase { +public: + using ChartLineItemBase::ChartLineItemBase; + void render() override; +}; + +// Implementation detail of templates - move to serparate header file +template +void HideableChartItem::setVisible(bool visibleIn) +{ + if (visible == visibleIn) + return; + visible = visibleIn; + visibleChanged = true; + markDirty(); +} + +template +template +void HideableChartItem::createNode(Args&&... args) +{ + node.reset(new Node(visible, std::forward(args)...)); + visibleChanged = false; +} + +template +HideableChartItem::HideableChartItem(ChartView &v, size_t z) : ChartItem(v, z), + visible(true), visibleChanged(false) +{ +} + +template +void HideableChartItem::updateVisible() +{ + if (visibleChanged) + node->setVisible(visible); + visibleChanged = false; +} + +#endif diff --git a/qt-quick/chartitem_ptr.h b/qt-quick/chartitem_ptr.h new file mode 100644 index 000000000..35401798b --- /dev/null +++ b/qt-quick/chartitem_ptr.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0 +// A stupid pointer class that initializes to null and can be copy +// assigned. This is for historical reasons: unique_ptrs to ChartItems +// were replaced by plain pointers. Instead of nulling the plain pointers +// in the constructors, use this. Ultimately, we might think about making +// this thing smarter, once removal of individual ChartItems is implemented. +#ifndef CHARITEM_PTR_H +#define CHARITEM_PTR_H + +template +class ChartItemPtr { + friend class ChartView; // Only the chart view can create these pointers + T *ptr; + ChartItemPtr(T *ptr) : ptr(ptr) + { + } +public: + ChartItemPtr() : ptr(nullptr) + { + } + ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr) + { + } + void reset() + { + ptr = nullptr; + } + ChartItemPtr &operator=(const ChartItemPtr &p) + { + ptr = p.ptr; + return *this; + } + operator bool() const + { + return !!ptr; + } + bool operator!() const + { + return !ptr; + } + T &operator*() const + { + return *ptr; + } + T *operator->() const + { + return ptr; + } +}; + +#endif diff --git a/qt-quick/chartitemhelper.h b/qt-quick/chartitemhelper.h new file mode 100644 index 000000000..9dd27793f --- /dev/null +++ b/qt-quick/chartitemhelper.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0 +// QSGNode template jugglery to overcome API flaws. + +#ifndef CHARTITEM_HELPER_H +#define CHARTITEM_HELPER_H + +#include +#include + +// In general, we want chart items to be hideable. For example to show/hide +// labels on demand. Very sadly, the QSG API is absolutely terrible with +// respect to temporarily disabling. Instead of simply having a flag, +// a QSGNode is queried using the "isSubtreeBlocked()" virtual function(!). +// +// Not only is this a slow operation performed on every single node, it +// also is often not possible to override this function: For improved +// performance, the documentation recommends to create QSG nodes via +// QQuickWindow. This provides nodes optimized for the actual hardware. +// However, this obviously means that these nodes cannot be derived from! +// +// In that case, there are two possibilities: Add a proxy node with an +// overridden "isSubtreeBlocked()" function or remove the node from the +// scene. The former was chosen here, because it is less complex. +// +// The following slightly cryptic templates are used to unify the two +// cases: The QSGNode is generated by our own code or the QSGNode is +// obtained from QQuickWindow. +// +// The "HideableQSGNode" template augments the QSGNode "Node" +// by a "setVisible()" function and overrides "isSubtreeBlocked()" +// +// The "QSGProxyNode" template is a QSGNode with a single +// child of type "Node". +// +// Thus, if the node can be created, use: +// HideableQSGNode node +// and if the node can only be obtained from QQuickWindow, use: +// HideableQSGNode> node +// The latter should obviously be typedef-ed. +// +// Yes, that's all horrible, but if nothing else it teaches us about +// composition. +template +class HideableQSGNode : public Node { + bool hidden; + bool isSubtreeBlocked() const override final; +public: + template + HideableQSGNode(bool visible, Args&&... args); + void setVisible(bool visible); +}; + +template +class QSGProxyNode : public QSGNode { +public: + std::unique_ptr node; + QSGProxyNode(Node *node); +}; + +// Implementation detail of templates - move to serparate header file +template +QSGProxyNode::QSGProxyNode(Node *node) : node(node) +{ + appendChildNode(node); +} + +template +bool HideableQSGNode::isSubtreeBlocked() const +{ + return hidden; +} + +template +template +HideableQSGNode::HideableQSGNode(bool visible, Args&&... args) : + Node(std::forward(args)...), + hidden(!visible) +{ +} + +template +void HideableQSGNode::setVisible(bool visible) +{ + hidden = !visible; + Node::markDirty(QSGNode::DirtySubtreeBlocked); +} + +// Helper function to set points +inline void setPoint(QSGGeometry::Point2D &v, const QPointF &p) +{ + v.set(static_cast(p.x()), static_cast(p.y())); +} + +inline void setPoint(QSGGeometry::TexturedPoint2D &v, const QPointF &p, const QPointF &t) +{ + v.set(static_cast(p.x()), static_cast(p.y()), + static_cast(t.x()), static_cast(t.y())); +} + +#endif diff --git a/qt-quick/chartview.cpp b/qt-quick/chartview.cpp new file mode 100644 index 000000000..6a4e66f90 --- /dev/null +++ b/qt-quick/chartview.cpp @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "chartview.h" +#include "chartitem.h" + +#include +#include + +ChartView::ChartView(QQuickItem *parent, size_t maxZ) : QQuickItem(parent), + maxZ(maxZ), + backgroundDirty(true), + rootNode(nullptr) +{ + setFlag(ItemHasContents, true); +} + +ChartView::~ChartView() +{ +} + +// Define a hideable dummy QSG node that is used as a parent node to make +// all objects of a z-level visible / invisible. +using ZNode = HideableQSGNode; + +class RootNode : public QSGNode +{ +public: + RootNode(ChartView &view, QColor backgroundColor, size_t maxZ); + ~RootNode(); + ChartView &view; + std::unique_ptr backgroundNode; // solid background + // We entertain one node per Z-level. + std::vector> zNodes; +}; + +RootNode::RootNode(ChartView &view, QColor backgroundColor, size_t maxZ) : view(view) +{ + zNodes.resize(maxZ); + + // Add a background rectangle with a solid color. This could + // also be done on the widget level, but would have to be done + // separately for desktop and mobile, so do it here. + backgroundNode.reset(view.w()->createRectangleNode()); + appendChildNode(backgroundNode.get()); + + for (auto &zNode: zNodes) { + zNode.reset(new ZNode(true)); + appendChildNode(zNode.get()); + } +} + +RootNode::~RootNode() +{ + view.emergencyShutdown(); +} + +void ChartView::freeDeletedChartItems() +{ + ChartItem *nextitem; + for (ChartItem *item = deletedItems.first; item; item = nextitem) { + nextitem = item->next; + delete item; + } + deletedItems.clear(); +} + +QSGNode *ChartView::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. + RootNode *n = static_cast(oldNode); + if (!n) + n = rootNode = new RootNode(*this, backgroundColor, maxZ); + + // Delete all chart items that are marked for deletion. + freeDeletedChartItems(); + + if (backgroundDirty) { + rootNode->backgroundNode->setRect(plotRect); + backgroundDirty = false; + } + + for (ChartItem *item = dirtyItems.first; item; item = item->next) { + item->render(); + item->dirty = false; + } + dirtyItems.splice(cleanItems); + + return n; +} + +// When reparenting the QQuickWidget, QtQuick decides to delete our rootNode +// and with it all the QSG nodes, even though we have *not* given the +// permission to do so! If the widget is reused, we try to delete the +// stale items, whose nodes have already been deleted by QtQuick, leading +// to a double-free(). Instead of searching for the cause of this behavior, +// let's just hook into the rootNodes destructor and delete the objects +// in a controlled manner, so that QtQuick has no more access to them. +void ChartView::emergencyShutdown() +{ + // Mark clean and dirty chart items for deletion... + cleanItems.splice(deletedItems); + dirtyItems.splice(deletedItems); + + // ...and delete them. + freeDeletedChartItems(); + + // Now delete all the pointers we might have to chart features, + // axes, etc. Note that all pointers to chart items are non + // owning, so this only resets stale references, but does not + // lead to any additional deletion of chart items. + resetPointers(); + + // The rootNode is being deleted -> remove the reference to that + rootNode = nullptr; +} + +void ChartView::clearItems() +{ + cleanItems.splice(deletedItems); + dirtyItems.splice(deletedItems); +} + +void ChartView::addQSGNode(QSGNode *node, size_t z) +{ + size_t idx = std::clamp(z, (size_t)0, maxZ); + rootNode->zNodes[idx]->appendChildNode(node); +} + +void ChartView::registerChartItem(ChartItem &item) +{ + cleanItems.append(item); +} + +void ChartView::registerDirtyChartItem(ChartItem &item) +{ + if (item.dirty) + return; + cleanItems.remove(item); + dirtyItems.append(item); + item.dirty = true; +} + +void ChartView::deleteChartItemInternal(ChartItem &item) +{ + if (item.dirty) + dirtyItems.remove(item); + else + cleanItems.remove(item); + deletedItems.append(item); +} + +ChartView::ChartItemList::ChartItemList() : first(nullptr), last(nullptr) +{ +} + +void ChartView::ChartItemList::clear() +{ + first = last = nullptr; +} + +void ChartView::ChartItemList::remove(ChartItem &item) +{ + if (item.next) + item.next->prev = item.prev; + else + last = item.prev; + if (item.prev) + item.prev->next = item.next; + else + first = item.next; + item.prev = item.next = nullptr; +} + +void ChartView::ChartItemList::append(ChartItem &item) +{ + if (!first) { + first = &item; + } else { + item.prev = last; + last->next = &item; + } + last = &item; +} + +void ChartView::ChartItemList::splice(ChartItemList &l2) +{ + if (!first) // if list is empty -> nothing to do. + return; + if (!l2.first) { + l2 = *this; + } else { + l2.last->next = first; + first->prev = l2.last; + l2.last = last; + } + clear(); +} + +QQuickWindow *ChartView::w() const +{ + return window(); +} + +void ChartView::setBackgroundColor(QColor color) +{ + backgroundColor = color; + if (rootNode) + rootNode->backgroundNode->setColor(color); +} + +QSizeF ChartView::size() const +{ + return boundingRect().size(); +} + +QRectF ChartView::plotArea() const +{ + return plotRect; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void ChartView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +#else +void ChartView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +#endif +{ + plotRect = QRectF(QPointF(0.0, 0.0), newGeometry.size()); + backgroundDirty = true; + plotAreaChanged(plotRect.size()); + + // Do we need to call the base-class' version of geometryChanged? Probably for QML? +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QQuickItem::geometryChange(newGeometry, oldGeometry); +#else + QQuickItem::geometryChanged(newGeometry, oldGeometry); +#endif +} + +void ChartView::setLayerVisibility(size_t z, bool visible) +{ + if (rootNode && z < rootNode->zNodes.size()) + rootNode->zNodes[z]->setVisible(visible); +} diff --git a/qt-quick/chartview.h b/qt-quick/chartview.h new file mode 100644 index 000000000..192665d85 --- /dev/null +++ b/qt-quick/chartview.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef CHART_VIEW_H +#define CHART_VIEW_H + +#include "chartitem_ptr.h" + +#include + +class ChartItem; +class QSGTexture; +class RootNode; // Internal implementation detail + +class ChartView : public QQuickItem { + Q_OBJECT +public: + ChartView(QQuickItem *parent, size_t maxZ); + ~ChartView(); + + QQuickWindow *w() const; // Make window available to items + QSizeF size() const; + QRectF plotArea() const; + void setBackgroundColor(QColor color); // Chart must be replot for color to become effective. + void addQSGNode(QSGNode *node, size_t z); // Must only be called in render thread! + void registerChartItem(ChartItem &item); + void registerDirtyChartItem(ChartItem &item); + void emergencyShutdown(); // Called when QQuick decides to delete our root node. + + // Create a chart item and add it to the scene. + // The item must not be deleted by the caller, but can be + // scheduled for deletion using deleteChartItem() below. + // Most items can be made invisible, which is preferred over deletion. + // All items on the scene will be deleted once the chart is reset. + template + ChartItemPtr createChartItem(Args&&... args); + + template + void deleteChartItem(ChartItemPtr &item); + +protected: + void setLayerVisibility(size_t z, bool visible); + void clearItems(); + + // This is called when Qt decided to reset our rootNode, which invalidates all items on the chart. + // The base class must invalidate all pointers and references. + virtual void resetPointers() = 0; +private: + // QtQuick related things + size_t maxZ; + bool backgroundDirty; + QRectF plotRect; + QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override; + QColor backgroundColor; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#else + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; +#endif + virtual void plotAreaChanged(const QSizeF &size) = 0; + + RootNode *rootNode; + + // There are three double linked lists of chart items: + // clean items, dirty items and items to be deleted. + // Note that only the render thread must delete chart items, + // and therefore these lists are the only owning pointers + // to chart items. All other pointers are non-owning and + // can therefore become stale. + struct ChartItemList { + ChartItemList(); + ChartItem *first, *last; + void append(ChartItem &item); + void remove(ChartItem &item); + void clear(); + void splice(ChartItemList &list); + }; + ChartItemList cleanItems, dirtyItems, deletedItems; + void deleteChartItemInternal(ChartItem &item); + void freeDeletedChartItems(); +}; + +// This implementation detail must be known to users of the class. +// Perhaps move it into a statsview_impl.h file. +template +ChartItemPtr ChartView::createChartItem(Args&&... args) +{ + return ChartItemPtr(new T(*this, std::forward(args)...)); +} + +template +void ChartView::deleteChartItem(ChartItemPtr &item) +{ + deleteChartItemInternal(*item); + item.reset(); +} + +#endif diff --git a/stats/barseries.cpp b/stats/barseries.cpp index 0b9897531..32959d681 100644 --- a/stats/barseries.cpp +++ b/stats/barseries.cpp @@ -412,7 +412,7 @@ bool BarSeries::hover(QPointF pos) Item &item = items[highlighted.bar]; item.highlight(index.subitem, true, binCount(), theme); if (!information) - information = view.createChartItem(); + information = view.createChartItem(theme); information->setText(makeInfo(item, highlighted.subitem), pos); information->setVisible(true); } else { diff --git a/stats/barseries.h b/stats/barseries.h index c741c76f6..c7f6f1dc3 100644 --- a/stats/barseries.h +++ b/stats/barseries.h @@ -5,9 +5,9 @@ #ifndef BAR_SERIES_H #define BAR_SERIES_H -#include "statshelper.h" #include "statsseries.h" #include "statsvariables.h" +#include "qt-quick/chartitem_ptr.h" #include #include diff --git a/stats/boxseries.cpp b/stats/boxseries.cpp index 66a8e3f8d..6abc7ce77 100644 --- a/stats/boxseries.cpp +++ b/stats/boxseries.cpp @@ -130,7 +130,7 @@ bool BoxSeries::hover(QPointF pos) Item &item = *items[highlighted]; item.highlight(true, theme); if (!information) - information = view.createChartItem(); + information = view.createChartItem(theme); information->setText(formatInformation(item), pos); information->setVisible(true); } else { diff --git a/stats/chartitem.cpp b/stats/chartitem.cpp index 034733188..7169bffda 100644 --- a/stats/chartitem.cpp +++ b/stats/chartitem.cpp @@ -3,6 +3,7 @@ #include "statscolors.h" #include "statsview.h" #include "core/globals.h" +#include "qt-quick/chartitemhelper.h" #include #include @@ -14,104 +15,10 @@ static int selectionOverlayPixelSize = 2; -static int round_up(double f) -{ - return static_cast(ceil(f)); -} - -ChartItem::ChartItem(StatsView &v, size_t z) : - dirty(false), prev(nullptr), next(nullptr), - zValue(z), view(v) -{ - // Register before the derived constructors run, so that the - // derived classes can mark the item as dirty in the constructor. - v.registerChartItem(*this); -} - -ChartItem::~ChartItem() -{ -} - -QSizeF ChartItem::sceneSize() const -{ - return view.size(); -} - -void ChartItem::markDirty() -{ - view.registerDirtyChartItem(*this); -} - -ChartPixmapItem::ChartPixmapItem(StatsView &v, size_t z) : HideableChartItem(v, z), - positionDirty(false), textureDirty(false) -{ -} - -ChartPixmapItem::~ChartPixmapItem() -{ - painter.reset(); // Make sure to destroy painter before image that is painted on -} - -void ChartPixmapItem::setTextureDirty() -{ - textureDirty = true; - markDirty(); -} - -void ChartPixmapItem::setPositionDirty() -{ - positionDirty = true; - markDirty(); -} - -void ChartPixmapItem::render() -{ - if (!node) { - createNode(view.w()->createImageNode()); - view.addQSGNode(node.get(), zValue); - } - updateVisible(); - - if (!img) { - resize(QSizeF(1,1)); - img->fill(Qt::transparent); - } - if (textureDirty) { - texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel)); - node->node->setTexture(texture.get()); - textureDirty = false; - } - if (positionDirty) { - node->node->setRect(rect); - positionDirty = false; - } -} - -void ChartPixmapItem::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 ChartPixmapItem::setPos(QPointF pos) -{ - rect.moveTopLeft(pos); - setPositionDirty(); -} - -QRectF ChartPixmapItem::getRect() const -{ - return rect; -} - static const int scatterItemDiameter = 10; static const int scatterItemBorder = 1; -ChartScatterItem::ChartScatterItem(StatsView &v, size_t z, const StatsTheme &theme, bool selected) : HideableChartItem(v, z), +ChartScatterItem::ChartScatterItem(ChartView &v, size_t z, const StatsTheme &theme, bool selected) : HideableChartItem(v, z), theme(theme), positionDirty(false), textureDirty(false), highlight(selected ? Highlight::Selected : Highlight::Unselected) @@ -123,7 +30,7 @@ ChartScatterItem::~ChartScatterItem() { } -static QSGTexture *createScatterTexture(StatsView &view, const QColor &color, const QColor &borderColor) +static QSGTexture *createScatterTexture(ChartView &view, const QColor &color, const QColor &borderColor) { QImage img(scatterItemDiameter, scatterItemDiameter, QImage::Format_ARGB32); img.fill(Qt::transparent); @@ -214,73 +121,7 @@ QRectF ChartScatterItem::getRect() const return rect; } -ChartRectItem::ChartRectItem(StatsView &v, size_t z, - const QPen &pen, const QBrush &brush, double radius) : ChartPixmapItem(v, z), - pen(pen), brush(brush), radius(radius) -{ -} - -ChartRectItem::~ChartRectItem() -{ -} - -void ChartRectItem::resize(QSizeF size) -{ - ChartPixmapItem::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); -} - -ChartTextItem::ChartTextItem(StatsView &v, size_t z, const QFont &f, const std::vector &text, bool center) : - ChartPixmapItem(v, z), f(f), center(center) -{ - QFontMetrics fm(f); - double totalWidth = 1.0; - fontHeight = static_cast(fm.height()); - double totalHeight = std::max(1.0, static_cast(text.size()) * fontHeight); - - items.reserve(text.size()); - for (const QString &s: text) { - double w = fm.size(Qt::TextSingleLine, s).width(); - items.push_back({ s, w }); - if (w > totalWidth) - totalWidth = w; - } - resize(QSizeF(totalWidth, totalHeight)); -} - -ChartTextItem::ChartTextItem(StatsView &v, size_t z, const QFont &f, const QString &text) : - ChartTextItem(v, z, f, std::vector({ text }), true) -{ -} - -void ChartTextItem::setColor(const QColor &c) -{ - setColor(c, Qt::transparent); -} - -void ChartTextItem::setColor(const QColor &c, const QColor &background) -{ - img->fill(background); - double y = 0.0; - painter->setPen(QPen(c)); - painter->setFont(f); - double totalWidth = getRect().width(); - for (const auto &[s, w]: items) { - double x = center ? round((totalWidth - w) / 2.0) : 0.0; - QRectF rect(x, y, w, fontHeight); - painter->drawText(rect, s); - y += fontHeight; - } - setTextureDirty(); -} - -ChartPieItem::ChartPieItem(StatsView &v, size_t z, const StatsTheme &theme, double borderWidth) : ChartPixmapItem(v, z), +ChartPieItem::ChartPieItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth) : ChartPixmapItem(v, z), theme(theme), borderWidth(borderWidth) { @@ -322,101 +163,12 @@ void ChartPieItem::resize(QSizeF size) img->fill(Qt::transparent); } -ChartLineItemBase::ChartLineItemBase(StatsView &v, size_t z, QColor color, double width) : HideableChartItem(v, z), +ChartLineItemBase::ChartLineItemBase(ChartView &v, size_t z, QColor color, double width) : HideableChartItem(v, z), color(color), width(width), positionDirty(false), materialDirty(false) { } -ChartLineItemBase::~ChartLineItemBase() -{ -} - -void ChartLineItemBase::setLine(QPointF fromIn, QPointF toIn) -{ - from = fromIn; - to = toIn; - positionDirty = true; - markDirty(); -} - -// Helper function to set points -void setPoint(QSGGeometry::Point2D &v, const QPointF &p) -{ - v.set(static_cast(p.x()), static_cast(p.y())); -} - -void setPoint(QSGGeometry::TexturedPoint2D &v, const QPointF &p, const QPointF &t) -{ - v.set(static_cast(p.x()), static_cast(p.y()), - static_cast(t.x()), static_cast(t.y())); -} - -void ChartLineItem::render() -{ - if (!node) { - geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2)); - geometry->setDrawingMode(QSGGeometry::DrawLines); - material.reset(new QSGFlatColorMaterial); - createNode(); - node->setGeometry(geometry.get()); - node->setMaterial(material.get()); - view.addQSGNode(node.get(), zValue); - positionDirty = materialDirty = true; - } - updateVisible(); - - if (positionDirty) { - // Attention: width is a geometry property and therefore handled by position dirty! - geometry->setLineWidth(static_cast(width)); - auto vertices = geometry->vertexDataAsPoint2D(); - setPoint(vertices[0], from); - setPoint(vertices[1], to); - node->markDirty(QSGNode::DirtyGeometry); - } - - if (materialDirty) { - material->setColor(color); - node->markDirty(QSGNode::DirtyMaterial); - } - - positionDirty = materialDirty = false; -} - -void ChartRectLineItem::render() -{ - if (!node) { - geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 5)); - geometry->setDrawingMode(QSGGeometry::DrawLineStrip); - material.reset(new QSGFlatColorMaterial); - createNode(); - node->setGeometry(geometry.get()); - node->setMaterial(material.get()); - view.addQSGNode(node.get(), zValue); - positionDirty = materialDirty = true; - } - updateVisible(); - - if (positionDirty) { - // Attention: width is a geometry property and therefore handled by position dirty! - geometry->setLineWidth(static_cast(width)); - auto vertices = geometry->vertexDataAsPoint2D(); - setPoint(vertices[0], from); - setPoint(vertices[1], QPointF(from.x(), to.y())); - setPoint(vertices[2], to); - setPoint(vertices[3], QPointF(to.x(), from.y())); - setPoint(vertices[4], from); - node->markDirty(QSGNode::DirtyGeometry); - } - - if (materialDirty) { - material->setColor(color); - node->markDirty(QSGNode::DirtyMaterial); - } - - positionDirty = materialDirty = false; -} - -ChartBarItem::ChartBarItem(StatsView &v, size_t z, const StatsTheme &theme, double borderWidth) : HideableChartItem(v, z), +ChartBarItem::ChartBarItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth) : HideableChartItem(v, z), theme(theme), borderWidth(borderWidth), selected(false), positionDirty(false), colorDirty(false), selectedDirty(false) @@ -548,7 +300,7 @@ QRectF ChartBarItem::getRect() const return rect; } -ChartBoxItem::ChartBoxItem(StatsView &v, size_t z, const StatsTheme &theme, double borderWidth) : +ChartBoxItem::ChartBoxItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth) : ChartBarItem(v, z, theme, borderWidth) { } diff --git a/stats/chartitem.h b/stats/chartitem.h index 0e68fc920..376e3912e 100644 --- a/stats/chartitem.h +++ b/stats/chartitem.h @@ -1,113 +1,17 @@ // 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 +// Chart item specific to the statistics module +#ifndef STATS_CHART_ITEM_H +#define STATS_CHART_ITEM_H -#include "statshelper.h" +#include "qt-quick/chartitem.h" -#include -#include - -class QSGGeometry; -class QSGGeometryNode; -class QSGFlatColorMaterial; -class QSGImageNode; -class QSGRectangleNode; -class QSGTexture; -class QSGTextureMaterial; class StatsTheme; -class StatsView; - -class ChartItem { -public: - // Only call on render thread! - virtual void render() = 0; - bool dirty; // If true, call render() when rebuilding the scene - ChartItem *prev, *next; // Double linked list of items - const size_t zValue; - virtual ~ChartItem(); // Attention: must only be called by render thread. -protected: - ChartItem(StatsView &v, size_t z); - QSizeF sceneSize() const; - StatsView &view; - void markDirty(); -}; - -template -class HideableChartItem : public ChartItem { -protected: - HideableChartItem(StatsView &v, size_t z); - std::unique_ptr node; - bool visible; - bool visibleChanged; - template - void createNode(Args&&... args); // Call to create node with visibility flag. - void updateVisible(); // Must be called by child class to update visibility flag! -public: - void setVisible(bool visible); -}; - -// A shortcut for ChartItems based on a hideable proxy item -template -using HideableChartProxyItem = HideableChartItem>>; - -// A chart item that blits a precalculated pixmap onto the scene. -class ChartPixmapItem : public HideableChartProxyItem { -public: - ChartPixmapItem(StatsView &v, size_t z); - ~ChartPixmapItem(); - - void setPos(QPointF pos); - void render() override; - QRectF getRect() const; -protected: - void resize(QSizeF size); // Resets the canvas. Attention: image is *unitialized*. - std::unique_ptr painter; - std::unique_ptr img; - void setTextureDirty(); - void setPositionDirty(); - QRectF rect; -private: - bool positionDirty; // true if the position changed since last render - bool textureDirty; // true if the pixmap changed since last render - std::unique_ptr texture; -}; - -// Draw a rectangular background after resize. Children are responsible for calling update(). -class ChartRectItem : public ChartPixmapItem { -public: - ChartRectItem(StatsView &v, size_t z, const QPen &pen, const QBrush &brush, double radius); - ~ChartRectItem(); - void resize(QSizeF size); -private: - QPen pen; - QBrush brush; - double radius; -}; - -// Attention: text is only drawn after calling setColor()! -class ChartTextItem : public ChartPixmapItem { -public: - ChartTextItem(StatsView &v, size_t z, const QFont &f, const std::vector &text, bool center); - ChartTextItem(StatsView &v, size_t z, const QFont &f, const QString &text); - void setColor(const QColor &color); // Draw on transparent background - void setColor(const QColor &color, const QColor &background); // Fill rectangle with given background color -private: - const QFont &f; - double fontHeight; - bool center; - struct Item { - QString s; - double width; - }; - std::vector items; -}; +class ChartView; // A pie chart item: draws disk segments onto a pixmap. class ChartPieItem : public ChartPixmapItem { public: - ChartPieItem(StatsView &v, size_t z, const StatsTheme &theme, double borderWidth); + ChartPieItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth); void drawSegment(double from, double to, QColor fill, QColor border, bool selected); // from and to are relative (0-1 is full disk). void resize(QSizeF size); // As in base class, but clears the canvas private: @@ -115,39 +19,10 @@ private: double borderWidth; }; -// Common data for line and rect items. Both are represented by two points. -class ChartLineItemBase : public HideableChartItem> { -public: - ChartLineItemBase(StatsView &v, size_t z, QColor color, double width); - ~ChartLineItemBase(); - void setLine(QPointF from, QPointF to); -protected: - QPointF from, to; - QColor color; - double width; - bool positionDirty; - bool materialDirty; - std::unique_ptr material; - std::unique_ptr geometry; -}; - -class ChartLineItem : public ChartLineItemBase { -public: - using ChartLineItemBase::ChartLineItemBase; - void render() override; -}; - -// A simple rectangle without fill. Specified by any two opposing vertices. -class ChartRectLineItem : public ChartLineItemBase { -public: - using ChartLineItemBase::ChartLineItemBase; - void render() override; -}; - // A bar in a bar chart: a rectangle bordered by lines. class ChartBarItem : public HideableChartProxyItem { public: - ChartBarItem(StatsView &v, size_t z, const StatsTheme &theme, double borderWidth); + ChartBarItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth); ~ChartBarItem(); void setColor(QColor color, QColor borderColor); void setRect(const QRectF &rect); @@ -177,7 +52,7 @@ private: // A box-and-whiskers item. This is a bit lazy: derive from the bar item and add whiskers. class ChartBoxItem : public ChartBarItem { public: - ChartBoxItem(StatsView &v, size_t z, const StatsTheme &theme, double borderWidth); + ChartBoxItem(ChartView &v, size_t z, const StatsTheme &theme, double borderWidth); ~ChartBoxItem(); void setBox(const QRectF &rect, double min, double max, double median); // The rect describes Q1, Q3. QRectF getRect() const; // Note: this extends the center rectangle to include the whiskers. @@ -195,7 +70,7 @@ private: // scatter item here, but so it is for now. class ChartScatterItem : public HideableChartProxyItem { public: - ChartScatterItem(StatsView &v, size_t z, const StatsTheme &theme, bool selected); + ChartScatterItem(ChartView &v, size_t z, const StatsTheme &theme, bool selected); ~ChartScatterItem(); // Currently, there is no highlighted and selected status. @@ -219,37 +94,4 @@ private: Highlight highlight; }; -// Implementation detail of templates - move to serparate header file -template -void HideableChartItem::setVisible(bool visibleIn) -{ - if (visible == visibleIn) - return; - visible = visibleIn; - visibleChanged = true; - markDirty(); -} - -template -template -void HideableChartItem::createNode(Args&&... args) -{ - node.reset(new Node(visible, std::forward(args)...)); - visibleChanged = false; -} - -template -HideableChartItem::HideableChartItem(StatsView &v, size_t z) : ChartItem(v, z), - visible(true), visibleChanged(false) -{ -} - -template -void HideableChartItem::updateVisible() -{ - if (visibleChanged) - node->setVisible(visible); - visibleChanged = false; -} - #endif diff --git a/stats/histogrammarker.cpp b/stats/histogrammarker.cpp index e7b2512e3..c24a350cf 100644 --- a/stats/histogrammarker.cpp +++ b/stats/histogrammarker.cpp @@ -5,7 +5,7 @@ static const double histogramMarkerWidth = 2.0; -HistogramMarker::HistogramMarker(StatsView &view, double val, bool horizontal, +HistogramMarker::HistogramMarker(ChartView &view, double val, bool horizontal, QColor color, StatsAxis *xAxis, StatsAxis *yAxis) : ChartLineItem(view, ChartZValue::ChartFeatures, color, histogramMarkerWidth), xAxis(xAxis), yAxis(yAxis), diff --git a/stats/histogrammarker.h b/stats/histogrammarker.h index 14d6410bd..4a6f4b32e 100644 --- a/stats/histogrammarker.h +++ b/stats/histogrammarker.h @@ -5,12 +5,11 @@ #include "chartitem.h" class StatsAxis; -class StatsView; // A line marking median or mean in histograms class HistogramMarker : public ChartLineItem { public: - HistogramMarker(StatsView &view, double val, bool horizontal, QColor color, StatsAxis *xAxis, StatsAxis *yAxis); + HistogramMarker(ChartView &view, double val, bool horizontal, QColor color, StatsAxis *xAxis, StatsAxis *yAxis); void updatePosition(); private: StatsAxis *xAxis, *yAxis; diff --git a/stats/informationbox.cpp b/stats/informationbox.cpp index ee90c2de6..c68cb0b5b 100644 --- a/stats/informationbox.cpp +++ b/stats/informationbox.cpp @@ -1,6 +1,5 @@ #include "informationbox.h" #include "statscolors.h" -#include "statsview.h" #include "zvalues.h" #include @@ -9,11 +8,11 @@ static const int informationBorder = 2; static const double informationBorderRadius = 4.0; // Radius of rounded corners static const int distanceFromPointer = 10; // Distance to place box from mouse pointer or scatter item -InformationBox::InformationBox(StatsView &v) : +InformationBox::InformationBox(ChartView &v, const StatsTheme &theme) : ChartRectItem(v, ChartZValue::InformationBox, - QPen(v.getCurrentTheme().informationBorderColor, informationBorder), - QBrush(v.getCurrentTheme().informationColor), informationBorderRadius), - theme(v.getCurrentTheme()), + QPen(theme.informationBorderColor, informationBorder), + QBrush(theme.informationColor), informationBorderRadius), + theme(theme), width(0.0), height(0.0) { diff --git a/stats/informationbox.h b/stats/informationbox.h index 724a56626..599047399 100644 --- a/stats/informationbox.h +++ b/stats/informationbox.h @@ -10,11 +10,12 @@ #include struct dive; -class StatsView; +class ChartView; +class StatsTheme; // Information window showing data of highlighted dive struct InformationBox : ChartRectItem { - InformationBox(StatsView &); + InformationBox(ChartView &, const StatsTheme &theme); void setText(const std::vector &text, QPointF pos); void setPos(QPointF pos); int recommendedMaxLines() const; diff --git a/stats/legend.cpp b/stats/legend.cpp index 0bab2d19d..029854800 100644 --- a/stats/legend.cpp +++ b/stats/legend.cpp @@ -14,12 +14,12 @@ static const double legendBoxBorderRadius = 4.0; // radius of rounded corners static const double legendBoxScale = 0.8; // 1.0: text-height of the used font static const double legendInternalBorderSize = 2.0; -Legend::Legend(StatsView &view, const std::vector &names) : +Legend::Legend(ChartView &view, const StatsTheme &theme, const std::vector &names) : ChartRectItem(view, ChartZValue::Legend, - QPen(view.getCurrentTheme().legendBorderColor, legendBorderSize), - QBrush(view.getCurrentTheme().legendColor), legendBoxBorderRadius), + QPen(theme.legendBorderColor, legendBorderSize), + QBrush(theme.legendColor), legendBoxBorderRadius), displayedItems(0), width(0.0), height(0.0), - theme(view.getCurrentTheme()), + theme(theme), posInitialized(false) { entries.reserve(names.size()); diff --git a/stats/legend.h b/stats/legend.h index cd43b09a9..eccc488ac 100644 --- a/stats/legend.h +++ b/stats/legend.h @@ -3,7 +3,7 @@ #ifndef STATS_LEGEND_H #define STATS_LEGEND_H -#include "chartitem.h" +#include "qt-quick/chartitem.h" #include #include @@ -13,7 +13,7 @@ class StatsTheme; class Legend : public ChartRectItem { public: - Legend(StatsView &view, const std::vector &names); + Legend(ChartView &view, const StatsTheme &theme, const std::vector &names); void resize(); // called when the chart size changes. void setPos(QPointF pos); // Attention: not virtual - always call on this class. private: diff --git a/stats/pieseries.cpp b/stats/pieseries.cpp index 9403e426f..34d9415aa 100644 --- a/stats/pieseries.cpp +++ b/stats/pieseries.cpp @@ -257,7 +257,7 @@ bool PieSeries::hover(QPointF pos) if (highlighted >= 0 && highlighted < (int)items.size()) { items[highlighted].highlight(*item, highlighted, true, (int)items.size(), theme); if (!information) - information = view.createChartItem(); + information = view.createChartItem(theme); information->setText(makeInfo(highlighted), pos); information->setVisible(true); } else { diff --git a/stats/pieseries.h b/stats/pieseries.h index f696006cb..bafd8f5f8 100644 --- a/stats/pieseries.h +++ b/stats/pieseries.h @@ -3,8 +3,8 @@ #ifndef PIE_SERIES_H #define PIE_SERIES_H -#include "statshelper.h" #include "statsseries.h" +#include "qt-quick/chartitem_ptr.h" #include #include diff --git a/stats/quartilemarker.cpp b/stats/quartilemarker.cpp index 967144dfd..bf40e1270 100644 --- a/stats/quartilemarker.cpp +++ b/stats/quartilemarker.cpp @@ -7,8 +7,8 @@ static const double quartileMarkerSize = 15.0; -QuartileMarker::QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis) : - ChartLineItem(view, ChartZValue::ChartFeatures, view.getCurrentTheme().quartileMarkerColor, 2.0), +QuartileMarker::QuartileMarker(ChartView &view, const StatsTheme &theme, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis) : + ChartLineItem(view, ChartZValue::ChartFeatures, theme.quartileMarkerColor, 2.0), xAxis(xAxis), yAxis(yAxis), pos(pos), value(value) diff --git a/stats/quartilemarker.h b/stats/quartilemarker.h index 2e754248d..387c617f0 100644 --- a/stats/quartilemarker.h +++ b/stats/quartilemarker.h @@ -5,11 +5,10 @@ #include "chartitem.h" class StatsAxis; -class StatsView; class QuartileMarker : public ChartLineItem { public: - QuartileMarker(StatsView &view, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis); + QuartileMarker(ChartView &view, const StatsTheme &theme, double pos, double value, StatsAxis *xAxis, StatsAxis *yAxis); ~QuartileMarker(); void updatePosition(); private: diff --git a/stats/regressionitem.cpp b/stats/regressionitem.cpp index 1bdabeb54..5a1fb4068 100644 --- a/stats/regressionitem.cpp +++ b/stats/regressionitem.cpp @@ -9,10 +9,10 @@ static const double regressionLineWidth = 2.0; -RegressionItem::RegressionItem(StatsView &view, regression_data reg, - StatsAxis *xAxis, StatsAxis *yAxis) : +RegressionItem::RegressionItem(ChartView &view, const StatsTheme &theme, regression_data reg, + StatsAxis *xAxis, StatsAxis *yAxis) : ChartPixmapItem(view, ChartZValue::ChartFeatures), - theme(view.getCurrentTheme()), + theme(theme), xAxis(xAxis), yAxis(yAxis), reg(reg), regression(true), confidence(true) { diff --git a/stats/regressionitem.h b/stats/regressionitem.h index 3e5edca03..6c3e94b9e 100644 --- a/stats/regressionitem.h +++ b/stats/regressionitem.h @@ -6,7 +6,6 @@ class StatsAxis; class StatsTheme; -class StatsView; struct regression_data { double a,b; @@ -16,7 +15,7 @@ struct regression_data { class RegressionItem : public ChartPixmapItem { public: - RegressionItem(StatsView &view, regression_data data, StatsAxis *xAxis, StatsAxis *yAxis); + RegressionItem(ChartView &view, const StatsTheme &theme, regression_data data, StatsAxis *xAxis, StatsAxis *yAxis); ~RegressionItem(); void updatePosition(); void setFeatures(bool regression, bool confidence); diff --git a/stats/scatterseries.cpp b/stats/scatterseries.cpp index 0cf097855..06d6a19da 100644 --- a/stats/scatterseries.cpp +++ b/stats/scatterseries.cpp @@ -3,7 +3,6 @@ #include "chartitem.h" #include "informationbox.h" #include "statscolors.h" -#include "statshelper.h" #include "statstranslations.h" #include "statsvariables.h" #include "statsview.h" @@ -183,7 +182,7 @@ bool ScatterSeries::hover(QPointF pos) return false; } else { if (!information) - information = view.createChartItem(); + information = view.createChartItem(theme); std::vector text; text.reserve(highlighted.size() * 5); diff --git a/stats/scatterseries.h b/stats/scatterseries.h index d4a43d224..128fab192 100644 --- a/stats/scatterseries.h +++ b/stats/scatterseries.h @@ -6,6 +6,7 @@ #include "statshelper.h" #include "statsseries.h" +#include "qt-quick/chartitem_ptr.h" #include #include diff --git a/stats/statsaxis.cpp b/stats/statsaxis.cpp index eb1affd8b..e94ef51aa 100644 --- a/stats/statsaxis.cpp +++ b/stats/statsaxis.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 #include "statsaxis.h" #include "statscolors.h" -#include "statshelper.h" #include "statstranslations.h" #include "statsvariables.h" #include "statsview.h" @@ -25,9 +24,9 @@ static const double axisLabelSpaceVertical = 2.0; // Space between axis or ticks static const double axisTitleSpaceHorizontal = 2.0; // Space between labels and title static const double axisTitleSpaceVertical = 2.0; // Space between labels and title -StatsAxis::StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks) : +StatsAxis::StatsAxis(ChartView &view, const StatsTheme &theme, const QString &title, bool horizontal, bool labelsBetweenTicks) : ChartPixmapItem(view, ChartZValue::Axes), - theme(view.getCurrentTheme()), + theme(theme), line(view.createChartItem(ChartZValue::Axes, theme.axisColor, axisWidth)), title(title), horizontal(horizontal), labelsBetweenTicks(labelsBetweenTicks), size(1.0), zeroOnScreen(0.0), min(0.0), max(1.0), labelWidth(0.0) @@ -256,8 +255,9 @@ void StatsAxis::setPos(QPointF pos) } } -ValueAxis::ValueAxis(StatsView &view, const QString &title, double min, double max, int decimals, bool horizontal) : - StatsAxis(view, title, horizontal, false), +ValueAxis::ValueAxis(ChartView &view, const StatsTheme &theme, + const QString &title, double min, double max, int decimals, bool horizontal) : + StatsAxis(view, theme, title, horizontal, false), min(min), max(max), decimals(decimals) { // Avoid degenerate cases @@ -317,8 +317,8 @@ void ValueAxis::updateLabels() } } -CountAxis::CountAxis(StatsView &view, const QString &title, int count, bool horizontal) : - ValueAxis(view, title, 0.0, (double)count, 0, horizontal), +CountAxis::CountAxis(ChartView &view, const StatsTheme &theme, const QString &title, int count, bool horizontal) : + ValueAxis(view, theme, title, 0.0, (double)count, 0, horizontal), count(count) { } @@ -376,8 +376,9 @@ void CountAxis::updateLabels() } } -CategoryAxis::CategoryAxis(StatsView &view, const QString &title, const std::vector &labels, bool horizontal) : - StatsAxis(view, title, horizontal, true), +CategoryAxis::CategoryAxis(ChartView &view, const StatsTheme &theme, + const QString &title, const std::vector &labels, bool horizontal) : + StatsAxis(view, theme, title, horizontal, true), labelsText(labels) { if (!labels.empty()) @@ -437,8 +438,9 @@ void CategoryAxis::updateLabels() } } -HistogramAxis::HistogramAxis(StatsView &view, const QString &title, std::vector bins, bool horizontal) : - StatsAxis(view, title, horizontal, false), +HistogramAxis::HistogramAxis(ChartView &view, const StatsTheme &theme, + const QString &title, std::vector bins, bool horizontal) : + StatsAxis(view, theme, title, horizontal, false), bin_values(std::move(bins)) { if (bin_values.size() < 2) // Less than two makes no sense -> there must be at least one category @@ -643,7 +645,7 @@ static std::vector timeRangeToBins(double from, double to) return res; } -DateAxis::DateAxis(StatsView &view, const QString &title, double from, double to, bool horizontal) : - HistogramAxis(view, title, timeRangeToBins(from, to), horizontal) +DateAxis::DateAxis(ChartView &view, const StatsTheme &theme, const QString &title, double from, double to, bool horizontal) : + HistogramAxis(view, theme, title, timeRangeToBins(from, to), horizontal) { } diff --git a/stats/statsaxis.h b/stats/statsaxis.h index 3a658a921..aa63060f7 100644 --- a/stats/statsaxis.h +++ b/stats/statsaxis.h @@ -3,12 +3,10 @@ #define STATS_AXIS_H #include "chartitem.h" -#include "statshelper.h" #include #include -class StatsView; class ChartLineItem; class QFontMetrics; @@ -34,7 +32,7 @@ public: std::vector ticksPositions() const; // Positions in screen coordinates protected: - StatsAxis(StatsView &view, const QString &title, bool horizontal, bool labelsBetweenTicks); + StatsAxis(ChartView &view, const StatsTheme &theme, const QString &title, bool horizontal, bool labelsBetweenTicks); const StatsTheme &theme; // Initialized once in constructor. ChartItemPtr line; @@ -73,7 +71,8 @@ private: class ValueAxis : public StatsAxis { public: - ValueAxis(StatsView &view, const QString &title, double min, double max, int decimals, bool horizontal); + ValueAxis(ChartView &view, const StatsTheme &theme, const QString &title, + double min, double max, int decimals, bool horizontal); private: double min, max; int decimals; @@ -83,7 +82,7 @@ private: class CountAxis : public ValueAxis { public: - CountAxis(StatsView &view, const QString &title, int count, bool horizontal); + CountAxis(ChartView &view, const StatsTheme &theme, const QString &title, int count, bool horizontal); private: int count; void updateLabels() override; @@ -92,7 +91,8 @@ private: class CategoryAxis : public StatsAxis { public: - CategoryAxis(StatsView &view, const QString &title, const std::vector &labels, bool horizontal); + CategoryAxis(ChartView &view, const StatsTheme &theme, const QString &title, + const std::vector &labels, bool horizontal); private: std::vector labelsText; void updateLabels(); @@ -107,7 +107,8 @@ struct HistogramAxisEntry { class HistogramAxis : public StatsAxis { public: - HistogramAxis(StatsView &view, const QString &title, std::vector bin_values, bool horizontal); + HistogramAxis(ChartView &view, const StatsTheme &theme, const QString &title, + std::vector bin_values, bool horizontal); private: void updateLabels() override; std::pair getFirstLastLabel() const override; @@ -117,7 +118,7 @@ private: class DateAxis : public HistogramAxis { public: - DateAxis(StatsView &view, const QString &title, double from, double to, bool horizontal); + DateAxis(ChartView &view, const StatsTheme &theme, const QString &title, double from, double to, bool horizontal); }; #endif diff --git a/stats/statsgrid.h b/stats/statsgrid.h index 80ab53620..4b14ec97f 100644 --- a/stats/statsgrid.h +++ b/stats/statsgrid.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 // The background grid of a chart -#include "statshelper.h" +#include "qt-quick/chartitem_ptr.h" #include #include diff --git a/stats/statshelper.h b/stats/statshelper.h index abd70cd95..b820016e2 100644 --- a/stats/statshelper.h +++ b/stats/statshelper.h @@ -1,14 +1,12 @@ // SPDX-License-Identifier: GPL-2.0 -// Helper functions to render the stats. Includes -// QSGNode template jugglery to overcome API flaws. +// Helper functions to render the stats. #ifndef STATSHELPER_H #define STATSHELPER_H -#include #include #include -#include -#include "core/dive.h" + +struct dive; // Round positions to integer values to avoid ugly artifacts QPointF roundPos(const QPointF &p); @@ -16,128 +14,4 @@ QPointF roundPos(const QPointF &p); // Are all dives in this vector selected? bool allDivesSelected(const std::vector &dives); -// A stupid pointer class that initializes to null and can be copy -// assigned. This is for historical reasons: unique_ptrs to ChartItems -// were replaced by plain pointers. Instead of nulling the plain pointers -// in the constructors, use this. Ultimately, we might think about making -// this thing smarter, once removal of individual ChartItems is implemented. -template -class ChartItemPtr { - friend class StatsView; // Only the stats view can create these pointers - T *ptr; - ChartItemPtr(T *ptr) : ptr(ptr) - { - } -public: - ChartItemPtr() : ptr(nullptr) - { - } - ChartItemPtr(const ChartItemPtr &p) : ptr(p.ptr) - { - } - void reset() - { - ptr = nullptr; - } - ChartItemPtr &operator=(const ChartItemPtr &p) - { - ptr = p.ptr; - return *this; - } - operator bool() const - { - return !!ptr; - } - bool operator!() const - { - return !ptr; - } - T &operator*() const - { - return *ptr; - } - T *operator->() const - { - return ptr; - } -}; - -// In general, we want chart items to be hideable. For example to show/hide -// labels on demand. Very sadly, the QSG API is absolutely terrible with -// respect to temporarily disabling. Instead of simply having a flag, -// a QSGNode is queried using the "isSubtreeBlocked()" virtual function(!). -// -// Not only is this a slow operation performed on every single node, it -// also is often not possible to override this function: For improved -// performance, the documentation recommends to create QSG nodes via -// QQuickWindow. This provides nodes optimized for the actual hardware. -// However, this obviously means that these nodes cannot be derived from! -// -// In that case, there are two possibilities: Add a proxy node with an -// overridden "isSubtreeBlocked()" function or remove the node from the -// scene. The former was chosen here, because it is less complex. -// -// The following slightly cryptic templates are used to unify the two -// cases: The QSGNode is generated by our own code or the QSGNode is -// obtained from QQuickWindow. -// -// The "HideableQSGNode" template augments the QSGNode "Node" -// by a "setVisible()" function and overrides "isSubtreeBlocked()" -// -// The "QSGProxyNode" template is a QSGNode with a single -// child of type "Node". -// -// Thus, if the node can be created, use: -// HideableQSGNode node -// and if the node can only be obtained from QQuickWindow, use: -// HideableQSGNode> node -// The latter should obviously be typedef-ed. -// -// Yes, that's all horrible, but if nothing else it teaches us about -// composition. -template -class HideableQSGNode : public Node { - bool hidden; - bool isSubtreeBlocked() const override final; -public: - template - HideableQSGNode(bool visible, Args&&... args); - void setVisible(bool visible); -}; - -template -class QSGProxyNode : public QSGNode { -public: - std::unique_ptr node; - QSGProxyNode(Node *node); -}; - -// Implementation detail of templates - move to serparate header file -template -QSGProxyNode::QSGProxyNode(Node *node) : node(node) -{ - appendChildNode(node); -} - -template -bool HideableQSGNode::isSubtreeBlocked() const -{ - return hidden; -} - -template -template -HideableQSGNode::HideableQSGNode(bool visible, Args&&... args) : - Node(std::forward(args)...), - hidden(!visible) -{ -} - -template -void HideableQSGNode::setVisible(bool visible) -{ - hidden = !visible; - Node::markDirty(QSGNode::DirtySubtreeBlocked); -} - #endif diff --git a/stats/statsview.cpp b/stats/statsview.cpp index 1fd7866e7..319173297 100644 --- a/stats/statsview.cpp +++ b/stats/statsview.cpp @@ -22,29 +22,21 @@ #include "core/trip.h" #include -#include -#include -#include -#include -#include // Constants that control the graph layouts static const double sceneBorder = 5.0; // Border between scene edges and statitistics view static const double titleBorder = 2.0; // Border between title and chart static const double selectionLassoWidth = 2.0; // Border between title and chart -StatsView::StatsView(QQuickItem *parent) : QQuickItem(parent), - maxZ(ChartZValue::Count), - backgroundDirty(true), +StatsView::StatsView(QQuickItem *parent) : ChartView(parent, ChartZValue::Count), currentTheme(&getStatsTheme(false)), - backgroundColor(currentTheme->backgroundColor), highlightedSeries(nullptr), xAxis(nullptr), yAxis(nullptr), draggedItem(nullptr), - restrictDives(false), - rootNode(nullptr) + restrictDives(false) { + setBackgroundColor(currentTheme->backgroundColor); setFlag(ItemHasContents, true); connect(&diveListNotifier, &DiveListNotifier::numShownChanged, this, &StatsView::replotIfVisible); @@ -122,190 +114,6 @@ void StatsView::mouseReleaseEvent(QMouseEvent *) } } -// Define a hideable dummy QSG node that is used as a parent node to make -// all objects of a z-level visible / invisible. -using ZNode = HideableQSGNode; - -class RootNode : public QSGNode -{ -public: - RootNode(StatsView &view, QColor backgroundColor, size_t maxZ); - ~RootNode(); - StatsView &view; - std::unique_ptr backgroundNode; // solid background - // We entertain one node per Z-level. - std::vector> zNodes; -}; - -RootNode::RootNode(StatsView &view, QColor backgroundColor, size_t maxZ) : view(view) -{ - zNodes.resize(maxZ); - - // Add a background rectangle with a solid color. This could - // also be done on the widget level, but would have to be done - // separately for desktop and mobile, so do it here. - backgroundNode.reset(view.w()->createRectangleNode()); - appendChildNode(backgroundNode.get()); - - for (auto &zNode: zNodes) { - zNode.reset(new ZNode(true)); - appendChildNode(zNode.get()); - } -} - -RootNode::~RootNode() -{ - view.emergencyShutdown(); -} - -void StatsView::freeDeletedChartItems() -{ - ChartItem *nextitem; - for (ChartItem *item = deletedItems.first; item; item = nextitem) { - nextitem = item->next; - delete item; - } - deletedItems.clear(); -} - -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. - RootNode *n = static_cast(oldNode); - if (!n) - n = rootNode = new RootNode(*this, backgroundColor, maxZ); - - // Delete all chart items that are marked for deletion. - freeDeletedChartItems(); - - if (backgroundDirty) { - rootNode->backgroundNode->setRect(plotRect); - backgroundDirty = false; - } - - for (ChartItem *item = dirtyItems.first; item; item = item->next) { - item->render(); - item->dirty = false; - } - dirtyItems.splice(cleanItems); - - return n; -} - -// When reparenting the QQuickWidget, QtQuick decides to delete our rootNode -// and with it all the QSG nodes, even though we have *not* given the -// permission to do so! If the widget is reused, we try to delete the -// stale items, whose nodes have already been deleted by QtQuick, leading -// to a double-free(). Instead of searching for the cause of this behavior, -// let's just hook into the rootNodes destructor and delete the objects -// in a controlled manner, so that QtQuick has no more access to them. -void StatsView::emergencyShutdown() -{ - // Mark clean and dirty chart items for deletion... - cleanItems.splice(deletedItems); - dirtyItems.splice(deletedItems); - - // ...and delete them. - freeDeletedChartItems(); - - // Now delete all the pointers we might have to chart features, - // axes, etc. Note that all pointers to chart items are non - // owning, so this only resets stale references, but does not - // lead to any additional deletion of chart items. - reset(); - - // The rootNode is being deleted -> remove the reference to that - rootNode = nullptr; -} - -void StatsView::addQSGNode(QSGNode *node, size_t z) -{ - size_t idx = std::clamp(z, (size_t)0, maxZ); - rootNode->zNodes[idx]->appendChildNode(node); -} - -void StatsView::registerChartItem(ChartItem &item) -{ - cleanItems.append(item); -} - -void StatsView::registerDirtyChartItem(ChartItem &item) -{ - if (item.dirty) - return; - cleanItems.remove(item); - dirtyItems.append(item); - item.dirty = true; -} - -void StatsView::deleteChartItemInternal(ChartItem &item) -{ - if (item.dirty) - dirtyItems.remove(item); - else - cleanItems.remove(item); - deletedItems.append(item); -} - -StatsView::ChartItemList::ChartItemList() : first(nullptr), last(nullptr) -{ -} - -void StatsView::ChartItemList::clear() -{ - first = last = nullptr; -} - -void StatsView::ChartItemList::remove(ChartItem &item) -{ - if (item.next) - item.next->prev = item.prev; - else - last = item.prev; - if (item.prev) - item.prev->next = item.next; - else - first = item.next; - item.prev = item.next = nullptr; -} - -void StatsView::ChartItemList::append(ChartItem &item) -{ - if (!first) { - first = &item; - } else { - item.prev = last; - last->next = &item; - } - last = &item; -} - -void StatsView::ChartItemList::splice(ChartItemList &l2) -{ - if (!first) // if list is empty -> nothing to do. - return; - if (!l2.first) { - l2 = *this; - } else { - l2.last->next = first; - first->prev = l2.last; - l2.last = last; - } - clear(); -} - -QQuickWindow *StatsView::w() const -{ - return window(); -} - -void StatsView::setBackgroundColor(QColor color) -{ - backgroundColor = color; - rootNode->backgroundNode->setColor(color); -} - void StatsView::setTheme(bool dark) { currentTheme = &getStatsTheme(dark); @@ -317,34 +125,6 @@ const StatsTheme &StatsView::getCurrentTheme() const return *currentTheme; } -QSizeF StatsView::size() const -{ - return boundingRect().size(); -} - -QRectF StatsView::plotArea() const -{ - return plotRect; -} - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -void StatsView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) -#else -void StatsView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) -#endif -{ - plotRect = QRectF(QPointF(0.0, 0.0), newGeometry.size()); - backgroundDirty = true; - plotAreaChanged(plotRect.size()); - - // Do we need to call the base-class' version of geometryChanged? Probably for QML? -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QQuickItem::geometryChange(newGeometry, oldGeometry); -#else - QQuickItem::geometryChanged(newGeometry, oldGeometry); -#endif -} - void StatsView::plotAreaChanged(const QSizeF &s) { double left = sceneBorder; @@ -491,7 +271,7 @@ void StatsView::updateTitlePos() template T *StatsView::createAxis(const QString &title, Args&&... args) { - return &*createChartItem(title, std::forward(args)...); + return &*createChartItem(*currentTheme, title, std::forward(args)...); } void StatsView::setAxes(StatsAxis *x, StatsAxis *y) @@ -503,6 +283,12 @@ void StatsView::setAxes(StatsAxis *x, StatsAxis *y) } void StatsView::reset() +{ + resetPointers(); + clearItems(); +} + +void StatsView::resetPointers() { highlightedSeries = nullptr; xAxis = yAxis = nullptr; @@ -514,10 +300,6 @@ void StatsView::reset() medianMarker.reset(); selectionRect.reset(); - // Mark clean and dirty chart items for deletion - cleanItems.splice(deletedItems); - dirtyItems.splice(deletedItems); - series.clear(); quartileMarkers.clear(); grid.reset(); @@ -547,7 +329,7 @@ void StatsView::plot(const StatsState &stateIn) state = stateIn; plotChart(); updateFeatures(); // Show / hide chart features, such as legend, etc. - plotAreaChanged(plotRect.size()); + plotAreaChanged(plotArea().size()); update(); } @@ -618,8 +400,7 @@ void StatsView::updateFeatures() legend->setVisible(state.legend); // For labels, we are brutal: simply show/hide the whole z-level with the labels - if (rootNode) - rootNode->zNodes[ChartZValue::SeriesLabels]->setVisible(state.labels); + setLayerVisibility(ChartZValue::SeriesLabels, state.labels); if (meanMarker) meanMarker->setVisible(state.mean); @@ -772,7 +553,7 @@ void StatsView::plotBarChart(const std::vector &dives, setAxes(catAxis, valAxis); // Paint legend first, because the bin-names will be moved away from. - legend = createChartItem(data.vbinNames); + legend = createChartItem(*currentTheme, data.vbinNames); std::vector items; items.reserve(data.hbins.size()); @@ -997,7 +778,7 @@ void StatsView::plotPieChart(const std::vector &dives, ChartSortMode sor PieSeries *series = createSeries(categoryVariable->name(), std::move(data), sortMode); - legend = createChartItem(series->binNames()); + legend = createChartItem(*currentTheme, series->binNames()); } void StatsView::plotDiscreteBoxChart(const std::vector &dives, @@ -1067,11 +848,11 @@ void StatsView::plotDiscreteScatter(const std::vector &dives, StatsQuartiles quartiles = StatsVariable::quartiles(array); if (quartiles.isValid()) { quartileMarkers.push_back(createChartItem( - x, quartiles.q1, catAxis, valAxis)); + *currentTheme, x, quartiles.q1, catAxis, valAxis)); quartileMarkers.push_back(createChartItem( - x, quartiles.q2, catAxis, valAxis)); + *currentTheme, x, quartiles.q2, catAxis, valAxis)); quartileMarkers.push_back(createChartItem( - x, quartiles.q3, catAxis, valAxis)); + *currentTheme, x, quartiles.q3, catAxis, valAxis)); } x += 1.0; } @@ -1222,7 +1003,7 @@ void StatsView::plotHistogramStackedChart(const std::vector &dives, *categoryBinner, categoryBins, !isHorizontal); BarPlotData data(categoryBins, *valueBinner); - legend = createChartItem(data.vbinNames); + legend = createChartItem(*currentTheme, data.vbinNames); CountAxis *valAxis = createCountAxis(data.maxCategoryCount, isHorizontal); @@ -1373,5 +1154,5 @@ void StatsView::plotScatter(const std::vector &dives, const StatsVariabl // y = ax + b struct regression_data reg = linear_regression(points); if (!std::isnan(reg.a)) - regressionItem = createChartItem(reg, xAxis, yAxis); + regressionItem = createChartItem(*currentTheme, reg, xAxis, yAxis); } diff --git a/stats/statsview.h b/stats/statsview.h index a51bf9a53..59e43bb34 100644 --- a/stats/statsview.h +++ b/stats/statsview.h @@ -3,12 +3,12 @@ #define STATS_VIEW_H #include "statsstate.h" -#include "statshelper.h" #include "statsselection.h" +#include "qt-quick/chartview.h" + #include #include #include -#include struct dive; struct StatsBinner; @@ -18,7 +18,6 @@ struct StatsVariable; class StatsSeries; class CategoryAxis; -class ChartItem; class ChartRectLineItem; class ChartTextItem; class CountAxis; @@ -30,14 +29,12 @@ class StatsAxis; class StatsGrid; class StatsTheme; class Legend; -class QSGTexture; -class RootNode; // Internal implementation detail enum class ChartSubType : int; enum class StatsOperation : int; enum class ChartSortMode : int; -class StatsView : public QQuickItem { +class StatsView : public ChartView { Q_OBJECT public: StatsView(); @@ -49,44 +46,15 @@ public: void restrictToSelection(); void unrestrict(); int restrictionCount() const; // <0: no restriction - QQuickWindow *w() const; // Make window available to items - QSizeF size() const; - QRectF plotArea() const; - void setBackgroundColor(QColor color); // Chart must be replot for color to become effective. void setTheme(bool dark); // Chart must be replot for theme to become effective. const StatsTheme &getCurrentTheme() const; - void addQSGNode(QSGNode *node, size_t z); // Must only be called in render thread! - void registerChartItem(ChartItem &item); - void registerDirtyChartItem(ChartItem &item); - void emergencyShutdown(); // Called when QQuick decides to delete our root node. - // Create a chart item and add it to the scene. - // The item must not be deleted by the caller, but can be - // scheduled for deletion using deleteChartItem() below. - // Most items can be made invisible, which is preferred over deletion. - // All items on the scene will be deleted once the chart is reset. - template - ChartItemPtr createChartItem(Args&&... args); - - template - void deleteChartItem(ChartItemPtr &item); -private slots: void replotIfVisible(); void divesSelected(const QVector &dives); private: - // QtQuick related things - size_t maxZ; - bool backgroundDirty; - QRectF plotRect; - QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override; - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; -#else - void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; -#endif - void plotAreaChanged(const QSizeF &size); + void plotAreaChanged(const QSizeF &size) override; void reset(); // clears all series and axes + void resetPointers() override; void setAxes(StatsAxis *x, StatsAxis *y); void plotBarChart(const std::vector &dives, ChartSubType subType, ChartSortMode sortMode, @@ -144,7 +112,6 @@ private: StatsState state; const StatsTheme *currentTheme; - QColor backgroundColor; std::vector> series; std::unique_ptr grid; std::vector> quartileMarkers; @@ -167,40 +134,6 @@ private: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; - RootNode *rootNode; - - // There are three double linked lists of chart items: - // clean items, dirty items and items to be deleted. - // Note that only the render thread must delete chart items, - // and therefore these lists are the only owning pointers - // to chart items. All other pointers are non-owning and - // can therefore become stale. - struct ChartItemList { - ChartItemList(); - ChartItem *first, *last; - void append(ChartItem &item); - void remove(ChartItem &item); - void clear(); - void splice(ChartItemList &list); - }; - ChartItemList cleanItems, dirtyItems, deletedItems; - void deleteChartItemInternal(ChartItem &item); - void freeDeletedChartItems(); }; -// This implementation detail must be known to users of the class. -// Perhaps move it into a statsview_impl.h file. -template -ChartItemPtr StatsView::createChartItem(Args&&... args) -{ - return ChartItemPtr(new T(*this, std::forward(args)...)); -} - -template -void StatsView::deleteChartItem(ChartItemPtr &item) -{ - deleteChartItemInternal(*item); - item.reset(); -} - #endif