2021-01-12 14:20:05 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
#include "chartitem.h"
|
2021-01-17 21:03:27 +00:00
|
|
|
#include "statscolors.h"
|
2021-01-12 14:20:05 +00:00
|
|
|
#include "statsview.h"
|
2022-03-13 17:49:48 +00:00
|
|
|
#include "core/globals.h"
|
2021-01-12 14:20:05 +00:00
|
|
|
|
|
|
|
#include <cmath>
|
|
|
|
#include <QQuickWindow>
|
2021-01-14 07:48:56 +00:00
|
|
|
#include <QSGFlatColorMaterial>
|
2021-01-12 14:20:05 +00:00
|
|
|
#include <QSGImageNode>
|
2021-01-15 21:48:32 +00:00
|
|
|
#include <QSGRectangleNode>
|
2021-01-12 14:20:05 +00:00
|
|
|
#include <QSGTexture>
|
2021-02-07 13:33:48 +00:00
|
|
|
#include <QSGTextureMaterial>
|
2021-01-12 14:20:05 +00:00
|
|
|
|
2021-02-07 19:48:43 +00:00
|
|
|
static int selectionOverlayPixelSize = 2;
|
|
|
|
|
2021-01-12 14:20:05 +00:00
|
|
|
static int round_up(double f)
|
|
|
|
{
|
|
|
|
return static_cast<int>(ceil(f));
|
|
|
|
}
|
|
|
|
|
2021-01-13 15:19:27 +00:00
|
|
|
ChartItem::ChartItem(StatsView &v, ChartZValue z) :
|
2021-01-18 21:29:34 +00:00
|
|
|
dirty(false), prev(nullptr), next(nullptr),
|
2021-01-15 11:22:32 +00:00
|
|
|
zValue(z), view(v)
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
2021-01-18 21:29:34 +00:00
|
|
|
// Register before the derived constructors run, so that the
|
|
|
|
// derived classes can mark the item as dirty in the constructor.
|
|
|
|
v.registerChartItem(*this);
|
2021-01-12 14:20:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ChartItem::~ChartItem()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
QSizeF ChartItem::sceneSize() const
|
|
|
|
{
|
|
|
|
return view.size();
|
|
|
|
}
|
|
|
|
|
2021-01-18 21:29:34 +00:00
|
|
|
void ChartItem::markDirty()
|
|
|
|
{
|
|
|
|
view.registerDirtyChartItem(*this);
|
|
|
|
}
|
|
|
|
|
2021-01-17 17:29:54 +00:00
|
|
|
ChartPixmapItem::ChartPixmapItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
|
2021-01-14 07:48:56 +00:00
|
|
|
positionDirty(false), textureDirty(false)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ChartPixmapItem::~ChartPixmapItem()
|
|
|
|
{
|
|
|
|
painter.reset(); // Make sure to destroy painter before image that is painted on
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChartPixmapItem::setTextureDirty()
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
|
|
|
textureDirty = true;
|
2021-01-18 21:29:34 +00:00
|
|
|
markDirty();
|
2021-01-12 14:20:05 +00:00
|
|
|
}
|
|
|
|
|
2021-01-14 07:48:56 +00:00
|
|
|
void ChartPixmapItem::setPositionDirty()
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
|
|
|
positionDirty = true;
|
2021-01-18 21:29:34 +00:00
|
|
|
markDirty();
|
2021-01-12 14:20:05 +00:00
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void ChartPixmapItem::render(const StatsTheme &)
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
|
|
|
if (!node) {
|
2021-01-17 17:29:54 +00:00
|
|
|
createNode(view.w()->createImageNode());
|
2021-01-13 15:19:27 +00:00
|
|
|
view.addQSGNode(node.get(), zValue);
|
2021-01-12 14:20:05 +00:00
|
|
|
}
|
2021-01-18 21:29:34 +00:00
|
|
|
updateVisible();
|
|
|
|
|
2021-01-12 14:20:05 +00:00
|
|
|
if (!img) {
|
|
|
|
resize(QSizeF(1,1));
|
|
|
|
img->fill(Qt::transparent);
|
|
|
|
}
|
|
|
|
if (textureDirty) {
|
|
|
|
texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel));
|
2021-01-17 17:29:54 +00:00
|
|
|
node->node->setTexture(texture.get());
|
2021-01-12 14:20:05 +00:00
|
|
|
textureDirty = false;
|
|
|
|
}
|
|
|
|
if (positionDirty) {
|
2021-01-17 17:29:54 +00:00
|
|
|
node->node->setRect(rect);
|
2021-01-12 14:20:05 +00:00
|
|
|
positionDirty = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-14 07:48:56 +00:00
|
|
|
void ChartPixmapItem::resize(QSizeF size)
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2021-01-14 07:48:56 +00:00
|
|
|
void ChartPixmapItem::setPos(QPointF pos)
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
|
|
|
rect.moveTopLeft(pos);
|
|
|
|
setPositionDirty();
|
|
|
|
}
|
|
|
|
|
2021-01-14 07:48:56 +00:00
|
|
|
QRectF ChartPixmapItem::getRect() const
|
2021-01-12 14:20:05 +00:00
|
|
|
{
|
|
|
|
return rect;
|
|
|
|
}
|
|
|
|
|
2021-01-17 21:03:27 +00:00
|
|
|
static const int scatterItemDiameter = 10;
|
|
|
|
static const int scatterItemBorder = 1;
|
|
|
|
|
2022-10-19 20:35:18 +00:00
|
|
|
ChartScatterItem::ChartScatterItem(StatsView &v, ChartZValue z, bool selected) : HideableChartItem(v, z),
|
|
|
|
positionDirty(false), textureDirty(false),
|
|
|
|
highlight(selected ? Highlight::Selected : Highlight::Unselected)
|
2021-01-17 21:03:27 +00:00
|
|
|
{
|
|
|
|
rect.setSize(QSizeF(static_cast<double>(scatterItemDiameter), static_cast<double>(scatterItemDiameter)));
|
|
|
|
}
|
|
|
|
|
|
|
|
ChartScatterItem::~ChartScatterItem()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-01-19 08:01:14 +00:00
|
|
|
static QSGTexture *createScatterTexture(StatsView &view, const QColor &color, const QColor &borderColor)
|
2021-01-17 21:03:27 +00:00
|
|
|
{
|
|
|
|
QImage img(scatterItemDiameter, scatterItemDiameter, QImage::Format_ARGB32);
|
|
|
|
img.fill(Qt::transparent);
|
|
|
|
QPainter painter(&img);
|
|
|
|
painter.setPen(Qt::NoPen);
|
|
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
painter.setBrush(borderColor);
|
|
|
|
painter.drawEllipse(0, 0, scatterItemDiameter, scatterItemDiameter);
|
|
|
|
painter.setBrush(color);
|
|
|
|
painter.drawEllipse(scatterItemBorder, scatterItemBorder,
|
|
|
|
scatterItemDiameter - 2 * scatterItemBorder,
|
|
|
|
scatterItemDiameter - 2 * scatterItemBorder);
|
2021-01-19 08:01:14 +00:00
|
|
|
return view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel);
|
2021-01-17 21:03:27 +00:00
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
QSGTexture *ChartScatterItem::getTexture(const StatsTheme &theme) const
|
2021-01-31 19:48:12 +00:00
|
|
|
{
|
|
|
|
switch (highlight) {
|
|
|
|
default:
|
|
|
|
case Highlight::Unselected:
|
2021-02-16 16:05:39 +00:00
|
|
|
return theme.scatterItemTexture;
|
2021-01-31 19:48:12 +00:00
|
|
|
case Highlight::Selected:
|
2021-02-16 16:05:39 +00:00
|
|
|
return theme.scatterItemSelectedTexture;
|
2021-01-31 19:48:12 +00:00
|
|
|
case Highlight::Highlighted:
|
2021-02-16 16:05:39 +00:00
|
|
|
return theme.scatterItemHighlightedTexture;
|
2021-01-31 19:48:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void ChartScatterItem::render(const StatsTheme &theme)
|
2021-01-17 21:03:27 +00:00
|
|
|
{
|
2021-02-16 16:05:39 +00:00
|
|
|
if (!theme.scatterItemTexture) {
|
|
|
|
theme.scatterItemTexture = register_global(createScatterTexture(view, theme.fillColor, theme.borderColor));
|
|
|
|
theme.scatterItemSelectedTexture = register_global(createScatterTexture(view, theme.selectedColor, theme.selectedBorderColor));
|
|
|
|
theme.scatterItemHighlightedTexture = register_global(createScatterTexture(view, theme.highlightedColor, theme.highlightedBorderColor));
|
2021-01-17 21:03:27 +00:00
|
|
|
}
|
|
|
|
if (!node) {
|
|
|
|
createNode(view.w()->createImageNode());
|
|
|
|
view.addQSGNode(node.get(), zValue);
|
|
|
|
textureDirty = positionDirty = true;
|
|
|
|
}
|
2021-01-18 21:29:34 +00:00
|
|
|
updateVisible();
|
2021-01-17 21:03:27 +00:00
|
|
|
if (textureDirty) {
|
2021-02-16 16:05:39 +00:00
|
|
|
node->node->setTexture(getTexture(theme));
|
2021-01-17 21:03:27 +00:00
|
|
|
textureDirty = false;
|
|
|
|
}
|
|
|
|
if (positionDirty) {
|
|
|
|
node->node->setRect(rect);
|
|
|
|
positionDirty = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChartScatterItem::setPos(QPointF pos)
|
|
|
|
{
|
|
|
|
pos -= QPointF(scatterItemDiameter / 2.0, scatterItemDiameter / 2.0);
|
|
|
|
rect.moveTopLeft(pos);
|
|
|
|
positionDirty = true;
|
2021-01-18 21:29:34 +00:00
|
|
|
markDirty();
|
2021-01-17 21:03:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static double squareDist(const QPointF &p1, const QPointF &p2)
|
|
|
|
{
|
|
|
|
QPointF diff = p1 - p2;
|
|
|
|
return QPointF::dotProduct(diff, diff);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ChartScatterItem::contains(QPointF point) const
|
|
|
|
{
|
|
|
|
return squareDist(point, rect.center()) <= (scatterItemDiameter / 2.0) * (scatterItemDiameter / 2.0);
|
|
|
|
}
|
|
|
|
|
2021-02-01 22:17:04 +00:00
|
|
|
// For rectangular selections, we are more crude: simply check whether the center is in the selection.
|
|
|
|
bool ChartScatterItem::inRect(const QRectF &selection) const
|
|
|
|
{
|
|
|
|
return selection.contains(rect.center());
|
|
|
|
}
|
|
|
|
|
2021-01-31 19:48:12 +00:00
|
|
|
void ChartScatterItem::setHighlight(Highlight highlightIn)
|
2021-01-17 21:03:27 +00:00
|
|
|
{
|
2021-01-31 19:48:12 +00:00
|
|
|
if (highlight == highlightIn)
|
2021-01-17 21:03:27 +00:00
|
|
|
return;
|
2021-01-31 19:48:12 +00:00
|
|
|
highlight = highlightIn;
|
2021-01-17 21:03:27 +00:00
|
|
|
textureDirty = true;
|
2021-01-18 21:29:34 +00:00
|
|
|
markDirty();
|
2021-01-17 21:03:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QRectF ChartScatterItem::getRect() const
|
|
|
|
{
|
|
|
|
return rect;
|
|
|
|
}
|
|
|
|
|
2021-01-13 15:19:27 +00:00
|
|
|
ChartRectItem::ChartRectItem(StatsView &v, ChartZValue z,
|
2021-01-14 07:48:56 +00:00
|
|
|
const QPen &pen, const QBrush &brush, double radius) : ChartPixmapItem(v, z),
|
2021-01-12 14:20:05 +00:00
|
|
|
pen(pen), brush(brush), radius(radius)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ChartRectItem::~ChartRectItem()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChartRectItem::resize(QSizeF size)
|
|
|
|
{
|
2021-01-14 07:48:56 +00:00
|
|
|
ChartPixmapItem::resize(size);
|
2021-01-12 14:20:05 +00:00
|
|
|
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);
|
|
|
|
}
|
2021-01-14 07:48:56 +00:00
|
|
|
|
2021-01-15 21:48:32 +00:00
|
|
|
ChartTextItem::ChartTextItem(StatsView &v, ChartZValue 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));
|
|
|
|
}
|
|
|
|
|
2021-01-18 11:08:46 +00:00
|
|
|
ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const QString &text) :
|
|
|
|
ChartTextItem(v, z, f, std::vector<QString>({ text }), true)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-01-15 21:48:32 +00:00
|
|
|
void ChartTextItem::setColor(const QColor &c)
|
|
|
|
{
|
2021-01-19 15:09:56 +00:00
|
|
|
setColor(c, Qt::transparent);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChartTextItem::setColor(const QColor &c, const QColor &background)
|
|
|
|
{
|
|
|
|
img->fill(background);
|
2021-01-15 21:48:32 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2021-01-18 11:08:46 +00:00
|
|
|
ChartPieItem::ChartPieItem(StatsView &v, ChartZValue z, double borderWidth) : ChartPixmapItem(v, z),
|
|
|
|
borderWidth(borderWidth)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
static QBrush makeBrush(QColor fill, bool selected, const StatsTheme &theme)
|
2021-02-07 19:48:43 +00:00
|
|
|
{
|
|
|
|
if (!selected)
|
|
|
|
return QBrush(fill);
|
|
|
|
QImage img(2 * selectionOverlayPixelSize, 2 * selectionOverlayPixelSize, QImage::Format_ARGB32);
|
|
|
|
img.fill(fill);
|
|
|
|
for (int x = 0; x < selectionOverlayPixelSize; ++x) {
|
|
|
|
for (int y = 0; y < selectionOverlayPixelSize; ++y) {
|
2021-02-16 16:05:39 +00:00
|
|
|
img.setPixelColor(x, y, theme.selectionOverlayColor);
|
2021-02-07 19:48:43 +00:00
|
|
|
img.setPixelColor(x + selectionOverlayPixelSize, y + selectionOverlayPixelSize,
|
2021-02-16 16:05:39 +00:00
|
|
|
theme.selectionOverlayColor);
|
2021-02-07 19:48:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return QBrush(img);
|
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void ChartPieItem::drawSegment(double from, double to, QColor fill, QColor border, bool selected, const StatsTheme &theme)
|
2021-01-18 11:08:46 +00:00
|
|
|
{
|
|
|
|
painter->setPen(QPen(border, borderWidth));
|
2021-02-16 16:05:39 +00:00
|
|
|
painter->setBrush(makeBrush(fill, selected, theme));
|
2021-01-18 11:08:46 +00:00
|
|
|
// For whatever obscure reason, angles of pie pieces are given as 16th of a degree...?
|
|
|
|
// Angles increase CCW, whereas pie charts usually are read CW. Therfore, startAngle
|
|
|
|
// is dervied from "from" and subtracted from the origin angle at 12:00.
|
|
|
|
int startAngle = 90 * 16 - static_cast<int>(round(to * 360.0 * 16.0));
|
|
|
|
int spanAngle = static_cast<int>(round((to - from) * 360.0 * 16.0));
|
|
|
|
QRectF drawRect(QPointF(0.0, 0.0), rect.size());
|
|
|
|
painter->drawPie(drawRect, startAngle, spanAngle);
|
|
|
|
setTextureDirty();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChartPieItem::resize(QSizeF size)
|
|
|
|
{
|
|
|
|
ChartPixmapItem::resize(size);
|
|
|
|
img->fill(Qt::transparent);
|
|
|
|
}
|
|
|
|
|
2021-02-01 22:17:04 +00:00
|
|
|
ChartLineItemBase::ChartLineItemBase(StatsView &v, ChartZValue z, QColor color, double width) : HideableChartItem(v, z),
|
2021-01-14 07:48:56 +00:00
|
|
|
color(color), width(width), positionDirty(false), materialDirty(false)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-02-01 22:17:04 +00:00
|
|
|
ChartLineItemBase::~ChartLineItemBase()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChartLineItemBase::setLine(QPointF fromIn, QPointF toIn)
|
2021-01-14 07:48:56 +00:00
|
|
|
{
|
2021-02-01 22:17:04 +00:00
|
|
|
from = fromIn;
|
|
|
|
to = toIn;
|
|
|
|
positionDirty = true;
|
|
|
|
markDirty();
|
2021-01-14 07:48:56 +00:00
|
|
|
}
|
|
|
|
|
2021-01-15 21:48:32 +00:00
|
|
|
// Helper function to set points
|
|
|
|
void setPoint(QSGGeometry::Point2D &v, const QPointF &p)
|
|
|
|
{
|
|
|
|
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()));
|
|
|
|
}
|
|
|
|
|
2021-02-07 13:33:48 +00:00
|
|
|
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()));
|
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void ChartLineItem::render(const StatsTheme &)
|
2021-01-14 07:48:56 +00:00
|
|
|
{
|
|
|
|
if (!node) {
|
|
|
|
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
|
|
|
|
geometry->setDrawingMode(QSGGeometry::DrawLines);
|
|
|
|
material.reset(new QSGFlatColorMaterial);
|
2021-01-17 17:29:54 +00:00
|
|
|
createNode();
|
2021-01-14 07:48:56 +00:00
|
|
|
node->setGeometry(geometry.get());
|
|
|
|
node->setMaterial(material.get());
|
|
|
|
view.addQSGNode(node.get(), zValue);
|
|
|
|
positionDirty = materialDirty = true;
|
|
|
|
}
|
2021-01-18 21:29:34 +00:00
|
|
|
updateVisible();
|
2021-01-14 07:48:56 +00:00
|
|
|
|
|
|
|
if (positionDirty) {
|
|
|
|
// Attention: width is a geometry property and therefore handled by position dirty!
|
|
|
|
geometry->setLineWidth(static_cast<float>(width));
|
|
|
|
auto vertices = geometry->vertexDataAsPoint2D();
|
2021-01-15 21:48:32 +00:00
|
|
|
setPoint(vertices[0], from);
|
|
|
|
setPoint(vertices[1], to);
|
2021-01-14 07:48:56 +00:00
|
|
|
node->markDirty(QSGNode::DirtyGeometry);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (materialDirty) {
|
|
|
|
material->setColor(color);
|
|
|
|
node->markDirty(QSGNode::DirtyMaterial);
|
|
|
|
}
|
|
|
|
|
|
|
|
positionDirty = materialDirty = false;
|
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void ChartRectLineItem::render(const StatsTheme &)
|
2021-01-14 07:48:56 +00:00
|
|
|
{
|
2021-02-01 22:17:04 +00:00
|
|
|
if (!node) {
|
|
|
|
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4));
|
|
|
|
geometry->setDrawingMode(QSGGeometry::DrawLineLoop);
|
|
|
|
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()));
|
|
|
|
node->markDirty(QSGNode::DirtyGeometry);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (materialDirty) {
|
|
|
|
material->setColor(color);
|
|
|
|
node->markDirty(QSGNode::DirtyMaterial);
|
|
|
|
}
|
|
|
|
|
|
|
|
positionDirty = materialDirty = false;
|
2021-01-14 07:48:56 +00:00
|
|
|
}
|
2021-01-15 21:48:32 +00:00
|
|
|
|
2021-02-07 13:39:53 +00:00
|
|
|
ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth) : HideableChartItem(v, z),
|
|
|
|
borderWidth(borderWidth), selected(false),
|
2021-02-07 13:33:48 +00:00
|
|
|
positionDirty(false), colorDirty(false), selectedDirty(false)
|
2021-01-15 21:48:32 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ChartBarItem::~ChartBarItem()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
QSGTexture *ChartBarItem::getSelectedTexture(const StatsTheme &theme) const
|
2021-02-07 13:33:48 +00:00
|
|
|
{
|
2021-02-16 16:05:39 +00:00
|
|
|
if (!theme.selectedTexture) {
|
2021-02-07 13:33:48 +00:00
|
|
|
QImage img(2, 2, QImage::Format_ARGB32);
|
|
|
|
img.fill(Qt::transparent);
|
2021-02-16 16:05:39 +00:00
|
|
|
img.setPixelColor(0, 0, theme.selectionOverlayColor);
|
|
|
|
img.setPixelColor(1, 1, theme.selectionOverlayColor);
|
|
|
|
theme.selectedTexture = register_global(view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel));
|
2021-02-07 13:33:48 +00:00
|
|
|
}
|
2021-02-16 16:05:39 +00:00
|
|
|
return theme.selectedTexture;
|
2021-02-07 13:33:48 +00:00
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void ChartBarItem::render(const StatsTheme &theme)
|
2021-01-15 21:48:32 +00:00
|
|
|
{
|
|
|
|
if (!node) {
|
2021-01-17 17:29:54 +00:00
|
|
|
createNode(view.w()->createRectangleNode());
|
2021-01-15 21:48:32 +00:00
|
|
|
|
|
|
|
borderGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4));
|
|
|
|
borderGeometry->setDrawingMode(QSGGeometry::DrawLineLoop);
|
|
|
|
borderGeometry->setLineWidth(static_cast<float>(borderWidth));
|
|
|
|
borderMaterial.reset(new QSGFlatColorMaterial);
|
|
|
|
borderNode.reset(new QSGGeometryNode);
|
|
|
|
borderNode->setGeometry(borderGeometry.get());
|
|
|
|
borderNode->setMaterial(borderMaterial.get());
|
|
|
|
|
2021-01-17 17:29:54 +00:00
|
|
|
node->node->appendChildNode(borderNode.get());
|
2021-01-15 21:48:32 +00:00
|
|
|
view.addQSGNode(node.get(), zValue);
|
2021-02-07 13:33:48 +00:00
|
|
|
positionDirty = colorDirty = selectedDirty = true;
|
2021-01-15 21:48:32 +00:00
|
|
|
}
|
2021-01-18 21:29:34 +00:00
|
|
|
updateVisible();
|
2021-01-15 21:48:32 +00:00
|
|
|
|
|
|
|
if (colorDirty) {
|
2021-01-17 17:29:54 +00:00
|
|
|
node->node->setColor(color);
|
2021-01-15 21:48:32 +00:00
|
|
|
borderMaterial->setColor(borderColor);
|
2021-01-17 17:29:54 +00:00
|
|
|
node->node->markDirty(QSGNode::DirtyMaterial);
|
2021-01-15 21:48:32 +00:00
|
|
|
borderNode->markDirty(QSGNode::DirtyMaterial);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (positionDirty) {
|
2021-01-17 17:29:54 +00:00
|
|
|
node->node->setRect(rect);
|
2021-01-15 21:48:32 +00:00
|
|
|
auto vertices = borderGeometry->vertexDataAsPoint2D();
|
2021-02-07 13:39:53 +00:00
|
|
|
setPoint(vertices[0], rect.topLeft());
|
|
|
|
setPoint(vertices[1], rect.topRight());
|
|
|
|
setPoint(vertices[2], rect.bottomRight());
|
|
|
|
setPoint(vertices[3], rect.bottomLeft());
|
2021-01-17 17:29:54 +00:00
|
|
|
node->node->markDirty(QSGNode::DirtyGeometry);
|
2021-01-15 21:48:32 +00:00
|
|
|
borderNode->markDirty(QSGNode::DirtyGeometry);
|
|
|
|
}
|
|
|
|
|
2021-02-07 13:33:48 +00:00
|
|
|
if (selectedDirty) {
|
|
|
|
if (selected) {
|
|
|
|
if (!selectionNode) {
|
|
|
|
// Create the selection overlay if it didn't exist up to now.
|
|
|
|
selectionGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4));
|
|
|
|
selectionGeometry->setDrawingMode(QSGGeometry::DrawTriangleFan);
|
|
|
|
selectionMaterial.reset(new QSGTextureMaterial);
|
2021-02-16 16:05:39 +00:00
|
|
|
selectionMaterial->setTexture(getSelectedTexture(theme));
|
2021-02-07 13:33:48 +00:00
|
|
|
selectionMaterial->setHorizontalWrapMode(QSGTexture::Repeat);
|
|
|
|
selectionMaterial->setVerticalWrapMode(QSGTexture::Repeat);
|
|
|
|
selectionNode.reset(new QSGGeometryNode);
|
|
|
|
selectionNode->setGeometry(selectionGeometry.get());
|
|
|
|
selectionNode->setMaterial(selectionMaterial.get());
|
|
|
|
}
|
|
|
|
|
|
|
|
node->node->appendChildNode(selectionNode.get());
|
|
|
|
|
|
|
|
// Update the position of the selection overlay, even if the position didn't change.
|
|
|
|
positionDirty = true;
|
|
|
|
} else {
|
|
|
|
if (selectionNode)
|
|
|
|
node->node->removeChildNode(selectionNode.get());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selected && positionDirty) {
|
2021-02-07 19:48:43 +00:00
|
|
|
double pixelFactor = 2.0 * selectionOverlayPixelSize; // The texture image is 2x2 pixels.
|
2021-02-07 13:33:48 +00:00
|
|
|
auto selectionVertices = selectionGeometry->vertexDataAsTexturedPoint2D();
|
|
|
|
selectionNode->markDirty(QSGNode::DirtyGeometry);
|
|
|
|
setPoint(selectionVertices[0], rect.topLeft(), QPointF());
|
2021-02-07 19:48:43 +00:00
|
|
|
setPoint(selectionVertices[1], rect.topRight(), QPointF(rect.width() / pixelFactor, 0.0));
|
|
|
|
setPoint(selectionVertices[2], rect.bottomRight(), QPointF(rect.width() / pixelFactor, rect.height() / pixelFactor));
|
|
|
|
setPoint(selectionVertices[3], rect.bottomLeft(), QPointF(0.0, rect.height() / pixelFactor));
|
2021-02-07 13:33:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
positionDirty = colorDirty = selectedDirty = false;
|
2021-01-15 21:48:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ChartBarItem::setColor(QColor colorIn, QColor borderColorIn)
|
|
|
|
{
|
2021-02-07 13:33:48 +00:00
|
|
|
if (color == colorIn)
|
|
|
|
return;
|
2021-01-15 21:48:32 +00:00
|
|
|
color = colorIn;
|
|
|
|
borderColor = borderColorIn;
|
|
|
|
colorDirty = true;
|
2021-01-18 21:29:34 +00:00
|
|
|
markDirty();
|
2021-01-15 21:48:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ChartBarItem::setRect(const QRectF &rectIn)
|
|
|
|
{
|
2021-02-07 13:33:48 +00:00
|
|
|
if (rect == rectIn)
|
|
|
|
return;
|
2021-01-15 21:48:32 +00:00
|
|
|
rect = rectIn;
|
|
|
|
positionDirty = true;
|
2021-01-18 21:29:34 +00:00
|
|
|
markDirty();
|
2021-01-15 21:48:32 +00:00
|
|
|
}
|
|
|
|
|
2021-02-07 13:33:48 +00:00
|
|
|
void ChartBarItem::setSelected(bool selectedIn)
|
|
|
|
{
|
|
|
|
if (selected == selectedIn)
|
|
|
|
return;
|
|
|
|
selected = selectedIn;
|
|
|
|
selectedDirty = true;
|
|
|
|
markDirty();
|
|
|
|
}
|
|
|
|
|
2021-01-15 21:48:32 +00:00
|
|
|
QRectF ChartBarItem::getRect() const
|
|
|
|
{
|
|
|
|
return rect;
|
|
|
|
}
|
2021-01-17 20:07:57 +00:00
|
|
|
|
|
|
|
ChartBoxItem::ChartBoxItem(StatsView &v, ChartZValue z, double borderWidth) :
|
2021-02-07 13:39:53 +00:00
|
|
|
ChartBarItem(v, z, borderWidth)
|
2021-01-17 20:07:57 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
ChartBoxItem::~ChartBoxItem()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-02-16 16:05:39 +00:00
|
|
|
void ChartBoxItem::render(const StatsTheme &theme)
|
2021-01-17 20:07:57 +00:00
|
|
|
{
|
|
|
|
// Remember old dirty values, since ChartBarItem::render() will clear them
|
|
|
|
bool oldPositionDirty = positionDirty;
|
|
|
|
bool oldColorDirty = colorDirty;
|
2021-02-16 16:05:39 +00:00
|
|
|
ChartBarItem::render(theme); // This will create the base node, so no need to check for that.
|
2021-01-17 20:07:57 +00:00
|
|
|
if (!whiskersNode) {
|
|
|
|
whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10));
|
|
|
|
whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines);
|
|
|
|
whiskersGeometry->setLineWidth(static_cast<float>(borderWidth));
|
|
|
|
whiskersMaterial.reset(new QSGFlatColorMaterial);
|
|
|
|
whiskersNode.reset(new QSGGeometryNode);
|
|
|
|
whiskersNode->setGeometry(whiskersGeometry.get());
|
|
|
|
whiskersNode->setMaterial(whiskersMaterial.get());
|
|
|
|
|
|
|
|
node->node->appendChildNode(whiskersNode.get());
|
|
|
|
// If this is the first time, make sure to update the geometry.
|
|
|
|
oldPositionDirty = oldColorDirty = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (oldColorDirty) {
|
|
|
|
whiskersMaterial->setColor(borderColor);
|
|
|
|
whiskersNode->markDirty(QSGNode::DirtyMaterial);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (oldPositionDirty) {
|
|
|
|
auto vertices = whiskersGeometry->vertexDataAsPoint2D();
|
|
|
|
double left = rect.left();
|
|
|
|
double right = rect.right();
|
|
|
|
double mid = (left + right) / 2.0;
|
|
|
|
// top bar
|
|
|
|
setPoint(vertices[0], QPointF(left, max));
|
|
|
|
setPoint(vertices[1], QPointF(right, max));
|
|
|
|
// top whisker
|
|
|
|
setPoint(vertices[2], QPointF(mid, max));
|
|
|
|
setPoint(vertices[3], QPointF(mid, rect.top()));
|
|
|
|
// bottom bar
|
|
|
|
setPoint(vertices[4], QPointF(left, min));
|
|
|
|
setPoint(vertices[5], QPointF(right, min));
|
|
|
|
// bottom whisker
|
|
|
|
setPoint(vertices[6], QPointF(mid, min));
|
|
|
|
setPoint(vertices[7], QPointF(mid, rect.bottom()));
|
|
|
|
// median indicator
|
|
|
|
setPoint(vertices[8], QPointF(left, median));
|
|
|
|
setPoint(vertices[9], QPointF(right, median));
|
|
|
|
whiskersNode->markDirty(QSGNode::DirtyGeometry);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ChartBoxItem::setBox(const QRectF &rect, double minIn, double maxIn, double medianIn)
|
|
|
|
{
|
|
|
|
min = minIn;
|
|
|
|
max = maxIn;
|
|
|
|
median = medianIn;
|
|
|
|
setRect(rect);
|
|
|
|
}
|
|
|
|
|
|
|
|
QRectF ChartBoxItem::getRect() const
|
|
|
|
{
|
|
|
|
QRectF res = rect;
|
|
|
|
res.setTop(min);
|
|
|
|
res.setBottom(max);
|
|
|
|
return rect;
|
|
|
|
}
|