[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,28 @@
#include "draw_probability_config.h"
QJsonObject DrawProbabilityConfig::toJson() const
{
QJsonObject o;
o["criteria"] = criteria;
o["atLeast"] = atLeast;
o["quantity"] = quantity;
o["drawn"] = drawn;
return o;
}
DrawProbabilityConfig DrawProbabilityConfig::fromJson(const QJsonObject &o)
{
DrawProbabilityConfig cfg;
if (o.contains("criteria")) {
cfg.criteria = o["criteria"].toString();
}
if (o.contains("atLeast")) {
cfg.atLeast = o["atLeast"].toBool(true);
}
if (o.contains("quantity")) {
cfg.quantity = o["quantity"].toInt(1);
}
if (o.contains("drawn")) {
cfg.drawn = o["drawn"].toInt(7);
}
return cfg;
}

View file

@ -0,0 +1,19 @@
#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H
#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H
#include <QJsonObject>
#include <QString>
struct DrawProbabilityConfig
{
QString criteria = "name"; // name, type, subtype, cmc
bool atLeast = true; // true = at least, false = exactly
int quantity = 1; // N
int drawn = 7; // M
QJsonObject toJson() const;
static DrawProbabilityConfig fromJson(const QJsonObject &o);
};
#endif

View file

@ -0,0 +1,92 @@
#include "draw_probability_config_dialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLabel>
#include <QSpinBox>
DrawProbabilityConfigDialog::DrawProbabilityConfigDialog(QWidget *parent) : QDialog(parent)
{
form = new QFormLayout(this);
// Criteria
labelCriteria = new QLabel(this);
criteria = new QComboBox(this);
criteria->addItem(QString(), "name");
criteria->addItem(QString(), "type");
criteria->addItem(QString(), "subtype");
criteria->addItem(QString(), "cmc");
form->addRow(labelCriteria, criteria);
// Exactness
labelExactness = new QLabel(this);
exactness = new QComboBox(this);
exactness->addItem(QString(), true);
exactness->addItem(QString(), false);
form->addRow(labelExactness, exactness);
// Quantity
labelQuantity = new QLabel(this);
quantity = new QSpinBox(this);
quantity->setRange(1, 60);
form->addRow(labelQuantity, quantity);
// Drawn
labelDrawn = new QLabel(this);
drawn = new QSpinBox(this);
drawn->setRange(1, 60);
drawn->setValue(7);
form->addRow(labelDrawn, drawn);
// Button box
auto *bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
form->addWidget(bb);
connect(bb, &QDialogButtonBox::accepted, this, &DrawProbabilityConfigDialog::accept);
connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject);
retranslateUi();
}
void DrawProbabilityConfigDialog::retranslateUi()
{
setWindowTitle(tr("Draw Probability Settings"));
labelCriteria->setText(tr("Criteria:"));
criteria->setItemText(0, tr("Card Name"));
criteria->setItemText(1, tr("Type"));
criteria->setItemText(2, tr("Subtype"));
criteria->setItemText(3, tr("Mana Value"));
labelExactness->setText(tr("Exactness:"));
exactness->setItemText(0, tr("At least"));
exactness->setItemText(1, tr("Exactly"));
labelQuantity->setText(tr("Quantity (N):"));
labelDrawn->setText(tr("Cards drawn (M):"));
// i18n-friendly suffixes
quantity->setSuffix(tr(" cards"));
drawn->setSuffix(tr(" cards"));
}
void DrawProbabilityConfigDialog::setFromConfig(const DrawProbabilityConfig &_config)
{
cfg = _config;
criteria->setCurrentIndex(criteria->findData(_config.criteria));
exactness->setCurrentIndex(exactness->findData(_config.atLeast));
quantity->setValue(_config.quantity);
drawn->setValue(_config.drawn);
}
void DrawProbabilityConfigDialog::accept()
{
cfg.criteria = criteria->currentData().toString();
cfg.atLeast = exactness->currentData().toBool();
cfg.quantity = quantity->value();
cfg.drawn = drawn->value();
QDialog::accept();
}

View file

@ -0,0 +1,44 @@
#pragma once
#include "draw_probability_config.h"
#include <QDialog>
#include <QFormLayout>
class QComboBox;
class QSpinBox;
class QLabel;
class DrawProbabilityConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit DrawProbabilityConfigDialog(QWidget *parent = nullptr);
void retranslateUi();
void setFromConfig(const DrawProbabilityConfig &_config);
DrawProbabilityConfig result() const
{
return cfg;
}
protected:
void accept() override;
private:
DrawProbabilityConfig cfg;
QFormLayout *form;
// Widgets
QComboBox *criteria;
QComboBox *exactness;
QSpinBox *quantity;
QSpinBox *drawn;
QLabel *labelCriteria;
QLabel *labelExactness;
QLabel *labelQuantity;
QLabel *labelDrawn;
};

View file

@ -0,0 +1,236 @@
#include "draw_probability_widget.h"
#include "draw_probability_config_dialog.h"
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMap>
#include <QSpinBox>
#include <QTableWidgetItem>
#include <QWidget>
#include <QtMath>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_manager.h>
DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer)
{
controls = new QWidget(this);
controlLayout = new QHBoxLayout(controls);
labelPrefix = new QLabel(this);
controlLayout->addWidget(labelPrefix);
criteriaCombo = new QComboBox(this);
// Give these things item-data so we can translate the actual user-facing strings
criteriaCombo->addItem(QString(), "name");
criteriaCombo->addItem(QString(), "type");
criteriaCombo->addItem(QString(), "subtype");
criteriaCombo->addItem(QString(), "cmc");
controlLayout->addWidget(criteriaCombo);
exactnessCombo = new QComboBox(this);
exactnessCombo->addItem(QString(), true); // At least
exactnessCombo->addItem(QString(), false); // Exactly
controlLayout->addWidget(exactnessCombo);
quantitySpin = new QSpinBox(this);
quantitySpin->setRange(1, 60);
controlLayout->addWidget(quantitySpin);
labelMiddle = new QLabel(this);
controlLayout->addWidget(labelMiddle);
drawnSpin = new QSpinBox(this);
drawnSpin->setRange(1, 60);
drawnSpin->setValue(7);
controlLayout->addWidget(drawnSpin);
labelSuffix = new QLabel(this);
controlLayout->addWidget(labelSuffix);
labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
controlLayout->addStretch(1);
layout->addWidget(controls);
// Table
resultTable = new QTableWidget(this);
resultTable->setColumnCount(3);
resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
layout->addWidget(resultTable);
// Connections
connect(criteriaCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
config.criteria = criteriaCombo->currentData().toString();
updateDisplay();
});
connect(exactnessCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
config.atLeast = exactnessCombo->currentData().toBool();
updateDisplay();
});
connect(quantitySpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int v) {
config.quantity = v;
updateDisplay();
});
connect(drawnSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int v) {
config.drawn = v;
updateDisplay();
});
retranslateUi();
applyConfigToToolbar();
updateFilterOptions();
}
void DrawProbabilityWidget::retranslateUi()
{
bannerWidget->setText(tr("Draw Probability"));
labelPrefix->setText(tr("Probability of drawing"));
criteriaCombo->setItemText(0, tr("Card Name"));
criteriaCombo->setItemText(1, tr("Type"));
criteriaCombo->setItemText(2, tr("Subtype"));
criteriaCombo->setItemText(3, tr("Mana Value"));
exactnessCombo->setItemText(0, tr("At least"));
exactnessCombo->setItemText(1, tr("Exactly"));
labelMiddle->setText(tr("card(s) having drawn at least"));
labelSuffix->setText(tr("cards"));
resultTable->setHorizontalHeaderLabels({tr("Category"), tr("Qty"), tr("Odds (%)")});
}
QDialog *DrawProbabilityWidget::createConfigDialog(QWidget *parent)
{
auto *dlg = new DrawProbabilityConfigDialog(parent);
dlg->setFromConfig(config);
return dlg;
}
QJsonObject DrawProbabilityWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *dp = qobject_cast<DrawProbabilityConfigDialog *>(dlg);
return dp ? dp->result().toJson() : QJsonObject{};
}
void DrawProbabilityWidget::applyConfigToToolbar()
{
auto setComboByData = [](QComboBox *combo, const QVariant &value) {
int idx = combo->findData(value);
if (idx >= 0) {
combo->setCurrentIndex(idx);
}
};
setComboByData(criteriaCombo, config.criteria);
setComboByData(exactnessCombo, config.atLeast);
quantitySpin->setValue(config.quantity);
drawnSpin->setValue(config.drawn);
}
void DrawProbabilityWidget::updateDisplay()
{
updateFilterOptions();
}
void DrawProbabilityWidget::loadConfig(const QJsonObject &cfg)
{
config = DrawProbabilityConfig::fromJson(cfg);
applyConfigToToolbar();
updateFilterOptions();
}
void DrawProbabilityWidget::updateFilterOptions()
{
if (!analyzer->getModel()->getDeckList()) {
return;
}
const QString criteria = config.criteria;
const bool atLeast = config.atLeast;
const int quantity = config.quantity;
const int drawn = config.drawn;
QMap<QString, int> categoryCounts;
int totalDeckCards = 0;
const auto nodes = analyzer->getModel()->getDeckList()->getCardNodes();
for (auto *node : nodes) {
CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr();
if (!info) {
continue;
}
totalDeckCards += node->getNumber();
QStringList categories;
if (criteria == "name") {
categories << info->getName();
} else if (criteria == "type") {
categories = info->getMainCardType().split(' ', Qt::SkipEmptyParts);
} else if (criteria == "subtype") {
categories = info->getCardType().split(' ', Qt::SkipEmptyParts);
} else if (criteria == "cmc") {
categories << QString::number(info->getCmc().toInt());
}
for (const QString &cat : categories) {
categoryCounts[cat] += node->getNumber();
}
}
resultTable->setRowCount(categoryCounts.size());
int row = 0;
for (auto it = categoryCounts.cbegin(); it != categoryCounts.cend(); ++it, ++row) {
const QString &cat = it.key();
const int copies = it.value();
double probability = 0.0;
if (atLeast) {
for (int k = quantity; k <= drawn && k <= copies; ++k) {
probability += hypergeometricProbability(totalDeckCards, copies, drawn, k);
}
} else {
probability = hypergeometricProbability(totalDeckCards, copies, drawn, quantity);
}
resultTable->setItem(row, 0, new QTableWidgetItem(cat));
resultTable->setItem(row, 1, new QTableWidgetItem(QString::number(copies)));
resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2)));
}
}
double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k)
{
if (k < 0 || k > n || K > N || n > N) {
return 0.0;
}
double logP = 0.0;
for (int i = 1; i <= k; ++i) {
logP += qLn(double(K - k + i) / i);
}
for (int i = 1; i <= n - k; ++i) {
logP += qLn(double(N - K - (n - k) + i) / i);
}
for (int i = 1; i <= n; ++i) {
logP -= qLn(double(N - n + i) / i);
}
return qExp(logP);
}

View file

@ -0,0 +1,54 @@
#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H
#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H
#include "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "draw_probability_config.h"
#include <QComboBox>
#include <QLineEdit>
#include <QSpinBox>
#include <QTableWidget>
class DrawProbabilityWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public:
DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
QDialog *createConfigDialog(QWidget *parent) override;
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
void applyConfigToToolbar();
public slots:
void updateDisplay() override;
void loadConfig(const QJsonObject &cfg) override;
void retranslateUi();
private slots:
void updateFilterOptions();
private:
DrawProbabilityConfig config;
QWidget *controls;
QHBoxLayout *controlLayout;
QLabel *labelPrefix;
QLabel *labelMiddle;
QLabel *labelSuffix;
QLineEdit *cardNameEdit;
QComboBox *criteriaCombo; // Card Name / Type / Subtype / Mana Value
QComboBox *filterCombo; // The actual value
QComboBox *exactnessCombo; // At least / Exactly
QSpinBox *quantitySpin; // N
QSpinBox *drawnSpin; // M
QSpinBox *manaValueSpin;
QTableWidget *resultTable;
double hypergeometricProbability(int N, int K, int n, int k);
double calculateProbability(int totalCards, int copies, int drawn, bool atLeast);
};
#endif // COCKATRICE_DRAW_PROBABILITY_WIDGET_H

View file

@ -0,0 +1,32 @@
#include "mana_base_config.h"
QJsonObject ManaBaseConfig::toJson() const
{
QJsonObject jsonObject;
QJsonArray jsonArray;
jsonObject["displayType"] = displayType;
for (auto &filter : filters) {
jsonArray.append(filter);
}
jsonObject["filters"] = jsonArray;
return jsonObject;
}
ManaBaseConfig ManaBaseConfig::fromJson(const QJsonObject &o)
{
ManaBaseConfig config;
if (o.contains("displayType")) {
config.displayType = o["displayType"].toString();
}
if (o.contains("filters")) {
config.filters.clear();
for (auto v : o["filters"].toArray()) {
config.filters << v.toString();
}
}
return config;
}

View file

@ -0,0 +1,19 @@
#ifndef COCKATRICE_MANA_BASE_CONFIG_H
#define COCKATRICE_MANA_BASE_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QStringList>
struct ManaBaseConfig
{
QString displayType; // "pie" or "bar" or "combinedBar"
QStringList filters; // which colors to show, empty = all
QJsonObject toJson() const;
static ManaBaseConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_BASE_CONFIG_H

View file

@ -0,0 +1,67 @@
#include "mana_base_config_dialog.h"
#include <QPushButton>
ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer,
ManaBaseConfig initial,
QWidget *parent)
: QDialog(parent), config(initial)
{
layout = new QVBoxLayout(this);
displayTypeLabel = new QLabel(this);
layout->addWidget(displayTypeLabel);
displayType = new QComboBox(this);
layout->addWidget(displayType);
filterLabel = new QLabel(this);
layout->addWidget(filterLabel);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
layout->addWidget(filterList);
QStringList colors = analyzer->getManaBase().keys();
colors.sort();
filterList->addItems(colors);
// select initial filters
for (int i = 0; i < filterList->count(); ++i) {
if (config.filters.contains(filterList->item(i)->text()))
filterList->item(i)->setSelected(true);
}
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &ManaBaseConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaBaseConfigDialog::reject);
retranslateUi();
}
void ManaBaseConfigDialog::retranslateUi()
{
setWindowTitle(tr("Mana Base Configuration"));
displayTypeLabel->setText(tr("Display type:"));
displayType->clear();
displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")});
filterLabel->setText(tr("Filter Colors (optional):"));
buttons->button(QDialogButtonBox::Ok)->setText(tr("OK"));
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
}
void ManaBaseConfigDialog::accept()
{
config.displayType = displayType->currentText();
config.filters.clear();
for (auto *item : filterList->selectedItems()) {
config.filters << item->text();
}
QDialog::accept();
}

View file

@ -0,0 +1,42 @@
#ifndef COCKATRICE_MANA_BASE_ADD_DIALOG_H
#define COCKATRICE_MANA_BASE_ADD_DIALOG_H
#include "../../deck_list_statistics_analyzer.h"
#include "mana_base_config.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
class ManaBaseConfigDialog : public QDialog
{
Q_OBJECT
public:
ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig initial = {}, QWidget *parent = nullptr);
void retranslateUi();
void accept() override;
ManaBaseConfig result() const
{
return config;
}
private:
ManaBaseConfig config;
QVBoxLayout *layout;
QLabel *displayTypeLabel;
QComboBox *displayType;
QLabel *filterLabel;
QListWidget *filterList;
QDialogButtonBox *buttons;
};
#endif // COCKATRICE_MANA_BASE_ADD_DIALOG_H

View file

@ -0,0 +1,115 @@
#include "mana_base_widget.h"
#include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
#include "../../analytics_panel_widget_registrar.h"
#include "mana_base_config_dialog.h"
#include <QDialog>
#include <QListWidget>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaBase{
"manaBase", ManaBaseWidget::tr("Mana Base"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }};
} // anonymous namespace
ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg)
: AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg))
{
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
layout->addWidget(barContainer);
updateDisplay();
}
void ManaBaseWidget::updateDisplay()
{
// Clear previous widgets
while (QLayoutItem *item = barLayout->takeAt(0)) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
auto &pipCount = analyzer->getProductionPipCount();
auto &cardCount = analyzer->getProductionCardCount();
QHash<QString, int> manaMap;
for (auto key : pipCount.keys()) {
manaMap[key] = pipCount[key];
}
// Apply filters
if (!config.filters.isEmpty()) {
QHash<QString, int> filtered;
for (auto f : config.filters) {
if (manaMap.contains(f)) {
filtered[f] = manaMap[f];
}
}
manaMap = filtered;
}
// Determine maximum for bar charts
int highest = 1;
for (auto val : manaMap) {
highest = std::max(highest, val);
}
// Convert to QMap for ColorBar / ColorPie (sorted)
QMap<QString, int> mapSorted;
for (auto it = manaMap.begin(); it != manaMap.end(); ++it) {
mapSorted.insert(it.key(), it.value());
}
// Choose display mode
if (config.displayType == "bar") {
QHash<QString, QColor> colors = {{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)},
{"B", QColor(21, 11, 0)}, {"R", QColor(211, 32, 42)},
{"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)}};
for (auto color : manaMap.keys()) {
QString label = QString("%1 %2 (%3)").arg(color).arg(manaMap[color]).arg(cardCount.value(color));
BarWidget *bar = new BarWidget(label, manaMap[color], highest, colors.value(color, Qt::gray), this);
barLayout->addWidget(bar);
}
} else if (config.displayType == "combinedBar") {
ColorBar *cb = new ColorBar(mapSorted, this);
cb->setMinimumHeight(30);
barLayout->addWidget(cb);
} else if (config.displayType == "pie") {
ColorPie *pie = new ColorPie(mapSorted, this);
pie->setMinimumSize(200, 200);
barLayout->addWidget(pie);
}
update();
}
QSize ManaBaseWidget::sizeHint() const
{
return QSize(800, 150);
}
QDialog *ManaBaseWidget::createConfigDialog(QWidget *parent)
{
ManaBaseConfigDialog *dlg = new ManaBaseConfigDialog(analyzer, config, parent);
return dlg;
}
QJsonObject ManaBaseWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *mc = qobject_cast<ManaBaseConfigDialog *>(dlg);
if (!mc) {
return {};
}
return mc->result().toJson();
}

View file

@ -0,0 +1,51 @@
/**
* @file mana_base_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_BASE_WIDGET_H
#define MANA_BASE_WIDGET_H
#include "../../../general/display/banner_widget.h"
#include "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "mana_base_config.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
class ManaBaseWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaBaseConfig::fromJson(o);
updateDisplay();
}
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaBaseConfig config;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_BASE_WIDGET_H

View file

@ -0,0 +1,121 @@
#include "mana_curve_category_widget.h"
#include "libcockatrice/utility/color.h"
#include "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config.h"
#include "mana_curve_total_widget.h"
constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row
ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent)
{
layout = new QVBoxLayout(this);
layout->setSpacing(4);
layout->setContentsMargins(0, 0, 0, 0);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
}
// Same as minimum for now
QSize ManaCurveCategoryWidget::sizeHint() const
{
if (layout->isEmpty()) {
return QSize(0, 0);
}
// Calculate exact height needed for all rows
int rowCount = layout->count();
int totalHeight = rowCount * MIN_ROW_HEIGHT;
totalHeight += (rowCount - 1) * layout->spacing();
return QSize(0, totalHeight);
}
QSize ManaCurveCategoryWidget::minimumSizeHint() const
{
if (layout->isEmpty()) {
return QSize(0, 0);
}
// Calculate actual minimum based on number of rows
int rowCount = layout->count();
int totalHeight = rowCount * MIN_ROW_HEIGHT;
totalHeight += (rowCount - 1) * layout->spacing();
return QSize(0, totalHeight);
}
void ManaCurveCategoryWidget::updateDisplay(int minCmc,
int maxCmc,
int highest,
QHash<QString, QHash<int, int>> qCategoryCounts,
QHash<QString, QHash<int, QStringList>> qCategoryCards,
const ManaCurveConfig &config)
{
// Clear previous content
QtUtils::clearLayoutRec(layout);
if (!config.showCategoryRows) {
return; // nothing to show
}
// Collect categories
QStringList categories = qCategoryCounts.keys();
// Apply filters
if (!config.filters.isEmpty()) {
QStringList filtered;
for (const QString &cat : categories) {
if (config.filters.contains(cat)) {
filtered.append(cat);
}
}
categories = filtered;
}
std::sort(categories.begin(), categories.end());
for (const QString &cat : categories) {
QWidget *row = new QWidget(this);
row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
row->setFixedHeight(MIN_ROW_HEIGHT);
QHBoxLayout *rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
rowLayout->setSpacing(4);
QLabel *categoryLabel = new QLabel(cat, row);
categoryLabel->setFixedWidth(80);
rowLayout->addWidget(categoryLabel);
QVector<BarData> catBars;
const auto cmcCounts = qCategoryCounts.value(cat);
const auto cmcCards = qCategoryCards.value(cat);
for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
int val = cmcCounts.value(cmc, 0);
QStringList cards = cmcCards.value(cmc);
QVector<BarSegment> segments;
if (val > 0) {
segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)});
}
catBars.push_back({QString::number(cmc), segments});
}
auto *catChart = new BarChartWidget(row);
catChart->setHighest(highest);
catChart->setBars(catBars);
catChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
rowLayout->addWidget(catChart);
layout->addWidget(row);
}
// Update geometry after adding all widgets
updateGeometry();
}

View file

@ -0,0 +1,32 @@
#ifndef COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
#define COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "mana_curve_config.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class ManaCurveCategoryWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCurveCategoryWidget(QWidget *parent);
void updateDisplay(int minCmc,
int maxCmc,
int highest,
QHash<QString, QHash<int, int>> qCategoryCounts,
QHash<QString, QHash<int, QStringList>> qCategoryCards,
const ManaCurveConfig &config);
public slots:
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
private:
QVBoxLayout *layout;
};
#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H

View file

@ -0,0 +1,41 @@
#include "mana_curve_config.h"
QJsonObject ManaCurveConfig::toJson() const
{
QJsonObject jsonObject;
jsonObject["groupBy"] = groupBy;
QJsonArray jsonArray;
for (auto &filter : filters) {
jsonArray.append(filter);
}
jsonObject["filters"] = jsonArray;
jsonObject["showMain"] = showMain;
jsonObject["showCategoryRows"] = showCategoryRows;
return jsonObject;
}
ManaCurveConfig ManaCurveConfig::fromJson(const QJsonObject &o)
{
ManaCurveConfig config;
if (o.contains("groupBy")) {
config.groupBy = o["groupBy"].toString();
}
if (o.contains("filters")) {
config.filters.clear();
for (auto v : o["filters"].toArray()) {
config.filters << v.toString();
}
}
if (o.contains("showMain")) {
config.showMain = o["showMain"].toBool(true);
}
if (o.contains("showCategoryRows")) {
config.showCategoryRows = o["showCategoryRows"].toBool(true);
}
return config;
}

View file

@ -0,0 +1,21 @@
#ifndef COCKATRICE_MANA_CURVE_CONFIG_H
#define COCKATRICE_MANA_CURVE_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QStringList>
struct ManaCurveConfig
{
QString groupBy = "type"; // "type", "color", "subtype", etc.
QStringList filters; // empty = all
bool showMain = true;
bool showCategoryRows = true;
QJsonObject toJson() const;
static ManaCurveConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_CURVE_CONFIG_H

View file

@ -0,0 +1,91 @@
#include "mana_curve_config_dialog.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
ManaCurveConfigDialog::ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
: QDialog(parent), analyzer(analyzer)
{
auto *lay = new QVBoxLayout(this);
labelGroupBy = new QLabel(this);
lay->addWidget(labelGroupBy);
groupBy = new QComboBox(this);
lay->addWidget(groupBy);
labelFilters = new QLabel(this);
lay->addWidget(labelFilters);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
lay->addWidget(filterList);
showMain = new QCheckBox(this);
showMain->setChecked(true);
lay->addWidget(showMain);
showCatRows = new QCheckBox(this);
showCatRows->setChecked(true);
lay->addWidget(showCatRows);
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
lay->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &ManaCurveConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaCurveConfigDialog::reject);
// populate dynamic data
QStringList cats = analyzer->getManaCurveByType().keys();
cats.append(analyzer->getManaCurveByColor().keys());
cats.removeDuplicates();
cats.sort();
filterList->addItems(cats);
groupBy->addItems({"type", "color", "subtype", "power", "toughness"});
retranslateUi();
}
void ManaCurveConfigDialog::retranslateUi()
{
labelGroupBy->setText(tr("Group By:"));
groupBy->setItemText(0, tr("type"));
groupBy->setItemText(1, tr("color"));
groupBy->setItemText(2, tr("subtype"));
groupBy->setItemText(3, tr("power"));
groupBy->setItemText(4, tr("toughness"));
labelFilters->setText(tr("Filters (optional):"));
showMain->setText(tr("Show main bar row"));
showCatRows->setText(tr("Show per-category rows"));
}
void ManaCurveConfigDialog::setFromConfig(const ManaCurveConfig &cfg)
{
groupBy->setCurrentText(cfg.groupBy);
// restore filters
for (int i = 0; i < filterList->count(); ++i)
filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text()));
showMain->setChecked(cfg.showMain);
showCatRows->setChecked(cfg.showCategoryRows);
}
void ManaCurveConfigDialog::accept()
{
cfg.groupBy = groupBy->currentText();
cfg.filters.clear();
for (auto *item : filterList->selectedItems())
cfg.filters << item->text();
cfg.showMain = showMain->isChecked();
cfg.showCategoryRows = showCatRows->isChecked();
QDialog::accept();
}

View file

@ -0,0 +1,44 @@
#ifndef COCKATRICE_MANA_CURVE_ADD_DIALOG_H
#define COCKATRICE_MANA_CURVE_ADD_DIALOG_H
#include "../../deck_list_statistics_analyzer.h"
#include "mana_curve_config.h"
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
class QListWidget;
class QCheckBox;
class QComboBox;
class ManaCurveConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr);
void retranslateUi();
void setFromConfig(const ManaCurveConfig &cfg);
ManaCurveConfig result() const
{
return cfg;
}
private:
ManaCurveConfig cfg;
DeckListStatisticsAnalyzer *analyzer;
QLabel *labelGroupBy;
QComboBox *groupBy;
QLabel *labelFilters;
QListWidget *filterList;
QDialogButtonBox *buttons;
QCheckBox *showMain;
QCheckBox *showCatRows;
private slots:
void accept() override;
};
#endif // COCKATRICE_MANA_CURVE_ADD_DIALOG_H

View file

@ -0,0 +1,78 @@
#include "mana_curve_total_widget.h"
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "libcockatrice/utility/color.h"
#include "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config.h"
#include <QHBoxLayout>
ManaCurveTotalWidget::ManaCurveTotalWidget(QWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
label = new QLabel(this);
label->setFixedWidth(80);
layout->addWidget(label);
barChart = new BarChartWidget(this);
layout->addWidget(barChart, 1);
setMinimumHeight(200);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}
QSize ManaCurveTotalWidget::sizeHint() const
{
return {0, 280};
}
QSize ManaCurveTotalWidget::minimumSizeHint() const
{
return {0, 200};
}
void ManaCurveTotalWidget::updateDisplay(const QString &categoryName,
int minCmc,
int maxCmc,
int highest,
const QMap<int, QMap<QString, int>> &cmcMap,
const QMap<QString, QMap<int, QStringList>> &cardsMap,
const ManaCurveConfig &config)
{
QVector<BarData> mainBars;
mainBars.reserve(maxCmc - minCmc + 1);
for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
QVector<BarSegment> segments;
const auto cmcIt = cmcMap.constFind(cmc);
if (cmcIt != cmcMap.cend()) {
for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) {
const QString &category = it.key();
if (!config.filters.isEmpty() && !config.filters.contains(category))
continue;
const int value = it.value();
QStringList cards;
const auto catIt = cardsMap.constFind(category);
if (catIt != cardsMap.cend())
cards = catIt->value(cmc);
segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)});
}
}
std::sort(segments.begin(), segments.end(),
[](const BarSegment &a, const BarSegment &b) { return a.category < b.category; });
mainBars.push_back({QString::number(cmc), segments});
}
label->setText(categoryName);
barChart->setHighest(highest);
barChart->setBars(mainBars);
}

View file

@ -0,0 +1,32 @@
#ifndef COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H
#define COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "mana_curve_config.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class ManaCurveTotalWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCurveTotalWidget(QWidget *parent);
QSize sizeHint() const;
QSize minimumSizeHint() const;
void updateDisplay(const QString &categoryName,
int minCmc,
int maxCmc,
int highest,
const QMap<int, QMap<QString, int>> &cmcMap,
const QMap<QString, QMap<int, QStringList>> &cardsMap,
const ManaCurveConfig &config);
private:
QHBoxLayout *layout;
QLabel *label;
BarChartWidget *barChart;
};
#endif // COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H

View file

@ -0,0 +1,148 @@
#include "mana_curve_widget.h"
#include "../../../general/display/charts/bars/bar_chart_background_widget.h"
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "../../../general/display/charts/bars/segmented_bar_widget.h"
#include "../../analytics_panel_widget_registrar.h"
#include "../../deck_list_statistics_analyzer.h"
#include "libcockatrice/utility/color.h"
#include "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config_dialog.h"
#include <QInputDialog>
#include <QJsonArray>
#include <QLabel>
#include <QPushButton>
#include <QSettings>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaCurve{
"manaCurve", ManaCurveWidget::tr("Mana Curve"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }};
} // anonymous namespace
ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg)
: AbstractAnalyticsPanelWidget(parent, analyzer), config(cfg)
{
setLayout(layout);
totalWidget = new ManaCurveTotalWidget(this);
totalWidget->setHidden(true);
layout->addWidget(totalWidget);
categoryWidget = new ManaCurveCategoryWidget(this);
categoryWidget->setHidden(true);
layout->addWidget(categoryWidget);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay);
updateDisplay();
}
QDialog *ManaCurveWidget::createConfigDialog(QWidget *parent)
{
auto *dlg = new ManaCurveConfigDialog(analyzer, parent);
dlg->setFromConfig(config);
return dlg;
}
QJsonObject ManaCurveWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *mc = qobject_cast<ManaCurveConfigDialog *>(dlg);
return mc ? mc->result().toJson() : QJsonObject{};
}
static void buildMapsByCategory(const QHash<QString, QHash<int, int>> &categoryCounts,
const QHash<QString, QHash<int, QStringList>> &categoryCards,
QMap<int, QMap<QString, int>> &outCmcMap,
QMap<QString, QMap<int, QStringList>> &outCardsMap)
{
outCmcMap.clear();
outCardsMap.clear();
for (auto catIt = categoryCounts.cbegin(); catIt != categoryCounts.cend(); ++catIt) {
const QString &category = catIt.key();
const auto &countsByCmc = catIt.value();
for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it)
outCmcMap[it.key()][category] = it.value();
}
for (auto catIt = categoryCards.cbegin(); catIt != categoryCards.cend(); ++catIt) {
const QString &category = catIt.key();
const auto &cardsByCmc = catIt.value();
for (auto it = cardsByCmc.cbegin(); it != cardsByCmc.cend(); ++it)
outCardsMap[category][it.key()] = it.value();
}
}
static void findGlobalCmcRange(const QHash<QString, QHash<int, int>> &categoryCounts, int &minCmc, int &maxCmc)
{
minCmc = 0;
maxCmc = 0;
for (const auto &countsByCmc : categoryCounts) {
for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it)
maxCmc = qMax(maxCmc, it.key());
}
}
void ManaCurveWidget::updateDisplay()
{
QHash<QString, QHash<int, int>> categoryCounts;
QHash<QString, QHash<int, QStringList>> categoryCards;
if (config.groupBy == "color") {
categoryCounts = analyzer->getManaCurveByColor();
categoryCards = analyzer->getManaCurveCardsByColor();
} else if (config.groupBy == "subtype") {
categoryCounts = analyzer->getManaCurveBySubtype();
categoryCards = analyzer->getManaCurveCardsBySubtype();
} else if (config.groupBy == "power") {
categoryCounts = analyzer->getManaCurveByPower();
categoryCards = analyzer->getManaCurveCardsByPower();
} else {
categoryCounts = analyzer->getManaCurveByType();
categoryCards = analyzer->getManaCurveCardsByType();
}
QMap<int, QMap<QString, int>> cmcMap;
QMap<QString, QMap<int, QStringList>> cardsMap;
buildMapsByCategory(categoryCounts, categoryCards, cmcMap, cardsMap);
int minCmc = 0;
int maxCmc = 0;
findGlobalCmcRange(categoryCounts, minCmc, maxCmc);
int highest = 1;
for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
int sum = 0;
const auto cmcIt = cmcMap.constFind(cmc);
if (cmcIt != cmcMap.cend()) {
for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) {
if (!config.filters.isEmpty() && !config.filters.contains(it.key())) {
continue;
}
sum += it.value();
}
}
highest = qMax(highest, sum);
}
totalWidget->updateDisplay(config.groupBy, minCmc, maxCmc, highest, cmcMap, cardsMap, config);
totalWidget->setVisible(config.showMain);
categoryWidget->updateDisplay(minCmc, maxCmc, highest, categoryCounts, categoryCards, config);
categoryWidget->setVisible(config.showCategoryRows);
}

View file

@ -0,0 +1,50 @@
/**
* @file mana_curve_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_CURVE_WIDGET_H
#define MANA_CURVE_WIDGET_H
#include "../../abstract_analytics_panel_widget.h"
#include "mana_curve_category_widget.h"
#include "mana_curve_config.h"
#include "mana_curve_total_widget.h"
#include <QVBoxLayout>
class SegmentedBarWidget;
class DeckListStatisticsAnalyzer;
class ManaCurveWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
// QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaCurveConfig::fromJson(o);
updateDisplay();
};
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaCurveConfig config;
ManaCurveTotalWidget *totalWidget;
ManaCurveCategoryWidget *categoryWidget;
};
#endif // MANA_CURVE_WIDGET_H

View file

@ -0,0 +1,31 @@
#include "mana_devotion_config.h"
QJsonObject ManaDevotionConfig::toJson() const
{
QJsonObject jsonObject;
QJsonArray jsonArray;
jsonObject["displayType"] = displayType;
for (auto &filter : filters) {
jsonArray.append(filter);
}
jsonObject["filters"] = jsonArray;
return jsonObject;
}
ManaDevotionConfig ManaDevotionConfig::fromJson(const QJsonObject &o)
{
ManaDevotionConfig config;
if (o.contains("displayType")) {
config.displayType = o["displayType"].toString();
}
if (o.contains("filters")) {
config.filters.clear();
for (auto v : o["filters"].toArray()) {
config.filters << v.toString();
}
}
return config;
}

View file

@ -0,0 +1,18 @@
#ifndef COCKATRICE_MANA_DEVOTION_CONFIG_H
#define COCKATRICE_MANA_DEVOTION_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QStringList>
struct ManaDevotionConfig
{
QString displayType; // "pie" or "bar" or "combinedBar"
QStringList filters; // which colors to show, empty = all
QJsonObject toJson() const;
static ManaDevotionConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_DEVOTION_CONFIG_H

View file

@ -0,0 +1,62 @@
#include "mana_devotion_config_dialog.h"
ManaDevotionConfigDialog::ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer,
ManaDevotionConfig initial,
QWidget *parent)
: QDialog(parent), config(initial)
{
layout = new QVBoxLayout(this);
labelDisplayType = new QLabel(this);
layout->addWidget(labelDisplayType);
displayType = new QComboBox(this);
layout->addWidget(displayType);
labelFilters = new QLabel(this);
layout->addWidget(labelFilters);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
QStringList colors = analyzer->getDevotionPipCount().keys();
colors.sort();
filterList->addItems(colors);
layout->addWidget(filterList);
// select initial filters
for (int i = 0; i < filterList->count(); ++i) {
if (config.filters.contains(filterList->item(i)->text()))
filterList->item(i)->setSelected(true);
}
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &ManaDevotionConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaDevotionConfigDialog::reject);
// populate combo box items
displayType->addItems({"pie", "bar", "combinedBar"});
retranslateUi();
}
void ManaDevotionConfigDialog::retranslateUi()
{
labelDisplayType->setText(tr("Display type:"));
displayType->setItemText(0, tr("pie"));
displayType->setItemText(1, tr("bar"));
displayType->setItemText(2, tr("combinedBar"));
labelFilters->setText(tr("Filter Colors (optional):"));
}
void ManaDevotionConfigDialog::accept()
{
config.displayType = displayType->currentText();
config.filters.clear();
for (auto *item : filterList->selectedItems()) {
config.filters << item->text();
}
QDialog::accept();
}

View file

@ -0,0 +1,42 @@
#ifndef COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H
#define COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H
#include "../../deck_list_statistics_analyzer.h"
#include "mana_devotion_config.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
class ManaDevotionConfigDialog : public QDialog
{
Q_OBJECT
public:
ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer,
ManaDevotionConfig initial = {},
QWidget *parent = nullptr);
void retranslateUi();
void accept() override;
ManaDevotionConfig result() const
{
return config;
}
private:
ManaDevotionConfig config;
QVBoxLayout *layout;
QLabel *labelDisplayType;
QComboBox *displayType;
QLabel *labelFilters;
QListWidget *filterList;
QDialogButtonBox *buttons;
};
#endif // COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H

View file

@ -0,0 +1,123 @@
#include "mana_devotion_widget.h"
#include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
#include "../../analytics_panel_widget_registrar.h"
#include "../../deck_list_statistics_analyzer.h"
#include "mana_devotion_config_dialog.h"
#include <QHash>
#include <QInputDialog>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaDevotion{
"manaDevotion", ManaDevotionWidget::tr("Mana Devotion"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }};
} // anonymous namespace
ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg)
: AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg))
{
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
barContainer->setLayout(barLayout);
layout->addWidget(barContainer);
updateDisplay();
}
void ManaDevotionWidget::updateDisplay()
{
// Clear previous widgets
while (QLayoutItem *item = barLayout->takeAt(0)) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
auto &pipCount = analyzer->getDevotionPipCount();
auto &cardCount = analyzer->getDevotionCardCount();
// Convert keys to single QChar form
QHash<QChar, int> devoMap;
for (auto key : pipCount.keys()) {
devoMap[key[0]] = pipCount[key];
}
// Apply filters
if (!config.filters.isEmpty()) {
QHash<QChar, int> filtered;
for (auto f : config.filters) {
if (devoMap.contains(f[0])) {
filtered[f[0]] = devoMap[f[0]];
}
}
devoMap = filtered;
}
// Determine maximum for bar charts
int highest = 1;
for (auto val : devoMap) {
highest = std::max(highest, val);
}
// Convert to QMap<QString,int> for ColorBar / ColorPie
QMap<QString, int> mapSorted;
for (auto it = devoMap.begin(); it != devoMap.end(); ++it) {
mapSorted.insert(QString(it.key()), it.value());
}
// Color map
QHash<QChar, QColor> colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)},
{'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)},
{'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}};
// Choose display mode
if (config.displayType == "bar") {
// One BarWidget per devotion color
for (auto c : devoMap.keys()) {
QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c)));
BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this);
barLayout->addWidget(bar);
}
} else if (config.displayType == "combinedBar") {
// Stacked devotion bar
ColorBar *cb = new ColorBar(mapSorted, this);
cb->setMinimumHeight(30);
barLayout->addWidget(cb);
} else if (config.displayType == "pie") {
// Devotion pie chart
ColorPie *pie = new ColorPie(mapSorted, this);
pie->setMinimumSize(200, 200);
barLayout->addWidget(pie);
}
update();
}
QDialog *ManaDevotionWidget::createConfigDialog(QWidget *parent)
{
ManaDevotionConfigDialog *dlg = new ManaDevotionConfigDialog(analyzer, config, parent);
return dlg;
}
QJsonObject ManaDevotionWidget::extractConfigFromDialog(QDialog *dlg) const
{
auto *mc = qobject_cast<ManaDevotionConfigDialog *>(dlg);
if (!mc) {
return {};
}
return mc->result().toJson();
}
QSize ManaDevotionWidget::sizeHint() const
{
return QSize(800, 150);
}

View file

@ -0,0 +1,45 @@
/**
* @file mana_devotion_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_DEVOTION_WIDGET_H
#define MANA_DEVOTION_WIDGET_H
#include "../../../general/display/banner_widget.h"
#include "../../abstract_analytics_panel_widget.h"
#include "mana_devotion_config.h"
#include <QHBoxLayout>
class ManaDevotionWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaDevotionConfig::fromJson(o);
updateDisplay();
}
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaDevotionConfig config;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_DEVOTION_WIDGET_H

View file

@ -0,0 +1,36 @@
#include "mana_distribution_config.h"
QJsonObject ManaDistributionConfig::toJson() const
{
QJsonObject o;
o["displayType"] = displayType;
QJsonArray jsonArray;
for (auto &s : filters) {
jsonArray.append(s);
}
o["filters"] = jsonArray;
o["showColorRows"] = showColorRows;
return o;
}
ManaDistributionConfig ManaDistributionConfig::fromJson(const QJsonObject &o)
{
ManaDistributionConfig config;
if (o.contains("displayType")) {
config.displayType = o["displayType"].toString();
}
if (o.contains("filters")) {
config.filters.clear();
for (auto v : o["filters"].toArray())
config.filters << v.toString();
}
if (o.contains("showColorRows")) {
config.showColorRows = o["showColorRows"].toBool(true);
}
return config;
}

View file

@ -0,0 +1,20 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_CONFIG_H
#define COCKATRICE_MANA_DISTRIBUTION_CONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QStringList>
struct ManaDistributionConfig
{
QString displayType = "pie"; // "pie" or "bar"
QStringList filters; // empty = all colors
bool showColorRows = true;
QJsonObject toJson() const;
static ManaDistributionConfig fromJson(const QJsonObject &o);
};
#endif // COCKATRICE_MANA_DISTRIBUTION_CONFIG_H

View file

@ -0,0 +1,83 @@
#include "mana_distribution_config_dialog.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListWidget>
#include <QVBoxLayout>
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
: QDialog(parent), analyzer(analyzer)
{
auto *lay = new QVBoxLayout(this);
// Labels
labelDisplayType = new QLabel(this);
lay->addWidget(labelDisplayType);
displayType = new QComboBox(this);
lay->addWidget(displayType);
labelFilters = new QLabel(this);
lay->addWidget(labelFilters);
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
filterList->addItems(kColors); // dynamic/fixed, no translation needed
lay->addWidget(filterList);
showColorRows = new QCheckBox(this);
showColorRows->setChecked(true);
lay->addWidget(showColorRows);
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
lay->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &ManaDistributionConfigDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &ManaDistributionConfigDialog::reject);
displayType->addItems({"pie", "bar"}); // combo items
retranslateUi();
}
void ManaDistributionConfigDialog::retranslateUi()
{
labelDisplayType->setText(tr("Top display type:"));
displayType->setItemText(0, tr("pie"));
displayType->setItemText(1, tr("bar"));
labelFilters->setText(tr("Colors:"));
showColorRows->setText(tr("Show per-color rows"));
// QDialogButtonBox buttons are automatically translated
}
void ManaDistributionConfigDialog::setFromConfig(const ManaDistributionConfig &cfgIn)
{
cfg = cfgIn;
displayType->setCurrentText(cfg.displayType);
for (int i = 0; i < filterList->count(); ++i)
filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text()));
showColorRows->setChecked(cfg.showColorRows);
}
void ManaDistributionConfigDialog::accept()
{
cfg.displayType = displayType->currentText();
// Filters
cfg.filters.clear();
for (auto *item : filterList->selectedItems())
cfg.filters << item->text();
cfg.showColorRows = showColorRows->isChecked();
QDialog::accept();
}

View file

@ -0,0 +1,45 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H
#define COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H
#include "mana_distribution_config.h"
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QStringList>
class QComboBox;
class QListWidget;
class QCheckBox;
class DeckListStatisticsAnalyzer;
class ManaDistributionConfigDialog : public QDialog
{
Q_OBJECT
public:
explicit ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr);
void retranslateUi();
void setFromConfig(const ManaDistributionConfig &cfg);
const ManaDistributionConfig &config() const
{
return cfg;
}
public slots:
void accept() override;
private:
DeckListStatisticsAnalyzer *analyzer;
QLabel *labelDisplayType;
QComboBox *displayType;
QLabel *labelFilters;
QListWidget *filterList;
QCheckBox *showColorRows;
QDialogButtonBox *buttons;
ManaDistributionConfig cfg;
};
#endif // COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H

View file

@ -0,0 +1,49 @@
#include "mana_distribution_single_display_widget.h"
#include "../../../cards/additional_info/mana_symbol_widget.h"
#include <QVBoxLayout>
ManaDistributionSingleDisplayWidget::ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent)
: QWidget(parent)
{
auto layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignHCenter);
symbolLabel = new ManaSymbolWidget(this, colorSymbol, true, false);
symbolLabel->setFixedSize(40, 40);
devotionBar = new QProgressBar(this);
devotionBar->setRange(0, 100);
devotionBar->setTextVisible(false);
devotionLabel = new QLabel(this);
devotionLabel->setAlignment(Qt::AlignCenter);
productionBar = new QProgressBar(this);
productionBar->setRange(0, 100);
productionBar->setTextVisible(false);
productionLabel = new QLabel(this);
productionLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(symbolLabel);
layout->addWidget(devotionBar);
layout->addWidget(devotionLabel);
layout->addWidget(productionBar);
layout->addWidget(productionLabel);
setLayout(layout);
}
void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int percent)
{
devotionBar->setValue(percent);
devotionLabel->setText(QString(tr("%1 pips (%2 cards)")).arg(pips).arg(cards));
}
void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent)
{
productionBar->setValue(percent);
productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards));
}

View file

@ -0,0 +1,28 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H
#define COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H
#include <QHBoxLayout>
#include <QLabel>
#include <QProgressBar>
#include <QWidget>
class ManaDistributionSingleDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent = nullptr);
void setDevotion(int pips, int cards, int percent);
void setProduction(int pips, int cards, int percent);
private:
QLabel *symbolLabel;
QProgressBar *devotionBar;
QLabel *devotionLabel;
QProgressBar *productionBar;
QLabel *productionLabel;
};
#endif // COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H

View file

@ -0,0 +1,129 @@
#include "mana_distribution_widget.h"
#include "../../analytics_panel_widget_registrar.h"
#include "mana_distribution_config_dialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QVBoxLayout>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaDistribution{
"manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }};
} // anonymous namespace
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer)
{
container = new QWidget(this);
containerLayout = new QVBoxLayout(container);
devotionBarTop = new ColorBar({}, this);
devotionPieTop = new ColorPie({}, this);
productionBarTop = new ColorBar({}, this);
productionPieTop = new ColorPie({}, this);
containerLayout->addWidget(devotionBarTop);
containerLayout->addWidget(devotionPieTop);
containerLayout->addWidget(productionBarTop);
containerLayout->addWidget(productionPieTop);
devotionPieTop->hide();
productionPieTop->hide();
row = new QHBoxLayout();
containerLayout->addLayout(row);
for (const QString &c : kColors) {
auto *w = new ManaDistributionSingleDisplayWidget(c, this);
row->addWidget(w);
rows[c] = w;
}
layout->addWidget(container);
}
void ManaDistributionWidget::updateDisplay()
{
const auto &devPips = analyzer->getDevotionPipCount();
const auto &devCards = analyzer->getDevotionCardCount();
const auto &prodPips = analyzer->getProductionPipCount();
const auto &prodCards = analyzer->getProductionCardCount();
QStringList filtered = config.filters.isEmpty() ? kColors : config.filters;
QMap<QString, int> devMap, prodMap;
for (const QString &c : filtered) {
devMap[c] = devPips.value(c, 0);
prodMap[c] = prodPips.value(c, 0);
}
bool showPie = (config.displayType == "pie");
devotionBarTop->setVisible(!showPie);
productionBarTop->setVisible(!showPie);
devotionPieTop->setVisible(showPie);
productionPieTop->setVisible(showPie);
if (showPie) {
devotionPieTop->setColors(devMap);
productionPieTop->setColors(prodMap);
} else {
devotionBarTop->setColors(devMap);
productionBarTop->setColors(prodMap);
}
for (const QString &c : kColors) {
auto *w = rows.value(c);
if (!w) {
continue;
}
bool visible = config.showColorRows && filtered.contains(c);
w->setVisible(visible);
if (!visible) {
continue;
}
int dp = devPips.value(c, 0);
int dc = devCards.value(c, 0);
int pp = prodPips.value(c, 0);
int pc = prodCards.value(c, 0);
// Compute percentages
int totalDev = 0;
int totalProd = 0;
for (const QString &cc : filtered) {
totalDev += devPips.value(cc, 0);
totalProd += prodPips.value(cc, 0);
}
int devPct = (totalDev > 0) ? int(100.0 * dp / totalDev) : 0;
int prodPct = (totalProd > 0) ? int(100.0 * pp / totalProd) : 0;
w->setDevotion(dp, dc, devPct);
w->setProduction(pp, pc, prodPct);
}
}
QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent)
{
auto *dlg = new ManaDistributionConfigDialog(analyzer, parent);
dlg->setWindowTitle(tr("Mana Distribution Settings"));
dlg->setFromConfig(config);
connect(dlg, &QDialog::accepted, [this, dlg]() {
config = dlg->config();
updateDisplay();
});
return dlg;
}

View file

@ -0,0 +1,45 @@
#ifndef COCKATRICE_MANA_DISTRIBUTION_WIDGET_H
#define COCKATRICE_MANA_DISTRIBUTION_WIDGET_H
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
#include "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "mana_distribution_config.h"
#include "mana_distribution_single_display_widget.h"
#include <QHBoxLayout>
#include <QMap>
#include <QVBoxLayout>
#include <QWidget>
class ManaDistributionWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public:
explicit ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
QJsonObject extractConfigFromDialog(QDialog *) const override
{
return {};
}
private:
ManaDistributionConfig config;
QWidget *container;
QVBoxLayout *containerLayout;
QVBoxLayout *topLayout;
ColorBar *devotionBarTop;
ColorPie *devotionPieTop;
ColorBar *productionBarTop;
ColorPie *productionPieTop;
QHBoxLayout *row;
QMap<QString, ManaDistributionSingleDisplayWidget *> rows;
};
#endif // COCKATRICE_MANA_DISTRIBUTION_WIDGET_H