[VDE] Deck Analytics Widgets overhaul (#6463)

* [VDE] Deck Analytics Widgets overhaul

Took 2 minutes

Took 3 minutes

Took 3 minutes

* Qt5 version guards.

Took 33 minutes


Took 3 seconds

* Include QtMath

Took 3 minutes

Took 8 seconds

* Use getCards()

Took 4 minutes

* Non pointer stuff

Took 52 seconds

* Add a newline to the tooltip

Took 2 minutes

Took 27 seconds

* Fix build failure on macOS 15

* Rename some things.

Took 17 minutes

Took 11 seconds


Took 18 seconds

* Address overloads, fix default configuration.

Took 1 hour 9 minutes

Took 8 seconds

* Fix mana curve default config.

Took 4 minutes

* Namespace to Qt libs

Took 5 minutes

* Selection overlay is transparent for mouse events.

Took 2 minutes

* Brace initialize.

Took 8 minutes

* Debian 11.

Took 5 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <ricky.rister.wang@gmail.com>
This commit is contained in:
BruebachL 2025-12-31 19:45:49 +01:00 committed by GitHub
parent 36d8280765
commit df9a8b2272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 4372 additions and 394 deletions

View file

@ -0,0 +1,43 @@
#include "bar_chart_background_widget.h"
BarChartBackgroundWidget::BarChartBackgroundWidget(QWidget *parent) : QWidget(parent)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}
QSize BarChartBackgroundWidget::sizeHint() const
{
return QSize(100, 150);
}
void BarChartBackgroundWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
constexpr int PAD = 4;
constexpr int LABEL_H = 20;
int left = 46; // axis space + internal padding
int right = width() - PAD;
int top = PAD;
int bottom = height() - PAD - LABEL_H;
int barAreaHeight = bottom - top;
int barAreaWidth = right - left;
p.fillRect(QRect(left, top, barAreaWidth, barAreaHeight), QColor(250, 250, 250));
int ticks = 5;
for (int i = 0; i <= ticks; i++) {
float r = float(i) / ticks;
int y = bottom - r * barAreaHeight;
p.setPen(QPen(QColor(180, 180, 180, 120), 1));
p.drawLine(left, y, right, y);
p.setPen(Qt::black);
p.drawText(left - 35, y - 6, 32, 12, Qt::AlignRight | Qt::AlignVCenter, QString::number(int(r * highest)));
}
}

View file

@ -0,0 +1,23 @@
#ifndef COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H
#define COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H
#include <QPainter>
#include <QWidget>
class BarChartBackgroundWidget : public QWidget
{
Q_OBJECT
public:
int highest = 0; // global maximum (shared across bars)
int barCount = 0; // number of CMC columns
int labelHeight = 20; // reserved for CMC numbers
explicit BarChartBackgroundWidget(QWidget *parent);
public slots:
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent *event) override;
};
#endif // COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H

View file

@ -0,0 +1,215 @@
#include "bar_chart_widget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QToolTip>
BarChartWidget::BarChartWidget(QWidget *parent) : QWidget(parent)
{
setMouseTracking(true);
}
void BarChartWidget::setBars(const QVector<BarData> &newBars)
{
bars = newBars;
update();
}
void BarChartWidget::setHighest(int h)
{
highest = qMax(1, h);
update();
}
QSize BarChartWidget::sizeHint() const
{
return QSize(300, 200);
}
QSize BarChartWidget::minimumSizeHint() const
{
return QSize(300, 50);
}
void BarChartWidget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
constexpr int PAD = 4;
constexpr int LABEL_H = 20;
int w = width();
int h = height();
int left = 46;
int right = w - PAD;
int top = PAD;
int bottom = h - PAD - LABEL_H;
int barAreaHeight = bottom - top;
int barAreaWidth = right - left;
int barCount = bars.size();
if (barCount == 0)
return;
int spacing = 6;
int barWidth = (barAreaWidth - (barCount - 1) * spacing) / barCount;
// background
p.fillRect(QRect(left, top, barAreaWidth, barAreaHeight), QColor(250, 250, 250));
// y-axis ticks
int ticks = 5;
// qInfo() << "Tick Positions ";
for (int i = 0; i <= ticks; i++) {
float r = float(i) / ticks;
int tickVal = i * highest / ticks; // integer value of tick
int y = bottom - (tickVal * barAreaHeight / highest);
// qInfo() << "Tick" << i << "value" << int(r * highest) << "y" << y;
p.setPen(QPen(QColor(180, 180, 180, 120), 1));
p.drawLine(left, y, right, y);
p.setPen(Qt::black);
p.drawText(left - 35, y - 6, 32, 12, Qt::AlignRight | Qt::AlignVCenter, QString::number(int(r * highest)));
}
// draw bars
// qInfo() << "Bar Segments";
int drawWidth = barWidth / 4; // 1/4 of allocated width
int xOffset = (barWidth - drawWidth) / 2; // center the narrow bar
for (int i = 0; i < barCount; i++) {
const BarData &bar = bars[i];
int x = left + i * (barWidth + spacing) + xOffset; // shift to center
int yCurrent = bottom;
for (int j = 0; j < bar.segments.size(); j++) {
const auto &seg = bar.segments[j];
int segHeight = (seg.value * barAreaHeight / highest);
if (segHeight < 2 && seg.value > 0)
segHeight = 2;
int topY = yCurrent - segHeight;
QRect r(x, topY, drawWidth, segHeight); // use drawWidth instead of barWidth
bool isTop = (j == bar.segments.size() - 1);
QLinearGradient g(r.topLeft(), r.bottomLeft());
g.setColorAt(0, seg.color.lighter(120));
g.setColorAt(1, seg.color.darker(110));
p.setBrush(g);
p.setPen(Qt::NoPen);
if (isTop) {
QPainterPath path;
int radius = 6;
int bx = r.x();
int by = r.y();
int bw = r.width();
int bh = r.height();
path.moveTo(bx, by + bh);
path.lineTo(bx, by + radius);
path.quadTo(bx, by, bx + radius, by);
path.lineTo(bx + bw - radius, by);
path.quadTo(bx + bw, by, bx + bw, by + radius);
path.lineTo(bx + bw, by + bh);
path.lineTo(bx, by + bh);
path.closeSubpath();
p.drawPath(path);
} else {
p.drawRect(r);
}
yCurrent -= segHeight;
}
// draw label below bar
QRect labelRect(left + i * (barWidth + spacing), bottom, barWidth, LABEL_H);
QFont f = p.font();
f.setBold(true);
p.setFont(f);
p.setPen(Qt::black);
p.drawText(labelRect, Qt::AlignCenter, bar.label);
}
}
void BarChartWidget::leaveEvent(QEvent *)
{
hoveredBar = -1;
hoveredSegment = -1;
QToolTip::hideText();
}
void BarChartWidget::mouseMoveEvent(QMouseEvent *e)
{
if (bars.isEmpty()) {
return;
}
constexpr int PAD = 4;
constexpr int LABEL_H = 20;
int w = width();
int h = height();
int left = 46;
int right = w - PAD;
int top = PAD;
int bottom = h - PAD - LABEL_H;
int barAreaHeight = bottom - top;
int barCount = bars.size();
int spacing = 6;
int barWidth = (right - left - (barCount - 1) * spacing) / barCount;
// find hovered bar
int mx = e->pos().x();
hoveredBar = -1;
for (int i = 0; i < barCount; i++) {
int x0 = left + i * (barWidth + spacing);
if (mx >= x0 && mx <= x0 + barWidth) {
hoveredBar = i;
break;
}
}
if (hoveredBar < 0) {
return;
}
// find hovered segment
int yCurrent = bottom;
const auto &segments = bars[hoveredBar].segments;
hoveredSegment = -1;
for (int i = 0; i < segments.size(); i++) {
const auto &seg = segments[i];
int segHeight = (seg.value * barAreaHeight / highest);
if (segHeight < 2 && seg.value > 0)
segHeight = 2;
int topY = yCurrent - segHeight;
int bottomY = yCurrent;
if (e->pos().y() >= topY && e->pos().y() <= bottomY) {
hoveredSegment = i;
break;
}
yCurrent -= segHeight;
}
if (hoveredSegment >= 0) {
const auto &s = segments[hoveredSegment];
QString text = QString("%1: %2 cards\n\n%3").arg(s.category).arg(s.value).arg(s.cards.join("\n"));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QToolTip::showText(e->globalPosition().toPoint(), text, this);
#else
QToolTip::showText(e->globalPos(), text, this);
#endif
} else {
QToolTip::hideText();
}
}

View file

@ -0,0 +1,52 @@
#ifndef COCKATRICE_BAR_CHART_WIDGET_H
#define COCKATRICE_BAR_CHART_WIDGET_H
#include <QColor>
#include <QString>
#include <QVector>
#include <QWidget>
struct BarSegment
{
QString category;
int value;
QStringList cards;
QColor color;
};
struct BarData
{
QString label;
QVector<BarSegment> segments;
};
class BarChartWidget : public QWidget
{
Q_OBJECT
public:
explicit BarChartWidget(QWidget *parent = nullptr);
void setBars(const QVector<BarData> &bars);
void setHighest(int h); // global max for scaling
int barCount() const
{
return bars.size();
}
protected:
void paintEvent(QPaintEvent *event) override;
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
void mouseMoveEvent(QMouseEvent *event) override;
void leaveEvent(QEvent *event) override;
private:
QVector<BarData> bars;
int highest = 1; // global maximum value
int hoveredBar = -1;
int hoveredSegment = -1;
};
#endif // COCKATRICE_BAR_CHART_WIDGET_H

View file

@ -0,0 +1,140 @@
#include "segmented_bar_widget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QToolTip>
SegmentedBarWidget::SegmentedBarWidget(QString label, QVector<Segment> segments, int total, QWidget *parent)
: QWidget(parent), label(std::move(label)), segments(std::move(segments)), total(total)
{
setMouseTracking(true);
setMinimumWidth(36);
setMaximumWidth(50);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
}
QSize SegmentedBarWidget::sizeHint() const
{
return QSize(50, 150);
}
void SegmentedBarWidget::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
constexpr int PAD = 4;
constexpr int LABEL_H = 20;
int w = width();
int h = height();
int barX = PAD;
int barWidth = w - PAD * 2;
int barTop = PAD;
int barBottom = h - PAD - LABEL_H;
int barHeight = barBottom - barTop;
int yCurrent = barBottom;
// draw stacked segments
for (int i = 0; i < segments.size(); i++) {
const auto &seg = segments[i];
int segHeight = total > 0 ? (seg.value * barHeight / total) : 0;
if (segHeight < 2)
segHeight = 2;
QRect r(barX, yCurrent - segHeight, barWidth, segHeight);
bool isTop = (i == segments.size() - 1);
QLinearGradient g(r.topLeft(), r.bottomLeft());
g.setColorAt(0, seg.color.lighter(120));
g.setColorAt(1, seg.color.darker(110));
p.setBrush(g);
p.setPen(Qt::NoPen);
if (isTop) {
QPainterPath path;
int radius = 6;
int x = r.x();
int y = r.y();
int w = r.width();
int h = r.height();
path.moveTo(x, y + h);
path.lineTo(x, y + radius);
path.quadTo(x, y, x + radius, y);
path.lineTo(x + w - radius, y);
path.quadTo(x + w, y, x + w, y + radius);
path.lineTo(x + w, y + h);
path.lineTo(x, y + h);
path.closeSubpath();
p.drawPath(path);
} else {
p.drawRect(r);
}
yCurrent -= segHeight;
}
// draw label
QRect labelRect(0, h - LABEL_H, w, LABEL_H);
QFont f = p.font();
f.setBold(true);
p.setFont(f);
p.setPen(Qt::black);
p.drawText(labelRect, Qt::AlignCenter, label);
}
int SegmentedBarWidget::segmentAt(int y) const
{
int padding = 4;
int labelHeight = 20;
int barHeight = height() - padding * 2 - labelHeight;
int barTop = padding;
int barBottom = barTop + barHeight;
int currentTop = barBottom;
for (int i = 0; i < segments.size(); i++) {
int segHeight = total > 0 ? (segments[i].value * barHeight / total) : 0;
if (segHeight < 1) {
segHeight = 1;
}
int top = currentTop - segHeight;
int bottom = currentTop;
if (y >= top && y <= bottom)
return i;
currentTop -= segHeight;
}
return -1;
}
void SegmentedBarWidget::mouseMoveEvent(QMouseEvent *e)
{
if (!hovered) {
return;
}
int idx = segmentAt(e->pos().y());
if (idx < 0) {
return;
}
const Segment &s = segments[idx];
QString text = QString("%1: %2 cards\n%3").arg(s.category).arg(s.value).arg(s.cards.join(", "));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QToolTip::showText(e->globalPosition().toPoint(), text, this);
#else
QToolTip::showText(e->globalPos(), text, this);
#endif
}

View file

@ -0,0 +1,38 @@
#ifndef COCKATRICE_SEGMENTED_BAR_WIDGET_H
#define COCKATRICE_SEGMENTED_BAR_WIDGET_H
#include <QColor>
#include <QVector>
#include <QWidget>
class SegmentedBarWidget : public QWidget
{
Q_OBJECT
public:
struct Segment
{
QString category;
int value = 0;
QStringList cards;
QColor color;
};
QString label;
QVector<Segment> segments;
float total = 1.0;
explicit SegmentedBarWidget(QString label, QVector<Segment> segments, int total, QWidget *parent = nullptr);
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent *event) override;
void mouseMoveEvent(QMouseEvent *e) override;
int segmentAt(int y) const;
private:
bool hovered = true;
};
#endif // COCKATRICE_SEGMENTED_BAR_WIDGET_H

View file

@ -0,0 +1,205 @@
#include "color_pie.h"
#include <QMouseEvent>
#include <QPainter>
#include <QToolTip>
#include <QtMath>
ColorPie::ColorPie(const QMap<QString, int> &_colors, QWidget *parent) : QWidget(parent), colors(_colors)
{
setMouseTracking(true);
}
void ColorPie::setColors(const QMap<QString, int> &_colors)
{
colors = _colors;
update();
}
QSize ColorPie::minimumSizeHint() const
{
return QSize(200, 200);
}
void ColorPie::paintEvent(QPaintEvent *)
{
if (colors.isEmpty()) {
return;
}
int total = 0;
for (int v : colors.values()) {
total += v;
}
if (total == 0) {
return;
}
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
int w = width();
int h = height();
int size = qMin(w, h) - 40; // leave space for labels
QRectF rect((w - size) / 2.0, (h - size) / 2.0, size, size);
// Draw border
p.setPen(QPen(Qt::black, 1));
p.setBrush(Qt::NoBrush);
p.drawEllipse(rect);
// Sorted keys for predictable order
QList<QString> sortedKeys = colors.keys();
std::sort(sortedKeys.begin(), sortedKeys.end());
double startAngle = 0.0;
for (const QString &key : sortedKeys) {
int value = colors[key];
double ratio = double(value) / total;
if (ratio <= minRatioThreshold) {
continue;
}
double spanAngle = ratio * 360.0;
QColor base = colorFromName(key);
// Gradient
QRadialGradient grad(rect.center(), size / 2);
grad.setColorAt(0, base.lighter(130));
grad.setColorAt(1, base.darker(130));
p.setBrush(grad);
p.setPen(Qt::NoPen);
// Draw slice
p.drawPie(rect, int(startAngle * 16), int(spanAngle * 16));
// Draw percent label
double midAngle = startAngle + spanAngle / 2;
double rad = qDegreesToRadians(midAngle);
double labelRadius = size / 2 + 15; // slightly outside the pie
QPointF center = rect.center();
QPointF labelPos(center.x() + labelRadius * qCos(rad), center.y() - labelRadius * qSin(rad));
QString label = QString("%1%").arg(int(ratio * 100 + 0.5));
QFontMetrics fm(p.font());
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
int labelWidth = fm.horizontalAdvance(label);
#else
int labelWidth = fm.width(label);
#endif
QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height());
p.setPen(Qt::black);
p.drawText(textRect, Qt::AlignCenter, label);
startAngle += spanAngle;
}
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void ColorPie::enterEvent(QEnterEvent *event)
{
Q_UNUSED(event);
isHovered = true;
}
#else
void ColorPie::enterEvent(QEvent *event)
{
Q_UNUSED(event);
isHovered = true;
}
#endif
void ColorPie::leaveEvent(QEvent *)
{
isHovered = false;
}
void ColorPie::mouseMoveEvent(QMouseEvent *event)
{
if (!isHovered || colors.isEmpty()) {
return;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPoint p = event->position().toPoint();
QPoint gp = event->globalPosition().toPoint();
#else
QPoint p = event->pos();
QPoint gp = event->globalPos();
#endif
QString text = tooltipForPoint(p);
if (!text.isEmpty()) {
QToolTip::showText(gp, text, this);
}
}
QString ColorPie::tooltipForPoint(const QPoint &pt) const
{
if (colors.isEmpty()) {
return {};
}
int total = 0;
for (int v : colors.values())
total += v;
if (total == 0)
return {};
int w = width();
int h = height();
int size = qMin(w, h) - 40;
QPointF center(w / 2.0, h / 2.0);
QPointF v = pt - center;
double distance = std::sqrt(v.x() * v.x() + v.y() * v.y());
if (distance > size / 2.0)
return {}; // outside pie
double angle = std::atan2(-v.y(), v.x()) * 180.0 / M_PI;
if (angle < 0) {
angle += 360.0;
}
double acc = 0.0;
QList<QString> keys = colors.keys();
std::sort(keys.begin(), keys.end());
for (const QString &key : keys) {
double span = (double(colors[key]) / total) * 360.0;
if (angle >= acc && angle < acc + span) {
double percent = (100.0 * colors[key]) / total;
return QString("%1: %2 cards (%3%)").arg(key).arg(colors[key]).arg(QString::number(percent, 'f', 1));
}
acc += span;
}
return {};
}
QColor ColorPie::colorFromName(const QString &name) const
{
static QMap<QString, QColor> map = {
{"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)},
{"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)},
};
if (map.contains(name)) {
return map[name];
}
QColor c(name);
if (!c.isValid()) {
c = Qt::gray;
}
return c;
}

View file

@ -0,0 +1,44 @@
#ifndef COCKATRICE_COLOR_PIE_H
#define COCKATRICE_COLOR_PIE_H
#ifndef COLOR_PIE_H
#define COLOR_PIE_H
#include <QMap>
#include <QString>
#include <QWidget>
class ColorPie : public QWidget
{
Q_OBJECT
public:
explicit ColorPie(const QMap<QString, int> &_colors = {}, QWidget *parent = nullptr);
void setColors(const QMap<QString, int> &_colors);
QSize minimumSizeHint() const override;
protected:
void paintEvent(QPaintEvent *) override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent *event) override;
#else
void enterEvent(QEvent *event) override;
#endif
void leaveEvent(QEvent *) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
QMap<QString, int> colors;
bool isHovered = false;
const double minRatioThreshold = 0.01; // skip tiny slices
QColor colorFromName(const QString &name) const;
QString tooltipForPoint(const QPoint &pt) const;
};
#endif // COLOR_PIE_H
#endif // COCKATRICE_COLOR_PIE_H