[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,56 @@
#include "bar_widget.h"
#include <QFontMetrics>
#include <QPainter>
BarWidget::BarWidget(QString label, int value, int total, QColor barColor, QWidget *parent)
: QWidget(parent), label(std::move(label)), value(value), total(total), barColor(barColor)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}
QSize BarWidget::sizeHint() const
{
QFontMetrics metrics(font());
int labelHeight = metrics.height();
int valueHeight = metrics.height();
// Calculate the height dynamically based on the total
int barHeight = (total > 0) ? (value * 200 / total) : 20; // Scale height proportionally
int totalHeight = barHeight + labelHeight + valueHeight + 30; // Extra space for text
return QSize(60, totalHeight); // Allow width to expand
}
void BarWidget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
int widgetWidth = width();
int widgetHeight = height();
// Calculate bar dimensions
int barWidth = widgetWidth * 0.8; // Use 80% of the available width
int fullBarHeight = widgetHeight - 40; // Leave space for labels
int valueBarHeight = (total > 0) ? (value * fullBarHeight / total) : 0;
// Draw full bar background (gray)
painter.setBrush(QColor(200, 200, 200));
painter.drawRect((widgetWidth - barWidth) / 2, 10, barWidth, fullBarHeight);
// Draw the value-specific bar using the assigned color
painter.setBrush(barColor);
painter.drawRect((widgetWidth - barWidth) / 2, 10 + fullBarHeight - valueBarHeight, barWidth, valueBarHeight);
// Draw the CMC label
painter.setPen(Qt::white);
QRect textRect(0, widgetHeight - 30, widgetWidth, 20);
painter.drawText(textRect, Qt::AlignCenter, label);
// Draw the value count
painter.setPen(Qt::black);
QRect valueRect(0, 10, widgetWidth, 20);
painter.drawText(valueRect, Qt::AlignCenter, QString::number(value));
(void)event; // Suppress unused parameter warning
}

View file

@ -0,0 +1,32 @@
/**
* @file bar_widget.h
* @ingroup Widgets
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef BAR_WIDGET_H
#define BAR_WIDGET_H
#include <QWidget>
class BarWidget : public QWidget
{
Q_OBJECT
public:
explicit BarWidget(QString label, int value, int total, QColor barColor = Qt::blue, QWidget *parent = nullptr);
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent *event) override;
private:
QString label;
int value;
int total;
QColor barColor; // Store the bar color
};
#endif // BAR_WIDGET_H

View file

@ -0,0 +1,162 @@
#include "color_bar.h"
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QToolTip>
ColorBar::ColorBar(const QMap<QString, int> &_colors, QWidget *parent) : QWidget(parent), colors(_colors)
{
setMouseTracking(true);
}
void ColorBar::setColors(const QMap<QString, int> &_colors)
{
colors = _colors;
update();
}
QSize ColorBar::minimumSizeHint() const
{
return QSize(200, 22);
}
void ColorBar::paintEvent(QPaintEvent *)
{
if (colors.isEmpty())
return;
int total = 0;
for (int v : colors.values())
total += v;
// Prevent divide-by-zero
if (total == 0)
return;
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
const int w = width();
const int h = height();
int x = 0;
// Draw rounded border background
QRectF bounds(0.5, 0.5, w - 1, h - 1);
p.setPen(QPen(Qt::black, 1));
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(bounds, 6, 6);
// Clip to inside the border
p.setClipRect(bounds.adjusted(2, 2, -2, -2));
// Ensure predictable order
QList<QString> sortedKeys = colors.keys();
std::sort(sortedKeys.begin(), sortedKeys.end()); // Sort alphabetically
// Draw each color segment in the sorted order
for (const QString &key : sortedKeys) {
int value = colors[key];
double ratio = double(value) / total;
if (ratio <= minRatioThreshold) {
continue;
}
int segmentWidth = int(ratio * w);
// Ensure the segment width is at least 1 to avoid degenerate rectangles
if (segmentWidth < 1)
segmentWidth = 1;
QColor base = colorFromName(key);
// Slight gradient for nicer look
QLinearGradient grad(x, 0, x, h);
grad.setColorAt(0, base.lighter(120));
grad.setColorAt(1, base.darker(120));
p.fillRect(QRect(x, 0, segmentWidth, h), grad);
x += segmentWidth;
}
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void ColorBar::enterEvent(QEnterEvent *event)
{
Q_UNUSED(event);
isHovered = true;
}
#else
void ColorBar::enterEvent(QEvent *event)
{
Q_UNUSED(event);
isHovered = true;
}
#endif
void ColorBar::leaveEvent(QEvent *)
{
isHovered = false;
}
void ColorBar::mouseMoveEvent(QMouseEvent *event)
{
if (!isHovered || colors.isEmpty())
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
int x = int(event->position().x());
QPoint gp = event->globalPosition().toPoint();
#else
int x = event->pos().x();
QPoint gp = event->globalPos();
#endif
QString text = tooltipForPosition(x);
if (!text.isEmpty())
QToolTip::showText(gp, text, this);
}
QString ColorBar::tooltipForPosition(int x) const
{
int total = 0;
for (int v : colors.values())
total += v;
if (total == 0)
return {};
int pos = 0;
for (auto it = colors.cbegin(); it != colors.cend(); ++it) {
const double ratio = double(it.value()) / total;
const int segmentWidth = int(ratio * width());
if (x >= pos && x < pos + segmentWidth) {
const double percent = (100.0 * it.value()) / total;
return QString("%1: %2 cards (%3%)").arg(it.key()).arg(it.value()).arg(QString::number(percent, 'f', 1));
}
pos += segmentWidth;
}
return {};
}
QColor ColorBar::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,128 @@
#ifndef COCKATRICE_COLOR_BAR_H
#define COCKATRICE_COLOR_BAR_H
#include <QColor>
#include <QMap>
#include <QString>
#include <QWidget>
/**
* @class ColorBar
* @brief A widget for visualizing proportional color distributions as a horizontal bar.
*
* This widget renders a horizontal bar divided into colored segments whose widths reflect
* the relative values associated with each color key in a `QMap<QString, int>`. The class
* is designed as a small, lightweight, and self-contained visualization component suitable
* for representing distributions such as color counts, mana statistics, categorical frequencies, and similar data sets.
*
* Key features:
* - Filled segments for better visual clarity.
* - Deterministic alphabetical ordering of color keys.
* - Optional minimum percentage threshold for filtering out insignificant segments.
* - Mouse-hover tooltips showing each segment’s key, count, and percentage of total.
*
* Default color mappings exist for `"R"`, `"G"`, `"U"`, `"W"`, and `"B"`, using named
* colors, but any string recognized by `QColor` may be used. If an unknown name is provided,
* the segment will fall back to gray.
*
* This component is display-only and does not interpret or mutate domain-level data.
*/
class ColorBar : public QWidget
{
Q_OBJECT
public:
/**
* @brief Constructs a ColorBar widget.
*
* @param colors Map of color identifiers to integer counts.
* @param parent Optional parent widget.
*/
explicit ColorBar(const QMap<QString, int> &colors, QWidget *parent = nullptr);
/**
* @brief Updates the color distribution map.
* @param colors New color → count mapping.
*
* Triggers an immediate repaint.
*/
void setColors(const QMap<QString, int> &colors);
/**
* @brief Sets a minimum percentage threshold below which segments are not drawn.
*
* @param treshold Percentage from 0 to 100.
*
* Internally converted into a ratio (0.05 = 5%).
*/
void setMinPercentThreshold(double treshold)
{
minRatioThreshold = treshold / 100.0;
}
/**
* @brief Returns the recommended minimum size.
*/
QSize minimumSizeHint() const override;
protected:
/**
* @brief Paints the color distribution bar.
*
* Draws:
* - A rounded border
* - Filled segments for each color
* - Only segments above the minimum ratio threshold
*/
void paintEvent(QPaintEvent *event) override;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
/**
* @brief Handles mouse hover entering (Qt6 version).
*/
void enterEvent(QEnterEvent *event) override;
#else
/**
* @brief Handles mouse hover entering (Qt5 version).
*/
void enterEvent(QEvent *event) override;
#endif
/**
* @brief Handles mouse hover leaving.
*/
void leaveEvent(QEvent *event) override;
/**
* @brief Handles mouse movement to update contextual tooltips.
*/
void mouseMoveEvent(QMouseEvent *event) override;
private:
/// Map of color keys to counts used for rendering.
QMap<QString, int> colors;
/// True if the mouse is currently inside the widget.
bool isHovered = false;
/// Minimum ratio a segment must exceed to be drawn.
double minRatioThreshold = 0.0;
/**
* @brief Converts a color name into a display QColor.
*
* Recognized special keys: `"R", "G", "U", "W", "B"`.
* Other strings are treated as QColor names or fall back to gray.
*/
QColor colorFromName(const QString &name) const;
/**
* @brief Returns tooltip text for a given x-coordinate in the bar.
*
* @param x Horizontal coordinate relative to widget.
* @return Tooltip text or empty string if no segment applies.
*/
QString tooltipForPosition(int x) const;
};
#endif // COCKATRICE_COLOR_BAR_H

View file

@ -0,0 +1,46 @@
#include "percent_bar_widget.h"
PercentBarWidget::PercentBarWidget(QWidget *parent, double initialValue) : QWidget(parent), valueToDisplay(initialValue)
{
setMinimumSize(50, 10);
}
void PercentBarWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
QRect rect = this->rect();
const int midX = rect.width() / 2;
const int height = rect.height();
// Draw background border (no fill)
painter.setPen(QPen(Qt::black, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(rect.adjusted(0, 0, -1, -1)); // Avoid right/bottom overflow
const double halfWidth = rect.width() / 2.0;
const int barLength = static_cast<int>((qAbs(valueToDisplay) / 100.0) * halfWidth);
QRect fillRect;
if (valueToDisplay > 0.0) {
fillRect = QRect(midX, 0, barLength, height);
painter.fillRect(fillRect, Qt::green);
} else if (valueToDisplay < 0.0) {
fillRect = QRect(midX - barLength, 0, barLength, height);
painter.fillRect(fillRect, Qt::red);
}
// Draw center line at 0
painter.fillRect(midX - 1, 0, 3, height, Qt::white);
// Draw tick marks every 10%
const int tickHeight = 4;
for (int percent = -100; percent <= 100; percent += 10) {
int x = midX + static_cast<int>((percent / 100.0) * halfWidth);
painter.drawLine(x, height - tickHeight, x, height);
}
}

View file

@ -0,0 +1,39 @@
/**
* @file percent_bar_widget.h
* @ingroup Widgets
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef PERCENT_BAR_WIDGET_H
#define PERCENT_BAR_WIDGET_H
#include <QPainter>
#include <QWidget>
class PercentBarWidget : public QWidget
{
Q_OBJECT
public:
explicit PercentBarWidget(QWidget *parent, double initialValue);
void setValue(double newValue)
{
valueToDisplay = qBound(-100.0, newValue, 100.0); // Clamp to [-100, 100]
update(); // Trigger repaint
}
double value() const
{
return valueToDisplay;
}
protected:
void paintEvent(QPaintEvent *event) override;
private:
double valueToDisplay; // Ranges from -100 to 100
};
#endif // PERCENT_BAR_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