mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-25 11:56:11 -07:00
[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:
parent
36d8280765
commit
df9a8b2272
81 changed files with 4372 additions and 394 deletions
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue