mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
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 <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
parent
533cff96f3
commit
2eebae13dd
34 changed files with 1031 additions and 915 deletions
19
qt-quick/CMakeLists.txt
Normal file
19
qt-quick/CMakeLists.txt
Normal file
|
|
@ -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})
|
||||
246
qt-quick/chartitem.cpp
Normal file
246
qt-quick/chartitem.cpp
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "chartitem.h"
|
||||
#include "chartitemhelper.h"
|
||||
#include "chartview.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <QQuickWindow>
|
||||
#include <QSGFlatColorMaterial>
|
||||
#include <QSGImageNode>
|
||||
|
||||
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<int>(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<QString> &text, bool center) :
|
||||
ChartPixmapItem(v, z), f(f), center(center)
|
||||
{
|
||||
QFontMetrics fm(f);
|
||||
double totalWidth = 1.0;
|
||||
fontHeight = static_cast<double>(fm.height());
|
||||
double totalHeight = std::max(1.0, static_cast<double>(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<QString>({ 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<float>(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<float>(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;
|
||||
}
|
||||
169
qt-quick/chartitem.h
Normal file
169
qt-quick/chartitem.h
Normal file
|
|
@ -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 <memory>
|
||||
#include <QPainter>
|
||||
|
||||
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 <typename Node>
|
||||
class HideableChartItem : public ChartItem {
|
||||
protected:
|
||||
HideableChartItem(ChartView &v, size_t z);
|
||||
std::unique_ptr<Node> node;
|
||||
bool visible;
|
||||
bool visibleChanged;
|
||||
template<class... Args>
|
||||
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 <typename Node>
|
||||
using HideableChartProxyItem = HideableChartItem<HideableQSGNode<QSGProxyNode<Node>>>;
|
||||
|
||||
// A chart item that blits a precalculated pixmap onto the scene.
|
||||
class ChartPixmapItem : public HideableChartProxyItem<QSGImageNode> {
|
||||
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<QPainter> painter;
|
||||
std::unique_ptr<QImage> 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<QSGTexture> 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<QString> &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<Item> items;
|
||||
};
|
||||
|
||||
// Common data for line and rect items. Both are represented by two points.
|
||||
class ChartLineItemBase : public HideableChartItem<HideableQSGNode<QSGGeometryNode>> {
|
||||
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<QSGFlatColorMaterial> material;
|
||||
std::unique_ptr<QSGGeometry> 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 <typename Node>
|
||||
void HideableChartItem<Node>::setVisible(bool visibleIn)
|
||||
{
|
||||
if (visible == visibleIn)
|
||||
return;
|
||||
visible = visibleIn;
|
||||
visibleChanged = true;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
template<class... Args>
|
||||
void HideableChartItem<Node>::createNode(Args&&... args)
|
||||
{
|
||||
node.reset(new Node(visible, std::forward<Args>(args)...));
|
||||
visibleChanged = false;
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
HideableChartItem<Node>::HideableChartItem(ChartView &v, size_t z) : ChartItem(v, z),
|
||||
visible(true), visibleChanged(false)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
void HideableChartItem<Node>::updateVisible()
|
||||
{
|
||||
if (visibleChanged)
|
||||
node->setVisible(visible);
|
||||
visibleChanged = false;
|
||||
}
|
||||
|
||||
#endif
|
||||
51
qt-quick/chartitem_ptr.h
Normal file
51
qt-quick/chartitem_ptr.h
Normal file
|
|
@ -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 <typename T>
|
||||
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
|
||||
100
qt-quick/chartitemhelper.h
Normal file
100
qt-quick/chartitemhelper.h
Normal file
|
|
@ -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 <memory>
|
||||
#include <QSGNode>
|
||||
|
||||
// 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<Node>" template augments the QSGNode "Node"
|
||||
// by a "setVisible()" function and overrides "isSubtreeBlocked()"
|
||||
//
|
||||
// The "QSGProxyNode<Node>" template is a QSGNode with a single
|
||||
// child of type "Node".
|
||||
//
|
||||
// Thus, if the node can be created, use:
|
||||
// HideableQSGNode<NodeTypeThatCanBeCreated> node
|
||||
// and if the node can only be obtained from QQuickWindow, use:
|
||||
// HideableQSGNode<QSGProxyNode<NodeThatCantBeCreated>> node
|
||||
// The latter should obviously be typedef-ed.
|
||||
//
|
||||
// Yes, that's all horrible, but if nothing else it teaches us about
|
||||
// composition.
|
||||
template <typename Node>
|
||||
class HideableQSGNode : public Node {
|
||||
bool hidden;
|
||||
bool isSubtreeBlocked() const override final;
|
||||
public:
|
||||
template<class... Args>
|
||||
HideableQSGNode(bool visible, Args&&... args);
|
||||
void setVisible(bool visible);
|
||||
};
|
||||
|
||||
template <typename Node>
|
||||
class QSGProxyNode : public QSGNode {
|
||||
public:
|
||||
std::unique_ptr<Node> node;
|
||||
QSGProxyNode(Node *node);
|
||||
};
|
||||
|
||||
// Implementation detail of templates - move to serparate header file
|
||||
template <typename Node>
|
||||
QSGProxyNode<Node>::QSGProxyNode(Node *node) : node(node)
|
||||
{
|
||||
appendChildNode(node);
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
bool HideableQSGNode<Node>::isSubtreeBlocked() const
|
||||
{
|
||||
return hidden;
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
template<class... Args>
|
||||
HideableQSGNode<Node>::HideableQSGNode(bool visible, Args&&... args) :
|
||||
Node(std::forward<Args>(args)...),
|
||||
hidden(!visible)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
void HideableQSGNode<Node>::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<float>(p.x()), static_cast<float>(p.y()));
|
||||
}
|
||||
|
||||
inline void setPoint(QSGGeometry::TexturedPoint2D &v, const QPointF &p, const QPointF &t)
|
||||
{
|
||||
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()),
|
||||
static_cast<float>(t.x()), static_cast<float>(t.y()));
|
||||
}
|
||||
|
||||
#endif
|
||||
243
qt-quick/chartview.cpp
Normal file
243
qt-quick/chartview.cpp
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "chartview.h"
|
||||
#include "chartitem.h"
|
||||
|
||||
#include <QQuickWindow>
|
||||
#include <QSGRectangleNode>
|
||||
|
||||
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<QSGNode>;
|
||||
|
||||
class RootNode : public QSGNode
|
||||
{
|
||||
public:
|
||||
RootNode(ChartView &view, QColor backgroundColor, size_t maxZ);
|
||||
~RootNode();
|
||||
ChartView &view;
|
||||
std::unique_ptr<QSGRectangleNode> backgroundNode; // solid background
|
||||
// We entertain one node per Z-level.
|
||||
std::vector<std::unique_ptr<ZNode>> 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<RootNode *>(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);
|
||||
}
|
||||
97
qt-quick/chartview.h
Normal file
97
qt-quick/chartview.h
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
#ifndef CHART_VIEW_H
|
||||
#define CHART_VIEW_H
|
||||
|
||||
#include "chartitem_ptr.h"
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
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 <typename T, class... Args>
|
||||
ChartItemPtr<T> createChartItem(Args&&... args);
|
||||
|
||||
template <typename T>
|
||||
void deleteChartItem(ChartItemPtr<T> &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 <typename T, class... Args>
|
||||
ChartItemPtr<T> ChartView::createChartItem(Args&&... args)
|
||||
{
|
||||
return ChartItemPtr(new T(*this, std::forward<Args>(args)...));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void ChartView::deleteChartItem(ChartItemPtr<T> &item)
|
||||
{
|
||||
deleteChartItemInternal(*item);
|
||||
item.reset();
|
||||
}
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue