diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index c6a29969f..8ebd177db 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -144,11 +144,31 @@ set(cockatrice_SOURCES src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/deck_card_zone_display_widget.cpp src/interface/widgets/cards/deck_preview_card_picture_widget.cpp + src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp + src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp src/interface/widgets/deck_analytics/deck_analytics_widget.cpp src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp - src/interface/widgets/deck_analytics/mana_base_widget.cpp - src/interface/widgets/deck_analytics/mana_curve_widget.cpp - src/interface/widgets/deck_analytics/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/resizable_panel.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -160,13 +180,17 @@ set(cockatrice_SOURCES src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/banner_widget.cpp - src/interface/widgets/general/display/bar_widget.cpp - src/interface/widgets/general/display/color_bar.cpp src/interface/widgets/general/display/dynamic_font_size_label.cpp src/interface/widgets/general/display/dynamic_font_size_push_button.cpp src/interface/widgets/general/display/labeled_input.cpp - src/interface/widgets/general/display/percent_bar_widget.cpp src/interface/widgets/general/display/shadow_background_label.cpp + src/interface/widgets/general/display/charts/bars/bar_widget.cpp + src/interface/widgets/general/display/charts/bars/color_bar.cpp + src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp + src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp + src/interface/widgets/general/display/charts/pies/color_pie.cpp src/interface/widgets/general/home_styled_button.cpp src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp new file mode 100644 index 000000000..bad883d27 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp @@ -0,0 +1,48 @@ +#include "abstract_analytics_panel_widget.h" + +#include "deck_list_statistics_analyzer.h" + +#include + +AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : QWidget(parent), analyzer(analyzer) +{ + layout = new QVBoxLayout(this); + + bannerAndSettingsContainer = new QWidget(this); + + bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer); + bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout); + bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100); + bannerWidget->setMaximumHeight(100); + + bannerAndSettingsLayout->addWidget(bannerWidget, 1); + + // config button + configureButton = new QPushButton(tr("Configure"), this); + configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); + bannerAndSettingsLayout->addWidget(configureButton, 0); + + layout->addWidget(bannerAndSettingsContainer); + + connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay); +} + +bool AbstractAnalyticsPanelWidget::applyConfigFromDialog() +{ + QDialog *dlg = createConfigDialog(this); + if (!dlg) { + return false; + } + + bool ok = dlg->exec() == QDialog::Accepted; + if (ok) { + // dialog must expose its final config as JSON + auto newCfg = extractConfigFromDialog(dlg); + loadConfig(newCfg); + updateDisplay(); + } + dlg->deleteLater(); + return ok; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h new file mode 100644 index 000000000..23374a9e1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h @@ -0,0 +1,61 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H + +#include "../general/display/banner_widget.h" + +#include +#include +#include +#include + +class DeckListStatisticsAnalyzer; + +class AbstractAnalyticsPanelWidget : public QWidget +{ + Q_OBJECT +public slots: + virtual void updateDisplay() = 0; + // Widgets must return a config dialog + virtual QDialog *createConfigDialog(QWidget *parent) = 0; + +public: + explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + void setDisplayTitle(const QString &title) + { + displayTitle = title; + if (bannerWidget) { + bannerWidget->setText(displayTitle); + } + } + + QString displayTitleText() const + { + return displayTitle; + } + + virtual QJsonObject saveConfig() const + { + return {}; + } + virtual void loadConfig(const QJsonObject &) + { + } + + // Unified helper to run config dialog and update widget + bool applyConfigFromDialog(); + + // Dialog → JSON must be supplied by each subclass + virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0; + +protected: + DeckListStatisticsAnalyzer *analyzer; + QVBoxLayout *layout; + QWidget *bannerAndSettingsContainer; + QHBoxLayout *bannerAndSettingsLayout; + QString displayTitle; + BannerWidget *bannerWidget; + QPushButton *configureButton; +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp new file mode 100644 index 000000000..c83e6d982 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp @@ -0,0 +1,32 @@ +#include "add_analytics_panel_dialog.h" + +#include "analytics_panel_widget_factory.h" + +#include +#include + +AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent) +{ + setWindowTitle(tr("Add Analytics Panel")); + + layout = new QVBoxLayout(this); + + typeCombo = new QComboBox(this); + + // Populate using descriptors + const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets(); + + for (const auto &desc : widgets) { + // Show translated title to user + typeCombo->addItem(desc.title, desc.type); + } + + layout->addWidget(typeCombo); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h new file mode 100644 index 000000000..aa44734c2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h @@ -0,0 +1,29 @@ + +#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H +#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H + +#include "analytics_panel_widget_factory.h" + +#include +#include +#include +#include + +class AddAnalyticsPanelDialog : public QDialog +{ + Q_OBJECT +public: + explicit AddAnalyticsPanelDialog(QWidget *parent); + + QString selectedType() const + { + return typeCombo->currentData().toString(); + } + +private: + QVBoxLayout *layout; + QComboBox *typeCombo; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp new file mode 100644 index 000000000..7af641689 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp @@ -0,0 +1,33 @@ +#include "analytics_panel_widget_factory.h" + +#include "abstract_analytics_panel_widget.h" + +AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance() +{ + static AnalyticsPanelWidgetFactory f; + return f; +} + +void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc) +{ + widgets.insert(desc.type, desc); +} + +AbstractAnalyticsPanelWidget * +AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const +{ + auto it = widgets.find(type); + if (it == widgets.end()) + return nullptr; + + auto w = it->creator(parent, analyzer); + + w->setDisplayTitle(it->title); + + return w; +} + +QList AnalyticsPanelWidgetFactory::availableWidgets() const +{ + return widgets.values(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h new file mode 100644 index 000000000..6c5856d70 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H + +#include +#include +#include +#include +#include + +class AbstractAnalyticsPanelWidget; +class DeckListStatisticsAnalyzer; + +class AnalyticsPanelWidgetFactory +{ +public: + using Creator = std::function; + + struct Descriptor + { + QString type; // stable ID ("manaProdDevotion") + QString title; // translated, user-facing + Creator creator; + }; + + static AnalyticsPanelWidgetFactory &instance(); + + // NEW: richer registration + void registerWidget(const Descriptor &desc); + + AbstractAnalyticsPanelWidget * + create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const; + + // NEW: expose widgets to UI + QList availableWidgets() const; + +private: + AnalyticsPanelWidgetFactory() = default; // Ensure private constructor + AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete; + AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete; + + QMap widgets; +}; + +#endif diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp new file mode 100644 index 000000000..d4129a3d0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp @@ -0,0 +1 @@ +#include "analytics_panel_widget_registrar.h" diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h new file mode 100644 index 000000000..70d6df94f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h @@ -0,0 +1,17 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H + +#include "analytics_panel_widget_factory.h" + +class AnalyticsPanelWidgetRegistrar +{ +public: + AnalyticsPanelWidgetRegistrar(const QString &type, + const QString &title, + AnalyticsPanelWidgetFactory::Creator creator) + { + AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator}); + } +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp new file mode 100644 index 000000000..51129a4b4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp @@ -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; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h new file mode 100644 index 000000000..bbe61a68e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H +#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H + +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp new file mode 100644 index 000000000..71f88c0fc --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp @@ -0,0 +1,92 @@ +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include + +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(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h new file mode 100644 index 000000000..44b1f0eec --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h @@ -0,0 +1,44 @@ +#pragma once + +#include "draw_probability_config.h" + +#include +#include + +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; +}; diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp new file mode 100644 index 000000000..a8bec834f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp @@ -0,0 +1,236 @@ +#include "draw_probability_widget.h" + +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::of(&QComboBox::currentIndexChanged), this, [this] { + config.criteria = criteriaCombo->currentData().toString(); + updateDisplay(); + }); + + connect(exactnessCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + config.atLeast = exactnessCombo->currentData().toBool(); + updateDisplay(); + }); + + connect(quantitySpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { + config.quantity = v; + updateDisplay(); + }); + + connect(drawnSpin, QOverload::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(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 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); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h new file mode 100644 index 000000000..80015999f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h @@ -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 +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp new file mode 100644 index 000000000..2f9f60752 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp @@ -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; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h new file mode 100644 index 000000000..d01f88b8b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h @@ -0,0 +1,19 @@ + +#ifndef COCKATRICE_MANA_BASE_CONFIG_H +#define COCKATRICE_MANA_BASE_CONFIG_H + +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp new file mode 100644 index 000000000..3317486ea --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp @@ -0,0 +1,67 @@ +#include "mana_base_config_dialog.h" + +#include + +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(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h new file mode 100644 index 000000000..a816f154f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp new file mode 100644 index 000000000..d38314b8c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp @@ -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 +#include + +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 manaMap; + for (auto key : pipCount.keys()) { + manaMap[key] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash 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 mapSorted; + for (auto it = manaMap.begin(); it != manaMap.end(); ++it) { + mapSorted.insert(it.key(), it.value()); + } + + // Choose display mode + if (config.displayType == "bar") { + QHash 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(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h new file mode 100644 index 000000000..39380c07e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h @@ -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 +#include +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp new file mode 100644 index 000000000..a0ccedc74 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp @@ -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> qCategoryCounts, + QHash> 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 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 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(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h new file mode 100644 index 000000000..c6aa6f1f0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h @@ -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 +#include +#include + +class ManaCurveCategoryWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveCategoryWidget(QWidget *parent); + void updateDisplay(int minCmc, + int maxCmc, + int highest, + QHash> qCategoryCounts, + QHash> qCategoryCards, + const ManaCurveConfig &config); + +public slots: + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +private: + QVBoxLayout *layout; +}; + +#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp new file mode 100644 index 000000000..0dfef1b66 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp @@ -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; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h new file mode 100644 index 000000000..55222dd98 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h @@ -0,0 +1,21 @@ + +#ifndef COCKATRICE_MANA_CURVE_CONFIG_H +#define COCKATRICE_MANA_CURVE_CONFIG_H + +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp new file mode 100644 index 000000000..38199656c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp @@ -0,0 +1,91 @@ +#include "mana_curve_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h new file mode 100644 index 000000000..17e6776e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h @@ -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 +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp new file mode 100644 index 000000000..f059ff873 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp @@ -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 + +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> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config) +{ + QVector mainBars; + mainBars.reserve(maxCmc - minCmc + 1); + + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + QVector 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); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h new file mode 100644 index 000000000..203f75345 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h @@ -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 +#include +#include + +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> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config); + +private: + QHBoxLayout *layout; + QLabel *label; + BarChartWidget *barChart; +}; + +#endif // COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp new file mode 100644 index 000000000..e09ecfe87 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp @@ -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 +#include +#include +#include +#include + +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(dlg); + return mc ? mc->result().toJson() : QJsonObject{}; +} + +static void buildMapsByCategory(const QHash> &categoryCounts, + const QHash> &categoryCards, + QMap> &outCmcMap, + QMap> &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> &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> categoryCounts; + QHash> 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> cmcMap; + QMap> 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); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h new file mode 100644 index 000000000..da59da9a8 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h @@ -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 + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp new file mode 100644 index 000000000..ee57cb1e2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp @@ -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; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h new file mode 100644 index 000000000..08be146e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h @@ -0,0 +1,18 @@ +#ifndef COCKATRICE_MANA_DEVOTION_CONFIG_H +#define COCKATRICE_MANA_DEVOTION_CONFIG_H + +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp new file mode 100644 index 000000000..80fd03928 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp @@ -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(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h new file mode 100644 index 000000000..fd804e2af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h @@ -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 +#include +#include +#include +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp new file mode 100644 index 000000000..709577ff2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp @@ -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 +#include + +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 devoMap; + for (auto key : pipCount.keys()) { + devoMap[key[0]] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash 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 for ColorBar / ColorPie + QMap mapSorted; + for (auto it = devoMap.begin(); it != devoMap.end(); ++it) { + mapSorted.insert(QString(it.key()), it.value()); + } + + // Color map + QHash 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(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} + +QSize ManaDevotionWidget::sizeHint() const +{ + return QSize(800, 150); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h new file mode 100644 index 000000000..833f12938 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h @@ -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 + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp new file mode 100644 index 000000000..f70f32d5b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp @@ -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; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h new file mode 100644 index 000000000..d1aa0f48e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_CONFIG_H +#define COCKATRICE_MANA_DISTRIBUTION_CONFIG_H + +#include +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp new file mode 100644 index 000000000..7fe4d94e4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp @@ -0,0 +1,83 @@ +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +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(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h new file mode 100644 index 000000000..57c88e29d --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H +#define COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H + +#include "mana_distribution_config.h" + +#include +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp new file mode 100644 index 000000000..8a5e14858 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp @@ -0,0 +1,49 @@ +#include "mana_distribution_single_display_widget.h" + +#include "../../../cards/additional_info/mana_symbol_widget.h" + +#include + +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)); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h new file mode 100644 index 000000000..e33d0cec1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H +#define COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H + +#include +#include +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp new file mode 100644 index 000000000..8eaea1426 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp @@ -0,0 +1,129 @@ +#include "mana_distribution_widget.h" + +#include "../../analytics_panel_widget_registrar.h" +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include + +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 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; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h new file mode 100644 index 000000000..2d834f3af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h @@ -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 +#include +#include +#include + +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 rows; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp index 45393bb5d..ea61302f0 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp @@ -1,35 +1,298 @@ #include "deck_analytics_widget.h" -DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel) - : QWidget(parent), deckListModel(_deckListModel) -{ - mainLayout = new QVBoxLayout(); - setLayout(mainLayout); +#include "abstract_analytics_panel_widget.h" +#include "add_analytics_panel_dialog.h" +#include "analytics_panel_widget_factory.h" +#include "analyzer_modules/mana_base/mana_base_config.h" +#include "analyzer_modules/mana_curve/mana_curve_config.h" +#include "analyzer_modules/mana_devotion/mana_devotion_config.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" +#include +#include +#include +#include +#include +#include +#include + +DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer) + : QWidget(parent), statsAnalyzer(_statsAnalyzer) +{ + layout = new QVBoxLayout(this); + + // Controls + controlContainer = new QWidget(this); + controlLayout = new QHBoxLayout(controlContainer); + addButton = new QPushButton(this); + removeButton = new QPushButton(this); + saveButton = new QPushButton(this); + loadButton = new QPushButton(this); + controlLayout->addWidget(addButton); + controlLayout->addWidget(removeButton); + controlLayout->addWidget(saveButton); + controlLayout->addWidget(loadButton); + + layout->addWidget(controlContainer); + + connect(addButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onAddPanel); + connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected); + connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout); + connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout); + + // Scroll area and container scrollArea = new QScrollArea(this); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setWidgetResizable(true); - mainLayout->addWidget(scrollArea); + scrollArea->setFrameShape(QFrame::NoFrame); - container = new QWidget(scrollArea); - containerLayout = new QVBoxLayout(container); - container->setLayout(containerLayout); - scrollArea->setWidget(container); + panelContainer = new QWidget(scrollArea); + panelLayout = new QVBoxLayout(panelContainer); + panelLayout->setSpacing(8); + panelLayout->setContentsMargins(4, 4, 4, 4); + panelLayout->addStretch(1); // push panels up - deckListStatisticsAnalyzer = new DeckListStatisticsAnalyzer(this, deckListModel); + scrollArea->setWidget(panelContainer); + layout->addWidget(scrollArea); - manaCurveWidget = new ManaCurveWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaCurveWidget); + loadLayout(); - manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaDevotionWidget); - - manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaBaseWidget); + retranslateUi(); } -void DeckAnalyticsWidget::refreshDisplays() +void DeckAnalyticsWidget::retranslateUi() { - deckListStatisticsAnalyzer->update(); + addButton->setText(tr("Add Panel")); + removeButton->setText(tr("Remove Panel")); + saveButton->setText(tr("Save Layout")); + loadButton->setText(tr("Load Layout")); } + +void DeckAnalyticsWidget::updateDisplays() +{ + statsAnalyzer->analyze(); +} + +void DeckAnalyticsWidget::onAddPanel() +{ + AddAnalyticsPanelDialog dlg(this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + QString selection = dlg.selectedType(); + if (selection.isEmpty()) { + return; + } + + AbstractAnalyticsPanelWidget *analyticsWidget = + AnalyticsPanelWidgetFactory::instance().create(selection, this, statsAnalyzer); + if (!analyticsWidget) { + return; + } + + if (!analyticsWidget->applyConfigFromDialog()) { + analyticsWidget->deleteLater(); + return; + } + + addPanelInstance(selection, analyticsWidget, analyticsWidget->saveConfig()); +} + +void DeckAnalyticsWidget::addPanelInstance(const QString &typeId, + AbstractAnalyticsPanelWidget *panel, + const QJsonObject &cfg) +{ + panel->loadConfig(cfg); + panel->updateDisplay(); + + auto *resPanel = new ResizablePanel(typeId, panel, panelContainer); + panelWrappers.push_back(resPanel); + + panelLayout->insertWidget(panelLayout->count() - 1, resPanel); + + // Event filter for selection + resPanel->installEventFilter(this); + panel->installEventFilter(this); + + // Connect drag-drop signals + connect(resPanel, &ResizablePanel::dropRequested, this, &DeckAnalyticsWidget::onPanelDropped); +} + +void DeckAnalyticsWidget::onRemoveSelected() +{ + int idx = indexOfSelectedWrapper(); + if (idx < 0) { + return; + } + + ResizablePanel *panel = panelWrappers.takeAt(idx); + selectWrapper(nullptr); + + panel->deleteLater(); +} + +void DeckAnalyticsWidget::saveLayout() +{ + QJsonArray arr; + + for (auto *wrapper : panelWrappers) { + QJsonObject entry; + entry["type"] = wrapper->getTypeId(); + entry["config"] = wrapper->panel->saveConfig(); + entry["height"] = wrapper->getCurrentHeight(); + arr.append(entry); + } + + QSettings s; + s.setValue("deckAnalytics/layout", QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact))); +} + +void DeckAnalyticsWidget::loadLayout() +{ + if (!loadLayoutInternal()) { + addDefaultPanels(); + } +} + +void DeckAnalyticsWidget::addDefaultPanels() +{ + struct DefaultPanel + { + QString type; + QJsonObject cfg; + }; + + // Prepare configs + QJsonObject manaCurveCfg = ManaCurveConfig{}.toJson(); + QJsonObject manaBaseCfg = ManaBaseConfig{"combinedBar", {}}.toJson(); + QJsonObject manaDevotionCfg = ManaDevotionConfig{"combinedBar", {}}.toJson(); + QVector defaults = { + {"manaCurve", manaCurveCfg}, {"manaBase", manaBaseCfg}, {"manaDevotion", manaDevotionCfg}}; + + for (auto &d : defaults) { + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(d.type, this, statsAnalyzer); + if (!w) { + continue; + } + + w->loadConfig(d.cfg); + addPanelInstance(d.type, w, d.cfg); + } +} + +bool DeckAnalyticsWidget::loadLayoutInternal() +{ + QSettings s; + QString layoutData = s.value("deckAnalytics/layout").toString(); + if (layoutData.isEmpty()) { + return false; + } + + QJsonDocument doc = QJsonDocument::fromJson(layoutData.toUtf8()); + if (!doc.isArray()) { + return false; + } + + clearPanels(); + + for (auto v : doc.array()) { + if (!v.isObject()) { + continue; + } + QJsonObject o = v.toObject(); + QString type = o["type"].toString(); + QJsonObject cfg = o["config"].toObject(); + + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(type, this, statsAnalyzer); + if (!w) { + continue; + } + + addPanelInstance(type, w, cfg); + + // Restore height AFTER adding the panel + if (o.contains("height")) { + panelWrappers.last()->setHeightFromSaved(o["height"].toInt()); + } + } + + return true; +} + +void DeckAnalyticsWidget::clearPanels() +{ + selectWrapper(nullptr); + while (!panelWrappers.isEmpty()) { + ResizablePanel *p = panelWrappers.takeLast(); + p->deleteLater(); + } +} + +bool DeckAnalyticsWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress) { + for (auto *p : panelWrappers) { + if (obj == p || obj == p->panel) { + selectWrapper(p); + break; + } + } + } + return QWidget::eventFilter(obj, event); +} + +void DeckAnalyticsWidget::selectWrapper(ResizablePanel *w) +{ + // Same wrapper + if (selectedWrapper == w) { + return; + } + // Deselect the old one + if (selectedWrapper) { + selectedWrapper->setSelected(false); + } + // Set current + selectedWrapper = w; + // Finally, select new + if (selectedWrapper) { + selectedWrapper->setSelected(true); + } +} + +int DeckAnalyticsWidget::indexOfSelectedWrapper() const +{ + if (!selectedWrapper) { + return -1; + } + return panelWrappers.indexOf(selectedWrapper); +} + +void DeckAnalyticsWidget::onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore) +{ + int draggedIdx = panelWrappers.indexOf(dragged); + int targetIdx = panelWrappers.indexOf(target); + + if (draggedIdx == -1 || targetIdx == -1 || draggedIdx == targetIdx) { + return; + } + + // Remove dragged panel from list and layout + panelWrappers.removeAt(draggedIdx); + panelLayout->removeWidget(dragged); + + // Adjust target index if needed + if (draggedIdx < targetIdx) { + targetIdx--; + } + + // Calculate insertion position + int insertIdx = insertBefore ? targetIdx : targetIdx + 1; + + // Insert back into list and layout + panelWrappers.insert(insertIdx, dragged); + panelLayout->insertWidget(insertIdx, dragged); + + // Clear selection + selectWrapper(nullptr); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h index 524362aed..31ee36fbb 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h @@ -1,44 +1,71 @@ /** * @file deck_analytics_widget.h * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. + * @brief Main analytics widget container with resizable panels for deck statistics. */ #ifndef DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H -#include "mana_base_widget.h" -#include "mana_curve_widget.h" -#include "mana_devotion_widget.h" +#include "abstract_analytics_panel_widget.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" -#include +#include #include +#include +#include #include -#include + +class LayoutInspector; class DeckAnalyticsWidget : public QWidget { Q_OBJECT +public slots: + void updateDisplays(); + public: - explicit DeckAnalyticsWidget(QWidget *parent, DeckListModel *deckListModel); - void setDeckList(const DeckList &_deckListModel); - std::map analyzeManaCurve(); - void refreshDisplays(); + explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + void retranslateUi(); + +private slots: + void onAddPanel(); + void onRemoveSelected(); + void onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore); + void saveLayout(); + void loadLayout(); + void addDefaultPanels(); + bool loadLayoutInternal(); + void clearPanels(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void selectWrapper(ResizablePanel *panel); + int indexOfSelectedWrapper() const; private: - DeckListModel *deckListModel; - DeckListStatisticsAnalyzer *deckListStatisticsAnalyzer; - QVBoxLayout *mainLayout; + void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {}); - QWidget *container; - QVBoxLayout *containerLayout; + QVBoxLayout *layout; + QWidget *controlContainer; + QHBoxLayout *controlLayout; + + QPushButton *addButton; + QPushButton *removeButton; + QPushButton *saveButton; + QPushButton *loadButton; QScrollArea *scrollArea; + QWidget *panelContainer; + QVBoxLayout *panelLayout; - ManaCurveWidget *manaCurveWidget; - ManaDevotionWidget *manaDevotionWidget; - ManaBaseWidget *manaBaseWidget; + QVector panelWrappers; + ResizablePanel *selectedWrapper = nullptr; + + DeckListStatisticsAnalyzer *statsAnalyzer; + LayoutInspector *insp = nullptr; }; #endif // DECK_ANALYTICS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp index 3ca18d21c..7d0259c72 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp @@ -9,38 +9,93 @@ DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *_model, - DeckListStatisticsAnalyzerConfig cfg) - : QObject(parent), model(_model), config(cfg) + DeckListStatisticsAnalyzerConfig _config) + : QObject(parent), model(_model), config(_config) { - connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::update); + connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::analyze); } -void DeckListStatisticsAnalyzer::update() +void DeckListStatisticsAnalyzer::analyze() { - manaBaseMap.clear(); - manaCurveMap.clear(); - manaDevotionMap.clear(); + clearData(); QList cards = model->getCards(); - for (const ExactCard &card : cards) { - // ---- Mana curve ---- + for (auto card : cards) { + auto info = card.getInfo(); + const int cmc = info.getCmc().toInt(); + + // Convert once + QStringList types = info.getMainCardType().split(' '); + QStringList subtypes = info.getCardType().split('-').last().split(" "); + QString colors = info.getColors(); + int power = info.getPowTough().split("/").first().toInt(); + int toughness = info.getPowTough().split("/").last().toInt(); + + // For each copy of card + // ---------------- Mana Curve ---------------- if (config.computeManaCurve) { - manaCurveMap[card.getInfo().getCmc().toInt()]++; + manaCurveMap[cmc]++; } - // ---- Mana base ---- + // per-type curve + for (auto &t : types) { + manaCurveByType[t][cmc]++; + manaCurveCardsByType[t][cmc].append(info.getName()); + } + + // Per-subtype curve + for (auto &st : subtypes) { + manaCurveBySubtype[st][cmc]++; + manaCurveCardsBySubtype[st][cmc].append(info.getName()); + } + + // per-color curve + for (auto &c : colors) { + manaCurveByColor[c][cmc]++; + manaCurveCardsByColor[c][cmc].append(info.getName()); + } + + // Power/toughness + manaCurveByPower[QString::number(power)][cmc]++; + manaCurveCardsByPower[QString::number(power)][cmc].append(info.getName()); + manaCurveByToughness[QString::number(toughness)][cmc]++; + manaCurveCardsByToughness[QString::number(toughness)][cmc].append(info.getName()); + + // ========== Category Counts =========== + for (auto &t : types) { + typeCount[t]++; + } + for (auto &st : subtypes) { + subtypeCount[st]++; + } + for (auto &c : colors) { + colorCount[c]++; + } + manaValueCount[cmc]++; + + // ---------------- Mana Base ---------------- if (config.computeManaBase) { - auto mana = determineManaProduction(card.getInfo().getText()); - for (auto it = mana.begin(); it != mana.end(); ++it) + auto prod = determineManaProduction(info.getText()); + for (auto it = prod.begin(); it != prod.end(); ++it) { + if (it.value() > 0) { + productionPipCount[it.key()] += it.value(); + productionCardCount[it.key()]++; + } manaBaseMap[it.key()] += it.value(); + } } - // ---- Devotion ---- + // ---------------- Devotion ---------------- if (config.computeDevotion) { - auto devo = countManaSymbols(card.getInfo().getManaCost()); - for (auto &d : devo) + auto devo = countManaSymbols(info.getManaCost()); + for (auto &d : devo) { + if (d.second > 0) { + devotionPipCount[QString(d.first)] += d.second; + devotionCardCount[QString(d.first)]++; + } manaDevotionMap[d.first] += d.second; + } } } @@ -112,3 +167,57 @@ std::unordered_map DeckListStatisticsAnalyzer::countManaSymbols(const return manaCounts; } + +// Hypergeometric probability: P(X=k) +double DeckListStatisticsAnalyzer::hypergeometric(int N, int K, int n, int k) +{ + if (k < 0 || k > n || K > N) { + return 0.0; + } + + auto choose = [](int n, int r) -> double { + if (r > n) + return 0.0; + if (r == 0 || r == n) + return 1.0; + double res = 1.0; + for (int i = 1; i <= r; ++i) { + res *= (n - r + i); + res /= i; + } + return res; + }; + + return choose(K, k) * choose(N - K, n - k) / choose(N, n); +} + +void DeckListStatisticsAnalyzer::clearData() +{ + manaBaseMap.clear(); + manaCurveMap.clear(); + manaDevotionMap.clear(); + + devotionPipCount.clear(); + devotionCardCount.clear(); + + productionPipCount.clear(); + productionCardCount.clear(); + + manaCurveByType.clear(); + manaCurveBySubtype.clear(); + manaCurveByColor.clear(); + manaCurveByPower.clear(); + manaCurveByToughness.clear(); + + manaCurveCardsByType.clear(); + manaCurveCardsBySubtype.clear(); + manaCurveCardsByColor.clear(); + manaCurveCardsByPower.clear(); + manaCurveCardsByToughness.clear(); + + typeCount.clear(); + subtypeCount.clear(); + colorCount.clear(); + rarityCount.clear(); + manaValueCount.clear(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h index 8fa033971..946bb0117 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h @@ -14,6 +14,9 @@ struct DeckListStatisticsAnalyzerConfig bool computeManaBase = true; bool computeManaCurve = true; bool computeDevotion = true; + bool computeCategories = true; + bool computeCurveBreakdowns = true; + bool computeProbabilities = true; }; class DeckListStatisticsAnalyzer : public QObject @@ -23,9 +26,9 @@ class DeckListStatisticsAnalyzer : public QObject public: explicit DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *model, - DeckListStatisticsAnalyzerConfig cfg = DeckListStatisticsAnalyzerConfig()); + DeckListStatisticsAnalyzerConfig _config = DeckListStatisticsAnalyzerConfig()); - void update(); + void analyze(); [[nodiscard]] const QHash &getManaBase() const { @@ -40,6 +43,96 @@ public: return manaDevotionMap; } + const QHash &getDevotionPipCount() const + { + return devotionPipCount; + } + const QHash &getDevotionCardCount() const + { + return devotionCardCount; + } + + const QHash &getProductionPipCount() const + { + return productionPipCount; + } + const QHash &getProductionCardCount() const + { + return productionCardCount; + } + + const QHash &getTypeCount() const + { + return typeCount; + } + const QHash &getSubtypeCount() const + { + return subtypeCount; + } + const QHash &getColorCount() const + { + return colorCount; + } + const QHash &getRarityCount() const + { + return rarityCount; + } + const QHash &getManaValueCount() const + { + return manaValueCount; + } + + const QHash> &getManaCurveByType() const + { + return manaCurveByType; + } + const QHash> &getManaCurveBySubtype() const + { + return manaCurveBySubtype; + } + const QHash> &getManaCurveByColor() const + { + return manaCurveByColor; + } + const QHash> &getManaCurveByPower() const + { + return manaCurveByPower; + } + const QHash> &getManaCurveByToughness() const + { + return manaCurveByToughness; + } + + const QHash> &getManaCurveCardsByType() const + { + return manaCurveCardsByType; + } + + const QHash> &getManaCurveCardsBySubtype() const + { + return manaCurveCardsBySubtype; + } + + const QHash> &getManaCurveCardsByColor() const + { + return manaCurveCardsByColor; + } + + const QHash> &getManaCurveCardsByPower() const + { + return manaCurveCardsByPower; + } + + const QHash> &getManaCurveCardsByToughness() const + { + return manaCurveCardsByToughness; + } + + DeckListModel *getModel() const + { + return model; + } + signals: void statsUpdated(); @@ -47,14 +140,42 @@ private: DeckListModel *model; DeckListStatisticsAnalyzerConfig config; - // Internal result containers QHash manaBaseMap; std::unordered_map manaCurveMap; std::unordered_map manaDevotionMap; - // Internal helper functions + QHash devotionPipCount; // W/U/B/R/G total symbols + QHash devotionCardCount; // how many cards provide devotion + + QHash productionPipCount; // mana produced by cards + QHash productionCardCount; // number of producers + + QHash typeCount; + QHash subtypeCount; + QHash colorCount; + QHash rarityCount; + QHash manaValueCount; + + QHash> manaCurveByType; + QHash> manaCurveBySubtype; + QHash> manaCurveByColor; + QHash> manaCurveByPower; + QHash> manaCurveByToughness; + + QHash> manaCurveCardsByType; + QHash> manaCurveCardsBySubtype; + QHash> manaCurveCardsByColor; + QHash> manaCurveCardsByPower; + QHash> manaCurveCardsByToughness; + + // Not storing card info — only numeric results. + QHash>> probabilityExact; + QHash>> probabilityAtLeast; + QHash determineManaProduction(const QString &); std::unordered_map countManaSymbols(const QString &); + double hypergeometric(int N, int K, int n, int k); + void clearData(); }; #endif // COCKATRICE_DECK_LIST_STATISTICS_ANALYZER_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp deleted file mode 100644 index 2e530db0b..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "mana_base_widget.h" - -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include -#include - -ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Base"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barContainer = new QWidget(this); - barLayout = new QHBoxLayout(barContainer); - layout->addWidget(barContainer); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaBaseWidget::updateDisplay); - - retranslateUi(); -} - -void ManaBaseWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Base")); -} - -void ManaBaseWidget::updateDisplay() -{ - // Clear the layout first - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - - auto manaBaseMap = deckStatAnalyzer->getManaBase(); - - int highestEntry = 0; - for (auto entry : manaBaseMap) { - if (entry > highestEntry) { - highestEntry = entry; - } - } - - // Define color mapping for mana types - QHash manaColors; - manaColors.insert("W", QColor(248, 231, 185)); - manaColors.insert("U", QColor(14, 104, 171)); - manaColors.insert("B", QColor(21, 11, 0)); - manaColors.insert("R", QColor(211, 32, 42)); - manaColors.insert("G", QColor(0, 115, 62)); - manaColors.insert("C", QColor(150, 150, 150)); - - for (auto manaColor : manaBaseMap.keys()) { - QColor barColor = manaColors.value(manaColor, Qt::gray); - BarWidget *barWidget = new BarWidget(QString(manaColor), manaBaseMap[manaColor], highestEntry, barColor, this); - barLayout->addWidget(barWidget); - } - - update(); -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h deleted file mode 100644 index 079449353..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @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 "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include -#include - -class ManaBaseWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - BannerWidget *bannerWidget; - QVBoxLayout *layout; - QWidget *barContainer; - QHBoxLayout *barLayout; -}; - -#endif // MANA_BASE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp deleted file mode 100644 index c094eb590..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "mana_curve_widget.h" - -#include "../../../main.h" -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include - -ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Curve"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barContainer = new QWidget(this); - barLayout = new QHBoxLayout(barContainer); - layout->addWidget(barContainer); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay); - - retranslateUi(); -} - -void ManaCurveWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Curve")); -} - -void ManaCurveWidget::updateDisplay() -{ - // Clear the layout first - if (barLayout != nullptr) { - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - } - - auto manaCurveMap = deckStatAnalyzer->getManaCurve(); - - int highestEntry = 0; - for (const auto &entry : manaCurveMap) { - if (entry.second > highestEntry) { - highestEntry = entry.second; - } - } - - // Convert unordered_map to ordered map to ensure sorting by CMC - std::map sortedManaCurve(manaCurveMap.begin(), manaCurveMap.end()); - - // Add new widgets to the layout in sorted order - for (const auto &entry : sortedManaCurve) { - BarWidget *barWidget = - new BarWidget(QString::number(entry.first), entry.second, highestEntry, QColor(122, 122, 122), this); - barLayout->addWidget(barWidget); - } - - update(); // Update the widget display -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h deleted file mode 100644 index fad1fb0f8..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file mana_curve_widget.h - * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. - */ - -#ifndef MANA_CURVE_WIDGET_H -#define MANA_CURVE_WIDGET_H - -#include "../general/display/banner_widget.h" -#include "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include - -class ManaCurveWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - QVBoxLayout *layout; - BannerWidget *bannerWidget; - QWidget *barContainer; - QHBoxLayout *barLayout; -}; - -#endif // MANA_CURVE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp deleted file mode 100644 index 476bb8077..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "mana_devotion_widget.h" - -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include -#include - -ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Devotion"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barLayout = new QHBoxLayout(); - layout->addLayout(barLayout); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaDevotionWidget::updateDisplay); - - retranslateUi(); -} - -void ManaDevotionWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Devotion")); -} - -void ManaDevotionWidget::updateDisplay() -{ - // Clear the layout first - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - - auto manaDevotionMap = deckStatAnalyzer->getDevotion(); - - int highestEntry = 0; - for (auto entry : manaDevotionMap) { - if (highestEntry < entry.second) { - highestEntry = entry.second; - } - } - - // Define color mapping for devotion bars - std::unordered_map manaColors = {{'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 entry : manaDevotionMap) { - QColor barColor = manaColors.count(entry.first) ? manaColors[entry.first] : Qt::gray; - BarWidget *barWidget = new BarWidget(QString(entry.first), entry.second, highestEntry, barColor, this); - barLayout->addWidget(barWidget); - } - - update(); // Update the widget display -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h deleted file mode 100644 index ff2e86159..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @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 "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include -#include - -class ManaDevotionWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - BannerWidget *bannerWidget; - QVBoxLayout *layout; - QHBoxLayout *barLayout; -}; - -#endif // MANA_DEVOTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp new file mode 100644 index 000000000..a0c971a75 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp @@ -0,0 +1,367 @@ +#include "resizable_panel.h" + +#include "libcockatrice/utility/qt_utils.h" + +#include +#include +#include +#include + +ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent) + : QWidget(parent), panel(analyticsPanel), typeId(_typeId) +{ + setAcceptDrops(true); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // Frame for selection highlight + frame = new QFrame(this); + frame->setFrameShape(QFrame::Box); + frame->setLineWidth(2); + frame->setStyleSheet("border: none;"); + + auto *frameLayout = new QVBoxLayout(frame); + frameLayout->setContentsMargins(0, 0, 0, 0); + frameLayout->setSpacing(0); + + // Add the analytics panel + frameLayout->addWidget(analyticsPanel); + + dropIndicator = new QFrame(frame); + dropIndicator->setStyleSheet("background-color: #3daee9;"); + dropIndicator->setFixedHeight(3); + dropIndicator->hide(); // hidden by default + dropIndicator->raise(); // make sure it's above children + + selectionOverlay = new QFrame(frame); + selectionOverlay->setStyleSheet("background-color: rgba(61,174,233,50);"); // semi-transparent blue + selectionOverlay->hide(); // hidden by default + selectionOverlay->raise(); // make sure it is above children + selectionOverlay->setAttribute(Qt::WA_TransparentForMouseEvents); + + // Bottom bar with drag button and resize handle + auto *bottomBar = new QWidget(frame); + auto *bottomLayout = new QHBoxLayout(bottomBar); + bottomLayout->setContentsMargins(0, 0, 0, 0); + bottomLayout->setSpacing(0); + + // Drag button on the left + dragButton = new QPushButton("☰", bottomBar); + dragButton->setFixedSize(40, 8); + dragButton->setCursor(Qt::OpenHandCursor); + dragButton->setStyleSheet("QPushButton { " + "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a4a4a, stop:1 #3a3a3a); " + "border: none; color: #888; font-size: 10px; }" + "QPushButton:hover { background: #5a5a5a; }"); + bottomLayout->addWidget(dragButton); + + // Resize handle fills the rest + resizeHandle = new QWidget(bottomBar); + resizeHandle->setFixedHeight(8); + resizeHandle->setCursor(Qt::SizeVerCursor); + resizeHandle->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:0, y2:1, " + "stop:0 #3a3a3a, stop:1 #2a2a2a);"); + bottomLayout->addWidget(resizeHandle, 1); + + frameLayout->addWidget(bottomBar); + + mainLayout->addWidget(frame); + + // Set size policy + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + // Calculate initial height - use panel's size hint if available + int panelHint = analyticsPanel->sizeHint().height(); + int panelMin = analyticsPanel->minimumSizeHint().height(); + + // Start with the larger of panel's hint and panel's minimum hint + currentHeight = qMax(panelHint + 8, panelMin + 8); + updateSizeConstraints(); + + // Install event filters + dragButton->installEventFilter(this); + resizeHandle->installEventFilter(this); + + // Timer for auto-scroll during drag + autoScrollTimer = new QTimer(this); + autoScrollTimer->setInterval(50); + connect(autoScrollTimer, &QTimer::timeout, this, &ResizablePanel::performAutoScroll); +} + +void ResizablePanel::setSelected(bool selected) +{ + if (selected) { + selectionOverlay->setGeometry(0, 0, width(), height()); + selectionOverlay->show(); + } else { + selectionOverlay->hide(); + } +} + +void ResizablePanel::setHeightFromSaved(int h) +{ + if (h > 0) { + currentHeight = qMax(h, getMinimumAllowedHeight()); + updateSizeConstraints(); + } +} + +int ResizablePanel::getCurrentHeight() const +{ + return currentHeight; +} + +QSize ResizablePanel::sizeHint() const +{ + return QSize(width(), currentHeight); +} + +QSize ResizablePanel::minimumSizeHint() const +{ + return QSize(0, getMinimumAllowedHeight()); +} + +// ===================================================================================================================== +// Event Handling +// ===================================================================================================================== + +bool ResizablePanel::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == dragButton) { + if (event->type() == QEvent::MouseButtonPress) { + auto *mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + dragStartPos = mouseEvent->globalPosition().toPoint(); +#else + dragStartPos = mouseEvent->globalPos(); +#endif + isDraggingPanel = false; + dragButton->setCursor(Qt::ClosedHandCursor); + } + return false; + } else if (event->type() == QEvent::MouseMove) { + auto *mouseEvent = static_cast(event); + if (mouseEvent->buttons() & Qt::LeftButton) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint currentPos = mouseEvent->globalPosition().toPoint(); +#else + QPoint currentPos = mouseEvent->globalPos(); +#endif + int distance = (currentPos - dragStartPos).manhattanLength(); + if (distance >= 5 && !isDraggingPanel) { + isDraggingPanel = true; + startDrag(); + return true; + } + } + return false; + } else if (event->type() == QEvent::MouseButtonRelease) { + dragButton->setCursor(Qt::OpenHandCursor); + isDraggingPanel = false; + return false; + } + } + + if (obj == resizeHandle) { + if (event->type() == QEvent::MouseButtonPress) { + auto *mouseEvent = static_cast(event); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + resizeStartY = mouseEvent->globalPosition().y(); +#else + resizeStartY = mouseEvent->globalPos().y(); +#endif + isResizing = true; + resizeStartHeight = currentHeight; + resizeHandle->grabMouse(); + return true; + } else if (event->type() == QEvent::MouseMove && isResizing) { + auto *mouseEvent = static_cast(event); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + int deltaY = mouseEvent->globalPosition().y() - resizeStartY; +#else + int deltaY = mouseEvent->globalPos().y() - resizeStartY; +#endif + int newHeight = resizeStartHeight + deltaY; + + int minAllowed = getMinimumAllowedHeight(); + newHeight = qMax(newHeight, minAllowed); + + currentHeight = newHeight; + updateSizeConstraints(); + + return true; + } else if (event->type() == QEvent::MouseButtonRelease) { + isResizing = false; + resizeHandle->releaseMouse(); + return true; + } + } + + return QWidget::eventFilter(obj, event); +} + +void ResizablePanel::dragEnterEvent(QDragEnterEvent *event) +{ + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + event->acceptProposedAction(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + showDropIndicator(event->position().y()); +#else + showDropIndicator(event->pos().y()); +#endif + } +} + +void ResizablePanel::dragMoveEvent(QDragMoveEvent *event) +{ + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + event->acceptProposedAction(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + showDropIndicator(event->position().y()); + lastDragPos = mapToGlobal(event->position().toPoint()); +#else + showDropIndicator(event->pos().y()); + lastDragPos = mapToGlobal(event->pos()); +#endif + + if (!autoScrollTimer->isActive()) { + autoScrollTimer->start(); + } + } +} + +void ResizablePanel::dragLeaveEvent(QDragLeaveEvent *event) +{ + Q_UNUSED(event); + hideDropIndicator(); + autoScrollTimer->stop(); +} + +void ResizablePanel::dropEvent(QDropEvent *event) +{ + hideDropIndicator(); + autoScrollTimer->stop(); + + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + QByteArray data = event->mimeData()->data("application/x-resizablepanel"); + quintptr ptr = *reinterpret_cast(data.constData()); + ResizablePanel *draggedPanel = reinterpret_cast(ptr); + + if (draggedPanel && draggedPanel != this) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + bool insertBefore = (event->position().y() < height() / 2); +#else + bool insertBefore = (event->pos().y() < height() / 2); +#endif + emit dropRequested(draggedPanel, this, insertBefore); + event->acceptProposedAction(); + } + } +} + +void ResizablePanel::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + if (selectionOverlay->isVisible()) { + selectionOverlay->setGeometry(0, 0, width(), height()); + } + + if (dropIndicator->isVisible()) { + dropIndicator->setGeometry(0, dropIndicator->y(), width(), dropIndicator->height()); + } +} + +// ===================================================================================================================== +// Private Helpers +// ===================================================================================================================== + +int ResizablePanel::getMinimumAllowedHeight() const +{ + QSize panelMin = panel->minimumSizeHint(); + int panelMinHeight = (panelMin.isValid() && panelMin.height() > 0) ? panelMin.height() : 100; + return panelMinHeight + 8; +} + +void ResizablePanel::updateSizeConstraints() +{ + setMinimumHeight(currentHeight); + setMaximumHeight(currentHeight); + updateGeometry(); +} + +void ResizablePanel::startDrag() +{ + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + + quintptr ptr = reinterpret_cast(this); + QByteArray data(reinterpret_cast(&ptr), sizeof(ptr)); + mimeData->setData("application/x-resizablepanel", data); + + drag->setMimeData(mimeData); + + QPixmap pixmap(width(), 40); + pixmap.fill(QColor(58, 58, 58, 200)); + drag->setPixmap(pixmap); + drag->setHotSpot(QPoint(width() / 2, 20)); + + emit dragStarted(this); + + autoScrollTimer->start(); + + Qt::DropAction result = drag->exec(Qt::MoveAction); + Q_UNUSED(result); + + autoScrollTimer->stop(); + dragButton->setCursor(Qt::OpenHandCursor); + isDraggingPanel = false; +} + +void ResizablePanel::performAutoScroll() +{ + QScrollArea *scrollArea = QtUtils::findParentOfType(this); + + if (!scrollArea) { + return; + } + + QScrollBar *scrollBar = scrollArea->verticalScrollBar(); + if (!scrollBar) { + return; + } + + QRect scrollRect = scrollArea->viewport()->rect(); + QPoint scrollTopLeft = scrollArea->viewport()->mapToGlobal(scrollRect.topLeft()); + QRect globalScrollRect(scrollTopLeft, scrollRect.size()); + + const int scrollMargin = 50; + int scrollSpeed = 0; + + if (lastDragPos.y() < globalScrollRect.top() + scrollMargin) { + scrollSpeed = -15; + } else if (lastDragPos.y() > globalScrollRect.bottom() - scrollMargin) { + scrollSpeed = 15; + } + + if (scrollSpeed != 0) { + int newValue = scrollBar->value() + scrollSpeed; + newValue = qBound(scrollBar->minimum(), newValue, scrollBar->maximum()); + scrollBar->setValue(newValue); + } +} + +void ResizablePanel::showDropIndicator(double y) +{ + bool before = (y < height() / 2); + dropIndicator->setGeometry(0, before ? 0 : height() - 3, width(), 3); + dropIndicator->show(); +} + +void ResizablePanel::hideDropIndicator() +{ + dropIndicator->hide(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h new file mode 100644 index 000000000..d958ec64d --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h @@ -0,0 +1,79 @@ +#ifndef COCKATRICE_RESIZABLE_PANEL_H +#define COCKATRICE_RESIZABLE_PANEL_H + +#include "abstract_analytics_panel_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ResizablePanel : public QWidget +{ + Q_OBJECT +public: + explicit ResizablePanel(const QString &typeId, + AbstractAnalyticsPanelWidget *analyticsPanel, + QWidget *parent = nullptr); + + void setSelected(bool selected); + void setHeightFromSaved(int h); + int getCurrentHeight() const; + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + QString getTypeId() const + { + return typeId; + } + + AbstractAnalyticsPanelWidget *panel; + +signals: + void dragStarted(ResizablePanel *panel); + void dropRequested(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private: + int getMinimumAllowedHeight() const; + void updateSizeConstraints(); + void startDrag(); + void performAutoScroll(); + void showDropIndicator(double y); + void hideDropIndicator(); + + QString typeId; + + QFrame *frame; + QFrame *selectionOverlay; + QFrame *dropIndicator; + QPushButton *dragButton; + QWidget *resizeHandle; + + int currentHeight; + bool isResizing = false; + bool isDraggingPanel = false; + double resizeStartY = 0; + int resizeStartHeight = 0; + + QPoint dragStartPos; + QPoint lastDragPos; + QTimer *autoScrollTimer; +}; + +#endif // COCKATRICE_RESIZABLE_PANEL_H diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp new file mode 100644 index 000000000..f7ca669f3 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp @@ -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))); + } +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h new file mode 100644 index 000000000..06a17c7c6 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h @@ -0,0 +1,23 @@ +#ifndef COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H +#define COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H + +#include +#include + +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 diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp new file mode 100644 index 000000000..998808307 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp @@ -0,0 +1,215 @@ +#include "bar_chart_widget.h" + +#include +#include +#include +#include + +BarChartWidget::BarChartWidget(QWidget *parent) : QWidget(parent) +{ + setMouseTracking(true); +} + +void BarChartWidget::setBars(const QVector &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(); + } +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h new file mode 100644 index 000000000..e80a3f8e8 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h @@ -0,0 +1,52 @@ +#ifndef COCKATRICE_BAR_CHART_WIDGET_H +#define COCKATRICE_BAR_CHART_WIDGET_H + +#include +#include +#include +#include + +struct BarSegment +{ + QString category; + int value; + QStringList cards; + QColor color; +}; + +struct BarData +{ + QString label; + QVector segments; +}; + +class BarChartWidget : public QWidget +{ + Q_OBJECT +public: + explicit BarChartWidget(QWidget *parent = nullptr); + + void setBars(const QVector &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 bars; + int highest = 1; // global maximum value + + int hoveredBar = -1; + int hoveredSegment = -1; +}; + +#endif // COCKATRICE_BAR_CHART_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.cpp similarity index 100% rename from cockatrice/src/interface/widgets/general/display/bar_widget.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.cpp diff --git a/cockatrice/src/interface/widgets/general/display/bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/bar_widget.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.h diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp similarity index 99% rename from cockatrice/src/interface/widgets/general/display/color_bar.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp index d1eb7ef4c..94e2420b5 100644 --- a/cockatrice/src/interface/widgets/general/display/color_bar.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp @@ -1,4 +1,3 @@ - #include "color_bar.h" #include diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.h b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/color_bar.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h diff --git a/cockatrice/src/interface/widgets/general/display/percent_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp similarity index 100% rename from cockatrice/src/interface/widgets/general/display/percent_bar_widget.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp diff --git a/cockatrice/src/interface/widgets/general/display/percent_bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/percent_bar_widget.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.h diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp new file mode 100644 index 000000000..e027aabdd --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp @@ -0,0 +1,140 @@ +#include "segmented_bar_widget.h" + +#include +#include +#include +#include + +SegmentedBarWidget::SegmentedBarWidget(QString label, QVector 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 +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h new file mode 100644 index 000000000..4df54e42e --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h @@ -0,0 +1,38 @@ +#ifndef COCKATRICE_SEGMENTED_BAR_WIDGET_H +#define COCKATRICE_SEGMENTED_BAR_WIDGET_H + +#include +#include +#include + +class SegmentedBarWidget : public QWidget +{ + Q_OBJECT + +public: + struct Segment + { + QString category; + int value = 0; + QStringList cards; + QColor color; + }; + + QString label; + QVector segments; + float total = 1.0; + + explicit SegmentedBarWidget(QString label, QVector 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 diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp new file mode 100644 index 000000000..e86793083 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp @@ -0,0 +1,205 @@ +#include "color_pie.h" + +#include +#include +#include +#include + +ColorPie::ColorPie(const QMap &_colors, QWidget *parent) : QWidget(parent), colors(_colors) +{ + setMouseTracking(true); +} + +void ColorPie::setColors(const QMap &_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 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 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 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; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h new file mode 100644 index 000000000..a8fe784a3 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_COLOR_PIE_H +#define COCKATRICE_COLOR_PIE_H + +#ifndef COLOR_PIE_H +#define COLOR_PIE_H + +#include +#include +#include + +class ColorPie : public QWidget +{ + Q_OBJECT + +public: + explicit ColorPie(const QMap &_colors = {}, QWidget *parent = nullptr); + + void setColors(const QMap &_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 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 diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp index 2998a03bc..736b69ea2 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp @@ -3,7 +3,7 @@ #include "../../../../../card_picture_loader/card_picture_loader.h" #include "../../../../cards/card_info_picture_with_text_overlay_widget.h" #include "../../../../general/display/background_plate_widget.h" -#include "../../../../general/display/color_bar.h" +#include "../../../../general/display/charts/bars/color_bar.h" #include "archidekt_deck_preview_image_display_widget.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h index 0174016f7..43baddb4c 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h @@ -7,7 +7,7 @@ #ifndef EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H #define EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H -#include "../../../../../general/display/percent_bar_widget.h" +#include "../../../../../general/display/charts/bars/percent_bar_widget.h" #include "../../api_response/cards/edhrec_api_response_card_details.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h index 39d26a409..c2e1c018c 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h @@ -7,7 +7,7 @@ #ifndef EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H #define EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H -#include "../../../../../general/display/percent_bar_widget.h" +#include "../../../../../general/display/charts/bars/percent_bar_widget.h" #include "../../api_response/cards/edhrec_api_response_card_details.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index f3d573d27..82f96860d 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -85,7 +85,7 @@ void TabDeckEditorVisual::onDeckChanged() { AbstractTabDeckEditor::onDeckModified(); tabContainer->visualDeckView->constructZoneWidgetsFromDeckListModel(); - tabContainer->deckAnalytics->refreshDisplays(); + tabContainer->deckAnalytics->updateDisplays(); tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel()); } diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index 98b6aff00..82aeb05a6 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -45,10 +45,13 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay); - deckAnalytics = new DeckAnalyticsWidget(this, deckModel); + statsAnalyzer = new DeckListStatisticsAnalyzer(this, deckModel); + statsAnalyzer->analyze(); + + deckAnalytics = new DeckAnalyticsWidget(this, statsAnalyzer); deckAnalytics->setObjectName("deckAnalytics"); - sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel); + sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel, statsAnalyzer); this->addNewTab(visualDeckView, tr("Visual Deck View")); this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display")); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h index 59c577024..9468df425 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -78,7 +78,8 @@ public: /// Get the total number of tabs. [[nodiscard]] int getTabCount() const; - VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget. + VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget. + DeckListStatisticsAnalyzer *statsAnalyzer; DeckAnalyticsWidget *deckAnalytics; ///< Deck analytics widget. VisualDatabaseDisplayWidget *visualDatabaseDisplay; ///< Database display widget. PrintingSelector *printingSelector; ///< Printing selector widget. diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 0badb76ff..24f521760 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -3,12 +3,16 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" #include "../cards/card_info_picture_widget.h" +#include "../deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h" +#include "../deck_analytics/deck_list_statistics_analyzer.h" #include #include -VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *_deckListModel) - : QWidget(parent), deckListModel(_deckListModel) +VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, + DeckListModel *_deckListModel, + DeckListStatisticsAnalyzer *_statsAnalyzer) + : QWidget(parent), deckListModel(_deckListModel), statsAnalyzer(_statsAnalyzer) { layout = new QVBoxLayout(this); setLayout(layout); @@ -35,6 +39,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); + drawProbabilityWidget = new DrawProbabilityWidget(this, statsAnalyzer); + layout->addWidget(drawProbabilityWidget); + cardSizeWidget = new CardSizeWidget(this, flowWidget); layout->addWidget(cardSizeWidget); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h index 751e16a3c..c63c74a4d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h @@ -8,6 +8,7 @@ #define VISUAL_DECK_EDITOR_SAMPLE_HAND_WIDGET_H #include "../cards/card_size_widget.h" +#include "../deck_analytics/deck_list_statistics_analyzer.h" #include "../general/layout_containers/flow_widget.h" #include @@ -15,11 +16,14 @@ #include #include +class DrawProbabilityWidget; class VisualDeckEditorSampleHandWidget : public QWidget { Q_OBJECT public: - VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *deckListModel); + VisualDeckEditorSampleHandWidget(QWidget *parent, + DeckListModel *deckListModel, + DeckListStatisticsAnalyzer *statsAnalyzer); QList getRandomCards(int amountToGet); public slots: @@ -29,12 +33,14 @@ public slots: private: DeckListModel *deckListModel; + DeckListStatisticsAnalyzer *statsAnalyzer; QVBoxLayout *layout; QWidget *resetAndHandSizeContainerWidget; QHBoxLayout *resetAndHandSizeLayout; QPushButton *resetButton; QSpinBox *handSizeSpinBox; FlowWidget *flowWidget; + DrawProbabilityWidget *drawProbabilityWidget; CardSizeWidget *cardSizeWidget; }; diff --git a/libcockatrice_utility/libcockatrice/utility/color.h b/libcockatrice_utility/libcockatrice/utility/color.h index 164c6ffd7..bf4759565 100644 --- a/libcockatrice_utility/libcockatrice/utility/color.h +++ b/libcockatrice_utility/libcockatrice/utility/color.h @@ -21,6 +21,43 @@ inline color convertQColorToColor(const QColor &c) result.set_b(c.blue()); return result; } + +namespace GameSpecificColors +{ +namespace MTG +{ +inline QColor colorHelper(const QString &name) +{ + static const QMap colorMap = { + {"W", QColor(245, 245, 220)}, + {"U", QColor(80, 140, 255)}, + {"B", QColor(60, 60, 60)}, + {"R", QColor(220, 60, 50)}, + {"G", QColor(70, 160, 70)}, + {"Creature", QColor(70, 130, 180)}, + {"Instant", QColor(138, 43, 226)}, + {"Sorcery", QColor(199, 21, 133)}, + {"Enchantment", QColor(218, 165, 32)}, + {"Artifact", QColor(169, 169, 169)}, + {"Planeswalker", QColor(210, 105, 30)}, + {"Land", QColor(110, 80, 50)}, + }; + + if (colorMap.contains(name)) + return colorMap[name]; + + if (name.length() == 1 && colorMap.contains(name.toUpper())) + return colorMap[name.toUpper()]; + + uint h = qHash(name); + int r = 100 + (h % 120); + int g = 100 + ((h >> 8) % 120); + int b = 100 + ((h >> 16) % 120); + + return QColor(r, g, b); +} +} // namespace MTG +} // namespace GameSpecificColors #endif inline color makeColor(int r, int g, int b) diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h index 855cc8b18..606947143 100644 --- a/libcockatrice_utility/libcockatrice/utility/qt_utils.h +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -15,6 +15,20 @@ template T *findParentOfType(const QObject *obj) } return nullptr; } + +static inline void clearLayoutRec(QLayout *l) +{ + if (!l) + return; + QLayoutItem *it; + while ((it = l->takeAt(0)) != nullptr) { + if (QWidget *w = it->widget()) + w->deleteLater(); + if (QLayout *sub = it->layout()) + clearLayoutRec(sub); + delete it; + } +} } // namespace QtUtils #endif // COCKATRICE_QT_UTILS_H