subsurface/qt-quick/chartview.cpp
Berthold Stoeger a527415ac9 profile: move dragging code from stats to general qt-code
So far, the only dragable item was the legend in the statistics
code. On the profile, we will have multiple dragable items.

Therefore, move the code up and make it more general. This
took some reorganization. Now, the size of the ChartItem
is saved in the base class. Also, setPos() became a virtual
function.

The dragable items are kept as an unsorted list.
If there will be many of them, this should be changed to
some sort of sorted list (maybe quadtree?).

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-09-12 15:15:31 +02:00

300 lines
7.1 KiB
C++

// 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);
}
// 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()
{
if (view)
view->emergencyShutdown();
}
ChartView::~ChartView()
{
// Sometimes the rootNode is destructed before the view,
// sometimes the other way around. QtQuick is a mess!
if (rootNode)
rootNode->view = nullptr;
}
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 rootNode's 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);
dragableItems.clear();
draggedItem.reset();
// ...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);
if (item.dragable)
dragableItems.push_back(&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);
if (item.dragable) {
// This becomes inefficient if there are many dragable items!
auto it = std::find(dragableItems.begin(), dragableItems.end(), &item);
if (it == dragableItems.end())
fprintf(stderr, "warning: dragable item not found\n");
else
dragableItems.erase(it);
}
}
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);
}
void ChartView::mousePressEvent(QMouseEvent *event)
{
QPointF pos = event->localPos();
for (auto item: dragableItems) {
QRectF rect = item->getRect();
if (rect.contains(pos)) {
dragStartMouse = pos;
dragStartItem = rect.topLeft();
draggedItem = item;
grabMouse();
setKeepMouseGrab(true); // don't allow Qt to steal the grab
event->accept();
return;
}
}
event->ignore();
}
void ChartView::mouseReleaseEvent(QMouseEvent *event)
{
if (draggedItem) {
draggedItem.reset();
ungrabMouse();
event->accept();
}
}
void ChartView::mouseMoveEvent(QMouseEvent *event)
{
if (draggedItem) {
QSizeF sceneSize = size();
if (sceneSize.width() <= 1.0 || sceneSize.height() <= 1.0)
return;
draggedItem->setPos(event->pos() - dragStartMouse + dragStartItem);
update();
}
}