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