diff --git a/README.md b/README.md
index 3d01e9f3d..c08aee0e7 100644
--- a/README.md
+++ b/README.md
@@ -44,10 +44,10 @@ Latest beta version:
# Related Repositories
-- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice
-- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to use in Cockatrice
+- [Magic-Token](https://github.com/Cockatrice/Magic-Token): File with MtG token data for use in Cockatrice
+- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Code to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) for use in Cockatrice
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage
-- [Cockatrice @Flathub](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration for our Linux `flatpak` package
+- [io.github.Cockatrice.cockatrice](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration of our Linux `flatpak` package hosted at [Flathub](https://flathub.org/en/apps/io.github.Cockatrice.cockatrice)
# Community Resources [](https://discord.gg/3Z9yzmA)
@@ -55,7 +55,6 @@ Latest beta version:
Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out.
- [Official Website](https://cockatrice.github.io)
- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki)
-- [Official Code Documentation](https://cockatrice.github.io/docs)
- [Official Discord](https://discord.gg/3Z9yzmA)
- [reddit r/Cockatrice](https://reddit.com/r/cockatrice)
@@ -64,6 +63,23 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
# Contribute
+
+ Code |
+ Documentation |
+ Translation
+
+
+#### Repository Activity
+
+
+
+Kudos to all our amazing contributors ❤️
+
+
+
+
+ Made with contrib.rocks
+
### Code
@@ -79,21 +95,17 @@ We'll happily advice on how best to implement a feature, or we can show you wher
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
+### Documentation [](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
+
+There are various places where useful information for different needs are maintained:
+- [Official Code Documentation](https://cockatrice.github.io/docs/)
+- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) `Community supported`
+- [Official Webpage](https://cockatrice.github.io/)
+- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file`
+
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide.
-#### Repository Activity
-
-
-
-Kudos to all our amazing contributors ❤️
-
-
-
-
- Made with contrib.rocks
-
-
-### Translations [](https://explore.transifex.com/cockatrice/cockatrice/)
+### Translation [](https://explore.transifex.com/cockatrice/cockatrice/)
Cockatrice uses Transifex to manage translations. You can help us bring Cockatrice, Oracle and Webatrice to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://explore.transifex.com/cockatrice/cockatrice/).
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index b2d387391..8ebd177db 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -144,11 +144,31 @@ set(cockatrice_SOURCES
src/interface/widgets/cards/card_size_widget.cpp
src/interface/widgets/cards/deck_card_zone_display_widget.cpp
src/interface/widgets/cards/deck_preview_card_picture_widget.cpp
+ src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
+ src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp
+ src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp
+ src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp
src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp
- src/interface/widgets/deck_analytics/mana_base_widget.cpp
- src/interface/widgets/deck_analytics/mana_curve_widget.cpp
- src/interface/widgets/deck_analytics/mana_devotion_widget.cpp
+ src/interface/widgets/deck_analytics/resizable_panel.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp
+ src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp
src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp
src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp
src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp
@@ -156,16 +176,21 @@ set(cockatrice_SOURCES
src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
+ src/interface/widgets/deck_editor/deck_state_manager.cpp
src/interface/widgets/general/background_sources.cpp
src/interface/widgets/general/display/background_plate_widget.cpp
src/interface/widgets/general/display/banner_widget.cpp
- src/interface/widgets/general/display/bar_widget.cpp
- src/interface/widgets/general/display/color_bar.cpp
src/interface/widgets/general/display/dynamic_font_size_label.cpp
src/interface/widgets/general/display/dynamic_font_size_push_button.cpp
src/interface/widgets/general/display/labeled_input.cpp
- src/interface/widgets/general/display/percent_bar_widget.cpp
src/interface/widgets/general/display/shadow_background_label.cpp
+ src/interface/widgets/general/display/charts/bars/bar_widget.cpp
+ src/interface/widgets/general/display/charts/bars/color_bar.cpp
+ src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp
+ src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp
+ src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp
+ src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp
+ src/interface/widgets/general/display/charts/pies/color_pie.cpp
src/interface/widgets/general/home_styled_button.cpp
src/interface/widgets/general/home_widget.cpp
src/interface/widgets/general/layout_containers/flow_widget.cpp
diff --git a/cockatrice/resources/help/deck_search.md b/cockatrice/resources/help/deck_search.md
index c42e41c42..4e1c5c557 100644
--- a/cockatrice/resources/help/deck_search.md
+++ b/cockatrice/resources/help/deck_search.md
@@ -1,11 +1,13 @@
@page deck_search_syntax_help Deck Search Syntax Help
## Deck Search Syntax Help
------
+
The search bar recognizes a set of special commands.
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.
+
+
- Display Name (The deck name, or the filename if the deck name isn't set):
- [red deck wins](#red deck wins) (Any deck with a display name containing the words red, deck, and wins)
- ["red deck wins"](#%22red deck wins%22) (Any deck with a display name containing the exact phrase "red deck wins")
diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md
index 3b3f0cc93..6f25e4013 100644
--- a/cockatrice/resources/help/search.md
+++ b/cockatrice/resources/help/search.md
@@ -1,10 +1,12 @@
@page search_syntax_help Search Syntax Help
## Search Syntax Help
------
+
The search bar recognizes a set of special commands similar to some other card databases.
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.
+
+
- Name:
- [birds of paradise](#birds of paradise) (Any card name containing the words birds, of, and paradise)
- ["birds of paradise"](#%22birds of paradise%22) (Any card name containing the exact phrase "birds of paradise")
diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp
index 7a518df67..2ad1f1ece 100644
--- a/cockatrice/src/client/settings/cache_settings.cpp
+++ b/cockatrice/src/client/settings/cache_settings.cpp
@@ -2,6 +2,7 @@
#include "../network/update/client/release_channel.h"
#include "card_counter_settings.h"
+#include "version_string.h"
#include
#include
@@ -198,7 +199,13 @@ SettingsCache::SettingsCache()
mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).toBool();
- checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool();
+ if (settings->contains("personal/startupUpdateCheck")) {
+ checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool();
+ } else if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) {
+ checkUpdatesOnStartup = false; // do not run auto updater on custom version
+ } else {
+ checkUpdatesOnStartup = true; // default to run auto updater
+ }
startupCardUpdateCheckPromptForUpdate =
settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool();
startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool();
@@ -206,7 +213,15 @@ SettingsCache::SettingsCache()
lastCardUpdateCheck = settings->value("personal/lastCardUpdateCheck", QDateTime::currentDateTime().date()).toDate();
notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool();
notifyAboutNewVersion = settings->value("personal/newversionnotification", true).toBool();
- updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt();
+
+ if (settings->contains("personal/updatereleasechannel")) {
+ updateReleaseChannel = settings->value("personal/updatereleasechannel").toInt();
+ } else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) {
+ // default to beta if this is a beta release
+ updateReleaseChannel = 1;
+ } else {
+ updateReleaseChannel = 0; // stable
+ }
lang = settings->value("personal/lang").toString();
keepalive = settings->value("personal/keepalive", 3).toInt();
@@ -273,6 +288,7 @@ SettingsCache::SettingsCache()
focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool();
showShortcuts = settings->value("menu/showshortcuts", true).toBool();
+ showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool();
displayCardNames = settings->value("cards/displaycardnames", true).toBool();
roundCardCorners = settings->value("cards/roundcardcorners", true).toBool();
overrideAllCardArtWithPersonalPreference =
@@ -700,6 +716,13 @@ void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts)
settings->setValue("menu/showshortcuts", showShortcuts);
}
+void SettingsCache::setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar)
+{
+ showGameSelectorFilterToolbar = static_cast(_showGameSelectorFilterToolbar);
+ settings->setValue("menu/showgameselectorfiltertoolbar", showGameSelectorFilterToolbar);
+ emit showGameSelectorFilterToolbarChanged(showGameSelectorFilterToolbar);
+}
+
void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames)
{
displayCardNames = static_cast(_displayCardNames);
diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h
index df68ce0c4..ca9a2a39b 100644
--- a/cockatrice/src/client/settings/cache_settings.h
+++ b/cockatrice/src/client/settings/cache_settings.h
@@ -145,6 +145,7 @@ signals:
void homeTabBackgroundShuffleFrequencyChanged();
void picDownloadChanged();
void showStatusBarChanged(bool state);
+ void showGameSelectorFilterToolbarChanged(bool state);
void displayCardNamesChanged();
void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference);
void bumpSetsWithCardsInDeckToTopChanged();
@@ -236,6 +237,7 @@ private:
bool annotateTokens;
QByteArray tabGameSplitterSizes;
bool showShortcuts;
+ bool showGameSelectorFilterToolbar;
bool displayCardNames;
bool overrideAllCardArtWithPersonalPreference;
bool bumpSetsWithCardsInDeckToTop;
@@ -553,6 +555,10 @@ public:
{
return showShortcuts;
}
+ [[nodiscard]] bool getShowGameSelectorFilterToolbar() const
+ {
+ return showGameSelectorFilterToolbar;
+ }
[[nodiscard]] bool getDisplayCardNames() const
{
return displayCardNames;
@@ -1017,6 +1023,7 @@ public slots:
void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens);
void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes);
void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts);
+ void setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar);
void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames);
void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt);
void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop);
diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h
index 736f0c896..156ee82ea 100644
--- a/cockatrice/src/client/settings/shortcuts_settings.h
+++ b/cockatrice/src/client/settings/shortcuts_settings.h
@@ -660,6 +660,12 @@ private:
{"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"),
parseSequenceString("Ctrl+M"),
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"),
parseSequenceString("Ctrl+D"),
ShortcutGroup::Drawing)},
diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game/player/menu/hand_menu.cpp
index e193d2e4a..019ab925c 100644
--- a/cockatrice/src/game/player/menu/hand_menu.cpp
+++ b/cockatrice/src/game/player/menu/hand_menu.cpp
@@ -60,6 +60,16 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T
connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan);
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();
mMoveHandMenu = addTearOffMenu(QString());
@@ -104,7 +114,9 @@ void HandMenu::retranslateUi()
aSortHandByType->setText(tr("Type"));
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..."));
aMoveHandToTopLibrary->setText(tr("&Top of library"));
@@ -128,6 +140,8 @@ void HandMenu::setShortcutsActive()
aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType"));
aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue"));
aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan"));
+ aMulliganSame->setShortcuts(shortcuts.getShortcut("Player/aMulliganSame"));
+ aMulliganMinusOne->setShortcuts(shortcuts.getShortcut("Player/aMulliganMinusOne"));
aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll"));
aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll"));
}
diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game/player/menu/hand_menu.h
index 2c776b857..51e071a62 100644
--- a/cockatrice/src/game/player/menu/hand_menu.h
+++ b/cockatrice/src/game/player/menu/hand_menu.h
@@ -46,6 +46,8 @@ private:
QAction *aViewHand = nullptr;
QAction *aMulligan = nullptr;
+ QAction *aMulliganSame = nullptr;
+ QAction *aMulliganMinusOne = nullptr;
QMenu *mSortHand = nullptr;
QAction *aSortHandByName = nullptr;
diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp
index 444eecb0d..ed77808b0 100644
--- a/cockatrice/src/game/player/player_actions.cpp
+++ b/cockatrice/src/game/player/player_actions.cpp
@@ -310,28 +310,48 @@ void PlayerActions::actMulligan()
{
int startSize = SettingsCache::instance().getStartingHandSize();
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;
int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"),
tr("Number of cards: (max. %1)").arg(deckSize) + '\n' +
tr("0 and lower are in comparison to current hand size"),
startSize, -handSize, deckSize, 1, &ok);
+
if (!ok) {
return;
}
- Command_Mulligan cmd;
+
if (number < 1) {
- if (handSize == 0) {
- return;
- }
- cmd.set_number(handSize + number);
- } else {
- cmd.set_number(number);
+ 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) {
+ return;
+ }
+
+ Command_Mulligan cmd;
+ cmd.set_number(number);
sendGameCommand(cmd);
- if (startSize != number) {
- SettingsCache::instance().setStartingHandSize(number);
- }
}
void PlayerActions::actDrawCards()
diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h
index e0f86e4fc..b294b5946 100644
--- a/cockatrice/src/game/player/player_actions.h
+++ b/cockatrice/src/game/player/player_actions.h
@@ -85,6 +85,9 @@ public slots:
void actDrawCards();
void actUndoDraw();
void actMulligan();
+ void actMulliganSameSize();
+ void actMulliganMinusOne();
+ void doMulligan(int number);
void actPlay();
void actPlayFacedown();
diff --git a/cockatrice/src/game/zones/view_zone_widget.cpp b/cockatrice/src/game/zones/view_zone_widget.cpp
index 91abb4663..3ea4eb119 100644
--- a/cockatrice/src/game/zones/view_zone_widget.cpp
+++ b/cockatrice/src/game/zones/view_zone_widget.cpp
@@ -13,12 +13,20 @@
#include
#include
#include
+#include
#include
#include
#include
+#include
#include
#include
+namespace
+{
+constexpr qreal kTitleBarHeight = 24.0;
+constexpr qreal kMinVisibleWidth = 100.0;
+} // namespace
+
/**
* @param _player the player the cards were revealed to.
* @param _origZone the zone the cards were revealed from.
@@ -241,33 +249,191 @@ void ZoneViewWidget::retranslateUi()
pileViewCheckBox.setText(tr("pile view"));
}
-void ZoneViewWidget::moveEvent(QGraphicsSceneMoveEvent * /* event */)
+void ZoneViewWidget::stopWindowDrag()
{
- if (!scene())
+ if (!draggingWindow)
return;
- int titleBarHeight = 24;
+ draggingWindow = false;
+ ungrabMouse();
+}
- QPointF scenePos = pos();
+void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event)
+{
+ draggingWindow = true;
+ dragStartItemPos = pos();
+ dragStartScreenPos = event->screenPos();
+ dragView = findDragView(event->widget());
- if (scenePos.x() < 0) {
- scenePos.setX(0);
- } else {
- qreal maxw = scene()->sceneRect().width() - 100;
- if (scenePos.x() > maxw)
- scenePos.setX(maxw);
+ // need to grab mouse to receive events and not miss initial movement
+ grabMouse();
+}
+
+QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
+{
+ const QRectF frameRectF = windowFrameRect();
+
+ // 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(frameRectF.width()), static_cast(kTitleBarHeight));
+
+ if (styleWidget) {
+ QStyleOptionTitleBar opt;
+ opt.initFrom(styleWidget);
+ opt.rect = titleBarRect;
+ opt.text = windowTitle();
+ opt.icon = styleWidget->windowIcon();
+ opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint;
+
+ opt.subControls = QStyle::SC_TitleBarCloseButton;
+ opt.activeSubControls = QStyle::SC_TitleBarCloseButton;
+ opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState;
+
+ if (styleWidget->isActiveWindow()) {
+ opt.state |= QStyle::State_Active;
+ }
+
+ QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton,
+ styleWidget);
+
+ if (r.isValid() && !r.isEmpty()) {
+ // Translate from local-titlebar coords → frame coords
+ r.translate(frameRectF.topLeft().toPoint());
+ return QRectF(r);
+ }
}
- if (scenePos.y() < titleBarHeight) {
- scenePos.setY(titleBarHeight);
- } else {
- qreal maxh = scene()->sceneRect().height() - titleBarHeight;
- if (scenePos.y() > maxh)
- scenePos.setY(maxh);
+ // Fallback: frame-relative top-right
+ return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight);
+}
+
+QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const
+{
+ QWidget *current = eventWidget;
+ while (current) {
+ if (auto *view = qobject_cast(current))
+ return view;
+ current = current->parentWidget();
}
- if (scenePos != pos())
- setPos(scenePos);
+ if (scene() && !scene()->views().isEmpty())
+ return scene()->views().constFirst();
+
+ return nullptr;
+}
+
+QPointF ZoneViewWidget::calcDraggedWindowPos(const QPoint &screenPos,
+ const QPointF &scenePos,
+ const QPointF &buttonDownScenePos) const
+{
+ if (dragView && dragView->viewport()) {
+ const QPoint vpStart = dragView->viewport()->mapFromGlobal(dragStartScreenPos);
+ const QPoint vpNow = dragView->viewport()->mapFromGlobal(screenPos);
+ const QPointF sceneStart = dragView->mapToScene(vpStart);
+ const QPointF sceneNow = dragView->mapToScene(vpNow);
+ return dragStartItemPos + (sceneNow - sceneStart);
+ }
+
+ return dragStartItemPos + (scenePos - buttonDownScenePos);
+}
+
+bool ZoneViewWidget::windowFrameEvent(QEvent *event)
+{
+ if (event->type() == QEvent::UngrabMouse) {
+ stopWindowDrag();
+ return QGraphicsWidget::windowFrameEvent(event);
+ }
+
+ auto *me = dynamic_cast(event);
+ if (!me)
+ return QGraphicsWidget::windowFrameEvent(event);
+
+ switch (event->type()) {
+ case QEvent::GraphicsSceneMousePress:
+ if (me->button() == Qt::LeftButton && windowFrameSectionAt(me->pos()) == Qt::TitleBarArea) {
+ // avoid drag on close button
+ if (closeButtonRect(me->widget()).contains(me->pos())) {
+ me->accept();
+ close();
+ return true;
+ }
+
+ startWindowDrag(me);
+ me->accept();
+ return true;
+ }
+ break;
+
+ case QEvent::GraphicsSceneMouseMove:
+ if (draggingWindow) {
+ if (!(me->buttons() & Qt::LeftButton)) {
+ stopWindowDrag();
+ } else {
+ setPos(
+ calcDraggedWindowPos(me->screenPos(), me->scenePos(), me->buttonDownScenePos(Qt::LeftButton)));
+ }
+ me->accept();
+ return true;
+ }
+ break;
+
+ case QEvent::GraphicsSceneMouseRelease:
+ if (draggingWindow && me->button() == Qt::LeftButton) {
+ stopWindowDrag();
+ me->accept();
+ return true;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return QGraphicsWidget::windowFrameEvent(event);
+}
+
+void ZoneViewWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
+{
+ // move if the scene routes moves while dragging
+ if (draggingWindow && (event->buttons() & Qt::LeftButton)) {
+ setPos(calcDraggedWindowPos(event->screenPos(), event->scenePos(), event->buttonDownScenePos(Qt::LeftButton)));
+ event->accept();
+ return;
+ }
+
+ QGraphicsWidget::mouseMoveEvent(event);
+}
+
+void ZoneViewWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
+{
+ if (draggingWindow && event->button() == Qt::LeftButton) {
+ stopWindowDrag();
+ event->accept();
+ return;
+ }
+
+ QGraphicsWidget::mouseReleaseEvent(event);
+}
+
+QVariant ZoneViewWidget::itemChange(GraphicsItemChange change, const QVariant &value)
+{
+ if (change == QGraphicsItem::ItemPositionChange && scene()) {
+ // Keep grab area in main view
+ const QRectF sceneRect = scene()->sceneRect();
+ const QPointF requestedPos = value.toPointF();
+ QPointF desiredPos = requestedPos;
+
+ const qreal minX = sceneRect.left();
+ const qreal maxX = qMax(minX, sceneRect.right() - kMinVisibleWidth);
+ const qreal minY = sceneRect.top() + kTitleBarHeight;
+ const qreal maxY = qMax(minY, sceneRect.bottom() - kTitleBarHeight);
+
+ desiredPos.setX(qBound(minX, desiredPos.x(), maxX));
+ desiredPos.setY(qBound(minY, desiredPos.y(), maxY));
+ return desiredPos;
+ }
+
+ return QGraphicsWidget::itemChange(change, value);
}
void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event)
@@ -350,6 +516,7 @@ void ZoneViewWidget::handleScrollBarChange(int value)
void ZoneViewWidget::closeEvent(QCloseEvent *event)
{
+ stopWindowDrag();
disconnect(zone, &ZoneViewZone::closed, this, 0);
// manually call zone->close in order to remove it from the origZones views
zone->close();
diff --git a/cockatrice/src/game/zones/view_zone_widget.h b/cockatrice/src/game/zones/view_zone_widget.h
index 272ff5560..1246192b8 100644
--- a/cockatrice/src/game/zones/view_zone_widget.h
+++ b/cockatrice/src/game/zones/view_zone_widget.h
@@ -3,7 +3,6 @@
* @ingroup GameGraphicsZones
* @brief TODO: Document this.
*/
-
#ifndef ZONEVIEWWIDGET_H
#define ZONEVIEWWIDGET_H
@@ -14,6 +13,7 @@
#include
#include
#include
+#include
#include
class QLabel;
@@ -28,6 +28,8 @@ class ServerInfo_Card;
class QGraphicsSceneMouseEvent;
class QGraphicsSceneWheelEvent;
class QStyleOption;
+class QGraphicsView;
+class QWidget;
class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget
{
@@ -52,7 +54,6 @@ private:
ZoneViewZone *zone;
QGraphicsWidget *zoneContainer;
- QPushButton *closeButton;
QScrollBar *scrollBar;
ScrollableGraphicsProxyWidget *scrollBarProxy;
@@ -66,6 +67,33 @@ private:
int extraHeight;
Player *player;
+ bool draggingWindow = false;
+ QPoint dragStartScreenPos;
+ QPointF dragStartItemPos;
+ QPointer dragView;
+
+ void stopWindowDrag();
+ void startWindowDrag(QGraphicsSceneMouseEvent *event);
+ QRectF closeButtonRect(QWidget *styleWidget) const;
+ /**
+ * @brief Resolves the QGraphicsView to use for drag coordinate mapping
+ *
+ * @param eventWidget QWidget that originated the mouse event
+ * @return The resolved QGraphicsView
+ */
+ QGraphicsView *findDragView(QWidget *eventWidget) const;
+ /**
+ * @brief Calculates the desired widget position while dragging
+ *
+ * @param screenPos Global screen coordinates of the current mouse position
+ * @param scenePos Scene coordinates of the current mouse position
+ * @param buttonDownScenePos Scene coordinates of the initial mouse press position
+ *
+ * @return The new widget position in scene coordinates
+ */
+ QPointF
+ calcDraggedWindowPos(const QPoint &screenPos, const QPointF &scenePos, const QPointF &buttonDownScenePos) const;
+
void resizeScrollbar(qreal newZoneHeight);
signals:
void closePressed(ZoneViewWidget *zv);
@@ -76,7 +104,6 @@ private slots:
void resizeToZoneContents(bool forceInitialHeight = false);
void handleScrollBarChange(int value);
void zoneDeleted();
- void moveEvent(QGraphicsSceneMoveEvent * /* event */) override;
void resizeEvent(QGraphicsSceneResizeEvent * /* event */) override;
void expandWindow();
@@ -101,6 +128,10 @@ public:
protected:
void closeEvent(QCloseEvent *event) override;
void initStyleOption(QStyleOption *option) const override;
+ bool windowFrameEvent(QEvent *event) override;
+ QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
+ void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
+ void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
};
diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
new file mode 100644
index 000000000..bad883d27
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp
@@ -0,0 +1,48 @@
+#include "abstract_analytics_panel_widget.h"
+
+#include "deck_list_statistics_analyzer.h"
+
+#include
+
+AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
+ : QWidget(parent), analyzer(analyzer)
+{
+ layout = new QVBoxLayout(this);
+
+ bannerAndSettingsContainer = new QWidget(this);
+
+ bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer);
+ bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout);
+ bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100);
+ bannerWidget->setMaximumHeight(100);
+
+ bannerAndSettingsLayout->addWidget(bannerWidget, 1);
+
+ // config button
+ configureButton = new QPushButton(tr("Configure"), this);
+ configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+ connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
+ bannerAndSettingsLayout->addWidget(configureButton, 0);
+
+ layout->addWidget(bannerAndSettingsContainer);
+
+ connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay);
+}
+
+bool AbstractAnalyticsPanelWidget::applyConfigFromDialog()
+{
+ QDialog *dlg = createConfigDialog(this);
+ if (!dlg) {
+ return false;
+ }
+
+ bool ok = dlg->exec() == QDialog::Accepted;
+ if (ok) {
+ // dialog must expose its final config as JSON
+ auto newCfg = extractConfigFromDialog(dlg);
+ loadConfig(newCfg);
+ updateDisplay();
+ }
+ dlg->deleteLater();
+ return ok;
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h
new file mode 100644
index 000000000..23374a9e1
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h
@@ -0,0 +1,61 @@
+#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
+#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
+
+#include "../general/display/banner_widget.h"
+
+#include
+#include
+#include
+#include
+
+class DeckListStatisticsAnalyzer;
+
+class AbstractAnalyticsPanelWidget : public QWidget
+{
+ Q_OBJECT
+public slots:
+ virtual void updateDisplay() = 0;
+ // Widgets must return a config dialog
+ virtual QDialog *createConfigDialog(QWidget *parent) = 0;
+
+public:
+ explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
+
+ void setDisplayTitle(const QString &title)
+ {
+ displayTitle = title;
+ if (bannerWidget) {
+ bannerWidget->setText(displayTitle);
+ }
+ }
+
+ QString displayTitleText() const
+ {
+ return displayTitle;
+ }
+
+ virtual QJsonObject saveConfig() const
+ {
+ return {};
+ }
+ virtual void loadConfig(const QJsonObject &)
+ {
+ }
+
+ // Unified helper to run config dialog and update widget
+ bool applyConfigFromDialog();
+
+ // Dialog → JSON must be supplied by each subclass
+ virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0;
+
+protected:
+ DeckListStatisticsAnalyzer *analyzer;
+ QVBoxLayout *layout;
+ QWidget *bannerAndSettingsContainer;
+ QHBoxLayout *bannerAndSettingsLayout;
+ QString displayTitle;
+ BannerWidget *bannerWidget;
+ QPushButton *configureButton;
+};
+
+#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp
new file mode 100644
index 000000000..c83e6d982
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp
@@ -0,0 +1,32 @@
+#include "add_analytics_panel_dialog.h"
+
+#include "analytics_panel_widget_factory.h"
+
+#include
+#include
+
+AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent)
+{
+ setWindowTitle(tr("Add Analytics Panel"));
+
+ layout = new QVBoxLayout(this);
+
+ typeCombo = new QComboBox(this);
+
+ // Populate using descriptors
+ const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets();
+
+ for (const auto &desc : widgets) {
+ // Show translated title to user
+ typeCombo->addItem(desc.title, desc.type);
+ }
+
+ layout->addWidget(typeCombo);
+
+ buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+
+ layout->addWidget(buttons);
+
+ connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h
new file mode 100644
index 000000000..aa44734c2
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h
@@ -0,0 +1,29 @@
+
+#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
+#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
+
+#include "analytics_panel_widget_factory.h"
+
+#include
+#include
+#include
+#include
+
+class AddAnalyticsPanelDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit AddAnalyticsPanelDialog(QWidget *parent);
+
+ QString selectedType() const
+ {
+ return typeCombo->currentData().toString();
+ }
+
+private:
+ QVBoxLayout *layout;
+ QComboBox *typeCombo;
+ QDialogButtonBox *buttons;
+};
+
+#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp
new file mode 100644
index 000000000..7af641689
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp
@@ -0,0 +1,33 @@
+#include "analytics_panel_widget_factory.h"
+
+#include "abstract_analytics_panel_widget.h"
+
+AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance()
+{
+ static AnalyticsPanelWidgetFactory f;
+ return f;
+}
+
+void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc)
+{
+ widgets.insert(desc.type, desc);
+}
+
+AbstractAnalyticsPanelWidget *
+AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const
+{
+ auto it = widgets.find(type);
+ if (it == widgets.end())
+ return nullptr;
+
+ auto w = it->creator(parent, analyzer);
+
+ w->setDisplayTitle(it->title);
+
+ return w;
+}
+
+QList AnalyticsPanelWidgetFactory::availableWidgets() const
+{
+ return widgets.values();
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h
new file mode 100644
index 000000000..6c5856d70
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h
@@ -0,0 +1,44 @@
+#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
+#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
+
+#include
+#include
+#include
+#include
+#include
+
+class AbstractAnalyticsPanelWidget;
+class DeckListStatisticsAnalyzer;
+
+class AnalyticsPanelWidgetFactory
+{
+public:
+ using Creator = std::function;
+
+ struct Descriptor
+ {
+ QString type; // stable ID ("manaProdDevotion")
+ QString title; // translated, user-facing
+ Creator creator;
+ };
+
+ static AnalyticsPanelWidgetFactory &instance();
+
+ // NEW: richer registration
+ void registerWidget(const Descriptor &desc);
+
+ AbstractAnalyticsPanelWidget *
+ create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const;
+
+ // NEW: expose widgets to UI
+ QList availableWidgets() const;
+
+private:
+ AnalyticsPanelWidgetFactory() = default; // Ensure private constructor
+ AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete;
+ AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete;
+
+ QMap widgets;
+};
+
+#endif
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp
new file mode 100644
index 000000000..d4129a3d0
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp
@@ -0,0 +1 @@
+#include "analytics_panel_widget_registrar.h"
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h
new file mode 100644
index 000000000..70d6df94f
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h
@@ -0,0 +1,17 @@
+#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
+#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
+
+#include "analytics_panel_widget_factory.h"
+
+class AnalyticsPanelWidgetRegistrar
+{
+public:
+ AnalyticsPanelWidgetRegistrar(const QString &type,
+ const QString &title,
+ AnalyticsPanelWidgetFactory::Creator creator)
+ {
+ AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator});
+ }
+};
+
+#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp
new file mode 100644
index 000000000..51129a4b4
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp
@@ -0,0 +1,28 @@
+#include "draw_probability_config.h"
+
+QJsonObject DrawProbabilityConfig::toJson() const
+{
+ QJsonObject o;
+ o["criteria"] = criteria;
+ o["atLeast"] = atLeast;
+ o["quantity"] = quantity;
+ o["drawn"] = drawn;
+ return o;
+}
+DrawProbabilityConfig DrawProbabilityConfig::fromJson(const QJsonObject &o)
+{
+ DrawProbabilityConfig cfg;
+ if (o.contains("criteria")) {
+ cfg.criteria = o["criteria"].toString();
+ }
+ if (o.contains("atLeast")) {
+ cfg.atLeast = o["atLeast"].toBool(true);
+ }
+ if (o.contains("quantity")) {
+ cfg.quantity = o["quantity"].toInt(1);
+ }
+ if (o.contains("drawn")) {
+ cfg.drawn = o["drawn"].toInt(7);
+ }
+ return cfg;
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h
new file mode 100644
index 000000000..bbe61a68e
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h
@@ -0,0 +1,19 @@
+#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H
+#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H
+
+#include
+#include
+
+struct DrawProbabilityConfig
+{
+ QString criteria = "name"; // name, type, subtype, cmc
+ bool atLeast = true; // true = at least, false = exactly
+ int quantity = 1; // N
+ int drawn = 7; // M
+
+ QJsonObject toJson() const;
+
+ static DrawProbabilityConfig fromJson(const QJsonObject &o);
+};
+
+#endif
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp
new file mode 100644
index 000000000..71f88c0fc
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp
@@ -0,0 +1,92 @@
+#include "draw_probability_config_dialog.h"
+
+#include
+#include
+#include
+#include
+#include
+
+DrawProbabilityConfigDialog::DrawProbabilityConfigDialog(QWidget *parent) : QDialog(parent)
+{
+ form = new QFormLayout(this);
+
+ // Criteria
+ labelCriteria = new QLabel(this);
+ criteria = new QComboBox(this);
+ criteria->addItem(QString(), "name");
+ criteria->addItem(QString(), "type");
+ criteria->addItem(QString(), "subtype");
+ criteria->addItem(QString(), "cmc");
+ form->addRow(labelCriteria, criteria);
+
+ // Exactness
+ labelExactness = new QLabel(this);
+ exactness = new QComboBox(this);
+ exactness->addItem(QString(), true);
+ exactness->addItem(QString(), false);
+ form->addRow(labelExactness, exactness);
+
+ // Quantity
+ labelQuantity = new QLabel(this);
+ quantity = new QSpinBox(this);
+ quantity->setRange(1, 60);
+ form->addRow(labelQuantity, quantity);
+
+ // Drawn
+ labelDrawn = new QLabel(this);
+ drawn = new QSpinBox(this);
+ drawn->setRange(1, 60);
+ drawn->setValue(7);
+ form->addRow(labelDrawn, drawn);
+
+ // Button box
+ auto *bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ form->addWidget(bb);
+
+ connect(bb, &QDialogButtonBox::accepted, this, &DrawProbabilityConfigDialog::accept);
+ connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+ retranslateUi();
+}
+
+void DrawProbabilityConfigDialog::retranslateUi()
+{
+ setWindowTitle(tr("Draw Probability Settings"));
+
+ labelCriteria->setText(tr("Criteria:"));
+ criteria->setItemText(0, tr("Card Name"));
+ criteria->setItemText(1, tr("Type"));
+ criteria->setItemText(2, tr("Subtype"));
+ criteria->setItemText(3, tr("Mana Value"));
+
+ labelExactness->setText(tr("Exactness:"));
+ exactness->setItemText(0, tr("At least"));
+ exactness->setItemText(1, tr("Exactly"));
+
+ labelQuantity->setText(tr("Quantity (N):"));
+ labelDrawn->setText(tr("Cards drawn (M):"));
+
+ // i18n-friendly suffixes
+ quantity->setSuffix(tr(" cards"));
+ drawn->setSuffix(tr(" cards"));
+}
+
+void DrawProbabilityConfigDialog::setFromConfig(const DrawProbabilityConfig &_config)
+{
+ cfg = _config;
+
+ criteria->setCurrentIndex(criteria->findData(_config.criteria));
+ exactness->setCurrentIndex(exactness->findData(_config.atLeast));
+ quantity->setValue(_config.quantity);
+ drawn->setValue(_config.drawn);
+}
+
+void DrawProbabilityConfigDialog::accept()
+{
+ cfg.criteria = criteria->currentData().toString();
+ cfg.atLeast = exactness->currentData().toBool();
+ cfg.quantity = quantity->value();
+ cfg.drawn = drawn->value();
+
+ QDialog::accept();
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h
new file mode 100644
index 000000000..44b1f0eec
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "draw_probability_config.h"
+
+#include
+#include
+
+class QComboBox;
+class QSpinBox;
+class QLabel;
+
+class DrawProbabilityConfigDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit DrawProbabilityConfigDialog(QWidget *parent = nullptr);
+
+ void retranslateUi();
+
+ void setFromConfig(const DrawProbabilityConfig &_config);
+ DrawProbabilityConfig result() const
+ {
+ return cfg;
+ }
+
+protected:
+ void accept() override;
+
+private:
+ DrawProbabilityConfig cfg;
+
+ QFormLayout *form;
+
+ // Widgets
+ QComboBox *criteria;
+ QComboBox *exactness;
+ QSpinBox *quantity;
+ QSpinBox *drawn;
+
+ QLabel *labelCriteria;
+ QLabel *labelExactness;
+ QLabel *labelQuantity;
+ QLabel *labelDrawn;
+};
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
new file mode 100644
index 000000000..a8bec834f
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
@@ -0,0 +1,236 @@
+#include "draw_probability_widget.h"
+
+#include "draw_probability_config_dialog.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
+ : AbstractAnalyticsPanelWidget(parent, analyzer)
+{
+ controls = new QWidget(this);
+ controlLayout = new QHBoxLayout(controls);
+
+ labelPrefix = new QLabel(this);
+ controlLayout->addWidget(labelPrefix);
+
+ criteriaCombo = new QComboBox(this);
+ // Give these things item-data so we can translate the actual user-facing strings
+ criteriaCombo->addItem(QString(), "name");
+ criteriaCombo->addItem(QString(), "type");
+ criteriaCombo->addItem(QString(), "subtype");
+ criteriaCombo->addItem(QString(), "cmc");
+ controlLayout->addWidget(criteriaCombo);
+
+ exactnessCombo = new QComboBox(this);
+ exactnessCombo->addItem(QString(), true); // At least
+ exactnessCombo->addItem(QString(), false); // Exactly
+ controlLayout->addWidget(exactnessCombo);
+
+ quantitySpin = new QSpinBox(this);
+ quantitySpin->setRange(1, 60);
+ controlLayout->addWidget(quantitySpin);
+
+ labelMiddle = new QLabel(this);
+ controlLayout->addWidget(labelMiddle);
+
+ drawnSpin = new QSpinBox(this);
+ drawnSpin->setRange(1, 60);
+ drawnSpin->setValue(7);
+ controlLayout->addWidget(drawnSpin);
+
+ labelSuffix = new QLabel(this);
+ controlLayout->addWidget(labelSuffix);
+
+ labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+ labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+ labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+
+ controlLayout->addStretch(1);
+ layout->addWidget(controls);
+
+ // Table
+ resultTable = new QTableWidget(this);
+ resultTable->setColumnCount(3);
+ resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
+ layout->addWidget(resultTable);
+
+ // Connections
+ connect(criteriaCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] {
+ config.criteria = criteriaCombo->currentData().toString();
+ updateDisplay();
+ });
+
+ connect(exactnessCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] {
+ config.atLeast = exactnessCombo->currentData().toBool();
+ updateDisplay();
+ });
+
+ connect(quantitySpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) {
+ config.quantity = v;
+ updateDisplay();
+ });
+
+ connect(drawnSpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) {
+ config.drawn = v;
+ updateDisplay();
+ });
+
+ retranslateUi();
+ applyConfigToToolbar();
+ updateFilterOptions();
+}
+
+void DrawProbabilityWidget::retranslateUi()
+{
+ bannerWidget->setText(tr("Draw Probability"));
+
+ labelPrefix->setText(tr("Probability of drawing"));
+
+ criteriaCombo->setItemText(0, tr("Card Name"));
+ criteriaCombo->setItemText(1, tr("Type"));
+ criteriaCombo->setItemText(2, tr("Subtype"));
+ criteriaCombo->setItemText(3, tr("Mana Value"));
+
+ exactnessCombo->setItemText(0, tr("At least"));
+ exactnessCombo->setItemText(1, tr("Exactly"));
+
+ labelMiddle->setText(tr("card(s) having drawn at least"));
+ labelSuffix->setText(tr("cards"));
+
+ resultTable->setHorizontalHeaderLabels({tr("Category"), tr("Qty"), tr("Odds (%)")});
+}
+
+QDialog *DrawProbabilityWidget::createConfigDialog(QWidget *parent)
+{
+ auto *dlg = new DrawProbabilityConfigDialog(parent);
+ dlg->setFromConfig(config);
+ return dlg;
+}
+
+QJsonObject DrawProbabilityWidget::extractConfigFromDialog(QDialog *dlg) const
+{
+ auto *dp = qobject_cast(dlg);
+ return dp ? dp->result().toJson() : QJsonObject{};
+}
+
+void DrawProbabilityWidget::applyConfigToToolbar()
+{
+ auto setComboByData = [](QComboBox *combo, const QVariant &value) {
+ int idx = combo->findData(value);
+ if (idx >= 0) {
+ combo->setCurrentIndex(idx);
+ }
+ };
+
+ setComboByData(criteriaCombo, config.criteria);
+ setComboByData(exactnessCombo, config.atLeast);
+
+ quantitySpin->setValue(config.quantity);
+ drawnSpin->setValue(config.drawn);
+}
+
+void DrawProbabilityWidget::updateDisplay()
+{
+ updateFilterOptions();
+}
+
+void DrawProbabilityWidget::loadConfig(const QJsonObject &cfg)
+{
+ config = DrawProbabilityConfig::fromJson(cfg);
+ applyConfigToToolbar();
+ updateFilterOptions();
+}
+
+void DrawProbabilityWidget::updateFilterOptions()
+{
+ if (!analyzer->getModel()->getDeckList()) {
+ return;
+ }
+
+ const QString criteria = config.criteria;
+ const bool atLeast = config.atLeast;
+ const int quantity = config.quantity;
+ const int drawn = config.drawn;
+
+ QMap categoryCounts;
+ int totalDeckCards = 0;
+
+ const auto nodes = analyzer->getModel()->getDeckList()->getCardNodes();
+ for (auto *node : nodes) {
+ CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr();
+ if (!info) {
+ continue;
+ }
+
+ totalDeckCards += node->getNumber();
+
+ QStringList categories;
+ if (criteria == "name") {
+ categories << info->getName();
+ } else if (criteria == "type") {
+ categories = info->getMainCardType().split(' ', Qt::SkipEmptyParts);
+ } else if (criteria == "subtype") {
+ categories = info->getCardType().split(' ', Qt::SkipEmptyParts);
+ } else if (criteria == "cmc") {
+ categories << QString::number(info->getCmc().toInt());
+ }
+
+ for (const QString &cat : categories) {
+ categoryCounts[cat] += node->getNumber();
+ }
+ }
+
+ resultTable->setRowCount(categoryCounts.size());
+
+ int row = 0;
+ for (auto it = categoryCounts.cbegin(); it != categoryCounts.cend(); ++it, ++row) {
+ const QString &cat = it.key();
+ const int copies = it.value();
+
+ double probability = 0.0;
+ if (atLeast) {
+ for (int k = quantity; k <= drawn && k <= copies; ++k) {
+ probability += hypergeometricProbability(totalDeckCards, copies, drawn, k);
+ }
+ } else {
+ probability = hypergeometricProbability(totalDeckCards, copies, drawn, quantity);
+ }
+
+ resultTable->setItem(row, 0, new QTableWidgetItem(cat));
+ resultTable->setItem(row, 1, new QTableWidgetItem(QString::number(copies)));
+ resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2)));
+ }
+}
+
+double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k)
+{
+ if (k < 0 || k > n || K > N || n > N) {
+ return 0.0;
+ }
+
+ double logP = 0.0;
+ for (int i = 1; i <= k; ++i) {
+ logP += qLn(double(K - k + i) / i);
+ }
+ for (int i = 1; i <= n - k; ++i) {
+ logP += qLn(double(N - K - (n - k) + i) / i);
+ }
+ for (int i = 1; i <= n; ++i) {
+ logP -= qLn(double(N - n + i) / i);
+ }
+
+ return qExp(logP);
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h
new file mode 100644
index 000000000..80015999f
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h
@@ -0,0 +1,54 @@
+#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H
+#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H
+
+#include "../../abstract_analytics_panel_widget.h"
+#include "../../deck_list_statistics_analyzer.h"
+#include "draw_probability_config.h"
+
+#include
+#include
+#include
+#include
+
+class DrawProbabilityWidget : public AbstractAnalyticsPanelWidget
+{
+ Q_OBJECT
+public:
+ DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
+
+ QDialog *createConfigDialog(QWidget *parent) override;
+ QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
+ void applyConfigToToolbar();
+
+public slots:
+ void updateDisplay() override;
+ void loadConfig(const QJsonObject &cfg) override;
+ void retranslateUi();
+
+private slots:
+ void updateFilterOptions();
+
+private:
+ DrawProbabilityConfig config;
+
+ QWidget *controls;
+ QHBoxLayout *controlLayout;
+ QLabel *labelPrefix;
+ QLabel *labelMiddle;
+ QLabel *labelSuffix;
+ QLineEdit *cardNameEdit;
+ QComboBox *criteriaCombo; // Card Name / Type / Subtype / Mana Value
+ QComboBox *filterCombo; // The actual value
+ QComboBox *exactnessCombo; // At least / Exactly
+ QSpinBox *quantitySpin; // N
+ QSpinBox *drawnSpin; // M
+
+ QSpinBox *manaValueSpin;
+
+ QTableWidget *resultTable;
+
+ double hypergeometricProbability(int N, int K, int n, int k);
+ double calculateProbability(int totalCards, int copies, int drawn, bool atLeast);
+};
+
+#endif // COCKATRICE_DRAW_PROBABILITY_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp
new file mode 100644
index 000000000..2f9f60752
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp
@@ -0,0 +1,32 @@
+#include "mana_base_config.h"
+
+QJsonObject ManaBaseConfig::toJson() const
+{
+ QJsonObject jsonObject;
+ QJsonArray jsonArray;
+ jsonObject["displayType"] = displayType;
+ for (auto &filter : filters) {
+ jsonArray.append(filter);
+ }
+ jsonObject["filters"] = jsonArray;
+ return jsonObject;
+}
+
+ManaBaseConfig ManaBaseConfig::fromJson(const QJsonObject &o)
+
+{
+ ManaBaseConfig config;
+
+ if (o.contains("displayType")) {
+ config.displayType = o["displayType"].toString();
+ }
+
+ if (o.contains("filters")) {
+ config.filters.clear();
+ for (auto v : o["filters"].toArray()) {
+ config.filters << v.toString();
+ }
+ }
+
+ return config;
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h
new file mode 100644
index 000000000..d01f88b8b
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h
@@ -0,0 +1,19 @@
+
+#ifndef COCKATRICE_MANA_BASE_CONFIG_H
+#define COCKATRICE_MANA_BASE_CONFIG_H
+
+#include
+#include
+#include
+
+struct ManaBaseConfig
+{
+ QString displayType; // "pie" or "bar" or "combinedBar"
+ QStringList filters; // which colors to show, empty = all
+
+ QJsonObject toJson() const;
+
+ static ManaBaseConfig fromJson(const QJsonObject &o);
+};
+
+#endif // COCKATRICE_MANA_BASE_CONFIG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp
new file mode 100644
index 000000000..3317486ea
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp
@@ -0,0 +1,67 @@
+#include "mana_base_config_dialog.h"
+
+#include
+
+ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer,
+ ManaBaseConfig initial,
+ QWidget *parent)
+ : QDialog(parent), config(initial)
+{
+ layout = new QVBoxLayout(this);
+
+ displayTypeLabel = new QLabel(this);
+ layout->addWidget(displayTypeLabel);
+
+ displayType = new QComboBox(this);
+ layout->addWidget(displayType);
+
+ filterLabel = new QLabel(this);
+ layout->addWidget(filterLabel);
+
+ filterList = new QListWidget(this);
+ filterList->setSelectionMode(QAbstractItemView::MultiSelection);
+ layout->addWidget(filterList);
+
+ QStringList colors = analyzer->getManaBase().keys();
+ colors.sort();
+ filterList->addItems(colors);
+
+ // select initial filters
+ for (int i = 0; i < filterList->count(); ++i) {
+ if (config.filters.contains(filterList->item(i)->text()))
+ filterList->item(i)->setSelected(true);
+ }
+
+ buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ layout->addWidget(buttons);
+
+ connect(buttons, &QDialogButtonBox::accepted, this, &ManaBaseConfigDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &ManaBaseConfigDialog::reject);
+
+ retranslateUi();
+}
+
+void ManaBaseConfigDialog::retranslateUi()
+{
+ setWindowTitle(tr("Mana Base Configuration"));
+
+ displayTypeLabel->setText(tr("Display type:"));
+
+ displayType->clear();
+ displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")});
+
+ filterLabel->setText(tr("Filter Colors (optional):"));
+
+ buttons->button(QDialogButtonBox::Ok)->setText(tr("OK"));
+ buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+}
+
+void ManaBaseConfigDialog::accept()
+{
+ config.displayType = displayType->currentText();
+ config.filters.clear();
+ for (auto *item : filterList->selectedItems()) {
+ config.filters << item->text();
+ }
+ QDialog::accept();
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h
new file mode 100644
index 000000000..a816f154f
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h
@@ -0,0 +1,42 @@
+
+#ifndef COCKATRICE_MANA_BASE_ADD_DIALOG_H
+#define COCKATRICE_MANA_BASE_ADD_DIALOG_H
+
+#include "../../deck_list_statistics_analyzer.h"
+#include "mana_base_config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class ManaBaseConfigDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig initial = {}, QWidget *parent = nullptr);
+ void retranslateUi();
+
+ void accept() override;
+
+ ManaBaseConfig result() const
+ {
+ return config;
+ }
+
+private:
+ ManaBaseConfig config;
+ QVBoxLayout *layout;
+ QLabel *displayTypeLabel;
+ QComboBox *displayType;
+ QLabel *filterLabel;
+ QListWidget *filterList;
+ QDialogButtonBox *buttons;
+};
+
+#endif // COCKATRICE_MANA_BASE_ADD_DIALOG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp
new file mode 100644
index 000000000..d38314b8c
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp
@@ -0,0 +1,115 @@
+#include "mana_base_widget.h"
+
+#include "../../../general/display/charts/bars/bar_widget.h"
+#include "../../../general/display/charts/bars/color_bar.h"
+#include "../../../general/display/charts/pies/color_pie.h"
+#include "../../analytics_panel_widget_registrar.h"
+#include "mana_base_config_dialog.h"
+
+#include
+#include
+
+namespace
+{
+
+AnalyticsPanelWidgetRegistrar registerManaBase{
+ "manaBase", ManaBaseWidget::tr("Mana Base"),
+ [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }};
+
+} // anonymous namespace
+
+ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg)
+ : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg))
+{
+ barContainer = new QWidget(this);
+ barLayout = new QHBoxLayout(barContainer);
+ layout->addWidget(barContainer);
+
+ updateDisplay();
+}
+
+void ManaBaseWidget::updateDisplay()
+{
+ // Clear previous widgets
+ while (QLayoutItem *item = barLayout->takeAt(0)) {
+ if (item->widget()) {
+ item->widget()->deleteLater();
+ }
+ delete item;
+ }
+
+ auto &pipCount = analyzer->getProductionPipCount();
+ auto &cardCount = analyzer->getProductionCardCount();
+
+ QHash manaMap;
+ for (auto key : pipCount.keys()) {
+ manaMap[key] = pipCount[key];
+ }
+
+ // Apply filters
+ if (!config.filters.isEmpty()) {
+ QHash filtered;
+ for (auto f : config.filters) {
+ if (manaMap.contains(f)) {
+ filtered[f] = manaMap[f];
+ }
+ }
+ manaMap = filtered;
+ }
+
+ // Determine maximum for bar charts
+ int highest = 1;
+ for (auto val : manaMap) {
+ highest = std::max(highest, val);
+ }
+
+ // Convert to QMap for ColorBar / ColorPie (sorted)
+ QMap mapSorted;
+ for (auto it = manaMap.begin(); it != manaMap.end(); ++it) {
+ mapSorted.insert(it.key(), it.value());
+ }
+
+ // Choose display mode
+ if (config.displayType == "bar") {
+ QHash colors = {{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)},
+ {"B", QColor(21, 11, 0)}, {"R", QColor(211, 32, 42)},
+ {"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)}};
+
+ for (auto color : manaMap.keys()) {
+ QString label = QString("%1 %2 (%3)").arg(color).arg(manaMap[color]).arg(cardCount.value(color));
+
+ BarWidget *bar = new BarWidget(label, manaMap[color], highest, colors.value(color, Qt::gray), this);
+
+ barLayout->addWidget(bar);
+ }
+ } else if (config.displayType == "combinedBar") {
+ ColorBar *cb = new ColorBar(mapSorted, this);
+ cb->setMinimumHeight(30);
+ barLayout->addWidget(cb);
+ } else if (config.displayType == "pie") {
+ ColorPie *pie = new ColorPie(mapSorted, this);
+ pie->setMinimumSize(200, 200);
+ barLayout->addWidget(pie);
+ }
+
+ update();
+}
+QSize ManaBaseWidget::sizeHint() const
+{
+ return QSize(800, 150);
+}
+
+QDialog *ManaBaseWidget::createConfigDialog(QWidget *parent)
+{
+ ManaBaseConfigDialog *dlg = new ManaBaseConfigDialog(analyzer, config, parent);
+ return dlg;
+}
+
+QJsonObject ManaBaseWidget::extractConfigFromDialog(QDialog *dlg) const
+{
+ auto *mc = qobject_cast(dlg);
+ if (!mc) {
+ return {};
+ }
+ return mc->result().toJson();
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h
new file mode 100644
index 000000000..39380c07e
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h
@@ -0,0 +1,51 @@
+/**
+ * @file mana_base_widget.h
+ * @ingroup DeckEditorAnalyticsWidgets
+ * @brief TODO: Document this.
+ */
+
+#ifndef MANA_BASE_WIDGET_H
+#define MANA_BASE_WIDGET_H
+
+#include "../../../general/display/banner_widget.h"
+#include "../../abstract_analytics_panel_widget.h"
+#include "../../deck_list_statistics_analyzer.h"
+#include "mana_base_config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+class ManaBaseWidget : public AbstractAnalyticsPanelWidget
+{
+ Q_OBJECT
+
+public slots:
+ QSize sizeHint() const override;
+ void updateDisplay() override;
+ QDialog *createConfigDialog(QWidget *parent) override;
+
+public:
+ ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {});
+
+ QJsonObject saveConfig() const override
+ {
+ return config.toJson();
+ }
+ void loadConfig(const QJsonObject &o) override
+ {
+ config = ManaBaseConfig::fromJson(o);
+ updateDisplay();
+ }
+
+ QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
+
+private:
+ ManaBaseConfig config;
+ QWidget *barContainer;
+ QHBoxLayout *barLayout;
+};
+
+#endif // MANA_BASE_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp
new file mode 100644
index 000000000..a0ccedc74
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp
@@ -0,0 +1,121 @@
+#include "mana_curve_category_widget.h"
+
+#include "libcockatrice/utility/color.h"
+#include "libcockatrice/utility/qt_utils.h"
+#include "mana_curve_config.h"
+#include "mana_curve_total_widget.h"
+
+constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row
+
+ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent)
+{
+ layout = new QVBoxLayout(this);
+ layout->setSpacing(4);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+}
+
+// Same as minimum for now
+QSize ManaCurveCategoryWidget::sizeHint() const
+{
+ if (layout->isEmpty()) {
+ return QSize(0, 0);
+ }
+
+ // Calculate exact height needed for all rows
+ int rowCount = layout->count();
+
+ int totalHeight = rowCount * MIN_ROW_HEIGHT;
+ totalHeight += (rowCount - 1) * layout->spacing();
+
+ return QSize(0, totalHeight);
+}
+
+QSize ManaCurveCategoryWidget::minimumSizeHint() const
+{
+ if (layout->isEmpty()) {
+ return QSize(0, 0);
+ }
+
+ // Calculate actual minimum based on number of rows
+ int rowCount = layout->count();
+
+ int totalHeight = rowCount * MIN_ROW_HEIGHT;
+ totalHeight += (rowCount - 1) * layout->spacing();
+
+ return QSize(0, totalHeight);
+}
+
+void ManaCurveCategoryWidget::updateDisplay(int minCmc,
+ int maxCmc,
+ int highest,
+ QHash> qCategoryCounts,
+ QHash> qCategoryCards,
+ const ManaCurveConfig &config)
+{
+ // Clear previous content
+ QtUtils::clearLayoutRec(layout);
+
+ if (!config.showCategoryRows) {
+ return; // nothing to show
+ }
+
+ // Collect categories
+ QStringList categories = qCategoryCounts.keys();
+
+ // Apply filters
+ if (!config.filters.isEmpty()) {
+ QStringList filtered;
+ for (const QString &cat : categories) {
+ if (config.filters.contains(cat)) {
+ filtered.append(cat);
+ }
+ }
+ categories = filtered;
+ }
+
+ std::sort(categories.begin(), categories.end());
+
+ for (const QString &cat : categories) {
+ QWidget *row = new QWidget(this);
+ row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+
+ row->setFixedHeight(MIN_ROW_HEIGHT);
+
+ QHBoxLayout *rowLayout = new QHBoxLayout(row);
+ rowLayout->setContentsMargins(0, 0, 0, 0);
+ rowLayout->setSpacing(4);
+
+ QLabel *categoryLabel = new QLabel(cat, row);
+ categoryLabel->setFixedWidth(80);
+ rowLayout->addWidget(categoryLabel);
+
+ QVector catBars;
+ const auto cmcCounts = qCategoryCounts.value(cat);
+ const auto cmcCards = qCategoryCards.value(cat);
+
+ for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
+ int val = cmcCounts.value(cmc, 0);
+ QStringList cards = cmcCards.value(cmc);
+
+ QVector segments;
+ if (val > 0) {
+ segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)});
+ }
+
+ catBars.push_back({QString::number(cmc), segments});
+ }
+
+ auto *catChart = new BarChartWidget(row);
+ catChart->setHighest(highest);
+ catChart->setBars(catBars);
+ catChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ rowLayout->addWidget(catChart);
+ layout->addWidget(row);
+ }
+
+ // Update geometry after adding all widgets
+ updateGeometry();
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h
new file mode 100644
index 000000000..c6aa6f1f0
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h
@@ -0,0 +1,32 @@
+#ifndef COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
+#define COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
+
+#include "../../../general/display/charts/bars/bar_chart_widget.h"
+#include "mana_curve_config.h"
+
+#include
+#include
+#include
+
+class ManaCurveCategoryWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ManaCurveCategoryWidget(QWidget *parent);
+ void updateDisplay(int minCmc,
+ int maxCmc,
+ int highest,
+ QHash> qCategoryCounts,
+ QHash> qCategoryCards,
+ const ManaCurveConfig &config);
+
+public slots:
+ QSize sizeHint() const override;
+ QSize minimumSizeHint() const override;
+
+private:
+ QVBoxLayout *layout;
+};
+
+#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp
new file mode 100644
index 000000000..0dfef1b66
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp
@@ -0,0 +1,41 @@
+#include "mana_curve_config.h"
+
+QJsonObject ManaCurveConfig::toJson() const
+{
+ QJsonObject jsonObject;
+ jsonObject["groupBy"] = groupBy;
+ QJsonArray jsonArray;
+ for (auto &filter : filters) {
+ jsonArray.append(filter);
+ }
+ jsonObject["filters"] = jsonArray;
+ jsonObject["showMain"] = showMain;
+ jsonObject["showCategoryRows"] = showCategoryRows;
+ return jsonObject;
+}
+
+ManaCurveConfig ManaCurveConfig::fromJson(const QJsonObject &o)
+{
+ ManaCurveConfig config;
+
+ if (o.contains("groupBy")) {
+ config.groupBy = o["groupBy"].toString();
+ }
+
+ if (o.contains("filters")) {
+ config.filters.clear();
+ for (auto v : o["filters"].toArray()) {
+ config.filters << v.toString();
+ }
+ }
+
+ if (o.contains("showMain")) {
+ config.showMain = o["showMain"].toBool(true);
+ }
+
+ if (o.contains("showCategoryRows")) {
+ config.showCategoryRows = o["showCategoryRows"].toBool(true);
+ }
+
+ return config;
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h
new file mode 100644
index 000000000..55222dd98
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h
@@ -0,0 +1,21 @@
+
+#ifndef COCKATRICE_MANA_CURVE_CONFIG_H
+#define COCKATRICE_MANA_CURVE_CONFIG_H
+
+#include
+#include
+#include
+
+struct ManaCurveConfig
+{
+ QString groupBy = "type"; // "type", "color", "subtype", etc.
+ QStringList filters; // empty = all
+ bool showMain = true;
+ bool showCategoryRows = true;
+
+ QJsonObject toJson() const;
+
+ static ManaCurveConfig fromJson(const QJsonObject &o);
+};
+
+#endif // COCKATRICE_MANA_CURVE_CONFIG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp
new file mode 100644
index 000000000..38199656c
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp
@@ -0,0 +1,91 @@
+#include "mana_curve_config_dialog.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+ManaCurveConfigDialog::ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
+ : QDialog(parent), analyzer(analyzer)
+{
+ auto *lay = new QVBoxLayout(this);
+
+ labelGroupBy = new QLabel(this);
+ lay->addWidget(labelGroupBy);
+
+ groupBy = new QComboBox(this);
+ lay->addWidget(groupBy);
+
+ labelFilters = new QLabel(this);
+ lay->addWidget(labelFilters);
+
+ filterList = new QListWidget(this);
+ filterList->setSelectionMode(QAbstractItemView::MultiSelection);
+ lay->addWidget(filterList);
+
+ showMain = new QCheckBox(this);
+ showMain->setChecked(true);
+ lay->addWidget(showMain);
+
+ showCatRows = new QCheckBox(this);
+ showCatRows->setChecked(true);
+ lay->addWidget(showCatRows);
+
+ buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ lay->addWidget(buttons);
+ connect(buttons, &QDialogButtonBox::accepted, this, &ManaCurveConfigDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &ManaCurveConfigDialog::reject);
+
+ // populate dynamic data
+ QStringList cats = analyzer->getManaCurveByType().keys();
+ cats.append(analyzer->getManaCurveByColor().keys());
+ cats.removeDuplicates();
+ cats.sort();
+ filterList->addItems(cats);
+
+ groupBy->addItems({"type", "color", "subtype", "power", "toughness"});
+
+ retranslateUi();
+}
+
+void ManaCurveConfigDialog::retranslateUi()
+{
+ labelGroupBy->setText(tr("Group By:"));
+ groupBy->setItemText(0, tr("type"));
+ groupBy->setItemText(1, tr("color"));
+ groupBy->setItemText(2, tr("subtype"));
+ groupBy->setItemText(3, tr("power"));
+ groupBy->setItemText(4, tr("toughness"));
+
+ labelFilters->setText(tr("Filters (optional):"));
+
+ showMain->setText(tr("Show main bar row"));
+ showCatRows->setText(tr("Show per-category rows"));
+}
+
+void ManaCurveConfigDialog::setFromConfig(const ManaCurveConfig &cfg)
+{
+ groupBy->setCurrentText(cfg.groupBy);
+ // restore filters
+ for (int i = 0; i < filterList->count(); ++i)
+ filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text()));
+
+ showMain->setChecked(cfg.showMain);
+ showCatRows->setChecked(cfg.showCategoryRows);
+}
+
+void ManaCurveConfigDialog::accept()
+{
+ cfg.groupBy = groupBy->currentText();
+
+ cfg.filters.clear();
+ for (auto *item : filterList->selectedItems())
+ cfg.filters << item->text();
+
+ cfg.showMain = showMain->isChecked();
+ cfg.showCategoryRows = showCatRows->isChecked();
+
+ QDialog::accept();
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h
new file mode 100644
index 000000000..17e6776e5
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h
@@ -0,0 +1,44 @@
+#ifndef COCKATRICE_MANA_CURVE_ADD_DIALOG_H
+#define COCKATRICE_MANA_CURVE_ADD_DIALOG_H
+
+#include "../../deck_list_statistics_analyzer.h"
+#include "mana_curve_config.h"
+
+#include
+#include
+#include
+
+class QListWidget;
+class QCheckBox;
+class QComboBox;
+
+class ManaCurveConfigDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr);
+ void retranslateUi();
+ void setFromConfig(const ManaCurveConfig &cfg);
+
+ ManaCurveConfig result() const
+ {
+ return cfg;
+ }
+
+private:
+ ManaCurveConfig cfg;
+ DeckListStatisticsAnalyzer *analyzer;
+
+ QLabel *labelGroupBy;
+ QComboBox *groupBy;
+ QLabel *labelFilters;
+ QListWidget *filterList;
+ QDialogButtonBox *buttons;
+ QCheckBox *showMain;
+ QCheckBox *showCatRows;
+
+private slots:
+ void accept() override;
+};
+
+#endif // COCKATRICE_MANA_CURVE_ADD_DIALOG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp
new file mode 100644
index 000000000..f059ff873
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp
@@ -0,0 +1,78 @@
+#include "mana_curve_total_widget.h"
+
+#include "../../../general/display/charts/bars/bar_chart_widget.h"
+#include "libcockatrice/utility/color.h"
+#include "libcockatrice/utility/qt_utils.h"
+#include "mana_curve_config.h"
+
+#include
+
+ManaCurveTotalWidget::ManaCurveTotalWidget(QWidget *parent) : QWidget(parent)
+{
+ layout = new QHBoxLayout(this);
+
+ label = new QLabel(this);
+ label->setFixedWidth(80);
+ layout->addWidget(label);
+
+ barChart = new BarChartWidget(this);
+ layout->addWidget(barChart, 1);
+
+ setMinimumHeight(200);
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+}
+
+QSize ManaCurveTotalWidget::sizeHint() const
+{
+ return {0, 280};
+}
+
+QSize ManaCurveTotalWidget::minimumSizeHint() const
+{
+ return {0, 200};
+}
+
+void ManaCurveTotalWidget::updateDisplay(const QString &categoryName,
+ int minCmc,
+ int maxCmc,
+ int highest,
+ const QMap> &cmcMap,
+ const QMap> &cardsMap,
+ const ManaCurveConfig &config)
+{
+ QVector mainBars;
+ mainBars.reserve(maxCmc - minCmc + 1);
+
+ for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
+ QVector segments;
+
+ const auto cmcIt = cmcMap.constFind(cmc);
+ if (cmcIt != cmcMap.cend()) {
+ for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) {
+ const QString &category = it.key();
+
+ if (!config.filters.isEmpty() && !config.filters.contains(category))
+ continue;
+
+ const int value = it.value();
+
+ QStringList cards;
+ const auto catIt = cardsMap.constFind(category);
+ if (catIt != cardsMap.cend())
+ cards = catIt->value(cmc);
+
+ segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)});
+ }
+ }
+
+ std::sort(segments.begin(), segments.end(),
+ [](const BarSegment &a, const BarSegment &b) { return a.category < b.category; });
+
+ mainBars.push_back({QString::number(cmc), segments});
+ }
+
+ label->setText(categoryName);
+
+ barChart->setHighest(highest);
+ barChart->setBars(mainBars);
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h
new file mode 100644
index 000000000..203f75345
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h
@@ -0,0 +1,32 @@
+#ifndef COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H
+#define COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H
+#include "../../../general/display/charts/bars/bar_chart_widget.h"
+#include "mana_curve_config.h"
+
+#include
+#include
+#include
+
+class ManaCurveTotalWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ManaCurveTotalWidget(QWidget *parent);
+ QSize sizeHint() const;
+ QSize minimumSizeHint() const;
+ void updateDisplay(const QString &categoryName,
+ int minCmc,
+ int maxCmc,
+ int highest,
+ const QMap> &cmcMap,
+ const QMap> &cardsMap,
+ const ManaCurveConfig &config);
+
+private:
+ QHBoxLayout *layout;
+ QLabel *label;
+ BarChartWidget *barChart;
+};
+
+#endif // COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp
new file mode 100644
index 000000000..e09ecfe87
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp
@@ -0,0 +1,148 @@
+#include "mana_curve_widget.h"
+
+#include "../../../general/display/charts/bars/bar_chart_background_widget.h"
+#include "../../../general/display/charts/bars/bar_chart_widget.h"
+#include "../../../general/display/charts/bars/segmented_bar_widget.h"
+#include "../../analytics_panel_widget_registrar.h"
+#include "../../deck_list_statistics_analyzer.h"
+#include "libcockatrice/utility/color.h"
+#include "libcockatrice/utility/qt_utils.h"
+#include "mana_curve_config_dialog.h"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+
+AnalyticsPanelWidgetRegistrar registerManaCurve{
+ "manaCurve", ManaCurveWidget::tr("Mana Curve"),
+ [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }};
+
+} // anonymous namespace
+
+ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg)
+ : AbstractAnalyticsPanelWidget(parent, analyzer), config(cfg)
+{
+ setLayout(layout);
+
+ totalWidget = new ManaCurveTotalWidget(this);
+ totalWidget->setHidden(true);
+ layout->addWidget(totalWidget);
+
+ categoryWidget = new ManaCurveCategoryWidget(this);
+ categoryWidget->setHidden(true);
+ layout->addWidget(categoryWidget);
+
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay);
+
+ updateDisplay();
+}
+
+QDialog *ManaCurveWidget::createConfigDialog(QWidget *parent)
+{
+ auto *dlg = new ManaCurveConfigDialog(analyzer, parent);
+ dlg->setFromConfig(config);
+ return dlg;
+}
+
+QJsonObject ManaCurveWidget::extractConfigFromDialog(QDialog *dlg) const
+{
+ auto *mc = qobject_cast(dlg);
+ return mc ? mc->result().toJson() : QJsonObject{};
+}
+
+static void buildMapsByCategory(const QHash> &categoryCounts,
+ const QHash> &categoryCards,
+ QMap> &outCmcMap,
+ QMap> &outCardsMap)
+{
+ outCmcMap.clear();
+ outCardsMap.clear();
+
+ for (auto catIt = categoryCounts.cbegin(); catIt != categoryCounts.cend(); ++catIt) {
+ const QString &category = catIt.key();
+ const auto &countsByCmc = catIt.value();
+
+ for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it)
+ outCmcMap[it.key()][category] = it.value();
+ }
+
+ for (auto catIt = categoryCards.cbegin(); catIt != categoryCards.cend(); ++catIt) {
+ const QString &category = catIt.key();
+ const auto &cardsByCmc = catIt.value();
+
+ for (auto it = cardsByCmc.cbegin(); it != cardsByCmc.cend(); ++it)
+ outCardsMap[category][it.key()] = it.value();
+ }
+}
+
+static void findGlobalCmcRange(const QHash> &categoryCounts, int &minCmc, int &maxCmc)
+{
+ minCmc = 0;
+ maxCmc = 0;
+
+ for (const auto &countsByCmc : categoryCounts) {
+ for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it)
+ maxCmc = qMax(maxCmc, it.key());
+ }
+}
+
+void ManaCurveWidget::updateDisplay()
+{
+ QHash> categoryCounts;
+ QHash> categoryCards;
+
+ if (config.groupBy == "color") {
+ categoryCounts = analyzer->getManaCurveByColor();
+ categoryCards = analyzer->getManaCurveCardsByColor();
+ } else if (config.groupBy == "subtype") {
+ categoryCounts = analyzer->getManaCurveBySubtype();
+ categoryCards = analyzer->getManaCurveCardsBySubtype();
+ } else if (config.groupBy == "power") {
+ categoryCounts = analyzer->getManaCurveByPower();
+ categoryCards = analyzer->getManaCurveCardsByPower();
+ } else {
+ categoryCounts = analyzer->getManaCurveByType();
+ categoryCards = analyzer->getManaCurveCardsByType();
+ }
+
+ QMap> cmcMap;
+ QMap> cardsMap;
+ buildMapsByCategory(categoryCounts, categoryCards, cmcMap, cardsMap);
+
+ int minCmc = 0;
+ int maxCmc = 0;
+ findGlobalCmcRange(categoryCounts, minCmc, maxCmc);
+
+ int highest = 1;
+ for (int cmc = minCmc; cmc <= maxCmc; ++cmc) {
+ int sum = 0;
+
+ const auto cmcIt = cmcMap.constFind(cmc);
+ if (cmcIt != cmcMap.cend()) {
+ for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) {
+ if (!config.filters.isEmpty() && !config.filters.contains(it.key())) {
+ continue;
+ }
+
+ sum += it.value();
+ }
+ }
+
+ highest = qMax(highest, sum);
+ }
+
+ totalWidget->updateDisplay(config.groupBy, minCmc, maxCmc, highest, cmcMap, cardsMap, config);
+
+ totalWidget->setVisible(config.showMain);
+
+ categoryWidget->updateDisplay(minCmc, maxCmc, highest, categoryCounts, categoryCards, config);
+
+ categoryWidget->setVisible(config.showCategoryRows);
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h
new file mode 100644
index 000000000..da59da9a8
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h
@@ -0,0 +1,50 @@
+/**
+ * @file mana_curve_widget.h
+ * @ingroup DeckEditorAnalyticsWidgets
+ * @brief TODO: Document this.
+ */
+
+#ifndef MANA_CURVE_WIDGET_H
+#define MANA_CURVE_WIDGET_H
+
+#include "../../abstract_analytics_panel_widget.h"
+#include "mana_curve_category_widget.h"
+#include "mana_curve_config.h"
+#include "mana_curve_total_widget.h"
+
+#include
+
+class SegmentedBarWidget;
+class DeckListStatisticsAnalyzer;
+
+class ManaCurveWidget : public AbstractAnalyticsPanelWidget
+{
+ Q_OBJECT
+
+public slots:
+ // QSize sizeHint() const override;
+ void updateDisplay() override;
+ QDialog *createConfigDialog(QWidget *parent) override;
+
+public:
+ ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg = {});
+
+ QJsonObject saveConfig() const override
+ {
+ return config.toJson();
+ }
+ void loadConfig(const QJsonObject &o) override
+ {
+ config = ManaCurveConfig::fromJson(o);
+ updateDisplay();
+ };
+
+ QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
+
+private:
+ ManaCurveConfig config;
+ ManaCurveTotalWidget *totalWidget;
+ ManaCurveCategoryWidget *categoryWidget;
+};
+
+#endif // MANA_CURVE_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp
new file mode 100644
index 000000000..ee57cb1e2
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp
@@ -0,0 +1,31 @@
+#include "mana_devotion_config.h"
+
+QJsonObject ManaDevotionConfig::toJson() const
+{
+ QJsonObject jsonObject;
+ QJsonArray jsonArray;
+ jsonObject["displayType"] = displayType;
+ for (auto &filter : filters) {
+ jsonArray.append(filter);
+ }
+ jsonObject["filters"] = jsonArray;
+ return jsonObject;
+}
+
+ManaDevotionConfig ManaDevotionConfig::fromJson(const QJsonObject &o)
+{
+ ManaDevotionConfig config;
+
+ if (o.contains("displayType")) {
+ config.displayType = o["displayType"].toString();
+ }
+
+ if (o.contains("filters")) {
+ config.filters.clear();
+ for (auto v : o["filters"].toArray()) {
+ config.filters << v.toString();
+ }
+ }
+
+ return config;
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h
new file mode 100644
index 000000000..08be146e5
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h
@@ -0,0 +1,18 @@
+#ifndef COCKATRICE_MANA_DEVOTION_CONFIG_H
+#define COCKATRICE_MANA_DEVOTION_CONFIG_H
+
+#include
+#include
+#include
+
+struct ManaDevotionConfig
+{
+ QString displayType; // "pie" or "bar" or "combinedBar"
+ QStringList filters; // which colors to show, empty = all
+
+ QJsonObject toJson() const;
+
+ static ManaDevotionConfig fromJson(const QJsonObject &o);
+};
+
+#endif // COCKATRICE_MANA_DEVOTION_CONFIG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp
new file mode 100644
index 000000000..80fd03928
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp
@@ -0,0 +1,62 @@
+#include "mana_devotion_config_dialog.h"
+
+ManaDevotionConfigDialog::ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer,
+ ManaDevotionConfig initial,
+ QWidget *parent)
+ : QDialog(parent), config(initial)
+{
+ layout = new QVBoxLayout(this);
+
+ labelDisplayType = new QLabel(this);
+ layout->addWidget(labelDisplayType);
+
+ displayType = new QComboBox(this);
+ layout->addWidget(displayType);
+
+ labelFilters = new QLabel(this);
+ layout->addWidget(labelFilters);
+
+ filterList = new QListWidget(this);
+ filterList->setSelectionMode(QAbstractItemView::MultiSelection);
+
+ QStringList colors = analyzer->getDevotionPipCount().keys();
+ colors.sort();
+ filterList->addItems(colors);
+ layout->addWidget(filterList);
+
+ // select initial filters
+ for (int i = 0; i < filterList->count(); ++i) {
+ if (config.filters.contains(filterList->item(i)->text()))
+ filterList->item(i)->setSelected(true);
+ }
+
+ buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ layout->addWidget(buttons);
+ connect(buttons, &QDialogButtonBox::accepted, this, &ManaDevotionConfigDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &ManaDevotionConfigDialog::reject);
+
+ // populate combo box items
+ displayType->addItems({"pie", "bar", "combinedBar"});
+
+ retranslateUi();
+}
+
+void ManaDevotionConfigDialog::retranslateUi()
+{
+ labelDisplayType->setText(tr("Display type:"));
+ displayType->setItemText(0, tr("pie"));
+ displayType->setItemText(1, tr("bar"));
+ displayType->setItemText(2, tr("combinedBar"));
+
+ labelFilters->setText(tr("Filter Colors (optional):"));
+}
+
+void ManaDevotionConfigDialog::accept()
+{
+ config.displayType = displayType->currentText();
+ config.filters.clear();
+ for (auto *item : filterList->selectedItems()) {
+ config.filters << item->text();
+ }
+ QDialog::accept();
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h
new file mode 100644
index 000000000..fd804e2af
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h
@@ -0,0 +1,42 @@
+
+#ifndef COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H
+#define COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H
+
+#include "../../deck_list_statistics_analyzer.h"
+#include "mana_devotion_config.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class ManaDevotionConfigDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer,
+ ManaDevotionConfig initial = {},
+ QWidget *parent = nullptr);
+ void retranslateUi();
+
+ void accept() override;
+
+ ManaDevotionConfig result() const
+ {
+ return config;
+ }
+
+private:
+ ManaDevotionConfig config;
+ QVBoxLayout *layout;
+ QLabel *labelDisplayType;
+ QComboBox *displayType;
+ QLabel *labelFilters;
+ QListWidget *filterList;
+ QDialogButtonBox *buttons;
+};
+
+#endif // COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp
new file mode 100644
index 000000000..709577ff2
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp
@@ -0,0 +1,123 @@
+#include "mana_devotion_widget.h"
+
+#include "../../../general/display/charts/bars/bar_widget.h"
+#include "../../../general/display/charts/bars/color_bar.h"
+#include "../../../general/display/charts/pies/color_pie.h"
+#include "../../analytics_panel_widget_registrar.h"
+#include "../../deck_list_statistics_analyzer.h"
+#include "mana_devotion_config_dialog.h"
+
+#include
+#include
+
+namespace
+{
+
+AnalyticsPanelWidgetRegistrar registerManaDevotion{
+ "manaDevotion", ManaDevotionWidget::tr("Mana Devotion"),
+ [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }};
+
+} // anonymous namespace
+
+ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg)
+ : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg))
+{
+ barContainer = new QWidget(this);
+ barLayout = new QHBoxLayout(barContainer);
+ barContainer->setLayout(barLayout);
+ layout->addWidget(barContainer);
+
+ updateDisplay();
+}
+
+void ManaDevotionWidget::updateDisplay()
+{
+ // Clear previous widgets
+ while (QLayoutItem *item = barLayout->takeAt(0)) {
+ if (item->widget()) {
+ item->widget()->deleteLater();
+ }
+ delete item;
+ }
+
+ auto &pipCount = analyzer->getDevotionPipCount();
+ auto &cardCount = analyzer->getDevotionCardCount();
+
+ // Convert keys to single QChar form
+ QHash devoMap;
+ for (auto key : pipCount.keys()) {
+ devoMap[key[0]] = pipCount[key];
+ }
+
+ // Apply filters
+ if (!config.filters.isEmpty()) {
+ QHash filtered;
+ for (auto f : config.filters) {
+ if (devoMap.contains(f[0])) {
+ filtered[f[0]] = devoMap[f[0]];
+ }
+ }
+ devoMap = filtered;
+ }
+
+ // Determine maximum for bar charts
+ int highest = 1;
+ for (auto val : devoMap) {
+ highest = std::max(highest, val);
+ }
+
+ // Convert to QMap for ColorBar / ColorPie
+ QMap mapSorted;
+ for (auto it = devoMap.begin(); it != devoMap.end(); ++it) {
+ mapSorted.insert(QString(it.key()), it.value());
+ }
+
+ // Color map
+ QHash colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)},
+ {'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)},
+ {'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}};
+
+ // Choose display mode
+ if (config.displayType == "bar") {
+ // One BarWidget per devotion color
+ for (auto c : devoMap.keys()) {
+ QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c)));
+
+ BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this);
+
+ barLayout->addWidget(bar);
+ }
+ } else if (config.displayType == "combinedBar") {
+ // Stacked devotion bar
+ ColorBar *cb = new ColorBar(mapSorted, this);
+ cb->setMinimumHeight(30);
+ barLayout->addWidget(cb);
+ } else if (config.displayType == "pie") {
+ // Devotion pie chart
+ ColorPie *pie = new ColorPie(mapSorted, this);
+ pie->setMinimumSize(200, 200);
+ barLayout->addWidget(pie);
+ }
+
+ update();
+}
+
+QDialog *ManaDevotionWidget::createConfigDialog(QWidget *parent)
+{
+ ManaDevotionConfigDialog *dlg = new ManaDevotionConfigDialog(analyzer, config, parent);
+ return dlg;
+}
+
+QJsonObject ManaDevotionWidget::extractConfigFromDialog(QDialog *dlg) const
+{
+ auto *mc = qobject_cast(dlg);
+ if (!mc) {
+ return {};
+ }
+ return mc->result().toJson();
+}
+
+QSize ManaDevotionWidget::sizeHint() const
+{
+ return QSize(800, 150);
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h
new file mode 100644
index 000000000..833f12938
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h
@@ -0,0 +1,45 @@
+/**
+ * @file mana_devotion_widget.h
+ * @ingroup DeckEditorAnalyticsWidgets
+ * @brief TODO: Document this.
+ */
+
+#ifndef MANA_DEVOTION_WIDGET_H
+#define MANA_DEVOTION_WIDGET_H
+#include "../../../general/display/banner_widget.h"
+#include "../../abstract_analytics_panel_widget.h"
+#include "mana_devotion_config.h"
+
+#include
+
+class ManaDevotionWidget : public AbstractAnalyticsPanelWidget
+{
+ Q_OBJECT
+
+public slots:
+ QSize sizeHint() const override;
+ void updateDisplay() override;
+ QDialog *createConfigDialog(QWidget *parent) override;
+
+public:
+ ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg = {});
+
+ QJsonObject saveConfig() const override
+ {
+ return config.toJson();
+ }
+ void loadConfig(const QJsonObject &o) override
+ {
+ config = ManaDevotionConfig::fromJson(o);
+ updateDisplay();
+ }
+
+ QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
+
+private:
+ ManaDevotionConfig config;
+ QWidget *barContainer;
+ QHBoxLayout *barLayout;
+};
+
+#endif // MANA_DEVOTION_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp
new file mode 100644
index 000000000..f70f32d5b
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp
@@ -0,0 +1,36 @@
+#include "mana_distribution_config.h"
+
+QJsonObject ManaDistributionConfig::toJson() const
+{
+ QJsonObject o;
+ o["displayType"] = displayType;
+
+ QJsonArray jsonArray;
+ for (auto &s : filters) {
+ jsonArray.append(s);
+ }
+ o["filters"] = jsonArray;
+
+ o["showColorRows"] = showColorRows;
+ return o;
+}
+
+ManaDistributionConfig ManaDistributionConfig::fromJson(const QJsonObject &o)
+{
+ ManaDistributionConfig config;
+ if (o.contains("displayType")) {
+ config.displayType = o["displayType"].toString();
+ }
+
+ if (o.contains("filters")) {
+ config.filters.clear();
+ for (auto v : o["filters"].toArray())
+ config.filters << v.toString();
+ }
+
+ if (o.contains("showColorRows")) {
+ config.showColorRows = o["showColorRows"].toBool(true);
+ }
+
+ return config;
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h
new file mode 100644
index 000000000..d1aa0f48e
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h
@@ -0,0 +1,20 @@
+#ifndef COCKATRICE_MANA_DISTRIBUTION_CONFIG_H
+#define COCKATRICE_MANA_DISTRIBUTION_CONFIG_H
+
+#include
+#include
+#include
+#include
+
+struct ManaDistributionConfig
+{
+ QString displayType = "pie"; // "pie" or "bar"
+ QStringList filters; // empty = all colors
+ bool showColorRows = true;
+
+ QJsonObject toJson() const;
+
+ static ManaDistributionConfig fromJson(const QJsonObject &o);
+};
+
+#endif // COCKATRICE_MANA_DISTRIBUTION_CONFIG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp
new file mode 100644
index 000000000..7fe4d94e4
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp
@@ -0,0 +1,83 @@
+#include "mana_distribution_config_dialog.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
+
+ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
+ : QDialog(parent), analyzer(analyzer)
+{
+ auto *lay = new QVBoxLayout(this);
+
+ // Labels
+ labelDisplayType = new QLabel(this);
+ lay->addWidget(labelDisplayType);
+
+ displayType = new QComboBox(this);
+ lay->addWidget(displayType);
+
+ labelFilters = new QLabel(this);
+ lay->addWidget(labelFilters);
+
+ filterList = new QListWidget(this);
+ filterList->setSelectionMode(QAbstractItemView::MultiSelection);
+ filterList->addItems(kColors); // dynamic/fixed, no translation needed
+ lay->addWidget(filterList);
+
+ showColorRows = new QCheckBox(this);
+ showColorRows->setChecked(true);
+ lay->addWidget(showColorRows);
+
+ buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ lay->addWidget(buttons);
+ connect(buttons, &QDialogButtonBox::accepted, this, &ManaDistributionConfigDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &ManaDistributionConfigDialog::reject);
+
+ displayType->addItems({"pie", "bar"}); // combo items
+
+ retranslateUi();
+}
+
+void ManaDistributionConfigDialog::retranslateUi()
+{
+ labelDisplayType->setText(tr("Top display type:"));
+ displayType->setItemText(0, tr("pie"));
+ displayType->setItemText(1, tr("bar"));
+
+ labelFilters->setText(tr("Colors:"));
+
+ showColorRows->setText(tr("Show per-color rows"));
+
+ // QDialogButtonBox buttons are automatically translated
+}
+
+void ManaDistributionConfigDialog::setFromConfig(const ManaDistributionConfig &cfgIn)
+{
+ cfg = cfgIn;
+
+ displayType->setCurrentText(cfg.displayType);
+
+ for (int i = 0; i < filterList->count(); ++i)
+ filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text()));
+
+ showColorRows->setChecked(cfg.showColorRows);
+}
+
+void ManaDistributionConfigDialog::accept()
+{
+ cfg.displayType = displayType->currentText();
+
+ // Filters
+ cfg.filters.clear();
+ for (auto *item : filterList->selectedItems())
+ cfg.filters << item->text();
+
+ cfg.showColorRows = showColorRows->isChecked();
+
+ QDialog::accept();
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h
new file mode 100644
index 000000000..57c88e29d
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h
@@ -0,0 +1,45 @@
+#ifndef COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H
+#define COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H
+
+#include "mana_distribution_config.h"
+
+#include
+#include
+#include
+#include
+
+class QComboBox;
+class QListWidget;
+class QCheckBox;
+class DeckListStatisticsAnalyzer;
+
+class ManaDistributionConfigDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr);
+ void retranslateUi();
+
+ void setFromConfig(const ManaDistributionConfig &cfg);
+ const ManaDistributionConfig &config() const
+ {
+ return cfg;
+ }
+
+public slots:
+ void accept() override;
+
+private:
+ DeckListStatisticsAnalyzer *analyzer;
+
+ QLabel *labelDisplayType;
+ QComboBox *displayType;
+ QLabel *labelFilters;
+ QListWidget *filterList;
+ QCheckBox *showColorRows;
+ QDialogButtonBox *buttons;
+
+ ManaDistributionConfig cfg;
+};
+
+#endif // COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp
new file mode 100644
index 000000000..8a5e14858
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp
@@ -0,0 +1,49 @@
+#include "mana_distribution_single_display_widget.h"
+
+#include "../../../cards/additional_info/mana_symbol_widget.h"
+
+#include
+
+ManaDistributionSingleDisplayWidget::ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent)
+ : QWidget(parent)
+{
+ auto layout = new QVBoxLayout(this);
+ layout->setAlignment(Qt::AlignHCenter);
+
+ symbolLabel = new ManaSymbolWidget(this, colorSymbol, true, false);
+ symbolLabel->setFixedSize(40, 40);
+
+ devotionBar = new QProgressBar(this);
+ devotionBar->setRange(0, 100);
+ devotionBar->setTextVisible(false);
+
+ devotionLabel = new QLabel(this);
+ devotionLabel->setAlignment(Qt::AlignCenter);
+
+ productionBar = new QProgressBar(this);
+ productionBar->setRange(0, 100);
+ productionBar->setTextVisible(false);
+
+ productionLabel = new QLabel(this);
+ productionLabel->setAlignment(Qt::AlignCenter);
+
+ layout->addWidget(symbolLabel);
+ layout->addWidget(devotionBar);
+ layout->addWidget(devotionLabel);
+ layout->addWidget(productionBar);
+ layout->addWidget(productionLabel);
+
+ setLayout(layout);
+}
+
+void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int percent)
+{
+ devotionBar->setValue(percent);
+ devotionLabel->setText(QString(tr("%1 pips (%2 cards)")).arg(pips).arg(cards));
+}
+
+void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent)
+{
+ productionBar->setValue(percent);
+ productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards));
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h
new file mode 100644
index 000000000..e33d0cec1
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h
@@ -0,0 +1,28 @@
+#ifndef COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H
+#define COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H
+
+#include
+#include
+#include
+#include
+
+class ManaDistributionSingleDisplayWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent = nullptr);
+
+ void setDevotion(int pips, int cards, int percent);
+ void setProduction(int pips, int cards, int percent);
+
+private:
+ QLabel *symbolLabel;
+
+ QProgressBar *devotionBar;
+ QLabel *devotionLabel;
+
+ QProgressBar *productionBar;
+ QLabel *productionLabel;
+};
+
+#endif // COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp
new file mode 100644
index 000000000..8eaea1426
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp
@@ -0,0 +1,129 @@
+#include "mana_distribution_widget.h"
+
+#include "../../analytics_panel_widget_registrar.h"
+#include "mana_distribution_config_dialog.h"
+
+#include
+#include
+#include
+#include
+
+namespace
+{
+AnalyticsPanelWidgetRegistrar registerManaDistribution{
+ "manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"),
+ [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }};
+
+} // anonymous namespace
+
+static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
+
+ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
+ : AbstractAnalyticsPanelWidget(parent, analyzer)
+{
+ container = new QWidget(this);
+ containerLayout = new QVBoxLayout(container);
+
+ devotionBarTop = new ColorBar({}, this);
+ devotionPieTop = new ColorPie({}, this);
+ productionBarTop = new ColorBar({}, this);
+ productionPieTop = new ColorPie({}, this);
+
+ containerLayout->addWidget(devotionBarTop);
+ containerLayout->addWidget(devotionPieTop);
+ containerLayout->addWidget(productionBarTop);
+ containerLayout->addWidget(productionPieTop);
+
+ devotionPieTop->hide();
+ productionPieTop->hide();
+
+ row = new QHBoxLayout();
+ containerLayout->addLayout(row);
+
+ for (const QString &c : kColors) {
+ auto *w = new ManaDistributionSingleDisplayWidget(c, this);
+ row->addWidget(w);
+ rows[c] = w;
+ }
+
+ layout->addWidget(container);
+}
+
+void ManaDistributionWidget::updateDisplay()
+{
+ const auto &devPips = analyzer->getDevotionPipCount();
+ const auto &devCards = analyzer->getDevotionCardCount();
+ const auto &prodPips = analyzer->getProductionPipCount();
+ const auto &prodCards = analyzer->getProductionCardCount();
+
+ QStringList filtered = config.filters.isEmpty() ? kColors : config.filters;
+
+ QMap devMap, prodMap;
+ for (const QString &c : filtered) {
+ devMap[c] = devPips.value(c, 0);
+ prodMap[c] = prodPips.value(c, 0);
+ }
+
+ bool showPie = (config.displayType == "pie");
+
+ devotionBarTop->setVisible(!showPie);
+ productionBarTop->setVisible(!showPie);
+
+ devotionPieTop->setVisible(showPie);
+ productionPieTop->setVisible(showPie);
+
+ if (showPie) {
+ devotionPieTop->setColors(devMap);
+ productionPieTop->setColors(prodMap);
+ } else {
+ devotionBarTop->setColors(devMap);
+ productionBarTop->setColors(prodMap);
+ }
+
+ for (const QString &c : kColors) {
+ auto *w = rows.value(c);
+
+ if (!w) {
+ continue;
+ }
+
+ bool visible = config.showColorRows && filtered.contains(c);
+ w->setVisible(visible);
+ if (!visible) {
+ continue;
+ }
+
+ int dp = devPips.value(c, 0);
+ int dc = devCards.value(c, 0);
+ int pp = prodPips.value(c, 0);
+ int pc = prodCards.value(c, 0);
+
+ // Compute percentages
+ int totalDev = 0;
+ int totalProd = 0;
+ for (const QString &cc : filtered) {
+ totalDev += devPips.value(cc, 0);
+ totalProd += prodPips.value(cc, 0);
+ }
+
+ int devPct = (totalDev > 0) ? int(100.0 * dp / totalDev) : 0;
+ int prodPct = (totalProd > 0) ? int(100.0 * pp / totalProd) : 0;
+
+ w->setDevotion(dp, dc, devPct);
+ w->setProduction(pp, pc, prodPct);
+ }
+}
+
+QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent)
+{
+ auto *dlg = new ManaDistributionConfigDialog(analyzer, parent);
+ dlg->setWindowTitle(tr("Mana Distribution Settings"));
+ dlg->setFromConfig(config);
+
+ connect(dlg, &QDialog::accepted, [this, dlg]() {
+ config = dlg->config();
+ updateDisplay();
+ });
+
+ return dlg;
+}
diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h
new file mode 100644
index 000000000..2d834f3af
--- /dev/null
+++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h
@@ -0,0 +1,45 @@
+#ifndef COCKATRICE_MANA_DISTRIBUTION_WIDGET_H
+#define COCKATRICE_MANA_DISTRIBUTION_WIDGET_H
+
+#include "../../../general/display/charts/bars/color_bar.h"
+#include "../../../general/display/charts/pies/color_pie.h"
+#include "../../abstract_analytics_panel_widget.h"
+#include "../../deck_list_statistics_analyzer.h"
+#include "mana_distribution_config.h"
+#include "mana_distribution_single_display_widget.h"
+
+#include
+#include
+#include
+#include
+
+class ManaDistributionWidget : public AbstractAnalyticsPanelWidget
+{
+ Q_OBJECT
+public:
+ explicit ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
+
+ void updateDisplay() override;
+ QDialog *createConfigDialog(QWidget *parent) override;
+ QJsonObject extractConfigFromDialog(QDialog *) const override
+ {
+ return {};
+ }
+
+private:
+ ManaDistributionConfig config;
+
+ QWidget *container;
+ QVBoxLayout *containerLayout;
+
+ QVBoxLayout *topLayout;
+ ColorBar *devotionBarTop;
+ ColorPie *devotionPieTop;
+ ColorBar *productionBarTop;
+ ColorPie *productionPieTop;
+
+ QHBoxLayout *row;
+ QMap rows;
+};
+
+#endif // COCKATRICE_MANA_DISTRIBUTION_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
index 45393bb5d..ea61302f0 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
+++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp
@@ -1,35 +1,298 @@
#include "deck_analytics_widget.h"
-DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel)
- : QWidget(parent), deckListModel(_deckListModel)
-{
- mainLayout = new QVBoxLayout();
- setLayout(mainLayout);
+#include "abstract_analytics_panel_widget.h"
+#include "add_analytics_panel_dialog.h"
+#include "analytics_panel_widget_factory.h"
+#include "analyzer_modules/mana_base/mana_base_config.h"
+#include "analyzer_modules/mana_curve/mana_curve_config.h"
+#include "analyzer_modules/mana_devotion/mana_devotion_config.h"
+#include "deck_list_statistics_analyzer.h"
+#include "resizable_panel.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer)
+ : QWidget(parent), statsAnalyzer(_statsAnalyzer)
+{
+ layout = new QVBoxLayout(this);
+
+ // Controls
+ controlContainer = new QWidget(this);
+ controlLayout = new QHBoxLayout(controlContainer);
+ addButton = new QPushButton(this);
+ removeButton = new QPushButton(this);
+ saveButton = new QPushButton(this);
+ loadButton = new QPushButton(this);
+ controlLayout->addWidget(addButton);
+ controlLayout->addWidget(removeButton);
+ controlLayout->addWidget(saveButton);
+ controlLayout->addWidget(loadButton);
+
+ layout->addWidget(controlContainer);
+
+ connect(addButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onAddPanel);
+ connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected);
+ connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout);
+ connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout);
+
+ // Scroll area and container
scrollArea = new QScrollArea(this);
- scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
- scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setWidgetResizable(true);
- mainLayout->addWidget(scrollArea);
+ scrollArea->setFrameShape(QFrame::NoFrame);
- container = new QWidget(scrollArea);
- containerLayout = new QVBoxLayout(container);
- container->setLayout(containerLayout);
- scrollArea->setWidget(container);
+ panelContainer = new QWidget(scrollArea);
+ panelLayout = new QVBoxLayout(panelContainer);
+ panelLayout->setSpacing(8);
+ panelLayout->setContentsMargins(4, 4, 4, 4);
+ panelLayout->addStretch(1); // push panels up
- deckListStatisticsAnalyzer = new DeckListStatisticsAnalyzer(this, deckListModel);
+ scrollArea->setWidget(panelContainer);
+ layout->addWidget(scrollArea);
- manaCurveWidget = new ManaCurveWidget(this, deckListStatisticsAnalyzer);
- containerLayout->addWidget(manaCurveWidget);
+ loadLayout();
- manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer);
- containerLayout->addWidget(manaDevotionWidget);
-
- manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer);
- containerLayout->addWidget(manaBaseWidget);
+ retranslateUi();
}
-void DeckAnalyticsWidget::refreshDisplays()
+void DeckAnalyticsWidget::retranslateUi()
{
- deckListStatisticsAnalyzer->update();
+ addButton->setText(tr("Add Panel"));
+ removeButton->setText(tr("Remove Panel"));
+ saveButton->setText(tr("Save Layout"));
+ loadButton->setText(tr("Load Layout"));
}
+
+void DeckAnalyticsWidget::updateDisplays()
+{
+ statsAnalyzer->analyze();
+}
+
+void DeckAnalyticsWidget::onAddPanel()
+{
+ AddAnalyticsPanelDialog dlg(this);
+ if (dlg.exec() != QDialog::Accepted) {
+ return;
+ }
+
+ QString selection = dlg.selectedType();
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ AbstractAnalyticsPanelWidget *analyticsWidget =
+ AnalyticsPanelWidgetFactory::instance().create(selection, this, statsAnalyzer);
+ if (!analyticsWidget) {
+ return;
+ }
+
+ if (!analyticsWidget->applyConfigFromDialog()) {
+ analyticsWidget->deleteLater();
+ return;
+ }
+
+ addPanelInstance(selection, analyticsWidget, analyticsWidget->saveConfig());
+}
+
+void DeckAnalyticsWidget::addPanelInstance(const QString &typeId,
+ AbstractAnalyticsPanelWidget *panel,
+ const QJsonObject &cfg)
+{
+ panel->loadConfig(cfg);
+ panel->updateDisplay();
+
+ auto *resPanel = new ResizablePanel(typeId, panel, panelContainer);
+ panelWrappers.push_back(resPanel);
+
+ panelLayout->insertWidget(panelLayout->count() - 1, resPanel);
+
+ // Event filter for selection
+ resPanel->installEventFilter(this);
+ panel->installEventFilter(this);
+
+ // Connect drag-drop signals
+ connect(resPanel, &ResizablePanel::dropRequested, this, &DeckAnalyticsWidget::onPanelDropped);
+}
+
+void DeckAnalyticsWidget::onRemoveSelected()
+{
+ int idx = indexOfSelectedWrapper();
+ if (idx < 0) {
+ return;
+ }
+
+ ResizablePanel *panel = panelWrappers.takeAt(idx);
+ selectWrapper(nullptr);
+
+ panel->deleteLater();
+}
+
+void DeckAnalyticsWidget::saveLayout()
+{
+ QJsonArray arr;
+
+ for (auto *wrapper : panelWrappers) {
+ QJsonObject entry;
+ entry["type"] = wrapper->getTypeId();
+ entry["config"] = wrapper->panel->saveConfig();
+ entry["height"] = wrapper->getCurrentHeight();
+ arr.append(entry);
+ }
+
+ QSettings s;
+ s.setValue("deckAnalytics/layout", QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact)));
+}
+
+void DeckAnalyticsWidget::loadLayout()
+{
+ if (!loadLayoutInternal()) {
+ addDefaultPanels();
+ }
+}
+
+void DeckAnalyticsWidget::addDefaultPanels()
+{
+ struct DefaultPanel
+ {
+ QString type;
+ QJsonObject cfg;
+ };
+
+ // Prepare configs
+ QJsonObject manaCurveCfg = ManaCurveConfig{}.toJson();
+ QJsonObject manaBaseCfg = ManaBaseConfig{"combinedBar", {}}.toJson();
+ QJsonObject manaDevotionCfg = ManaDevotionConfig{"combinedBar", {}}.toJson();
+ QVector defaults = {
+ {"manaCurve", manaCurveCfg}, {"manaBase", manaBaseCfg}, {"manaDevotion", manaDevotionCfg}};
+
+ for (auto &d : defaults) {
+ AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(d.type, this, statsAnalyzer);
+ if (!w) {
+ continue;
+ }
+
+ w->loadConfig(d.cfg);
+ addPanelInstance(d.type, w, d.cfg);
+ }
+}
+
+bool DeckAnalyticsWidget::loadLayoutInternal()
+{
+ QSettings s;
+ QString layoutData = s.value("deckAnalytics/layout").toString();
+ if (layoutData.isEmpty()) {
+ return false;
+ }
+
+ QJsonDocument doc = QJsonDocument::fromJson(layoutData.toUtf8());
+ if (!doc.isArray()) {
+ return false;
+ }
+
+ clearPanels();
+
+ for (auto v : doc.array()) {
+ if (!v.isObject()) {
+ continue;
+ }
+ QJsonObject o = v.toObject();
+ QString type = o["type"].toString();
+ QJsonObject cfg = o["config"].toObject();
+
+ AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(type, this, statsAnalyzer);
+ if (!w) {
+ continue;
+ }
+
+ addPanelInstance(type, w, cfg);
+
+ // Restore height AFTER adding the panel
+ if (o.contains("height")) {
+ panelWrappers.last()->setHeightFromSaved(o["height"].toInt());
+ }
+ }
+
+ return true;
+}
+
+void DeckAnalyticsWidget::clearPanels()
+{
+ selectWrapper(nullptr);
+ while (!panelWrappers.isEmpty()) {
+ ResizablePanel *p = panelWrappers.takeLast();
+ p->deleteLater();
+ }
+}
+
+bool DeckAnalyticsWidget::eventFilter(QObject *obj, QEvent *event)
+{
+ if (event->type() == QEvent::MouseButtonPress) {
+ for (auto *p : panelWrappers) {
+ if (obj == p || obj == p->panel) {
+ selectWrapper(p);
+ break;
+ }
+ }
+ }
+ return QWidget::eventFilter(obj, event);
+}
+
+void DeckAnalyticsWidget::selectWrapper(ResizablePanel *w)
+{
+ // Same wrapper
+ if (selectedWrapper == w) {
+ return;
+ }
+ // Deselect the old one
+ if (selectedWrapper) {
+ selectedWrapper->setSelected(false);
+ }
+ // Set current
+ selectedWrapper = w;
+ // Finally, select new
+ if (selectedWrapper) {
+ selectedWrapper->setSelected(true);
+ }
+}
+
+int DeckAnalyticsWidget::indexOfSelectedWrapper() const
+{
+ if (!selectedWrapper) {
+ return -1;
+ }
+ return panelWrappers.indexOf(selectedWrapper);
+}
+
+void DeckAnalyticsWidget::onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore)
+{
+ int draggedIdx = panelWrappers.indexOf(dragged);
+ int targetIdx = panelWrappers.indexOf(target);
+
+ if (draggedIdx == -1 || targetIdx == -1 || draggedIdx == targetIdx) {
+ return;
+ }
+
+ // Remove dragged panel from list and layout
+ panelWrappers.removeAt(draggedIdx);
+ panelLayout->removeWidget(dragged);
+
+ // Adjust target index if needed
+ if (draggedIdx < targetIdx) {
+ targetIdx--;
+ }
+
+ // Calculate insertion position
+ int insertIdx = insertBefore ? targetIdx : targetIdx + 1;
+
+ // Insert back into list and layout
+ panelWrappers.insert(insertIdx, dragged);
+ panelLayout->insertWidget(insertIdx, dragged);
+
+ // Clear selection
+ selectWrapper(nullptr);
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h
index 524362aed..31ee36fbb 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h
+++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h
@@ -1,44 +1,71 @@
/**
* @file deck_analytics_widget.h
* @ingroup DeckEditorAnalyticsWidgets
- * @brief TODO: Document this.
+ * @brief Main analytics widget container with resizable panels for deck statistics.
*/
#ifndef DECK_ANALYTICS_WIDGET_H
#define DECK_ANALYTICS_WIDGET_H
-#include "mana_base_widget.h"
-#include "mana_curve_widget.h"
-#include "mana_devotion_widget.h"
+#include "abstract_analytics_panel_widget.h"
+#include "deck_list_statistics_analyzer.h"
+#include "resizable_panel.h"
-#include
+#include
#include
+#include
+#include
#include
-#include
+
+class LayoutInspector;
class DeckAnalyticsWidget : public QWidget
{
Q_OBJECT
+public slots:
+ void updateDisplays();
+
public:
- explicit DeckAnalyticsWidget(QWidget *parent, DeckListModel *deckListModel);
- void setDeckList(const DeckList &_deckListModel);
- std::map analyzeManaCurve();
- void refreshDisplays();
+ explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
+ void retranslateUi();
+
+private slots:
+ void onAddPanel();
+ void onRemoveSelected();
+ void onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore);
+ void saveLayout();
+ void loadLayout();
+ void addDefaultPanels();
+ bool loadLayoutInternal();
+ void clearPanels();
+
+protected:
+ bool eventFilter(QObject *obj, QEvent *event) override;
+ void selectWrapper(ResizablePanel *panel);
+ int indexOfSelectedWrapper() const;
private:
- DeckListModel *deckListModel;
- DeckListStatisticsAnalyzer *deckListStatisticsAnalyzer;
- QVBoxLayout *mainLayout;
+ void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {});
- QWidget *container;
- QVBoxLayout *containerLayout;
+ QVBoxLayout *layout;
+ QWidget *controlContainer;
+ QHBoxLayout *controlLayout;
+
+ QPushButton *addButton;
+ QPushButton *removeButton;
+ QPushButton *saveButton;
+ QPushButton *loadButton;
QScrollArea *scrollArea;
+ QWidget *panelContainer;
+ QVBoxLayout *panelLayout;
- ManaCurveWidget *manaCurveWidget;
- ManaDevotionWidget *manaDevotionWidget;
- ManaBaseWidget *manaBaseWidget;
+ QVector panelWrappers;
+ ResizablePanel *selectedWrapper = nullptr;
+
+ DeckListStatisticsAnalyzer *statsAnalyzer;
+ LayoutInspector *insp = nullptr;
};
#endif // DECK_ANALYTICS_WIDGET_H
diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp
index 3ca18d21c..7d0259c72 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp
+++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp
@@ -9,38 +9,93 @@
DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent,
DeckListModel *_model,
- DeckListStatisticsAnalyzerConfig cfg)
- : QObject(parent), model(_model), config(cfg)
+ DeckListStatisticsAnalyzerConfig _config)
+ : QObject(parent), model(_model), config(_config)
{
- connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::update);
+ connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::analyze);
}
-void DeckListStatisticsAnalyzer::update()
+void DeckListStatisticsAnalyzer::analyze()
{
- manaBaseMap.clear();
- manaCurveMap.clear();
- manaDevotionMap.clear();
+ clearData();
QList cards = model->getCards();
- for (const ExactCard &card : cards) {
- // ---- Mana curve ----
+ for (auto card : cards) {
+ auto info = card.getInfo();
+ const int cmc = info.getCmc().toInt();
+
+ // Convert once
+ QStringList types = info.getMainCardType().split(' ');
+ QStringList subtypes = info.getCardType().split('-').last().split(" ");
+ QString colors = info.getColors();
+ int power = info.getPowTough().split("/").first().toInt();
+ int toughness = info.getPowTough().split("/").last().toInt();
+
+ // For each copy of card
+ // ---------------- Mana Curve ----------------
if (config.computeManaCurve) {
- manaCurveMap[card.getInfo().getCmc().toInt()]++;
+ manaCurveMap[cmc]++;
}
- // ---- Mana base ----
+ // per-type curve
+ for (auto &t : types) {
+ manaCurveByType[t][cmc]++;
+ manaCurveCardsByType[t][cmc].append(info.getName());
+ }
+
+ // Per-subtype curve
+ for (auto &st : subtypes) {
+ manaCurveBySubtype[st][cmc]++;
+ manaCurveCardsBySubtype[st][cmc].append(info.getName());
+ }
+
+ // per-color curve
+ for (auto &c : colors) {
+ manaCurveByColor[c][cmc]++;
+ manaCurveCardsByColor[c][cmc].append(info.getName());
+ }
+
+ // Power/toughness
+ manaCurveByPower[QString::number(power)][cmc]++;
+ manaCurveCardsByPower[QString::number(power)][cmc].append(info.getName());
+ manaCurveByToughness[QString::number(toughness)][cmc]++;
+ manaCurveCardsByToughness[QString::number(toughness)][cmc].append(info.getName());
+
+ // ========== Category Counts ===========
+ for (auto &t : types) {
+ typeCount[t]++;
+ }
+ for (auto &st : subtypes) {
+ subtypeCount[st]++;
+ }
+ for (auto &c : colors) {
+ colorCount[c]++;
+ }
+ manaValueCount[cmc]++;
+
+ // ---------------- Mana Base ----------------
if (config.computeManaBase) {
- auto mana = determineManaProduction(card.getInfo().getText());
- for (auto it = mana.begin(); it != mana.end(); ++it)
+ auto prod = determineManaProduction(info.getText());
+ for (auto it = prod.begin(); it != prod.end(); ++it) {
+ if (it.value() > 0) {
+ productionPipCount[it.key()] += it.value();
+ productionCardCount[it.key()]++;
+ }
manaBaseMap[it.key()] += it.value();
+ }
}
- // ---- Devotion ----
+ // ---------------- Devotion ----------------
if (config.computeDevotion) {
- auto devo = countManaSymbols(card.getInfo().getManaCost());
- for (auto &d : devo)
+ auto devo = countManaSymbols(info.getManaCost());
+ for (auto &d : devo) {
+ if (d.second > 0) {
+ devotionPipCount[QString(d.first)] += d.second;
+ devotionCardCount[QString(d.first)]++;
+ }
manaDevotionMap[d.first] += d.second;
+ }
}
}
@@ -112,3 +167,57 @@ std::unordered_map DeckListStatisticsAnalyzer::countManaSymbols(const
return manaCounts;
}
+
+// Hypergeometric probability: P(X=k)
+double DeckListStatisticsAnalyzer::hypergeometric(int N, int K, int n, int k)
+{
+ if (k < 0 || k > n || K > N) {
+ return 0.0;
+ }
+
+ auto choose = [](int n, int r) -> double {
+ if (r > n)
+ return 0.0;
+ if (r == 0 || r == n)
+ return 1.0;
+ double res = 1.0;
+ for (int i = 1; i <= r; ++i) {
+ res *= (n - r + i);
+ res /= i;
+ }
+ return res;
+ };
+
+ return choose(K, k) * choose(N - K, n - k) / choose(N, n);
+}
+
+void DeckListStatisticsAnalyzer::clearData()
+{
+ manaBaseMap.clear();
+ manaCurveMap.clear();
+ manaDevotionMap.clear();
+
+ devotionPipCount.clear();
+ devotionCardCount.clear();
+
+ productionPipCount.clear();
+ productionCardCount.clear();
+
+ manaCurveByType.clear();
+ manaCurveBySubtype.clear();
+ manaCurveByColor.clear();
+ manaCurveByPower.clear();
+ manaCurveByToughness.clear();
+
+ manaCurveCardsByType.clear();
+ manaCurveCardsBySubtype.clear();
+ manaCurveCardsByColor.clear();
+ manaCurveCardsByPower.clear();
+ manaCurveCardsByToughness.clear();
+
+ typeCount.clear();
+ subtypeCount.clear();
+ colorCount.clear();
+ rarityCount.clear();
+ manaValueCount.clear();
+}
\ No newline at end of file
diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h
index 8fa033971..946bb0117 100644
--- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h
+++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h
@@ -14,6 +14,9 @@ struct DeckListStatisticsAnalyzerConfig
bool computeManaBase = true;
bool computeManaCurve = true;
bool computeDevotion = true;
+ bool computeCategories = true;
+ bool computeCurveBreakdowns = true;
+ bool computeProbabilities = true;
};
class DeckListStatisticsAnalyzer : public QObject
@@ -23,9 +26,9 @@ class DeckListStatisticsAnalyzer : public QObject
public:
explicit DeckListStatisticsAnalyzer(QObject *parent,
DeckListModel *model,
- DeckListStatisticsAnalyzerConfig cfg = DeckListStatisticsAnalyzerConfig());
+ DeckListStatisticsAnalyzerConfig _config = DeckListStatisticsAnalyzerConfig());
- void update();
+ void analyze();
[[nodiscard]] const QHash &getManaBase() const
{
@@ -40,6 +43,96 @@ public:
return manaDevotionMap;
}
+ const QHash &getDevotionPipCount() const
+ {
+ return devotionPipCount;
+ }
+ const QHash &getDevotionCardCount() const
+ {
+ return devotionCardCount;
+ }
+
+ const QHash &getProductionPipCount() const
+ {
+ return productionPipCount;
+ }
+ const QHash &getProductionCardCount() const
+ {
+ return productionCardCount;
+ }
+
+ const QHash &getTypeCount() const
+ {
+ return typeCount;
+ }
+ const QHash &getSubtypeCount() const
+ {
+ return subtypeCount;
+ }
+ const QHash &getColorCount() const
+ {
+ return colorCount;
+ }
+ const QHash &getRarityCount() const
+ {
+ return rarityCount;
+ }
+ const QHash &getManaValueCount() const
+ {
+ return manaValueCount;
+ }
+
+ const QHash> &getManaCurveByType() const
+ {
+ return manaCurveByType;
+ }
+ const QHash> &getManaCurveBySubtype() const
+ {
+ return manaCurveBySubtype;
+ }
+ const QHash> &getManaCurveByColor() const
+ {
+ return manaCurveByColor;
+ }
+ const QHash> &getManaCurveByPower() const
+ {
+ return manaCurveByPower;
+ }
+ const QHash> &getManaCurveByToughness() const
+ {
+ return manaCurveByToughness;
+ }
+
+ const QHash> &getManaCurveCardsByType() const
+ {
+ return manaCurveCardsByType;
+ }
+
+ const QHash> &getManaCurveCardsBySubtype() const
+ {
+ return manaCurveCardsBySubtype;
+ }
+
+ const QHash> &getManaCurveCardsByColor() const
+ {
+ return manaCurveCardsByColor;
+ }
+
+ const QHash> &getManaCurveCardsByPower() const
+ {
+ return manaCurveCardsByPower;
+ }
+
+ const QHash> &getManaCurveCardsByToughness() const
+ {
+ return manaCurveCardsByToughness;
+ }
+
+ DeckListModel *getModel() const
+ {
+ return model;
+ }
+
signals:
void statsUpdated();
@@ -47,14 +140,42 @@ private:
DeckListModel *model;
DeckListStatisticsAnalyzerConfig config;
- // Internal result containers
QHash manaBaseMap;
std::unordered_map manaCurveMap;
std::unordered_map manaDevotionMap;
- // Internal helper functions
+ QHash devotionPipCount; // W/U/B/R/G total symbols
+ QHash devotionCardCount; // how many cards provide devotion
+
+ QHash productionPipCount; // mana produced by cards
+ QHash productionCardCount; // number of producers
+
+ QHash typeCount;
+ QHash subtypeCount;
+ QHash colorCount;
+ QHash rarityCount;
+ QHash manaValueCount;
+
+ QHash> manaCurveByType;
+ QHash> manaCurveBySubtype;
+ QHash> manaCurveByColor;
+ QHash