Merge branch 'master' into 6269/ci/strip-fat-bins

This commit is contained in:
Bruno Alexandre Rosa 2026-01-02 22:54:44 -03:00
commit b635ca367d
105 changed files with 4907 additions and 842 deletions

View file

@ -144,11 +144,31 @@ set(cockatrice_SOURCES
src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/card_size_widget.cpp
src/interface/widgets/cards/deck_card_zone_display_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/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_analytics_widget.cpp
src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.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/resizable_panel.cpp
src/interface/widgets/deck_analytics/mana_curve_widget.cpp src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp
src/interface/widgets/deck_analytics/mana_devotion_widget.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_list_history_manager_widget.cpp
src/interface/widgets/deck_editor/deck_editor_card_info_dock_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 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/background_sources.cpp
src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/background_plate_widget.cpp
src/interface/widgets/general/display/banner_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_label.cpp
src/interface/widgets/general/display/dynamic_font_size_push_button.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/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/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_styled_button.cpp
src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/home_widget.cpp
src/interface/widgets/general/layout_containers/flow_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp

View file

@ -1,11 +1,13 @@
@page deck_search_syntax_help Deck Search Syntax Help @page deck_search_syntax_help Deck Search Syntax Help
## Deck Search Syntax Help ## Deck Search Syntax Help
-----
The search bar recognizes a set of special commands.<br> The search bar recognizes a set of special commands.<br>
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all
searches are case insensitive. searches are case insensitive.
<dl> <dl>
<dt>Display Name (The deck name, or the filename if the deck name isn't set):</dt> <dt>Display Name (The deck name, or the filename if the deck name isn't set):</dt>
<dd>[red deck wins](#red deck wins) <small>(Any deck with a display name containing the words red, deck, and wins)</small></dd> <dd>[red deck wins](#red deck wins) <small>(Any deck with a display name containing the words red, deck, and wins)</small></dd>
<dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck with a display name containing the exact phrase "red deck wins")</small></dd> <dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck with a display name containing the exact phrase "red deck wins")</small></dd>

View file

@ -1,10 +1,12 @@
@page search_syntax_help Search Syntax Help @page search_syntax_help Search Syntax Help
## Search Syntax Help ## Search Syntax Help
-----
The search bar recognizes a set of special commands similar to some other card databases.<br> The search bar recognizes a set of special commands similar to some other card databases.<br>
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive. In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive.
<dl> <dl>
<dt>Name:</dt> <dt>Name:</dt>
<dd>[birds of paradise](#birds of paradise) <small>(Any card name containing the words birds, of, and paradise)</small></dd> <dd>[birds of paradise](#birds of paradise) <small>(Any card name containing the words birds, of, and paradise)</small></dd>
<dd>["birds of paradise"](#%22birds of paradise%22) <small>(Any card name containing the exact phrase "birds of paradise")</small></dd> <dd>["birds of paradise"](#%22birds of paradise%22) <small>(Any card name containing the exact phrase "birds of paradise")</small></dd>

View file

@ -288,6 +288,7 @@ SettingsCache::SettingsCache()
focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool();
showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool();
showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool();
displayCardNames = settings->value("cards/displaycardnames", true).toBool(); displayCardNames = settings->value("cards/displaycardnames", true).toBool();
roundCardCorners = settings->value("cards/roundcardcorners", true).toBool(); roundCardCorners = settings->value("cards/roundcardcorners", true).toBool();
overrideAllCardArtWithPersonalPreference = overrideAllCardArtWithPersonalPreference =
@ -715,6 +716,13 @@ void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts)
settings->setValue("menu/showshortcuts", showShortcuts); settings->setValue("menu/showshortcuts", showShortcuts);
} }
void SettingsCache::setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar)
{
showGameSelectorFilterToolbar = static_cast<bool>(_showGameSelectorFilterToolbar);
settings->setValue("menu/showgameselectorfiltertoolbar", showGameSelectorFilterToolbar);
emit showGameSelectorFilterToolbarChanged(showGameSelectorFilterToolbar);
}
void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames) void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames)
{ {
displayCardNames = static_cast<bool>(_displayCardNames); displayCardNames = static_cast<bool>(_displayCardNames);

View file

@ -145,6 +145,7 @@ signals:
void homeTabBackgroundShuffleFrequencyChanged(); void homeTabBackgroundShuffleFrequencyChanged();
void picDownloadChanged(); void picDownloadChanged();
void showStatusBarChanged(bool state); void showStatusBarChanged(bool state);
void showGameSelectorFilterToolbarChanged(bool state);
void displayCardNamesChanged(); void displayCardNamesChanged();
void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference); void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference);
void bumpSetsWithCardsInDeckToTopChanged(); void bumpSetsWithCardsInDeckToTopChanged();
@ -236,6 +237,7 @@ private:
bool annotateTokens; bool annotateTokens;
QByteArray tabGameSplitterSizes; QByteArray tabGameSplitterSizes;
bool showShortcuts; bool showShortcuts;
bool showGameSelectorFilterToolbar;
bool displayCardNames; bool displayCardNames;
bool overrideAllCardArtWithPersonalPreference; bool overrideAllCardArtWithPersonalPreference;
bool bumpSetsWithCardsInDeckToTop; bool bumpSetsWithCardsInDeckToTop;
@ -553,6 +555,10 @@ public:
{ {
return showShortcuts; return showShortcuts;
} }
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const
{
return showGameSelectorFilterToolbar;
}
[[nodiscard]] bool getDisplayCardNames() const [[nodiscard]] bool getDisplayCardNames() const
{ {
return displayCardNames; return displayCardNames;
@ -1017,6 +1023,7 @@ public slots:
void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens); void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens);
void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes); void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes);
void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts); void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts);
void setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar);
void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames); void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames);
void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt); void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt);
void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop); void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop);

View file

@ -660,6 +660,12 @@ private:
{"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"), {"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"),
parseSequenceString("Ctrl+M"), parseSequenceString("Ctrl+M"),
ShortcutGroup::Drawing)}, ShortcutGroup::Drawing)},
{"Player/aMulliganSame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Same hand size)"),
parseSequenceString("Ctrl+Shift+M"),
ShortcutGroup::Drawing)},
{"Player/aMulliganMinusOne", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Hand size - 1)"),
parseSequenceString("Ctrl+Shift+Alt+M"),
ShortcutGroup::Drawing)},
{"Player/aDrawCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw a Card"), {"Player/aDrawCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw a Card"),
parseSequenceString("Ctrl+D"), parseSequenceString("Ctrl+D"),
ShortcutGroup::Drawing)}, ShortcutGroup::Drawing)},

View file

@ -60,6 +60,16 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T
connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan); connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan);
addAction(aMulligan); addAction(aMulligan);
// Mulligan same size
aMulliganSame = new QAction(this);
connect(aMulliganSame, &QAction::triggered, actions, &PlayerActions::actMulliganSameSize);
addAction(aMulliganSame);
// Mulligan -1
aMulliganMinusOne = new QAction(this);
connect(aMulliganMinusOne, &QAction::triggered, actions, &PlayerActions::actMulliganMinusOne);
addAction(aMulliganMinusOne);
addSeparator(); addSeparator();
mMoveHandMenu = addTearOffMenu(QString()); mMoveHandMenu = addTearOffMenu(QString());
@ -104,7 +114,9 @@ void HandMenu::retranslateUi()
aSortHandByType->setText(tr("Type")); aSortHandByType->setText(tr("Type"));
aSortHandByManaValue->setText(tr("Mana Value")); aSortHandByManaValue->setText(tr("Mana Value"));
aMulligan->setText(tr("Take &mulligan")); aMulligan->setText(tr("Take &mulligan (Choose hand size)"));
aMulliganSame->setText(tr("Take mulligan (Same hand size)"));
aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)"));
mMoveHandMenu->setTitle(tr("&Move hand to...")); mMoveHandMenu->setTitle(tr("&Move hand to..."));
aMoveHandToTopLibrary->setText(tr("&Top of library")); aMoveHandToTopLibrary->setText(tr("&Top of library"));
@ -128,6 +140,8 @@ void HandMenu::setShortcutsActive()
aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType")); aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType"));
aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue")); aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue"));
aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan")); aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan"));
aMulliganSame->setShortcuts(shortcuts.getShortcut("Player/aMulliganSame"));
aMulliganMinusOne->setShortcuts(shortcuts.getShortcut("Player/aMulliganMinusOne"));
aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll")); aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll"));
aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll")); aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll"));
} }

View file

@ -46,6 +46,8 @@ private:
QAction *aViewHand = nullptr; QAction *aViewHand = nullptr;
QAction *aMulligan = nullptr; QAction *aMulligan = nullptr;
QAction *aMulliganSame = nullptr;
QAction *aMulliganMinusOne = nullptr;
QMenu *mSortHand = nullptr; QMenu *mSortHand = nullptr;
QAction *aSortHandByName = nullptr; QAction *aSortHandByName = nullptr;

View file

@ -310,28 +310,48 @@ void PlayerActions::actMulligan()
{ {
int startSize = SettingsCache::instance().getStartingHandSize(); int startSize = SettingsCache::instance().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size(); int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck int deckSize = player->getDeckZone()->getCards().size() + handSize;
bool ok; bool ok;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"), int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"),
tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + tr("Number of cards: (max. %1)").arg(deckSize) + '\n' +
tr("0 and lower are in comparison to current hand size"), tr("0 and lower are in comparison to current hand size"),
startSize, -handSize, deckSize, 1, &ok); startSize, -handSize, deckSize, 1, &ok);
if (!ok) { if (!ok) {
return; return;
} }
Command_Mulligan cmd;
if (number < 1) {
number = handSize + number;
}
doMulligan(number);
SettingsCache::instance().setStartingHandSize(number);
}
void PlayerActions::actMulliganSameSize()
{
int handSize = player->getHandZone()->getCards().size();
doMulligan(handSize);
}
void PlayerActions::actMulliganMinusOne()
{
int handSize = player->getHandZone()->getCards().size();
int targetSize = qMax(1, handSize - 1);
doMulligan(targetSize);
}
void PlayerActions::doMulligan(int number)
{
if (number < 1) { if (number < 1) {
if (handSize == 0) {
return; return;
} }
cmd.set_number(handSize + number);
} else { Command_Mulligan cmd;
cmd.set_number(number); cmd.set_number(number);
}
sendGameCommand(cmd); sendGameCommand(cmd);
if (startSize != number) {
SettingsCache::instance().setStartingHandSize(number);
}
} }
void PlayerActions::actDrawCards() void PlayerActions::actDrawCards()

View file

@ -85,6 +85,9 @@ public slots:
void actDrawCards(); void actDrawCards();
void actUndoDraw(); void actUndoDraw();
void actMulligan(); void actMulligan();
void actMulliganSameSize();
void actMulliganMinusOne();
void doMulligan(int number);
void actPlay(); void actPlay();
void actPlayFacedown(); void actPlayFacedown();

View file

@ -272,10 +272,11 @@ void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event)
QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
{ {
const QRectF frameRectF = windowFrameRect(); const QRectF frameRectF = windowFrameRect();
const QRect titleBarRect(frameRectF.toRect().x(), frameRectF.toRect().y(), frameRectF.toRect().width(),
static_cast<int>(kTitleBarHeight));
// query the style for the close button position (handles macOS top-left placement) // query the style for the close button position (handles macOS top-left placement)
// Title bar rect MUST be local (0,0-based) for QStyle
const QRect titleBarRect(0, 0, static_cast<int>(frameRectF.width()), static_cast<int>(kTitleBarHeight));
if (styleWidget) { if (styleWidget) {
QStyleOptionTitleBar opt; QStyleOptionTitleBar opt;
opt.initFrom(styleWidget); opt.initFrom(styleWidget);
@ -283,19 +284,26 @@ QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
opt.text = windowTitle(); opt.text = windowTitle();
opt.icon = styleWidget->windowIcon(); opt.icon = styleWidget->windowIcon();
opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint; opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint;
opt.subControls = QStyle::SC_TitleBarCloseButton; opt.subControls = QStyle::SC_TitleBarCloseButton;
opt.activeSubControls = QStyle::SC_TitleBarCloseButton; opt.activeSubControls = QStyle::SC_TitleBarCloseButton;
opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState; opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState;
if (styleWidget->isActiveWindow())
if (styleWidget->isActiveWindow()) {
opt.state |= QStyle::State_Active; opt.state |= QStyle::State_Active;
const QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton, }
QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton,
styleWidget); styleWidget);
if (r.isValid() && !r.isEmpty()) { if (r.isValid() && !r.isEmpty()) {
// Translate from local-titlebar coords → frame coords
r.translate(frameRectF.topLeft().toPoint());
return QRectF(r); return QRectF(r);
} }
} }
// fallback: square at right end of titlebar (Windows/Linux style) // Fallback: frame-relative top-right
return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight); return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight);
} }
@ -349,6 +357,7 @@ bool ZoneViewWidget::windowFrameEvent(QEvent *event)
close(); close();
return true; return true;
} }
startWindowDrag(me); startWindowDrag(me);
me->accept(); me->accept();
return true; return true;

View file

@ -54,7 +54,6 @@ private:
ZoneViewZone *zone; ZoneViewZone *zone;
QGraphicsWidget *zoneContainer; QGraphicsWidget *zoneContainer;
QPushButton *closeButton;
QScrollBar *scrollBar; QScrollBar *scrollBar;
ScrollableGraphicsProxyWidget *scrollBarProxy; ScrollableGraphicsProxyWidget *scrollBarProxy;

View file

@ -0,0 +1,48 @@
#include "abstract_analytics_panel_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QPushButton>
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;
}

View file

@ -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 <QDialog>
#include <QJsonObject>
#include <QVBoxLayout>
#include <QWidget>
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

View file

@ -0,0 +1,32 @@
#include "add_analytics_panel_dialog.h"
#include "analytics_panel_widget_factory.h"
#include <QDialogButtonBox>
#include <QVBoxLayout>
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);
}

View file

@ -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 <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QVBoxLayout>
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

View file

@ -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::Descriptor> AnalyticsPanelWidgetFactory::availableWidgets() const
{
return widgets.values();
}

View file

@ -0,0 +1,44 @@
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
#include <QMap>
#include <QString>
#include <QStringList>
#include <QWidget>
#include <functional>
class AbstractAnalyticsPanelWidget;
class DeckListStatisticsAnalyzer;
class AnalyticsPanelWidgetFactory
{
public:
using Creator = std::function<AbstractAnalyticsPanelWidget *(QWidget *, DeckListStatisticsAnalyzer *)>;
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<Descriptor> availableWidgets() const;
private:
AnalyticsPanelWidgetFactory() = default; // Ensure private constructor
AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete;
AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete;
QMap<QString, Descriptor> widgets;
};
#endif

View file

@ -0,0 +1 @@
#include "analytics_panel_widget_registrar.h"

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,35 +1,298 @@
#include "deck_analytics_widget.h" #include "deck_analytics_widget.h"
DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel) #include "abstract_analytics_panel_widget.h"
: QWidget(parent), deckListModel(_deckListModel) #include "add_analytics_panel_dialog.h"
{ #include "analytics_panel_widget_factory.h"
mainLayout = new QVBoxLayout(); #include "analyzer_modules/mana_base/mana_base_config.h"
setLayout(mainLayout); #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 <QEvent>
#include <QJsonArray>
#include <QJsonDocument>
#include <QPushButton>
#include <QScrollArea>
#include <QSettings>
#include <QVBoxLayout>
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 = new QScrollArea(this);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setWidgetResizable(true); scrollArea->setWidgetResizable(true);
mainLayout->addWidget(scrollArea); scrollArea->setFrameShape(QFrame::NoFrame);
container = new QWidget(scrollArea); panelContainer = new QWidget(scrollArea);
containerLayout = new QVBoxLayout(container); panelLayout = new QVBoxLayout(panelContainer);
container->setLayout(containerLayout); panelLayout->setSpacing(8);
scrollArea->setWidget(container); 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); loadLayout();
containerLayout->addWidget(manaCurveWidget);
manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer); retranslateUi();
containerLayout->addWidget(manaDevotionWidget);
manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer);
containerLayout->addWidget(manaBaseWidget);
} }
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<DefaultPanel> 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);
} }

View file

@ -1,44 +1,71 @@
/** /**
* @file deck_analytics_widget.h * @file deck_analytics_widget.h
* @ingroup DeckEditorAnalyticsWidgets * @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this. * @brief Main analytics widget container with resizable panels for deck statistics.
*/ */
#ifndef DECK_ANALYTICS_WIDGET_H #ifndef DECK_ANALYTICS_WIDGET_H
#define DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H
#include "mana_base_widget.h" #include "abstract_analytics_panel_widget.h"
#include "mana_curve_widget.h" #include "deck_list_statistics_analyzer.h"
#include "mana_devotion_widget.h" #include "resizable_panel.h"
#include <QHBoxLayout> #include <QJsonObject>
#include <QScrollArea> #include <QScrollArea>
#include <QVBoxLayout>
#include <QVector>
#include <QWidget> #include <QWidget>
#include <libcockatrice/models/deck_list/deck_list_model.h>
class LayoutInspector;
class DeckAnalyticsWidget : public QWidget class DeckAnalyticsWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
public slots:
void updateDisplays();
public: public:
explicit DeckAnalyticsWidget(QWidget *parent, DeckListModel *deckListModel); explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void setDeckList(const DeckList &_deckListModel); void retranslateUi();
std::map<int, int> analyzeManaCurve();
void refreshDisplays(); 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: private:
DeckListModel *deckListModel; void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {});
DeckListStatisticsAnalyzer *deckListStatisticsAnalyzer;
QVBoxLayout *mainLayout;
QWidget *container; QVBoxLayout *layout;
QVBoxLayout *containerLayout; QWidget *controlContainer;
QHBoxLayout *controlLayout;
QPushButton *addButton;
QPushButton *removeButton;
QPushButton *saveButton;
QPushButton *loadButton;
QScrollArea *scrollArea; QScrollArea *scrollArea;
QWidget *panelContainer;
QVBoxLayout *panelLayout;
ManaCurveWidget *manaCurveWidget; QVector<ResizablePanel *> panelWrappers;
ManaDevotionWidget *manaDevotionWidget; ResizablePanel *selectedWrapper = nullptr;
ManaBaseWidget *manaBaseWidget;
DeckListStatisticsAnalyzer *statsAnalyzer;
LayoutInspector *insp = nullptr;
}; };
#endif // DECK_ANALYTICS_WIDGET_H #endif // DECK_ANALYTICS_WIDGET_H

View file

@ -9,40 +9,95 @@
DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent, DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent,
DeckListModel *_model, DeckListModel *_model,
DeckListStatisticsAnalyzerConfig cfg) DeckListStatisticsAnalyzerConfig _config)
: QObject(parent), model(_model), config(cfg) : 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(); clearData();
manaCurveMap.clear();
manaDevotionMap.clear();
QList<ExactCard> cards = model->getCards(); QList<ExactCard> cards = model->getCards();
for (const ExactCard &card : cards) { for (auto card : cards) {
// ---- Mana curve ---- 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) { 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) { if (config.computeManaBase) {
auto mana = determineManaProduction(card.getInfo().getText()); auto prod = determineManaProduction(info.getText());
for (auto it = mana.begin(); it != mana.end(); ++it) 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(); manaBaseMap[it.key()] += it.value();
} }
}
// ---- Devotion ---- // ---------------- Devotion ----------------
if (config.computeDevotion) { if (config.computeDevotion) {
auto devo = countManaSymbols(card.getInfo().getManaCost()); auto devo = countManaSymbols(info.getManaCost());
for (auto &d : devo) for (auto &d : devo) {
if (d.second > 0) {
devotionPipCount[QString(d.first)] += d.second;
devotionCardCount[QString(d.first)]++;
}
manaDevotionMap[d.first] += d.second; manaDevotionMap[d.first] += d.second;
} }
} }
}
emit statsUpdated(); emit statsUpdated();
} }
@ -112,3 +167,57 @@ std::unordered_map<char, int> DeckListStatisticsAnalyzer::countManaSymbols(const
return manaCounts; 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();
}

View file

@ -14,6 +14,9 @@ struct DeckListStatisticsAnalyzerConfig
bool computeManaBase = true; bool computeManaBase = true;
bool computeManaCurve = true; bool computeManaCurve = true;
bool computeDevotion = true; bool computeDevotion = true;
bool computeCategories = true;
bool computeCurveBreakdowns = true;
bool computeProbabilities = true;
}; };
class DeckListStatisticsAnalyzer : public QObject class DeckListStatisticsAnalyzer : public QObject
@ -23,9 +26,9 @@ class DeckListStatisticsAnalyzer : public QObject
public: public:
explicit DeckListStatisticsAnalyzer(QObject *parent, explicit DeckListStatisticsAnalyzer(QObject *parent,
DeckListModel *model, DeckListModel *model,
DeckListStatisticsAnalyzerConfig cfg = DeckListStatisticsAnalyzerConfig()); DeckListStatisticsAnalyzerConfig _config = DeckListStatisticsAnalyzerConfig());
void update(); void analyze();
[[nodiscard]] const QHash<QString, int> &getManaBase() const [[nodiscard]] const QHash<QString, int> &getManaBase() const
{ {
@ -40,6 +43,96 @@ public:
return manaDevotionMap; return manaDevotionMap;
} }
const QHash<QString, int> &getDevotionPipCount() const
{
return devotionPipCount;
}
const QHash<QString, int> &getDevotionCardCount() const
{
return devotionCardCount;
}
const QHash<QString, int> &getProductionPipCount() const
{
return productionPipCount;
}
const QHash<QString, int> &getProductionCardCount() const
{
return productionCardCount;
}
const QHash<QString, int> &getTypeCount() const
{
return typeCount;
}
const QHash<QString, int> &getSubtypeCount() const
{
return subtypeCount;
}
const QHash<QString, int> &getColorCount() const
{
return colorCount;
}
const QHash<QString, int> &getRarityCount() const
{
return rarityCount;
}
const QHash<int, int> &getManaValueCount() const
{
return manaValueCount;
}
const QHash<QString, QHash<int, int>> &getManaCurveByType() const
{
return manaCurveByType;
}
const QHash<QString, QHash<int, int>> &getManaCurveBySubtype() const
{
return manaCurveBySubtype;
}
const QHash<QString, QHash<int, int>> &getManaCurveByColor() const
{
return manaCurveByColor;
}
const QHash<QString, QHash<int, int>> &getManaCurveByPower() const
{
return manaCurveByPower;
}
const QHash<QString, QHash<int, int>> &getManaCurveByToughness() const
{
return manaCurveByToughness;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByType() const
{
return manaCurveCardsByType;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsBySubtype() const
{
return manaCurveCardsBySubtype;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByColor() const
{
return manaCurveCardsByColor;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByPower() const
{
return manaCurveCardsByPower;
}
const QHash<QString, QHash<int, QStringList>> &getManaCurveCardsByToughness() const
{
return manaCurveCardsByToughness;
}
DeckListModel *getModel() const
{
return model;
}
signals: signals:
void statsUpdated(); void statsUpdated();
@ -47,14 +140,42 @@ private:
DeckListModel *model; DeckListModel *model;
DeckListStatisticsAnalyzerConfig config; DeckListStatisticsAnalyzerConfig config;
// Internal result containers
QHash<QString, int> manaBaseMap; QHash<QString, int> manaBaseMap;
std::unordered_map<int, int> manaCurveMap; std::unordered_map<int, int> manaCurveMap;
std::unordered_map<char, int> manaDevotionMap; std::unordered_map<char, int> manaDevotionMap;
// Internal helper functions QHash<QString, int> devotionPipCount; // W/U/B/R/G total symbols
QHash<QString, int> devotionCardCount; // how many cards provide devotion
QHash<QString, int> productionPipCount; // mana produced by cards
QHash<QString, int> productionCardCount; // number of producers
QHash<QString, int> typeCount;
QHash<QString, int> subtypeCount;
QHash<QString, int> colorCount;
QHash<QString, int> rarityCount;
QHash<int, int> manaValueCount;
QHash<QString, QHash<int, int>> manaCurveByType;
QHash<QString, QHash<int, int>> manaCurveBySubtype;
QHash<QString, QHash<int, int>> manaCurveByColor;
QHash<QString, QHash<int, int>> manaCurveByPower;
QHash<QString, QHash<int, int>> manaCurveByToughness;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByType;
QHash<QString, QHash<int, QStringList>> manaCurveCardsBySubtype;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByColor;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByPower;
QHash<QString, QHash<int, QStringList>> manaCurveCardsByToughness;
// Not storing card info — only numeric results.
QHash<QString, QHash<int, QHash<int, double>>> probabilityExact;
QHash<QString, QHash<int, QHash<int, double>>> probabilityAtLeast;
QHash<QString, int> determineManaProduction(const QString &); QHash<QString, int> determineManaProduction(const QString &);
std::unordered_map<char, int> countManaSymbols(const QString &); std::unordered_map<char, int> countManaSymbols(const QString &);
double hypergeometric(int N, int K, int n, int k);
void clearData();
}; };
#endif // COCKATRICE_DECK_LIST_STATISTICS_ANALYZER_H #endif // COCKATRICE_DECK_LIST_STATISTICS_ANALYZER_H

View file

@ -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 <QHash>
#include <QRegularExpression>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
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<QString, QColor> 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();
}

View file

@ -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 <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
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

View file

@ -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 <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <unordered_map>
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<int, int> 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
}

View file

@ -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 <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <unordered_map>
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

View file

@ -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 <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <regex>
#include <unordered_map>
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<char, QColor> 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
}

View file

@ -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 <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
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

View file

@ -0,0 +1,367 @@
#include "resizable_panel.h"
#include "libcockatrice/utility/qt_utils.h"
#include <QColor>
#include <QHBoxLayout>
#include <QPixmap>
#include <QtGlobal>
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<QMouseEvent *>(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<QMouseEvent *>(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<QMouseEvent *>(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<QMouseEvent *>(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<const quintptr *>(data.constData());
ResizablePanel *draggedPanel = reinterpret_cast<ResizablePanel *>(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<quintptr>(this);
QByteArray data(reinterpret_cast<const char *>(&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<QScrollArea>(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();
}

View file

@ -0,0 +1,79 @@
#ifndef COCKATRICE_RESIZABLE_PANEL_H
#define COCKATRICE_RESIZABLE_PANEL_H
#include "abstract_analytics_panel_widget.h"
#include <QApplication>
#include <QDrag>
#include <QFrame>
#include <QMimeData>
#include <QMouseEvent>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
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

View file

@ -64,6 +64,8 @@ void DeckEditorDeckDockWidget::createDeckDock()
connect(deckStateManager, &DeckStateManager::focusIndexChanged, this, &DeckEditorDeckDockWidget::setSelectedIndex); connect(deckStateManager, &DeckStateManager::focusIndexChanged, this, &DeckEditorDeckDockWidget::setSelectedIndex);
connect(deckStateManager, &DeckStateManager::deckReplaced, this, connect(deckStateManager, &DeckStateManager::deckReplaced, this,
&DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel);
connect(deckStateManager, &DeckStateManager::deckReplaced, this,
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
deckView = new QTreeView(); deckView = new QTreeView();
deckView->setObjectName("deckView"); deckView->setObjectName("deckView");
@ -174,11 +176,8 @@ void DeckEditorDeckDockWidget::createDeckDock()
activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE); activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE);
activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST); activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST);
activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR); activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR);
connect(activeGroupCriteriaComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() { connect(activeGroupCriteriaComboBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
getModel()->setActiveGroupCriteria(static_cast<DeckListModelGroupCriteria::Type>( &DeckEditorDeckDockWidget::applyActiveGroupCriteria);
activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
});
aIncrement = new QAction(QString(), this); aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment")); aIncrement->setIcon(QPixmap("theme:icons/increment"));
@ -297,7 +296,6 @@ void DeckEditorDeckDockWidget::initializeFormats()
QString format = deckStateManager->getMetadata().gameFormat; QString format = deckStateManager->getMetadata().gameFormat;
if (!format.isEmpty()) { if (!format.isEmpty()) {
getModel()->setActiveFormat(format);
formatComboBox->setCurrentIndex(formatComboBox->findData(format)); formatComboBox->setCurrentIndex(formatComboBox->findData(format));
} else { } else {
// Ensure no selection is visible initially // Ensure no selection is visible initially
@ -429,6 +427,13 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index)
deckStateManager->setBannerCard(bannerCard); deckStateManager->setBannerCard(bannerCard);
} }
void DeckEditorDeckDockWidget::applyActiveGroupCriteria()
{
getModel()->setActiveGroupCriteria(
static_cast<DeckListModelGroupCriteria::Type>(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
}
void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible) void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible)
{ {
bannerCardLabel->setHidden(!visible); bannerCardLabel->setHidden(!visible);
@ -477,16 +482,14 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
updateBannerCardComboBox(); updateBannerCardComboBox();
bannerCardComboBox->blockSignals(false); bannerCardComboBox->blockSignals(false);
updateHash(); updateHash();
sortDeckModelToDeckView();
deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); formatComboBox->blockSignals(true);
}
void DeckEditorDeckDockWidget::sortDeckModelToDeckView()
{
getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
getModel()->setActiveFormat(deckStateManager->getMetadata().gameFormat);
formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat));
formatComboBox->blockSignals(false);
deckTagsDisplayWidget->blockSignals(true);
deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags);
deckTagsDisplayWidget->blockSignals(false);
} }
/** /**
@ -575,18 +578,19 @@ void DeckEditorDeckDockWidget::expandAll()
} }
/** /**
* Gets the index of all the currently selected card nodes in the decklist table. * Gets the source index of all the currently selected card nodes in the decklist table.
* The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them. * The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them.
* *
* @return A model index list containing all selected card nodes * @return A list containing the source indices of all selected card nodes
*/ */
QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodeSourceIndices() const
{ {
auto selectedRows = deckView->selectionModel()->selectedRows(); auto selectedRows = deckView->selectionModel()->selectedRows();
const auto notLeafNode = [this](const QModelIndex &index) { const auto mapToSource = [this](const QModelIndex &index) { return proxy->mapToSource(index); };
return getModel()->hasChildren(proxy->mapToSource(index)); std::transform(selectedRows.begin(), selectedRows.end(), selectedRows.begin(), mapToSource);
};
const auto notLeafNode = [this](const QModelIndex &sourceIndex) { return getModel()->hasChildren(sourceIndex); };
selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end()); selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end());
std::reverse(selectedRows.begin(), selectedRows.end()); std::reverse(selectedRows.begin(), selectedRows.end());
@ -605,10 +609,10 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString &
void DeckEditorDeckDockWidget::actIncrementSelection() void DeckEditorDeckDockWidget::actIncrementSelection()
{ {
auto selectedRows = getSelectedCardNodes(); auto selectedRows = getSelectedCardNodeSourceIndices();
for (const auto &index : selectedRows) { for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(index, true); offsetCountAtIndex(sourceIndex, true);
} }
} }
@ -627,7 +631,7 @@ void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString
void DeckEditorDeckDockWidget::actSwapSelection() void DeckEditorDeckDockWidget::actSwapSelection()
{ {
auto selectedRows = getSelectedCardNodes(); auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@ -635,8 +639,8 @@ void DeckEditorDeckDockWidget::actSwapSelection()
deckView->setSelectionMode(QAbstractItemView::SingleSelection); deckView->setSelectionMode(QAbstractItemView::SingleSelection);
} }
for (const auto &currentIndex : selectedRows) { for (const auto &sourceIndex : selectedRows) {
deckStateManager->swapCardAtIndex(currentIndex); deckStateManager->swapCardAtIndex(sourceIndex);
} }
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@ -656,7 +660,7 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z
void DeckEditorDeckDockWidget::actDecrementSelection() void DeckEditorDeckDockWidget::actDecrementSelection()
{ {
auto selectedRows = getSelectedCardNodes(); auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@ -664,8 +668,8 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
deckView->setSelectionMode(QAbstractItemView::SingleSelection); deckView->setSelectionMode(QAbstractItemView::SingleSelection);
} }
for (const auto &index : selectedRows) { for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(index, false); offsetCountAtIndex(sourceIndex, false);
} }
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@ -673,7 +677,7 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
void DeckEditorDeckDockWidget::actRemoveCard() void DeckEditorDeckDockWidget::actRemoveCard()
{ {
auto selectedRows = getSelectedCardNodes(); auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@ -681,8 +685,8 @@ void DeckEditorDeckDockWidget::actRemoveCard()
deckView->setSelectionMode(QAbstractItemView::SingleSelection); deckView->setSelectionMode(QAbstractItemView::SingleSelection);
} }
for (const auto &row : selectedRows) { for (const auto &sourceIndex : selectedRows) {
deckStateManager->removeCardAtIndex(row); deckStateManager->removeCardAtIndex(sourceIndex);
} }
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@ -690,7 +694,7 @@ void DeckEditorDeckDockWidget::actRemoveCard()
/** /**
* @brief Increments or decrements the amount of the card node at the index by 1. * @brief Increments or decrements the amount of the card node at the index by 1.
* @param idx The proxy index * @param idx The source index
* @param isIncrement If true, increments the count. If false, decrements the count * @param isIncrement If true, increments the count. If false, decrements the count
*/ */
void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement) void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement)
@ -699,12 +703,10 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i
return; return;
} }
QModelIndex sourceIndex = proxy->mapToSource(idx);
if (isIncrement) { if (isIncrement) {
deckStateManager->incrementCountAtIndex(sourceIndex); deckStateManager->incrementCountAtIndex(idx);
} else { } else {
deckStateManager->decrementCountAtIndex(sourceIndex); deckStateManager->decrementCountAtIndex(idx);
} }
} }

View file

@ -51,7 +51,6 @@ public slots:
void selectNextCard(); void selectNextCard();
void updateBannerCardComboBox(); void updateBannerCardComboBox();
void syncDisplayWidgetsToModel(); void syncDisplayWidgetsToModel();
void sortDeckModelToDeckView();
void actAddCard(const ExactCard &card, const QString &zoneName); void actAddCard(const ExactCard &card, const QString &zoneName);
void actIncrementSelection(); void actIncrementSelection();
void actDecrementCard(const ExactCard &card, QString zoneName); void actDecrementCard(const ExactCard &card, QString zoneName);
@ -91,7 +90,7 @@ private:
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
DeckListModel *getModel() const; DeckListModel *getModel() const;
[[nodiscard]] QModelIndexList getSelectedCardNodes() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
private slots: private slots:
@ -100,6 +99,7 @@ private slots:
void writeName(); void writeName();
void writeComments(); void writeComments();
void writeBannerCard(int); void writeBannerCard(int);
void applyActiveGroupCriteria();
void setSelectedIndex(const QModelIndex &newCardIndex); void setSelectedIndex(const QModelIndex &newCardIndex);
void updateHash(); void updateHash();
void refreshShortcuts(); void refreshShortcuts();

View file

@ -462,8 +462,13 @@ AppearanceSettingsPage::AppearanceSettingsPage()
showShortcutsCheckBox.setChecked(settings.getShowShortcuts()); showShortcutsCheckBox.setChecked(settings.getShowShortcuts());
connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged);
showGameSelectorFilterToolbarCheckBox.setChecked(settings.getShowGameSelectorFilterToolbar());
connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings,
&SettingsCache::setShowGameSelectorFilterToolbar);
auto *menuGrid = new QGridLayout; auto *menuGrid = new QGridLayout;
menuGrid->addWidget(&showShortcutsCheckBox, 0, 0); menuGrid->addWidget(&showShortcutsCheckBox, 0, 0);
menuGrid->addWidget(&showGameSelectorFilterToolbarCheckBox, 1, 0);
menuGroupBox = new QGroupBox; menuGroupBox = new QGroupBox;
menuGroupBox->setLayout(menuGrid); menuGroupBox->setLayout(menuGrid);
@ -727,6 +732,7 @@ void AppearanceSettingsPage::retranslateUi()
menuGroupBox->setTitle(tr("Menu settings")); menuGroupBox->setTitle(tr("Menu settings"));
showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus")); showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus"));
showGameSelectorFilterToolbarCheckBox.setText(tr("Show game filter toolbar above list in room tab"));
cardsGroupBox->setTitle(tr("Card rendering")); cardsGroupBox->setTitle(tr("Card rendering"));
displayCardNamesCheckBox.setText(tr("Display card names on cards having a picture")); displayCardNamesCheckBox.setText(tr("Display card names on cards having a picture"));

View file

@ -120,6 +120,7 @@ private:
QLabel minPlayersForMultiColumnLayoutLabel; QLabel minPlayersForMultiColumnLayoutLabel;
QLabel maxFontSizeForCardsLabel; QLabel maxFontSizeForCardsLabel;
QCheckBox showShortcutsCheckBox; QCheckBox showShortcutsCheckBox;
QCheckBox showGameSelectorFilterToolbarCheckBox;
QCheckBox displayCardNamesCheckBox; QCheckBox displayCardNamesCheckBox;
QCheckBox autoRotateSidewaysLayoutCardsCheckBox; QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
#include "color_bar.h" #include "color_bar.h"
#include <QLinearGradient> #include <QLinearGradient>

View file

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

View file

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

View file

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

View file

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

View file

@ -73,6 +73,12 @@ GameSelector::GameSelector(AbstractClient *_client,
if (showFilters && restoresettings) { if (showFilters && restoresettings) {
quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap); quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap);
quickFilterToolBar->setVisible(showFilters && restoresettings &&
SettingsCache::instance().getShowGameSelectorFilterToolbar());
connect(&SettingsCache::instance(), &SettingsCache::showGameSelectorFilterToolbarChanged, this, [this] {
quickFilterToolBar->setVisible(SettingsCache::instance().getShowGameSelectorFilterToolbar());
});
} else { } else {
quickFilterToolBar = nullptr; quickFilterToolBar = nullptr;
} }

View file

@ -3,7 +3,7 @@
#include "../../../../../card_picture_loader/card_picture_loader.h" #include "../../../../../card_picture_loader/card_picture_loader.h"
#include "../../../../cards/card_info_picture_with_text_overlay_widget.h" #include "../../../../cards/card_info_picture_with_text_overlay_widget.h"
#include "../../../../general/display/background_plate_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 "archidekt_deck_preview_image_display_widget.h"
#include <QHBoxLayout> #include <QHBoxLayout>

View file

@ -7,7 +7,7 @@
#ifndef EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H #ifndef EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H
#define 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 "../../api_response/cards/edhrec_api_response_card_details.h"
#include <QLabel> #include <QLabel>

View file

@ -7,7 +7,7 @@
#ifndef EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H #ifndef EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H
#define 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 "../../api_response/cards/edhrec_api_response_card_details.h"
#include <QLabel> #include <QLabel>

View file

@ -85,7 +85,7 @@ void TabDeckEditorVisual::onDeckChanged()
{ {
AbstractTabDeckEditor::onDeckModified(); AbstractTabDeckEditor::onDeckModified();
tabContainer->visualDeckView->constructZoneWidgetsFromDeckListModel(); tabContainer->visualDeckView->constructZoneWidgetsFromDeckListModel();
tabContainer->deckAnalytics->refreshDisplays(); tabContainer->deckAnalytics->updateDisplays();
tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel()); tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel());
} }

View file

@ -45,10 +45,13 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent,
connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this, connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this,
&TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay); &TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay);
deckAnalytics = new DeckAnalyticsWidget(this, deckModel); statsAnalyzer = new DeckListStatisticsAnalyzer(this, deckModel);
statsAnalyzer->analyze();
deckAnalytics = new DeckAnalyticsWidget(this, statsAnalyzer);
deckAnalytics->setObjectName("deckAnalytics"); deckAnalytics->setObjectName("deckAnalytics");
sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel); sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel, statsAnalyzer);
this->addNewTab(visualDeckView, tr("Visual Deck View")); this->addNewTab(visualDeckView, tr("Visual Deck View"));
this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display")); this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display"));

View file

@ -79,6 +79,7 @@ public:
[[nodiscard]] int getTabCount() const; [[nodiscard]] int getTabCount() const;
VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget. VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget.
DeckListStatisticsAnalyzer *statsAnalyzer;
DeckAnalyticsWidget *deckAnalytics; ///< Deck analytics widget. DeckAnalyticsWidget *deckAnalytics; ///< Deck analytics widget.
VisualDatabaseDisplayWidget *visualDatabaseDisplay; ///< Database display widget. VisualDatabaseDisplayWidget *visualDatabaseDisplay; ///< Database display widget.
PrintingSelector *printingSelector; ///< Printing selector widget. PrintingSelector *printingSelector; ///< Printing selector widget.

View file

@ -3,12 +3,16 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h" #include "../../deck_loader/deck_loader.h"
#include "../cards/card_info_picture_widget.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 <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <random> #include <random>
VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *_deckListModel) VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent,
: QWidget(parent), deckListModel(_deckListModel) DeckListModel *_deckListModel,
DeckListStatisticsAnalyzer *_statsAnalyzer)
: QWidget(parent), deckListModel(_deckListModel), statsAnalyzer(_statsAnalyzer)
{ {
layout = new QVBoxLayout(this); layout = new QVBoxLayout(this);
setLayout(layout); setLayout(layout);
@ -35,6 +39,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
layout->addWidget(flowWidget); layout->addWidget(flowWidget);
drawProbabilityWidget = new DrawProbabilityWidget(this, statsAnalyzer);
layout->addWidget(drawProbabilityWidget);
cardSizeWidget = new CardSizeWidget(this, flowWidget); cardSizeWidget = new CardSizeWidget(this, flowWidget);
layout->addWidget(cardSizeWidget); layout->addWidget(cardSizeWidget);

View file

@ -8,6 +8,7 @@
#define VISUAL_DECK_EDITOR_SAMPLE_HAND_WIDGET_H #define VISUAL_DECK_EDITOR_SAMPLE_HAND_WIDGET_H
#include "../cards/card_size_widget.h" #include "../cards/card_size_widget.h"
#include "../deck_analytics/deck_list_statistics_analyzer.h"
#include "../general/layout_containers/flow_widget.h" #include "../general/layout_containers/flow_widget.h"
#include <QPushButton> #include <QPushButton>
@ -15,11 +16,14 @@
#include <QWidget> #include <QWidget>
#include <libcockatrice/models/deck_list/deck_list_model.h> #include <libcockatrice/models/deck_list/deck_list_model.h>
class DrawProbabilityWidget;
class VisualDeckEditorSampleHandWidget : public QWidget class VisualDeckEditorSampleHandWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *deckListModel); VisualDeckEditorSampleHandWidget(QWidget *parent,
DeckListModel *deckListModel,
DeckListStatisticsAnalyzer *statsAnalyzer);
QList<ExactCard> getRandomCards(int amountToGet); QList<ExactCard> getRandomCards(int amountToGet);
public slots: public slots:
@ -29,12 +33,14 @@ public slots:
private: private:
DeckListModel *deckListModel; DeckListModel *deckListModel;
DeckListStatisticsAnalyzer *statsAnalyzer;
QVBoxLayout *layout; QVBoxLayout *layout;
QWidget *resetAndHandSizeContainerWidget; QWidget *resetAndHandSizeContainerWidget;
QHBoxLayout *resetAndHandSizeLayout; QHBoxLayout *resetAndHandSizeLayout;
QPushButton *resetButton; QPushButton *resetButton;
QSpinBox *handSizeSpinBox; QSpinBox *handSizeSpinBox;
FlowWidget *flowWidget; FlowWidget *flowWidget;
DrawProbabilityWidget *drawProbabilityWidget;
CardSizeWidget *cardSizeWidget; CardSizeWidget *cardSizeWidget;
}; };

File diff suppressed because it is too large Load diff

View file

@ -7,4 +7,4 @@ This is the **main landing page** of the Cockatrice documentation.
- Go to the @subpage user_reference page - Go to the @subpage user_reference page
- Review the @subpage developer_reference - Review the @subpage developer_reference
Or check out the [Cockatrice Webpage](https://cockatrice.github.io/). Or check out the <a href="https://cockatrice.github.io/" target="_blank" rel="noopener noreferrer">Cockatrice Webpage</a>.

View file

@ -9,7 +9,9 @@ set(HEADERS
libcockatrice/deck_list/tree/inner_deck_list_node.h libcockatrice/deck_list/tree/inner_deck_list_node.h
libcockatrice/deck_list/deck_list.h libcockatrice/deck_list/deck_list.h
libcockatrice/deck_list/deck_list_history_manager.h libcockatrice/deck_list/deck_list_history_manager.h
libcockatrice/deck_list/deck_list_node_tree.h
libcockatrice/deck_list/deck_list_memento.h libcockatrice/deck_list/deck_list_memento.h
libcockatrice/deck_list/sideboard_plan.h
) )
if(Qt6_FOUND) if(Qt6_FOUND)
@ -28,7 +30,7 @@ add_library(
libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/deck_list.cpp
libcockatrice/deck_list/deck_list_history_manager.cpp libcockatrice/deck_list/deck_list_history_manager.cpp
libcockatrice/deck_list/deck_list_node_tree.cpp libcockatrice/deck_list/deck_list_node_tree.cpp
libcockatrice/deck_list/deck_list_node_tree.h libcockatrice/deck_list/sideboard_plan.cpp
) )
add_dependencies(libcockatrice_deck_list libcockatrice_protocol) add_dependencies(libcockatrice_deck_list libcockatrice_protocol)

View file

@ -21,61 +21,7 @@ uint qHash(const QRegularExpression &key, uint seed) noexcept
} }
#endif #endif
SideboardPlan::SideboardPlan(const QString &_name, const QList<MoveCard_ToZone> &_moveList) static const QString CURRENT_SIDEBOARD_PLAN_KEY = "";
: name(_name), moveList(_moveList)
{
}
void SideboardPlan::setMoveList(const QList<MoveCard_ToZone> &_moveList)
{
moveList = _moveList;
}
bool SideboardPlan::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "name")
name = xml->readElementText();
else if (childName == "move_card_to_zone") {
MoveCard_ToZone m;
while (!xml->atEnd()) {
xml->readNext();
const QString childName2 = xml->name().toString();
if (xml->isStartElement()) {
if (childName2 == "card_name")
m.set_card_name(xml->readElementText().toStdString());
else if (childName2 == "start_zone")
m.set_start_zone(xml->readElementText().toStdString());
else if (childName2 == "target_zone")
m.set_target_zone(xml->readElementText().toStdString());
} else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) {
moveList.append(m);
break;
}
}
}
} else if (xml->isEndElement() && (childName == "sideboard_plan"))
return true;
}
return false;
}
void SideboardPlan::write(QXmlStreamWriter *xml)
{
xml->writeStartElement("sideboard_plan");
xml->writeTextElement("name", name);
for (auto &i : moveList) {
xml->writeStartElement("move_card_to_zone");
xml->writeTextElement("card_name", QString::fromStdString(i.card_name()));
xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone()));
xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone()));
xml->writeEndElement();
}
xml->writeEndElement();
}
bool DeckList::Metadata::isEmpty() const bool DeckList::Metadata::isEmpty() const
{ {
@ -93,36 +39,23 @@ DeckList::DeckList(const QString &nativeString)
DeckList::DeckList(const Metadata &metadata, DeckList::DeckList(const Metadata &metadata,
const DecklistNodeTree &tree, const DecklistNodeTree &tree,
const QMap<QString, SideboardPlan *> &sideboardPlans) const QMap<QString, SideboardPlan> &sideboardPlans)
: metadata(metadata), sideboardPlans(sideboardPlans), tree(tree) : metadata(metadata), sideboardPlans(sideboardPlans), tree(tree)
{ {
} }
DeckList::~DeckList() QList<MoveCard_ToZone> DeckList::getCurrentSideboardPlan() const
{ {
QMapIterator<QString, SideboardPlan *> i(sideboardPlans); if (!sideboardPlans.contains(CURRENT_SIDEBOARD_PLAN_KEY)) {
while (i.hasNext()) return {};
delete i.next().value(); }
}
QList<MoveCard_ToZone> DeckList::getCurrentSideboardPlan() return sideboardPlans.value(CURRENT_SIDEBOARD_PLAN_KEY).getMoveList();
{
SideboardPlan *current = sideboardPlans.value(QString(), 0);
if (!current)
return QList<MoveCard_ToZone>();
else
return current->getMoveList();
} }
void DeckList::setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan) void DeckList::setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan)
{ {
SideboardPlan *current = sideboardPlans.value(QString(), 0); sideboardPlans[CURRENT_SIDEBOARD_PLAN_KEY].setMoveList(plan);
if (!current) {
current = new SideboardPlan;
sideboardPlans.insert(QString(), current);
}
current->setMoveList(plan);
} }
bool DeckList::readElement(QXmlStreamReader *xml) bool DeckList::readElement(QXmlStreamReader *xml)
@ -151,11 +84,9 @@ bool DeckList::readElement(QXmlStreamReader *xml)
} else if (childName == "zone") { } else if (childName == "zone") {
tree.readZoneElement(xml); tree.readZoneElement(xml);
} else if (childName == "sideboard_plan") { } else if (childName == "sideboard_plan") {
SideboardPlan *newSideboardPlan = new SideboardPlan; SideboardPlan newSideboardPlan;
if (newSideboardPlan->readElement(xml)) { if (newSideboardPlan.readElement(xml)) {
sideboardPlans.insert(newSideboardPlan->getName(), newSideboardPlan); sideboardPlans.insert(newSideboardPlan.getName(), newSideboardPlan);
} else {
delete newSideboardPlan;
} }
} }
} else if (xml->isEndElement() && (childName == "cockatrice_deck")) { } else if (xml->isEndElement() && (childName == "cockatrice_deck")) {
@ -194,9 +125,8 @@ void DeckList::write(QXmlStreamWriter *xml) const
tree.write(xml); tree.write(xml);
// Write sideboard plans // Write sideboard plans
QMapIterator<QString, SideboardPlan *> i(sideboardPlans); for (auto &sideboardPlan : sideboardPlans.values()) {
while (i.hasNext()) { sideboardPlan.write(xml);
i.next().value()->write(xml);
} }
xml->writeEndElement(); // Close "cockatrice_deck" xml->writeEndElement(); // Close "cockatrice_deck"

View file

@ -1,6 +1,6 @@
/** /**
* @file deck_list.h * @file deck_list.h
* @brief Defines the DeckList class and supporting types for managing a full * @brief Defines the DeckList class, which manages a full
* deck structure including cards, zones, sideboard plans, and * deck structure including cards, zones, sideboard plans, and
* serialization to/from multiple formats. This is a logic class which * serialization to/from multiple formats. This is a logic class which
* does not care about Qt or user facing views. * does not care about Qt or user facing views.
@ -12,12 +12,12 @@
#include "deck_list_memento.h" #include "deck_list_memento.h"
#include "deck_list_node_tree.h" #include "deck_list_node_tree.h"
#include "sideboard_plan.h"
#include "tree/inner_deck_list_node.h" #include "tree/inner_deck_list_node.h"
#include <QMap> #include <QMap>
#include <QVector> #include <QVector>
#include <QtCore/QXmlStreamReader> #include <QtCore/QXmlStreamReader>
#include <libcockatrice/protocol/pb/move_card_to_zone.pb.h>
#include <libcockatrice/utility/card_ref.h> #include <libcockatrice/utility/card_ref.h>
class AbstractDecklistNode; class AbstractDecklistNode;
@ -27,67 +27,6 @@ class QIODevice;
class QTextStream; class QTextStream;
class InnerDecklistNode; class InnerDecklistNode;
/**
* @class SideboardPlan
* @ingroup Decks
* @brief Represents a predefined sideboarding strategy for a deck.
*
* Sideboard plans store a named list of card movements that should be applied
* between the mainboard and sideboard for a specific matchup. Each movement
* is expressed using a `MoveCard_ToZone` protobuf message.
*
* ### Responsibilities:
* - Store the plan name and list of moves.
* - Support XML serialization/deserialization.
*
* ### Typical usage:
* A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"),
* each describing how to transform the main deck into its intended configuration.
*/
class SideboardPlan
{
private:
QString name; ///< Human-readable name of this plan.
QList<MoveCard_ToZone> moveList; ///< List of move instructions for this plan.
public:
/**
* @brief Construct a new SideboardPlan.
* @param _name The plan name.
* @param _moveList Initial list of card move instructions.
*/
explicit SideboardPlan(const QString &_name = QString(),
const QList<MoveCard_ToZone> &_moveList = QList<MoveCard_ToZone>());
/**
* @brief Read a SideboardPlan from an XML stream.
* @param xml XML reader positioned at the plan element.
* @return true if parsing succeeded.
*/
bool readElement(QXmlStreamReader *xml);
/**
* @brief Write this SideboardPlan to XML.
* @param xml Stream to append the serialized element to.
*/
void write(QXmlStreamWriter *xml);
/// @return The plan name.
[[nodiscard]] QString getName() const
{
return name;
}
/// @return Const reference to the move list.
[[nodiscard]] const QList<MoveCard_ToZone> &getMoveList() const
{
return moveList;
}
/// @brief Replace the move list with a new one.
void setMoveList(const QList<MoveCard_ToZone> &_moveList);
};
/** /**
* @class DeckList * @class DeckList
* @ingroup Decks * @ingroup Decks
@ -140,7 +79,7 @@ public:
private: private:
Metadata metadata; ///< Deck metadata that is stored in the deck file Metadata metadata; ///< Deck metadata that is stored in the deck file
QMap<QString, SideboardPlan *> sideboardPlans; ///< Named sideboard plans. QMap<QString, SideboardPlan> sideboardPlans; ///< Named sideboard plans.
DecklistNodeTree tree; ///< The deck tree (zones + cards). DecklistNodeTree tree; ///< The deck tree (zones + cards).
/** /**
@ -193,8 +132,7 @@ public:
/// @brief Construct from components /// @brief Construct from components
DeckList(const Metadata &metadata, DeckList(const Metadata &metadata,
const DecklistNodeTree &tree, const DecklistNodeTree &tree,
const QMap<QString, SideboardPlan *> &sideboardPlans = {}); const QMap<QString, SideboardPlan> &sideboardPlans = {});
virtual ~DeckList();
/** /**
* @brief Gets a pointer to the underlying node tree. * @brief Gets a pointer to the underlying node tree.
@ -247,9 +185,9 @@ public:
/// @name Sideboard plans /// @name Sideboard plans
///@{ ///@{
QList<MoveCard_ToZone> getCurrentSideboardPlan(); QList<MoveCard_ToZone> getCurrentSideboardPlan() const;
void setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan); void setCurrentSideboardPlan(const QList<MoveCard_ToZone> &plan);
const QMap<QString, SideboardPlan *> &getSideboardPlans() const const QMap<QString, SideboardPlan> &getSideboardPlans() const
{ {
return sideboardPlans; return sideboardPlans;
} }

Some files were not shown because too many files have changed in this diff Show more