diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index c6a29969f..8ebd177db 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -144,11 +144,31 @@ set(cockatrice_SOURCES src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/deck_card_zone_display_widget.cpp src/interface/widgets/cards/deck_preview_card_picture_widget.cpp + src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp + src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp src/interface/widgets/deck_analytics/deck_analytics_widget.cpp src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp - src/interface/widgets/deck_analytics/mana_base_widget.cpp - src/interface/widgets/deck_analytics/mana_curve_widget.cpp - src/interface/widgets/deck_analytics/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/resizable_panel.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -160,13 +180,17 @@ set(cockatrice_SOURCES src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/banner_widget.cpp - src/interface/widgets/general/display/bar_widget.cpp - src/interface/widgets/general/display/color_bar.cpp src/interface/widgets/general/display/dynamic_font_size_label.cpp src/interface/widgets/general/display/dynamic_font_size_push_button.cpp src/interface/widgets/general/display/labeled_input.cpp - src/interface/widgets/general/display/percent_bar_widget.cpp src/interface/widgets/general/display/shadow_background_label.cpp + src/interface/widgets/general/display/charts/bars/bar_widget.cpp + src/interface/widgets/general/display/charts/bars/color_bar.cpp + src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp + src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp + src/interface/widgets/general/display/charts/pies/color_pie.cpp src/interface/widgets/general/home_styled_button.cpp src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp diff --git a/cockatrice/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 fde8e9b34..2ad1f1ece 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -288,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 = @@ -715,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 e15a466c3..3ea4eb119 100644 --- a/cockatrice/src/game/zones/view_zone_widget.cpp +++ b/cockatrice/src/game/zones/view_zone_widget.cpp @@ -272,10 +272,11 @@ void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event) QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const { const QRectF frameRectF = windowFrameRect(); - const QRect titleBarRect(frameRectF.toRect().x(), frameRectF.toRect().y(), frameRectF.toRect().width(), - static_cast(kTitleBarHeight)); // 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); @@ -283,19 +284,26 @@ QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const 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()) + + if (styleWidget->isActiveWindow()) { opt.state |= QStyle::State_Active; - const QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton, - styleWidget); + } + + 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); } } - // fallback: square at right end of titlebar (Windows/Linux style) + // Fallback: frame-relative top-right return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight); } @@ -349,6 +357,7 @@ bool ZoneViewWidget::windowFrameEvent(QEvent *event) close(); return true; } + startWindowDrag(me); me->accept(); return true; diff --git a/cockatrice/src/game/zones/view_zone_widget.h b/cockatrice/src/game/zones/view_zone_widget.h index 4ed8f74d8..1246192b8 100644 --- a/cockatrice/src/game/zones/view_zone_widget.h +++ b/cockatrice/src/game/zones/view_zone_widget.h @@ -54,7 +54,6 @@ private: ZoneViewZone *zone; QGraphicsWidget *zoneContainer; - QPushButton *closeButton; QScrollBar *scrollBar; ScrollableGraphicsProxyWidget *scrollBarProxy; diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp new file mode 100644 index 000000000..bad883d27 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp @@ -0,0 +1,48 @@ +#include "abstract_analytics_panel_widget.h" + +#include "deck_list_statistics_analyzer.h" + +#include + +AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : QWidget(parent), analyzer(analyzer) +{ + layout = new QVBoxLayout(this); + + bannerAndSettingsContainer = new QWidget(this); + + bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer); + bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout); + bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100); + bannerWidget->setMaximumHeight(100); + + bannerAndSettingsLayout->addWidget(bannerWidget, 1); + + // config button + configureButton = new QPushButton(tr("Configure"), this); + configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); + bannerAndSettingsLayout->addWidget(configureButton, 0); + + layout->addWidget(bannerAndSettingsContainer); + + connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay); +} + +bool AbstractAnalyticsPanelWidget::applyConfigFromDialog() +{ + QDialog *dlg = createConfigDialog(this); + if (!dlg) { + return false; + } + + bool ok = dlg->exec() == QDialog::Accepted; + if (ok) { + // dialog must expose its final config as JSON + auto newCfg = extractConfigFromDialog(dlg); + loadConfig(newCfg); + updateDisplay(); + } + dlg->deleteLater(); + return ok; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h new file mode 100644 index 000000000..23374a9e1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h @@ -0,0 +1,61 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H + +#include "../general/display/banner_widget.h" + +#include +#include +#include +#include + +class DeckListStatisticsAnalyzer; + +class AbstractAnalyticsPanelWidget : public QWidget +{ + Q_OBJECT +public slots: + virtual void updateDisplay() = 0; + // Widgets must return a config dialog + virtual QDialog *createConfigDialog(QWidget *parent) = 0; + +public: + explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + void setDisplayTitle(const QString &title) + { + displayTitle = title; + if (bannerWidget) { + bannerWidget->setText(displayTitle); + } + } + + QString displayTitleText() const + { + return displayTitle; + } + + virtual QJsonObject saveConfig() const + { + return {}; + } + virtual void loadConfig(const QJsonObject &) + { + } + + // Unified helper to run config dialog and update widget + bool applyConfigFromDialog(); + + // Dialog → JSON must be supplied by each subclass + virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0; + +protected: + DeckListStatisticsAnalyzer *analyzer; + QVBoxLayout *layout; + QWidget *bannerAndSettingsContainer; + QHBoxLayout *bannerAndSettingsLayout; + QString displayTitle; + BannerWidget *bannerWidget; + QPushButton *configureButton; +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp new file mode 100644 index 000000000..c83e6d982 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp @@ -0,0 +1,32 @@ +#include "add_analytics_panel_dialog.h" + +#include "analytics_panel_widget_factory.h" + +#include +#include + +AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent) +{ + setWindowTitle(tr("Add Analytics Panel")); + + layout = new QVBoxLayout(this); + + typeCombo = new QComboBox(this); + + // Populate using descriptors + const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets(); + + for (const auto &desc : widgets) { + // Show translated title to user + typeCombo->addItem(desc.title, desc.type); + } + + layout->addWidget(typeCombo); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h new file mode 100644 index 000000000..aa44734c2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h @@ -0,0 +1,29 @@ + +#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H +#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H + +#include "analytics_panel_widget_factory.h" + +#include +#include +#include +#include + +class AddAnalyticsPanelDialog : public QDialog +{ + Q_OBJECT +public: + explicit AddAnalyticsPanelDialog(QWidget *parent); + + QString selectedType() const + { + return typeCombo->currentData().toString(); + } + +private: + QVBoxLayout *layout; + QComboBox *typeCombo; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp new file mode 100644 index 000000000..7af641689 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp @@ -0,0 +1,33 @@ +#include "analytics_panel_widget_factory.h" + +#include "abstract_analytics_panel_widget.h" + +AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance() +{ + static AnalyticsPanelWidgetFactory f; + return f; +} + +void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc) +{ + widgets.insert(desc.type, desc); +} + +AbstractAnalyticsPanelWidget * +AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const +{ + auto it = widgets.find(type); + if (it == widgets.end()) + return nullptr; + + auto w = it->creator(parent, analyzer); + + w->setDisplayTitle(it->title); + + return w; +} + +QList AnalyticsPanelWidgetFactory::availableWidgets() const +{ + return widgets.values(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h new file mode 100644 index 000000000..6c5856d70 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H + +#include +#include +#include +#include +#include + +class AbstractAnalyticsPanelWidget; +class DeckListStatisticsAnalyzer; + +class AnalyticsPanelWidgetFactory +{ +public: + using Creator = std::function; + + struct Descriptor + { + QString type; // stable ID ("manaProdDevotion") + QString title; // translated, user-facing + Creator creator; + }; + + static AnalyticsPanelWidgetFactory &instance(); + + // NEW: richer registration + void registerWidget(const Descriptor &desc); + + AbstractAnalyticsPanelWidget * + create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const; + + // NEW: expose widgets to UI + QList availableWidgets() const; + +private: + AnalyticsPanelWidgetFactory() = default; // Ensure private constructor + AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete; + AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete; + + QMap widgets; +}; + +#endif diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp new file mode 100644 index 000000000..d4129a3d0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp @@ -0,0 +1 @@ +#include "analytics_panel_widget_registrar.h" diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h new file mode 100644 index 000000000..70d6df94f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h @@ -0,0 +1,17 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H + +#include "analytics_panel_widget_factory.h" + +class AnalyticsPanelWidgetRegistrar +{ +public: + AnalyticsPanelWidgetRegistrar(const QString &type, + const QString &title, + AnalyticsPanelWidgetFactory::Creator creator) + { + AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator}); + } +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp new file mode 100644 index 000000000..51129a4b4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp @@ -0,0 +1,28 @@ +#include "draw_probability_config.h" + +QJsonObject DrawProbabilityConfig::toJson() const +{ + QJsonObject o; + o["criteria"] = criteria; + o["atLeast"] = atLeast; + o["quantity"] = quantity; + o["drawn"] = drawn; + return o; +} +DrawProbabilityConfig DrawProbabilityConfig::fromJson(const QJsonObject &o) +{ + DrawProbabilityConfig cfg; + if (o.contains("criteria")) { + cfg.criteria = o["criteria"].toString(); + } + if (o.contains("atLeast")) { + cfg.atLeast = o["atLeast"].toBool(true); + } + if (o.contains("quantity")) { + cfg.quantity = o["quantity"].toInt(1); + } + if (o.contains("drawn")) { + cfg.drawn = o["drawn"].toInt(7); + } + return cfg; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h new file mode 100644 index 000000000..bbe61a68e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H +#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H + +#include +#include + +struct DrawProbabilityConfig +{ + QString criteria = "name"; // name, type, subtype, cmc + bool atLeast = true; // true = at least, false = exactly + int quantity = 1; // N + int drawn = 7; // M + + QJsonObject toJson() const; + + static DrawProbabilityConfig fromJson(const QJsonObject &o); +}; + +#endif diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp new file mode 100644 index 000000000..71f88c0fc --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp @@ -0,0 +1,92 @@ +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include + +DrawProbabilityConfigDialog::DrawProbabilityConfigDialog(QWidget *parent) : QDialog(parent) +{ + form = new QFormLayout(this); + + // Criteria + labelCriteria = new QLabel(this); + criteria = new QComboBox(this); + criteria->addItem(QString(), "name"); + criteria->addItem(QString(), "type"); + criteria->addItem(QString(), "subtype"); + criteria->addItem(QString(), "cmc"); + form->addRow(labelCriteria, criteria); + + // Exactness + labelExactness = new QLabel(this); + exactness = new QComboBox(this); + exactness->addItem(QString(), true); + exactness->addItem(QString(), false); + form->addRow(labelExactness, exactness); + + // Quantity + labelQuantity = new QLabel(this); + quantity = new QSpinBox(this); + quantity->setRange(1, 60); + form->addRow(labelQuantity, quantity); + + // Drawn + labelDrawn = new QLabel(this); + drawn = new QSpinBox(this); + drawn->setRange(1, 60); + drawn->setValue(7); + form->addRow(labelDrawn, drawn); + + // Button box + auto *bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + form->addWidget(bb); + + connect(bb, &QDialogButtonBox::accepted, this, &DrawProbabilityConfigDialog::accept); + connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject); + + retranslateUi(); +} + +void DrawProbabilityConfigDialog::retranslateUi() +{ + setWindowTitle(tr("Draw Probability Settings")); + + labelCriteria->setText(tr("Criteria:")); + criteria->setItemText(0, tr("Card Name")); + criteria->setItemText(1, tr("Type")); + criteria->setItemText(2, tr("Subtype")); + criteria->setItemText(3, tr("Mana Value")); + + labelExactness->setText(tr("Exactness:")); + exactness->setItemText(0, tr("At least")); + exactness->setItemText(1, tr("Exactly")); + + labelQuantity->setText(tr("Quantity (N):")); + labelDrawn->setText(tr("Cards drawn (M):")); + + // i18n-friendly suffixes + quantity->setSuffix(tr(" cards")); + drawn->setSuffix(tr(" cards")); +} + +void DrawProbabilityConfigDialog::setFromConfig(const DrawProbabilityConfig &_config) +{ + cfg = _config; + + criteria->setCurrentIndex(criteria->findData(_config.criteria)); + exactness->setCurrentIndex(exactness->findData(_config.atLeast)); + quantity->setValue(_config.quantity); + drawn->setValue(_config.drawn); +} + +void DrawProbabilityConfigDialog::accept() +{ + cfg.criteria = criteria->currentData().toString(); + cfg.atLeast = exactness->currentData().toBool(); + cfg.quantity = quantity->value(); + cfg.drawn = drawn->value(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h new file mode 100644 index 000000000..44b1f0eec --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h @@ -0,0 +1,44 @@ +#pragma once + +#include "draw_probability_config.h" + +#include +#include + +class QComboBox; +class QSpinBox; +class QLabel; + +class DrawProbabilityConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit DrawProbabilityConfigDialog(QWidget *parent = nullptr); + + void retranslateUi(); + + void setFromConfig(const DrawProbabilityConfig &_config); + DrawProbabilityConfig result() const + { + return cfg; + } + +protected: + void accept() override; + +private: + DrawProbabilityConfig cfg; + + QFormLayout *form; + + // Widgets + QComboBox *criteria; + QComboBox *exactness; + QSpinBox *quantity; + QSpinBox *drawn; + + QLabel *labelCriteria; + QLabel *labelExactness; + QLabel *labelQuantity; + QLabel *labelDrawn; +}; diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp new file mode 100644 index 000000000..a8bec834f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp @@ -0,0 +1,236 @@ +#include "draw_probability_widget.h" + +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : AbstractAnalyticsPanelWidget(parent, analyzer) +{ + controls = new QWidget(this); + controlLayout = new QHBoxLayout(controls); + + labelPrefix = new QLabel(this); + controlLayout->addWidget(labelPrefix); + + criteriaCombo = new QComboBox(this); + // Give these things item-data so we can translate the actual user-facing strings + criteriaCombo->addItem(QString(), "name"); + criteriaCombo->addItem(QString(), "type"); + criteriaCombo->addItem(QString(), "subtype"); + criteriaCombo->addItem(QString(), "cmc"); + controlLayout->addWidget(criteriaCombo); + + exactnessCombo = new QComboBox(this); + exactnessCombo->addItem(QString(), true); // At least + exactnessCombo->addItem(QString(), false); // Exactly + controlLayout->addWidget(exactnessCombo); + + quantitySpin = new QSpinBox(this); + quantitySpin->setRange(1, 60); + controlLayout->addWidget(quantitySpin); + + labelMiddle = new QLabel(this); + controlLayout->addWidget(labelMiddle); + + drawnSpin = new QSpinBox(this); + drawnSpin->setRange(1, 60); + drawnSpin->setValue(7); + controlLayout->addWidget(drawnSpin); + + labelSuffix = new QLabel(this); + controlLayout->addWidget(labelSuffix); + + labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + controlLayout->addStretch(1); + layout->addWidget(controls); + + // Table + resultTable = new QTableWidget(this); + resultTable->setColumnCount(3); + resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + layout->addWidget(resultTable); + + // Connections + connect(criteriaCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + config.criteria = criteriaCombo->currentData().toString(); + updateDisplay(); + }); + + connect(exactnessCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + config.atLeast = exactnessCombo->currentData().toBool(); + updateDisplay(); + }); + + connect(quantitySpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { + config.quantity = v; + updateDisplay(); + }); + + connect(drawnSpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { + config.drawn = v; + updateDisplay(); + }); + + retranslateUi(); + applyConfigToToolbar(); + updateFilterOptions(); +} + +void DrawProbabilityWidget::retranslateUi() +{ + bannerWidget->setText(tr("Draw Probability")); + + labelPrefix->setText(tr("Probability of drawing")); + + criteriaCombo->setItemText(0, tr("Card Name")); + criteriaCombo->setItemText(1, tr("Type")); + criteriaCombo->setItemText(2, tr("Subtype")); + criteriaCombo->setItemText(3, tr("Mana Value")); + + exactnessCombo->setItemText(0, tr("At least")); + exactnessCombo->setItemText(1, tr("Exactly")); + + labelMiddle->setText(tr("card(s) having drawn at least")); + labelSuffix->setText(tr("cards")); + + resultTable->setHorizontalHeaderLabels({tr("Category"), tr("Qty"), tr("Odds (%)")}); +} + +QDialog *DrawProbabilityWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new DrawProbabilityConfigDialog(parent); + dlg->setFromConfig(config); + return dlg; +} + +QJsonObject DrawProbabilityWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *dp = qobject_cast(dlg); + return dp ? dp->result().toJson() : QJsonObject{}; +} + +void DrawProbabilityWidget::applyConfigToToolbar() +{ + auto setComboByData = [](QComboBox *combo, const QVariant &value) { + int idx = combo->findData(value); + if (idx >= 0) { + combo->setCurrentIndex(idx); + } + }; + + setComboByData(criteriaCombo, config.criteria); + setComboByData(exactnessCombo, config.atLeast); + + quantitySpin->setValue(config.quantity); + drawnSpin->setValue(config.drawn); +} + +void DrawProbabilityWidget::updateDisplay() +{ + updateFilterOptions(); +} + +void DrawProbabilityWidget::loadConfig(const QJsonObject &cfg) +{ + config = DrawProbabilityConfig::fromJson(cfg); + applyConfigToToolbar(); + updateFilterOptions(); +} + +void DrawProbabilityWidget::updateFilterOptions() +{ + if (!analyzer->getModel()->getDeckList()) { + return; + } + + const QString criteria = config.criteria; + const bool atLeast = config.atLeast; + const int quantity = config.quantity; + const int drawn = config.drawn; + + QMap categoryCounts; + int totalDeckCards = 0; + + const auto nodes = analyzer->getModel()->getDeckList()->getCardNodes(); + for (auto *node : nodes) { + CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr(); + if (!info) { + continue; + } + + totalDeckCards += node->getNumber(); + + QStringList categories; + if (criteria == "name") { + categories << info->getName(); + } else if (criteria == "type") { + categories = info->getMainCardType().split(' ', Qt::SkipEmptyParts); + } else if (criteria == "subtype") { + categories = info->getCardType().split(' ', Qt::SkipEmptyParts); + } else if (criteria == "cmc") { + categories << QString::number(info->getCmc().toInt()); + } + + for (const QString &cat : categories) { + categoryCounts[cat] += node->getNumber(); + } + } + + resultTable->setRowCount(categoryCounts.size()); + + int row = 0; + for (auto it = categoryCounts.cbegin(); it != categoryCounts.cend(); ++it, ++row) { + const QString &cat = it.key(); + const int copies = it.value(); + + double probability = 0.0; + if (atLeast) { + for (int k = quantity; k <= drawn && k <= copies; ++k) { + probability += hypergeometricProbability(totalDeckCards, copies, drawn, k); + } + } else { + probability = hypergeometricProbability(totalDeckCards, copies, drawn, quantity); + } + + resultTable->setItem(row, 0, new QTableWidgetItem(cat)); + resultTable->setItem(row, 1, new QTableWidgetItem(QString::number(copies))); + resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2))); + } +} + +double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k) +{ + if (k < 0 || k > n || K > N || n > N) { + return 0.0; + } + + double logP = 0.0; + for (int i = 1; i <= k; ++i) { + logP += qLn(double(K - k + i) / i); + } + for (int i = 1; i <= n - k; ++i) { + logP += qLn(double(N - K - (n - k) + i) / i); + } + for (int i = 1; i <= n; ++i) { + logP -= qLn(double(N - n + i) / i); + } + + return qExp(logP); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h new file mode 100644 index 000000000..80015999f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H +#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H + +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "draw_probability_config.h" + +#include +#include +#include +#include + +class DrawProbabilityWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT +public: + DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + QDialog *createConfigDialog(QWidget *parent) override; + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + void applyConfigToToolbar(); + +public slots: + void updateDisplay() override; + void loadConfig(const QJsonObject &cfg) override; + void retranslateUi(); + +private slots: + void updateFilterOptions(); + +private: + DrawProbabilityConfig config; + + QWidget *controls; + QHBoxLayout *controlLayout; + QLabel *labelPrefix; + QLabel *labelMiddle; + QLabel *labelSuffix; + QLineEdit *cardNameEdit; + QComboBox *criteriaCombo; // Card Name / Type / Subtype / Mana Value + QComboBox *filterCombo; // The actual value + QComboBox *exactnessCombo; // At least / Exactly + QSpinBox *quantitySpin; // N + QSpinBox *drawnSpin; // M + + QSpinBox *manaValueSpin; + + QTableWidget *resultTable; + + double hypergeometricProbability(int N, int K, int n, int k); + double calculateProbability(int totalCards, int copies, int drawn, bool atLeast); +}; + +#endif // COCKATRICE_DRAW_PROBABILITY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp new file mode 100644 index 000000000..2f9f60752 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp @@ -0,0 +1,32 @@ +#include "mana_base_config.h" + +QJsonObject ManaBaseConfig::toJson() const +{ + QJsonObject jsonObject; + QJsonArray jsonArray; + jsonObject["displayType"] = displayType; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + return jsonObject; +} + +ManaBaseConfig ManaBaseConfig::fromJson(const QJsonObject &o) + +{ + ManaBaseConfig config; + + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h new file mode 100644 index 000000000..d01f88b8b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h @@ -0,0 +1,19 @@ + +#ifndef COCKATRICE_MANA_BASE_CONFIG_H +#define COCKATRICE_MANA_BASE_CONFIG_H + +#include +#include +#include + +struct ManaBaseConfig +{ + QString displayType; // "pie" or "bar" or "combinedBar" + QStringList filters; // which colors to show, empty = all + + QJsonObject toJson() const; + + static ManaBaseConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_BASE_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp new file mode 100644 index 000000000..3317486ea --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp @@ -0,0 +1,67 @@ +#include "mana_base_config_dialog.h" + +#include + +ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaBaseConfig initial, + QWidget *parent) + : QDialog(parent), config(initial) +{ + layout = new QVBoxLayout(this); + + displayTypeLabel = new QLabel(this); + layout->addWidget(displayTypeLabel); + + displayType = new QComboBox(this); + layout->addWidget(displayType); + + filterLabel = new QLabel(this); + layout->addWidget(filterLabel); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + layout->addWidget(filterList); + + QStringList colors = analyzer->getManaBase().keys(); + colors.sort(); + filterList->addItems(colors); + + // select initial filters + for (int i = 0; i < filterList->count(); ++i) { + if (config.filters.contains(filterList->item(i)->text())) + filterList->item(i)->setSelected(true); + } + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &ManaBaseConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaBaseConfigDialog::reject); + + retranslateUi(); +} + +void ManaBaseConfigDialog::retranslateUi() +{ + setWindowTitle(tr("Mana Base Configuration")); + + displayTypeLabel->setText(tr("Display type:")); + + displayType->clear(); + displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")}); + + filterLabel->setText(tr("Filter Colors (optional):")); + + buttons->button(QDialogButtonBox::Ok)->setText(tr("OK")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); +} + +void ManaBaseConfigDialog::accept() +{ + config.displayType = displayType->currentText(); + config.filters.clear(); + for (auto *item : filterList->selectedItems()) { + config.filters << item->text(); + } + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h new file mode 100644 index 000000000..a816f154f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h @@ -0,0 +1,42 @@ + +#ifndef COCKATRICE_MANA_BASE_ADD_DIALOG_H +#define COCKATRICE_MANA_BASE_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_base_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ManaBaseConfigDialog : public QDialog +{ + Q_OBJECT +public: + ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig initial = {}, QWidget *parent = nullptr); + void retranslateUi(); + + void accept() override; + + ManaBaseConfig result() const + { + return config; + } + +private: + ManaBaseConfig config; + QVBoxLayout *layout; + QLabel *displayTypeLabel; + QComboBox *displayType; + QLabel *filterLabel; + QListWidget *filterList; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_MANA_BASE_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp new file mode 100644 index 000000000..d38314b8c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp @@ -0,0 +1,115 @@ +#include "mana_base_widget.h" + +#include "../../../general/display/charts/bars/bar_widget.h" +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../analytics_panel_widget_registrar.h" +#include "mana_base_config_dialog.h" + +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaBase{ + "manaBase", ManaBaseWidget::tr("Mana Base"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg)) +{ + barContainer = new QWidget(this); + barLayout = new QHBoxLayout(barContainer); + layout->addWidget(barContainer); + + updateDisplay(); +} + +void ManaBaseWidget::updateDisplay() +{ + // Clear previous widgets + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + auto &pipCount = analyzer->getProductionPipCount(); + auto &cardCount = analyzer->getProductionCardCount(); + + QHash manaMap; + for (auto key : pipCount.keys()) { + manaMap[key] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash filtered; + for (auto f : config.filters) { + if (manaMap.contains(f)) { + filtered[f] = manaMap[f]; + } + } + manaMap = filtered; + } + + // Determine maximum for bar charts + int highest = 1; + for (auto val : manaMap) { + highest = std::max(highest, val); + } + + // Convert to QMap for ColorBar / ColorPie (sorted) + QMap mapSorted; + for (auto it = manaMap.begin(); it != manaMap.end(); ++it) { + mapSorted.insert(it.key(), it.value()); + } + + // Choose display mode + if (config.displayType == "bar") { + QHash colors = {{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)}, + {"B", QColor(21, 11, 0)}, {"R", QColor(211, 32, 42)}, + {"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)}}; + + for (auto color : manaMap.keys()) { + QString label = QString("%1 %2 (%3)").arg(color).arg(manaMap[color]).arg(cardCount.value(color)); + + BarWidget *bar = new BarWidget(label, manaMap[color], highest, colors.value(color, Qt::gray), this); + + barLayout->addWidget(bar); + } + } else if (config.displayType == "combinedBar") { + ColorBar *cb = new ColorBar(mapSorted, this); + cb->setMinimumHeight(30); + barLayout->addWidget(cb); + } else if (config.displayType == "pie") { + ColorPie *pie = new ColorPie(mapSorted, this); + pie->setMinimumSize(200, 200); + barLayout->addWidget(pie); + } + + update(); +} +QSize ManaBaseWidget::sizeHint() const +{ + return QSize(800, 150); +} + +QDialog *ManaBaseWidget::createConfigDialog(QWidget *parent) +{ + ManaBaseConfigDialog *dlg = new ManaBaseConfigDialog(analyzer, config, parent); + return dlg; +} + +QJsonObject ManaBaseWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h new file mode 100644 index 000000000..39380c07e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h @@ -0,0 +1,51 @@ +/** + * @file mana_base_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_BASE_WIDGET_H +#define MANA_BASE_WIDGET_H + +#include "../../../general/display/banner_widget.h" +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_base_config.h" + +#include +#include +#include +#include +#include + +class ManaBaseWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaBaseConfig::fromJson(o); + updateDisplay(); + } + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaBaseConfig config; + QWidget *barContainer; + QHBoxLayout *barLayout; +}; + +#endif // MANA_BASE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp new file mode 100644 index 000000000..a0ccedc74 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp @@ -0,0 +1,121 @@ +#include "mana_curve_category_widget.h" + +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config.h" +#include "mana_curve_total_widget.h" + +constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row + +ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent) +{ + layout = new QVBoxLayout(this); + layout->setSpacing(4); + layout->setContentsMargins(0, 0, 0, 0); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +// Same as minimum for now +QSize ManaCurveCategoryWidget::sizeHint() const +{ + if (layout->isEmpty()) { + return QSize(0, 0); + } + + // Calculate exact height needed for all rows + int rowCount = layout->count(); + + int totalHeight = rowCount * MIN_ROW_HEIGHT; + totalHeight += (rowCount - 1) * layout->spacing(); + + return QSize(0, totalHeight); +} + +QSize ManaCurveCategoryWidget::minimumSizeHint() const +{ + if (layout->isEmpty()) { + return QSize(0, 0); + } + + // Calculate actual minimum based on number of rows + int rowCount = layout->count(); + + int totalHeight = rowCount * MIN_ROW_HEIGHT; + totalHeight += (rowCount - 1) * layout->spacing(); + + return QSize(0, totalHeight); +} + +void ManaCurveCategoryWidget::updateDisplay(int minCmc, + int maxCmc, + int highest, + QHash> qCategoryCounts, + QHash> qCategoryCards, + const ManaCurveConfig &config) +{ + // Clear previous content + QtUtils::clearLayoutRec(layout); + + if (!config.showCategoryRows) { + return; // nothing to show + } + + // Collect categories + QStringList categories = qCategoryCounts.keys(); + + // Apply filters + if (!config.filters.isEmpty()) { + QStringList filtered; + for (const QString &cat : categories) { + if (config.filters.contains(cat)) { + filtered.append(cat); + } + } + categories = filtered; + } + + std::sort(categories.begin(), categories.end()); + + for (const QString &cat : categories) { + QWidget *row = new QWidget(this); + row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + row->setFixedHeight(MIN_ROW_HEIGHT); + + QHBoxLayout *rowLayout = new QHBoxLayout(row); + rowLayout->setContentsMargins(0, 0, 0, 0); + rowLayout->setSpacing(4); + + QLabel *categoryLabel = new QLabel(cat, row); + categoryLabel->setFixedWidth(80); + rowLayout->addWidget(categoryLabel); + + QVector catBars; + const auto cmcCounts = qCategoryCounts.value(cat); + const auto cmcCards = qCategoryCards.value(cat); + + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + int val = cmcCounts.value(cmc, 0); + QStringList cards = cmcCards.value(cmc); + + QVector segments; + if (val > 0) { + segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)}); + } + + catBars.push_back({QString::number(cmc), segments}); + } + + auto *catChart = new BarChartWidget(row); + catChart->setHighest(highest); + catChart->setBars(catBars); + catChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + rowLayout->addWidget(catChart); + layout->addWidget(row); + } + + // Update geometry after adding all widgets + updateGeometry(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h new file mode 100644 index 000000000..c6aa6f1f0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H +#define COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H + +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class ManaCurveCategoryWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveCategoryWidget(QWidget *parent); + void updateDisplay(int minCmc, + int maxCmc, + int highest, + QHash> qCategoryCounts, + QHash> qCategoryCards, + const ManaCurveConfig &config); + +public slots: + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +private: + QVBoxLayout *layout; +}; + +#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp new file mode 100644 index 000000000..0dfef1b66 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp @@ -0,0 +1,41 @@ +#include "mana_curve_config.h" + +QJsonObject ManaCurveConfig::toJson() const +{ + QJsonObject jsonObject; + jsonObject["groupBy"] = groupBy; + QJsonArray jsonArray; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + jsonObject["showMain"] = showMain; + jsonObject["showCategoryRows"] = showCategoryRows; + return jsonObject; +} + +ManaCurveConfig ManaCurveConfig::fromJson(const QJsonObject &o) +{ + ManaCurveConfig config; + + if (o.contains("groupBy")) { + config.groupBy = o["groupBy"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + if (o.contains("showMain")) { + config.showMain = o["showMain"].toBool(true); + } + + if (o.contains("showCategoryRows")) { + config.showCategoryRows = o["showCategoryRows"].toBool(true); + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h new file mode 100644 index 000000000..55222dd98 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h @@ -0,0 +1,21 @@ + +#ifndef COCKATRICE_MANA_CURVE_CONFIG_H +#define COCKATRICE_MANA_CURVE_CONFIG_H + +#include +#include +#include + +struct ManaCurveConfig +{ + QString groupBy = "type"; // "type", "color", "subtype", etc. + QStringList filters; // empty = all + bool showMain = true; + bool showCategoryRows = true; + + QJsonObject toJson() const; + + static ManaCurveConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_CURVE_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp new file mode 100644 index 000000000..38199656c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp @@ -0,0 +1,91 @@ +#include "mana_curve_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +ManaCurveConfigDialog::ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent) + : QDialog(parent), analyzer(analyzer) +{ + auto *lay = new QVBoxLayout(this); + + labelGroupBy = new QLabel(this); + lay->addWidget(labelGroupBy); + + groupBy = new QComboBox(this); + lay->addWidget(groupBy); + + labelFilters = new QLabel(this); + lay->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + lay->addWidget(filterList); + + showMain = new QCheckBox(this); + showMain->setChecked(true); + lay->addWidget(showMain); + + showCatRows = new QCheckBox(this); + showCatRows->setChecked(true); + lay->addWidget(showCatRows); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + lay->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaCurveConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaCurveConfigDialog::reject); + + // populate dynamic data + QStringList cats = analyzer->getManaCurveByType().keys(); + cats.append(analyzer->getManaCurveByColor().keys()); + cats.removeDuplicates(); + cats.sort(); + filterList->addItems(cats); + + groupBy->addItems({"type", "color", "subtype", "power", "toughness"}); + + retranslateUi(); +} + +void ManaCurveConfigDialog::retranslateUi() +{ + labelGroupBy->setText(tr("Group By:")); + groupBy->setItemText(0, tr("type")); + groupBy->setItemText(1, tr("color")); + groupBy->setItemText(2, tr("subtype")); + groupBy->setItemText(3, tr("power")); + groupBy->setItemText(4, tr("toughness")); + + labelFilters->setText(tr("Filters (optional):")); + + showMain->setText(tr("Show main bar row")); + showCatRows->setText(tr("Show per-category rows")); +} + +void ManaCurveConfigDialog::setFromConfig(const ManaCurveConfig &cfg) +{ + groupBy->setCurrentText(cfg.groupBy); + // restore filters + for (int i = 0; i < filterList->count(); ++i) + filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text())); + + showMain->setChecked(cfg.showMain); + showCatRows->setChecked(cfg.showCategoryRows); +} + +void ManaCurveConfigDialog::accept() +{ + cfg.groupBy = groupBy->currentText(); + + cfg.filters.clear(); + for (auto *item : filterList->selectedItems()) + cfg.filters << item->text(); + + cfg.showMain = showMain->isChecked(); + cfg.showCategoryRows = showCatRows->isChecked(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h new file mode 100644 index 000000000..17e6776e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_MANA_CURVE_ADD_DIALOG_H +#define COCKATRICE_MANA_CURVE_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class QListWidget; +class QCheckBox; +class QComboBox; + +class ManaCurveConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr); + void retranslateUi(); + void setFromConfig(const ManaCurveConfig &cfg); + + ManaCurveConfig result() const + { + return cfg; + } + +private: + ManaCurveConfig cfg; + DeckListStatisticsAnalyzer *analyzer; + + QLabel *labelGroupBy; + QComboBox *groupBy; + QLabel *labelFilters; + QListWidget *filterList; + QDialogButtonBox *buttons; + QCheckBox *showMain; + QCheckBox *showCatRows; + +private slots: + void accept() override; +}; + +#endif // COCKATRICE_MANA_CURVE_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp new file mode 100644 index 000000000..f059ff873 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp @@ -0,0 +1,78 @@ +#include "mana_curve_total_widget.h" + +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config.h" + +#include + +ManaCurveTotalWidget::ManaCurveTotalWidget(QWidget *parent) : QWidget(parent) +{ + layout = new QHBoxLayout(this); + + label = new QLabel(this); + label->setFixedWidth(80); + layout->addWidget(label); + + barChart = new BarChartWidget(this); + layout->addWidget(barChart, 1); + + setMinimumHeight(200); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +QSize ManaCurveTotalWidget::sizeHint() const +{ + return {0, 280}; +} + +QSize ManaCurveTotalWidget::minimumSizeHint() const +{ + return {0, 200}; +} + +void ManaCurveTotalWidget::updateDisplay(const QString &categoryName, + int minCmc, + int maxCmc, + int highest, + const QMap> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config) +{ + QVector mainBars; + mainBars.reserve(maxCmc - minCmc + 1); + + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + QVector segments; + + const auto cmcIt = cmcMap.constFind(cmc); + if (cmcIt != cmcMap.cend()) { + for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) { + const QString &category = it.key(); + + if (!config.filters.isEmpty() && !config.filters.contains(category)) + continue; + + const int value = it.value(); + + QStringList cards; + const auto catIt = cardsMap.constFind(category); + if (catIt != cardsMap.cend()) + cards = catIt->value(cmc); + + segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)}); + } + } + + std::sort(segments.begin(), segments.end(), + [](const BarSegment &a, const BarSegment &b) { return a.category < b.category; }); + + mainBars.push_back({QString::number(cmc), segments}); + } + + label->setText(categoryName); + + barChart->setHighest(highest); + barChart->setBars(mainBars); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h new file mode 100644 index 000000000..203f75345 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H +#define COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class ManaCurveTotalWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveTotalWidget(QWidget *parent); + QSize sizeHint() const; + QSize minimumSizeHint() const; + void updateDisplay(const QString &categoryName, + int minCmc, + int maxCmc, + int highest, + const QMap> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config); + +private: + QHBoxLayout *layout; + QLabel *label; + BarChartWidget *barChart; +}; + +#endif // COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp new file mode 100644 index 000000000..e09ecfe87 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp @@ -0,0 +1,148 @@ +#include "mana_curve_widget.h" + +#include "../../../general/display/charts/bars/bar_chart_background_widget.h" +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "../../../general/display/charts/bars/segmented_bar_widget.h" +#include "../../analytics_panel_widget_registrar.h" +#include "../../deck_list_statistics_analyzer.h" +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config_dialog.h" + +#include +#include +#include +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaCurve{ + "manaCurve", ManaCurveWidget::tr("Mana Curve"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(cfg) +{ + setLayout(layout); + + totalWidget = new ManaCurveTotalWidget(this); + totalWidget->setHidden(true); + layout->addWidget(totalWidget); + + categoryWidget = new ManaCurveCategoryWidget(this); + categoryWidget->setHidden(true); + layout->addWidget(categoryWidget); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay); + + updateDisplay(); +} + +QDialog *ManaCurveWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new ManaCurveConfigDialog(analyzer, parent); + dlg->setFromConfig(config); + return dlg; +} + +QJsonObject ManaCurveWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + return mc ? mc->result().toJson() : QJsonObject{}; +} + +static void buildMapsByCategory(const QHash> &categoryCounts, + const QHash> &categoryCards, + QMap> &outCmcMap, + QMap> &outCardsMap) +{ + outCmcMap.clear(); + outCardsMap.clear(); + + for (auto catIt = categoryCounts.cbegin(); catIt != categoryCounts.cend(); ++catIt) { + const QString &category = catIt.key(); + const auto &countsByCmc = catIt.value(); + + for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it) + outCmcMap[it.key()][category] = it.value(); + } + + for (auto catIt = categoryCards.cbegin(); catIt != categoryCards.cend(); ++catIt) { + const QString &category = catIt.key(); + const auto &cardsByCmc = catIt.value(); + + for (auto it = cardsByCmc.cbegin(); it != cardsByCmc.cend(); ++it) + outCardsMap[category][it.key()] = it.value(); + } +} + +static void findGlobalCmcRange(const QHash> &categoryCounts, int &minCmc, int &maxCmc) +{ + minCmc = 0; + maxCmc = 0; + + for (const auto &countsByCmc : categoryCounts) { + for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it) + maxCmc = qMax(maxCmc, it.key()); + } +} + +void ManaCurveWidget::updateDisplay() +{ + QHash> categoryCounts; + QHash> categoryCards; + + if (config.groupBy == "color") { + categoryCounts = analyzer->getManaCurveByColor(); + categoryCards = analyzer->getManaCurveCardsByColor(); + } else if (config.groupBy == "subtype") { + categoryCounts = analyzer->getManaCurveBySubtype(); + categoryCards = analyzer->getManaCurveCardsBySubtype(); + } else if (config.groupBy == "power") { + categoryCounts = analyzer->getManaCurveByPower(); + categoryCards = analyzer->getManaCurveCardsByPower(); + } else { + categoryCounts = analyzer->getManaCurveByType(); + categoryCards = analyzer->getManaCurveCardsByType(); + } + + QMap> cmcMap; + QMap> cardsMap; + buildMapsByCategory(categoryCounts, categoryCards, cmcMap, cardsMap); + + int minCmc = 0; + int maxCmc = 0; + findGlobalCmcRange(categoryCounts, minCmc, maxCmc); + + int highest = 1; + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + int sum = 0; + + const auto cmcIt = cmcMap.constFind(cmc); + if (cmcIt != cmcMap.cend()) { + for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) { + if (!config.filters.isEmpty() && !config.filters.contains(it.key())) { + continue; + } + + sum += it.value(); + } + } + + highest = qMax(highest, sum); + } + + totalWidget->updateDisplay(config.groupBy, minCmc, maxCmc, highest, cmcMap, cardsMap, config); + + totalWidget->setVisible(config.showMain); + + categoryWidget->updateDisplay(minCmc, maxCmc, highest, categoryCounts, categoryCards, config); + + categoryWidget->setVisible(config.showCategoryRows); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h new file mode 100644 index 000000000..da59da9a8 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h @@ -0,0 +1,50 @@ +/** + * @file mana_curve_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_CURVE_WIDGET_H +#define MANA_CURVE_WIDGET_H + +#include "../../abstract_analytics_panel_widget.h" +#include "mana_curve_category_widget.h" +#include "mana_curve_config.h" +#include "mana_curve_total_widget.h" + +#include + +class SegmentedBarWidget; +class DeckListStatisticsAnalyzer; + +class ManaCurveWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + // QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaCurveConfig::fromJson(o); + updateDisplay(); + }; + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaCurveConfig config; + ManaCurveTotalWidget *totalWidget; + ManaCurveCategoryWidget *categoryWidget; +}; + +#endif // MANA_CURVE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp new file mode 100644 index 000000000..ee57cb1e2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp @@ -0,0 +1,31 @@ +#include "mana_devotion_config.h" + +QJsonObject ManaDevotionConfig::toJson() const +{ + QJsonObject jsonObject; + QJsonArray jsonArray; + jsonObject["displayType"] = displayType; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + return jsonObject; +} + +ManaDevotionConfig ManaDevotionConfig::fromJson(const QJsonObject &o) +{ + ManaDevotionConfig config; + + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h new file mode 100644 index 000000000..08be146e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h @@ -0,0 +1,18 @@ +#ifndef COCKATRICE_MANA_DEVOTION_CONFIG_H +#define COCKATRICE_MANA_DEVOTION_CONFIG_H + +#include +#include +#include + +struct ManaDevotionConfig +{ + QString displayType; // "pie" or "bar" or "combinedBar" + QStringList filters; // which colors to show, empty = all + + QJsonObject toJson() const; + + static ManaDevotionConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_DEVOTION_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp new file mode 100644 index 000000000..80fd03928 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp @@ -0,0 +1,62 @@ +#include "mana_devotion_config_dialog.h" + +ManaDevotionConfigDialog::ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaDevotionConfig initial, + QWidget *parent) + : QDialog(parent), config(initial) +{ + layout = new QVBoxLayout(this); + + labelDisplayType = new QLabel(this); + layout->addWidget(labelDisplayType); + + displayType = new QComboBox(this); + layout->addWidget(displayType); + + labelFilters = new QLabel(this); + layout->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + + QStringList colors = analyzer->getDevotionPipCount().keys(); + colors.sort(); + filterList->addItems(colors); + layout->addWidget(filterList); + + // select initial filters + for (int i = 0; i < filterList->count(); ++i) { + if (config.filters.contains(filterList->item(i)->text())) + filterList->item(i)->setSelected(true); + } + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaDevotionConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaDevotionConfigDialog::reject); + + // populate combo box items + displayType->addItems({"pie", "bar", "combinedBar"}); + + retranslateUi(); +} + +void ManaDevotionConfigDialog::retranslateUi() +{ + labelDisplayType->setText(tr("Display type:")); + displayType->setItemText(0, tr("pie")); + displayType->setItemText(1, tr("bar")); + displayType->setItemText(2, tr("combinedBar")); + + labelFilters->setText(tr("Filter Colors (optional):")); +} + +void ManaDevotionConfigDialog::accept() +{ + config.displayType = displayType->currentText(); + config.filters.clear(); + for (auto *item : filterList->selectedItems()) { + config.filters << item->text(); + } + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h new file mode 100644 index 000000000..fd804e2af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h @@ -0,0 +1,42 @@ + +#ifndef COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H +#define COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_devotion_config.h" + +#include +#include +#include +#include +#include +#include +#include + +class ManaDevotionConfigDialog : public QDialog +{ + Q_OBJECT +public: + ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaDevotionConfig initial = {}, + QWidget *parent = nullptr); + void retranslateUi(); + + void accept() override; + + ManaDevotionConfig result() const + { + return config; + } + +private: + ManaDevotionConfig config; + QVBoxLayout *layout; + QLabel *labelDisplayType; + QComboBox *displayType; + QLabel *labelFilters; + QListWidget *filterList; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp new file mode 100644 index 000000000..709577ff2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp @@ -0,0 +1,123 @@ +#include "mana_devotion_widget.h" + +#include "../../../general/display/charts/bars/bar_widget.h" +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../analytics_panel_widget_registrar.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_devotion_config_dialog.h" + +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaDevotion{ + "manaDevotion", ManaDevotionWidget::tr("Mana Devotion"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg)) +{ + barContainer = new QWidget(this); + barLayout = new QHBoxLayout(barContainer); + barContainer->setLayout(barLayout); + layout->addWidget(barContainer); + + updateDisplay(); +} + +void ManaDevotionWidget::updateDisplay() +{ + // Clear previous widgets + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + auto &pipCount = analyzer->getDevotionPipCount(); + auto &cardCount = analyzer->getDevotionCardCount(); + + // Convert keys to single QChar form + QHash devoMap; + for (auto key : pipCount.keys()) { + devoMap[key[0]] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash filtered; + for (auto f : config.filters) { + if (devoMap.contains(f[0])) { + filtered[f[0]] = devoMap[f[0]]; + } + } + devoMap = filtered; + } + + // Determine maximum for bar charts + int highest = 1; + for (auto val : devoMap) { + highest = std::max(highest, val); + } + + // Convert to QMap for ColorBar / ColorPie + QMap mapSorted; + for (auto it = devoMap.begin(); it != devoMap.end(); ++it) { + mapSorted.insert(QString(it.key()), it.value()); + } + + // Color map + QHash colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)}, + {'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)}, + {'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}}; + + // Choose display mode + if (config.displayType == "bar") { + // One BarWidget per devotion color + for (auto c : devoMap.keys()) { + QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c))); + + BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this); + + barLayout->addWidget(bar); + } + } else if (config.displayType == "combinedBar") { + // Stacked devotion bar + ColorBar *cb = new ColorBar(mapSorted, this); + cb->setMinimumHeight(30); + barLayout->addWidget(cb); + } else if (config.displayType == "pie") { + // Devotion pie chart + ColorPie *pie = new ColorPie(mapSorted, this); + pie->setMinimumSize(200, 200); + barLayout->addWidget(pie); + } + + update(); +} + +QDialog *ManaDevotionWidget::createConfigDialog(QWidget *parent) +{ + ManaDevotionConfigDialog *dlg = new ManaDevotionConfigDialog(analyzer, config, parent); + return dlg; +} + +QJsonObject ManaDevotionWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} + +QSize ManaDevotionWidget::sizeHint() const +{ + return QSize(800, 150); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h new file mode 100644 index 000000000..833f12938 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h @@ -0,0 +1,45 @@ +/** + * @file mana_devotion_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_DEVOTION_WIDGET_H +#define MANA_DEVOTION_WIDGET_H +#include "../../../general/display/banner_widget.h" +#include "../../abstract_analytics_panel_widget.h" +#include "mana_devotion_config.h" + +#include + +class ManaDevotionWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaDevotionConfig::fromJson(o); + updateDisplay(); + } + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaDevotionConfig config; + QWidget *barContainer; + QHBoxLayout *barLayout; +}; + +#endif // MANA_DEVOTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp new file mode 100644 index 000000000..f70f32d5b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp @@ -0,0 +1,36 @@ +#include "mana_distribution_config.h" + +QJsonObject ManaDistributionConfig::toJson() const +{ + QJsonObject o; + o["displayType"] = displayType; + + QJsonArray jsonArray; + for (auto &s : filters) { + jsonArray.append(s); + } + o["filters"] = jsonArray; + + o["showColorRows"] = showColorRows; + return o; +} + +ManaDistributionConfig ManaDistributionConfig::fromJson(const QJsonObject &o) +{ + ManaDistributionConfig config; + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) + config.filters << v.toString(); + } + + if (o.contains("showColorRows")) { + config.showColorRows = o["showColorRows"].toBool(true); + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h new file mode 100644 index 000000000..d1aa0f48e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_CONFIG_H +#define COCKATRICE_MANA_DISTRIBUTION_CONFIG_H + +#include +#include +#include +#include + +struct ManaDistributionConfig +{ + QString displayType = "pie"; // "pie" or "bar" + QStringList filters; // empty = all colors + bool showColorRows = true; + + QJsonObject toJson() const; + + static ManaDistributionConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp new file mode 100644 index 000000000..7fe4d94e4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp @@ -0,0 +1,83 @@ +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +static const QStringList kColors = {"W", "U", "B", "R", "G", "C"}; + +ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent) + : QDialog(parent), analyzer(analyzer) +{ + auto *lay = new QVBoxLayout(this); + + // Labels + labelDisplayType = new QLabel(this); + lay->addWidget(labelDisplayType); + + displayType = new QComboBox(this); + lay->addWidget(displayType); + + labelFilters = new QLabel(this); + lay->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + filterList->addItems(kColors); // dynamic/fixed, no translation needed + lay->addWidget(filterList); + + showColorRows = new QCheckBox(this); + showColorRows->setChecked(true); + lay->addWidget(showColorRows); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + lay->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaDistributionConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaDistributionConfigDialog::reject); + + displayType->addItems({"pie", "bar"}); // combo items + + retranslateUi(); +} + +void ManaDistributionConfigDialog::retranslateUi() +{ + labelDisplayType->setText(tr("Top display type:")); + displayType->setItemText(0, tr("pie")); + displayType->setItemText(1, tr("bar")); + + labelFilters->setText(tr("Colors:")); + + showColorRows->setText(tr("Show per-color rows")); + + // QDialogButtonBox buttons are automatically translated +} + +void ManaDistributionConfigDialog::setFromConfig(const ManaDistributionConfig &cfgIn) +{ + cfg = cfgIn; + + displayType->setCurrentText(cfg.displayType); + + for (int i = 0; i < filterList->count(); ++i) + filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text())); + + showColorRows->setChecked(cfg.showColorRows); +} + +void ManaDistributionConfigDialog::accept() +{ + cfg.displayType = displayType->currentText(); + + // Filters + cfg.filters.clear(); + for (auto *item : filterList->selectedItems()) + cfg.filters << item->text(); + + cfg.showColorRows = showColorRows->isChecked(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h new file mode 100644 index 000000000..57c88e29d --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H +#define COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H + +#include "mana_distribution_config.h" + +#include +#include +#include +#include + +class QComboBox; +class QListWidget; +class QCheckBox; +class DeckListStatisticsAnalyzer; + +class ManaDistributionConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr); + void retranslateUi(); + + void setFromConfig(const ManaDistributionConfig &cfg); + const ManaDistributionConfig &config() const + { + return cfg; + } + +public slots: + void accept() override; + +private: + DeckListStatisticsAnalyzer *analyzer; + + QLabel *labelDisplayType; + QComboBox *displayType; + QLabel *labelFilters; + QListWidget *filterList; + QCheckBox *showColorRows; + QDialogButtonBox *buttons; + + ManaDistributionConfig cfg; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp new file mode 100644 index 000000000..8a5e14858 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp @@ -0,0 +1,49 @@ +#include "mana_distribution_single_display_widget.h" + +#include "../../../cards/additional_info/mana_symbol_widget.h" + +#include + +ManaDistributionSingleDisplayWidget::ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent) + : QWidget(parent) +{ + auto layout = new QVBoxLayout(this); + layout->setAlignment(Qt::AlignHCenter); + + symbolLabel = new ManaSymbolWidget(this, colorSymbol, true, false); + symbolLabel->setFixedSize(40, 40); + + devotionBar = new QProgressBar(this); + devotionBar->setRange(0, 100); + devotionBar->setTextVisible(false); + + devotionLabel = new QLabel(this); + devotionLabel->setAlignment(Qt::AlignCenter); + + productionBar = new QProgressBar(this); + productionBar->setRange(0, 100); + productionBar->setTextVisible(false); + + productionLabel = new QLabel(this); + productionLabel->setAlignment(Qt::AlignCenter); + + layout->addWidget(symbolLabel); + layout->addWidget(devotionBar); + layout->addWidget(devotionLabel); + layout->addWidget(productionBar); + layout->addWidget(productionLabel); + + setLayout(layout); +} + +void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int percent) +{ + devotionBar->setValue(percent); + devotionLabel->setText(QString(tr("%1 pips (%2 cards)")).arg(pips).arg(cards)); +} + +void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent) +{ + productionBar->setValue(percent); + productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards)); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h new file mode 100644 index 000000000..e33d0cec1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H +#define COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H + +#include +#include +#include +#include + +class ManaDistributionSingleDisplayWidget : public QWidget +{ + Q_OBJECT +public: + explicit ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent = nullptr); + + void setDevotion(int pips, int cards, int percent); + void setProduction(int pips, int cards, int percent); + +private: + QLabel *symbolLabel; + + QProgressBar *devotionBar; + QLabel *devotionLabel; + + QProgressBar *productionBar; + QLabel *productionLabel; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp new file mode 100644 index 000000000..8eaea1426 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp @@ -0,0 +1,129 @@ +#include "mana_distribution_widget.h" + +#include "../../analytics_panel_widget_registrar.h" +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include + +namespace +{ +AnalyticsPanelWidgetRegistrar registerManaDistribution{ + "manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }}; + +} // anonymous namespace + +static const QStringList kColors = {"W", "U", "B", "R", "G", "C"}; + +ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : AbstractAnalyticsPanelWidget(parent, analyzer) +{ + container = new QWidget(this); + containerLayout = new QVBoxLayout(container); + + devotionBarTop = new ColorBar({}, this); + devotionPieTop = new ColorPie({}, this); + productionBarTop = new ColorBar({}, this); + productionPieTop = new ColorPie({}, this); + + containerLayout->addWidget(devotionBarTop); + containerLayout->addWidget(devotionPieTop); + containerLayout->addWidget(productionBarTop); + containerLayout->addWidget(productionPieTop); + + devotionPieTop->hide(); + productionPieTop->hide(); + + row = new QHBoxLayout(); + containerLayout->addLayout(row); + + for (const QString &c : kColors) { + auto *w = new ManaDistributionSingleDisplayWidget(c, this); + row->addWidget(w); + rows[c] = w; + } + + layout->addWidget(container); +} + +void ManaDistributionWidget::updateDisplay() +{ + const auto &devPips = analyzer->getDevotionPipCount(); + const auto &devCards = analyzer->getDevotionCardCount(); + const auto &prodPips = analyzer->getProductionPipCount(); + const auto &prodCards = analyzer->getProductionCardCount(); + + QStringList filtered = config.filters.isEmpty() ? kColors : config.filters; + + QMap devMap, prodMap; + for (const QString &c : filtered) { + devMap[c] = devPips.value(c, 0); + prodMap[c] = prodPips.value(c, 0); + } + + bool showPie = (config.displayType == "pie"); + + devotionBarTop->setVisible(!showPie); + productionBarTop->setVisible(!showPie); + + devotionPieTop->setVisible(showPie); + productionPieTop->setVisible(showPie); + + if (showPie) { + devotionPieTop->setColors(devMap); + productionPieTop->setColors(prodMap); + } else { + devotionBarTop->setColors(devMap); + productionBarTop->setColors(prodMap); + } + + for (const QString &c : kColors) { + auto *w = rows.value(c); + + if (!w) { + continue; + } + + bool visible = config.showColorRows && filtered.contains(c); + w->setVisible(visible); + if (!visible) { + continue; + } + + int dp = devPips.value(c, 0); + int dc = devCards.value(c, 0); + int pp = prodPips.value(c, 0); + int pc = prodCards.value(c, 0); + + // Compute percentages + int totalDev = 0; + int totalProd = 0; + for (const QString &cc : filtered) { + totalDev += devPips.value(cc, 0); + totalProd += prodPips.value(cc, 0); + } + + int devPct = (totalDev > 0) ? int(100.0 * dp / totalDev) : 0; + int prodPct = (totalProd > 0) ? int(100.0 * pp / totalProd) : 0; + + w->setDevotion(dp, dc, devPct); + w->setProduction(pp, pc, prodPct); + } +} + +QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new ManaDistributionConfigDialog(analyzer, parent); + dlg->setWindowTitle(tr("Mana Distribution Settings")); + dlg->setFromConfig(config); + + connect(dlg, &QDialog::accepted, [this, dlg]() { + config = dlg->config(); + updateDisplay(); + }); + + return dlg; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h new file mode 100644 index 000000000..2d834f3af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_WIDGET_H +#define COCKATRICE_MANA_DISTRIBUTION_WIDGET_H + +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_distribution_config.h" +#include "mana_distribution_single_display_widget.h" + +#include +#include +#include +#include + +class ManaDistributionWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT +public: + explicit ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + QJsonObject extractConfigFromDialog(QDialog *) const override + { + return {}; + } + +private: + ManaDistributionConfig config; + + QWidget *container; + QVBoxLayout *containerLayout; + + QVBoxLayout *topLayout; + ColorBar *devotionBarTop; + ColorPie *devotionPieTop; + ColorBar *productionBarTop; + ColorPie *productionPieTop; + + QHBoxLayout *row; + QMap rows; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp index 45393bb5d..ea61302f0 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp @@ -1,35 +1,298 @@ #include "deck_analytics_widget.h" -DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel) - : QWidget(parent), deckListModel(_deckListModel) -{ - mainLayout = new QVBoxLayout(); - setLayout(mainLayout); +#include "abstract_analytics_panel_widget.h" +#include "add_analytics_panel_dialog.h" +#include "analytics_panel_widget_factory.h" +#include "analyzer_modules/mana_base/mana_base_config.h" +#include "analyzer_modules/mana_curve/mana_curve_config.h" +#include "analyzer_modules/mana_devotion/mana_devotion_config.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" +#include +#include +#include +#include +#include +#include +#include + +DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer) + : QWidget(parent), statsAnalyzer(_statsAnalyzer) +{ + layout = new QVBoxLayout(this); + + // Controls + controlContainer = new QWidget(this); + controlLayout = new QHBoxLayout(controlContainer); + addButton = new QPushButton(this); + removeButton = new QPushButton(this); + saveButton = new QPushButton(this); + loadButton = new QPushButton(this); + controlLayout->addWidget(addButton); + controlLayout->addWidget(removeButton); + controlLayout->addWidget(saveButton); + controlLayout->addWidget(loadButton); + + layout->addWidget(controlContainer); + + connect(addButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onAddPanel); + connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected); + connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout); + connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout); + + // Scroll area and container scrollArea = new QScrollArea(this); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setWidgetResizable(true); - mainLayout->addWidget(scrollArea); + scrollArea->setFrameShape(QFrame::NoFrame); - container = new QWidget(scrollArea); - containerLayout = new QVBoxLayout(container); - container->setLayout(containerLayout); - scrollArea->setWidget(container); + panelContainer = new QWidget(scrollArea); + panelLayout = new QVBoxLayout(panelContainer); + panelLayout->setSpacing(8); + panelLayout->setContentsMargins(4, 4, 4, 4); + panelLayout->addStretch(1); // push panels up - deckListStatisticsAnalyzer = new DeckListStatisticsAnalyzer(this, deckListModel); + scrollArea->setWidget(panelContainer); + layout->addWidget(scrollArea); - manaCurveWidget = new ManaCurveWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaCurveWidget); + loadLayout(); - manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaDevotionWidget); - - manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaBaseWidget); + retranslateUi(); } -void DeckAnalyticsWidget::refreshDisplays() +void DeckAnalyticsWidget::retranslateUi() { - deckListStatisticsAnalyzer->update(); + addButton->setText(tr("Add Panel")); + removeButton->setText(tr("Remove Panel")); + saveButton->setText(tr("Save Layout")); + loadButton->setText(tr("Load Layout")); } + +void DeckAnalyticsWidget::updateDisplays() +{ + statsAnalyzer->analyze(); +} + +void DeckAnalyticsWidget::onAddPanel() +{ + AddAnalyticsPanelDialog dlg(this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + QString selection = dlg.selectedType(); + if (selection.isEmpty()) { + return; + } + + AbstractAnalyticsPanelWidget *analyticsWidget = + AnalyticsPanelWidgetFactory::instance().create(selection, this, statsAnalyzer); + if (!analyticsWidget) { + return; + } + + if (!analyticsWidget->applyConfigFromDialog()) { + analyticsWidget->deleteLater(); + return; + } + + addPanelInstance(selection, analyticsWidget, analyticsWidget->saveConfig()); +} + +void DeckAnalyticsWidget::addPanelInstance(const QString &typeId, + AbstractAnalyticsPanelWidget *panel, + const QJsonObject &cfg) +{ + panel->loadConfig(cfg); + panel->updateDisplay(); + + auto *resPanel = new ResizablePanel(typeId, panel, panelContainer); + panelWrappers.push_back(resPanel); + + panelLayout->insertWidget(panelLayout->count() - 1, resPanel); + + // Event filter for selection + resPanel->installEventFilter(this); + panel->installEventFilter(this); + + // Connect drag-drop signals + connect(resPanel, &ResizablePanel::dropRequested, this, &DeckAnalyticsWidget::onPanelDropped); +} + +void DeckAnalyticsWidget::onRemoveSelected() +{ + int idx = indexOfSelectedWrapper(); + if (idx < 0) { + return; + } + + ResizablePanel *panel = panelWrappers.takeAt(idx); + selectWrapper(nullptr); + + panel->deleteLater(); +} + +void DeckAnalyticsWidget::saveLayout() +{ + QJsonArray arr; + + for (auto *wrapper : panelWrappers) { + QJsonObject entry; + entry["type"] = wrapper->getTypeId(); + entry["config"] = wrapper->panel->saveConfig(); + entry["height"] = wrapper->getCurrentHeight(); + arr.append(entry); + } + + QSettings s; + s.setValue("deckAnalytics/layout", QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact))); +} + +void DeckAnalyticsWidget::loadLayout() +{ + if (!loadLayoutInternal()) { + addDefaultPanels(); + } +} + +void DeckAnalyticsWidget::addDefaultPanels() +{ + struct DefaultPanel + { + QString type; + QJsonObject cfg; + }; + + // Prepare configs + QJsonObject manaCurveCfg = ManaCurveConfig{}.toJson(); + QJsonObject manaBaseCfg = ManaBaseConfig{"combinedBar", {}}.toJson(); + QJsonObject manaDevotionCfg = ManaDevotionConfig{"combinedBar", {}}.toJson(); + QVector defaults = { + {"manaCurve", manaCurveCfg}, {"manaBase", manaBaseCfg}, {"manaDevotion", manaDevotionCfg}}; + + for (auto &d : defaults) { + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(d.type, this, statsAnalyzer); + if (!w) { + continue; + } + + w->loadConfig(d.cfg); + addPanelInstance(d.type, w, d.cfg); + } +} + +bool DeckAnalyticsWidget::loadLayoutInternal() +{ + QSettings s; + QString layoutData = s.value("deckAnalytics/layout").toString(); + if (layoutData.isEmpty()) { + return false; + } + + QJsonDocument doc = QJsonDocument::fromJson(layoutData.toUtf8()); + if (!doc.isArray()) { + return false; + } + + clearPanels(); + + for (auto v : doc.array()) { + if (!v.isObject()) { + continue; + } + QJsonObject o = v.toObject(); + QString type = o["type"].toString(); + QJsonObject cfg = o["config"].toObject(); + + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(type, this, statsAnalyzer); + if (!w) { + continue; + } + + addPanelInstance(type, w, cfg); + + // Restore height AFTER adding the panel + if (o.contains("height")) { + panelWrappers.last()->setHeightFromSaved(o["height"].toInt()); + } + } + + return true; +} + +void DeckAnalyticsWidget::clearPanels() +{ + selectWrapper(nullptr); + while (!panelWrappers.isEmpty()) { + ResizablePanel *p = panelWrappers.takeLast(); + p->deleteLater(); + } +} + +bool DeckAnalyticsWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress) { + for (auto *p : panelWrappers) { + if (obj == p || obj == p->panel) { + selectWrapper(p); + break; + } + } + } + return QWidget::eventFilter(obj, event); +} + +void DeckAnalyticsWidget::selectWrapper(ResizablePanel *w) +{ + // Same wrapper + if (selectedWrapper == w) { + return; + } + // Deselect the old one + if (selectedWrapper) { + selectedWrapper->setSelected(false); + } + // Set current + selectedWrapper = w; + // Finally, select new + if (selectedWrapper) { + selectedWrapper->setSelected(true); + } +} + +int DeckAnalyticsWidget::indexOfSelectedWrapper() const +{ + if (!selectedWrapper) { + return -1; + } + return panelWrappers.indexOf(selectedWrapper); +} + +void DeckAnalyticsWidget::onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore) +{ + int draggedIdx = panelWrappers.indexOf(dragged); + int targetIdx = panelWrappers.indexOf(target); + + if (draggedIdx == -1 || targetIdx == -1 || draggedIdx == targetIdx) { + return; + } + + // Remove dragged panel from list and layout + panelWrappers.removeAt(draggedIdx); + panelLayout->removeWidget(dragged); + + // Adjust target index if needed + if (draggedIdx < targetIdx) { + targetIdx--; + } + + // Calculate insertion position + int insertIdx = insertBefore ? targetIdx : targetIdx + 1; + + // Insert back into list and layout + panelWrappers.insert(insertIdx, dragged); + panelLayout->insertWidget(insertIdx, dragged); + + // Clear selection + selectWrapper(nullptr); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h index 524362aed..31ee36fbb 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h @@ -1,44 +1,71 @@ /** * @file deck_analytics_widget.h * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. + * @brief Main analytics widget container with resizable panels for deck statistics. */ #ifndef DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H -#include "mana_base_widget.h" -#include "mana_curve_widget.h" -#include "mana_devotion_widget.h" +#include "abstract_analytics_panel_widget.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" -#include +#include #include +#include +#include #include -#include + +class LayoutInspector; class DeckAnalyticsWidget : public QWidget { Q_OBJECT +public slots: + void updateDisplays(); + public: - explicit DeckAnalyticsWidget(QWidget *parent, DeckListModel *deckListModel); - void setDeckList(const DeckList &_deckListModel); - std::map analyzeManaCurve(); - void refreshDisplays(); + explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + void retranslateUi(); + +private slots: + void onAddPanel(); + void onRemoveSelected(); + void onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore); + void saveLayout(); + void loadLayout(); + void addDefaultPanels(); + bool loadLayoutInternal(); + void clearPanels(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void selectWrapper(ResizablePanel *panel); + int indexOfSelectedWrapper() const; private: - DeckListModel *deckListModel; - DeckListStatisticsAnalyzer *deckListStatisticsAnalyzer; - QVBoxLayout *mainLayout; + void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {}); - QWidget *container; - QVBoxLayout *containerLayout; + QVBoxLayout *layout; + QWidget *controlContainer; + QHBoxLayout *controlLayout; + + QPushButton *addButton; + QPushButton *removeButton; + QPushButton *saveButton; + QPushButton *loadButton; QScrollArea *scrollArea; + QWidget *panelContainer; + QVBoxLayout *panelLayout; - ManaCurveWidget *manaCurveWidget; - ManaDevotionWidget *manaDevotionWidget; - ManaBaseWidget *manaBaseWidget; + QVector panelWrappers; + ResizablePanel *selectedWrapper = nullptr; + + DeckListStatisticsAnalyzer *statsAnalyzer; + LayoutInspector *insp = nullptr; }; #endif // DECK_ANALYTICS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp index 3ca18d21c..7d0259c72 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp @@ -9,38 +9,93 @@ DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *_model, - DeckListStatisticsAnalyzerConfig cfg) - : QObject(parent), model(_model), config(cfg) + DeckListStatisticsAnalyzerConfig _config) + : QObject(parent), model(_model), config(_config) { - connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::update); + connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::analyze); } -void DeckListStatisticsAnalyzer::update() +void DeckListStatisticsAnalyzer::analyze() { - manaBaseMap.clear(); - manaCurveMap.clear(); - manaDevotionMap.clear(); + clearData(); QList cards = model->getCards(); - for (const ExactCard &card : cards) { - // ---- Mana curve ---- + for (auto card : cards) { + auto info = card.getInfo(); + const int cmc = info.getCmc().toInt(); + + // Convert once + QStringList types = info.getMainCardType().split(' '); + QStringList subtypes = info.getCardType().split('-').last().split(" "); + QString colors = info.getColors(); + int power = info.getPowTough().split("/").first().toInt(); + int toughness = info.getPowTough().split("/").last().toInt(); + + // For each copy of card + // ---------------- Mana Curve ---------------- if (config.computeManaCurve) { - manaCurveMap[card.getInfo().getCmc().toInt()]++; + manaCurveMap[cmc]++; } - // ---- Mana base ---- + // per-type curve + for (auto &t : types) { + manaCurveByType[t][cmc]++; + manaCurveCardsByType[t][cmc].append(info.getName()); + } + + // Per-subtype curve + for (auto &st : subtypes) { + manaCurveBySubtype[st][cmc]++; + manaCurveCardsBySubtype[st][cmc].append(info.getName()); + } + + // per-color curve + for (auto &c : colors) { + manaCurveByColor[c][cmc]++; + manaCurveCardsByColor[c][cmc].append(info.getName()); + } + + // Power/toughness + manaCurveByPower[QString::number(power)][cmc]++; + manaCurveCardsByPower[QString::number(power)][cmc].append(info.getName()); + manaCurveByToughness[QString::number(toughness)][cmc]++; + manaCurveCardsByToughness[QString::number(toughness)][cmc].append(info.getName()); + + // ========== Category Counts =========== + for (auto &t : types) { + typeCount[t]++; + } + for (auto &st : subtypes) { + subtypeCount[st]++; + } + for (auto &c : colors) { + colorCount[c]++; + } + manaValueCount[cmc]++; + + // ---------------- Mana Base ---------------- if (config.computeManaBase) { - auto mana = determineManaProduction(card.getInfo().getText()); - for (auto it = mana.begin(); it != mana.end(); ++it) + auto prod = determineManaProduction(info.getText()); + for (auto it = prod.begin(); it != prod.end(); ++it) { + if (it.value() > 0) { + productionPipCount[it.key()] += it.value(); + productionCardCount[it.key()]++; + } manaBaseMap[it.key()] += it.value(); + } } - // ---- Devotion ---- + // ---------------- Devotion ---------------- if (config.computeDevotion) { - auto devo = countManaSymbols(card.getInfo().getManaCost()); - for (auto &d : devo) + auto devo = countManaSymbols(info.getManaCost()); + for (auto &d : devo) { + if (d.second > 0) { + devotionPipCount[QString(d.first)] += d.second; + devotionCardCount[QString(d.first)]++; + } manaDevotionMap[d.first] += d.second; + } } } @@ -112,3 +167,57 @@ std::unordered_map DeckListStatisticsAnalyzer::countManaSymbols(const return manaCounts; } + +// Hypergeometric probability: P(X=k) +double DeckListStatisticsAnalyzer::hypergeometric(int N, int K, int n, int k) +{ + if (k < 0 || k > n || K > N) { + return 0.0; + } + + auto choose = [](int n, int r) -> double { + if (r > n) + return 0.0; + if (r == 0 || r == n) + return 1.0; + double res = 1.0; + for (int i = 1; i <= r; ++i) { + res *= (n - r + i); + res /= i; + } + return res; + }; + + return choose(K, k) * choose(N - K, n - k) / choose(N, n); +} + +void DeckListStatisticsAnalyzer::clearData() +{ + manaBaseMap.clear(); + manaCurveMap.clear(); + manaDevotionMap.clear(); + + devotionPipCount.clear(); + devotionCardCount.clear(); + + productionPipCount.clear(); + productionCardCount.clear(); + + manaCurveByType.clear(); + manaCurveBySubtype.clear(); + manaCurveByColor.clear(); + manaCurveByPower.clear(); + manaCurveByToughness.clear(); + + manaCurveCardsByType.clear(); + manaCurveCardsBySubtype.clear(); + manaCurveCardsByColor.clear(); + manaCurveCardsByPower.clear(); + manaCurveCardsByToughness.clear(); + + typeCount.clear(); + subtypeCount.clear(); + colorCount.clear(); + rarityCount.clear(); + manaValueCount.clear(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h index 8fa033971..946bb0117 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h @@ -14,6 +14,9 @@ struct DeckListStatisticsAnalyzerConfig bool computeManaBase = true; bool computeManaCurve = true; bool computeDevotion = true; + bool computeCategories = true; + bool computeCurveBreakdowns = true; + bool computeProbabilities = true; }; class DeckListStatisticsAnalyzer : public QObject @@ -23,9 +26,9 @@ class DeckListStatisticsAnalyzer : public QObject public: explicit DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *model, - DeckListStatisticsAnalyzerConfig cfg = DeckListStatisticsAnalyzerConfig()); + DeckListStatisticsAnalyzerConfig _config = DeckListStatisticsAnalyzerConfig()); - void update(); + void analyze(); [[nodiscard]] const QHash &getManaBase() const { @@ -40,6 +43,96 @@ public: return manaDevotionMap; } + const QHash &getDevotionPipCount() const + { + return devotionPipCount; + } + const QHash &getDevotionCardCount() const + { + return devotionCardCount; + } + + const QHash &getProductionPipCount() const + { + return productionPipCount; + } + const QHash &getProductionCardCount() const + { + return productionCardCount; + } + + const QHash &getTypeCount() const + { + return typeCount; + } + const QHash &getSubtypeCount() const + { + return subtypeCount; + } + const QHash &getColorCount() const + { + return colorCount; + } + const QHash &getRarityCount() const + { + return rarityCount; + } + const QHash &getManaValueCount() const + { + return manaValueCount; + } + + const QHash> &getManaCurveByType() const + { + return manaCurveByType; + } + const QHash> &getManaCurveBySubtype() const + { + return manaCurveBySubtype; + } + const QHash> &getManaCurveByColor() const + { + return manaCurveByColor; + } + const QHash> &getManaCurveByPower() const + { + return manaCurveByPower; + } + const QHash> &getManaCurveByToughness() const + { + return manaCurveByToughness; + } + + const QHash> &getManaCurveCardsByType() const + { + return manaCurveCardsByType; + } + + const QHash> &getManaCurveCardsBySubtype() const + { + return manaCurveCardsBySubtype; + } + + const QHash> &getManaCurveCardsByColor() const + { + return manaCurveCardsByColor; + } + + const QHash> &getManaCurveCardsByPower() const + { + return manaCurveCardsByPower; + } + + const QHash> &getManaCurveCardsByToughness() const + { + return manaCurveCardsByToughness; + } + + DeckListModel *getModel() const + { + return model; + } + signals: void statsUpdated(); @@ -47,14 +140,42 @@ private: DeckListModel *model; DeckListStatisticsAnalyzerConfig config; - // Internal result containers QHash manaBaseMap; std::unordered_map manaCurveMap; std::unordered_map manaDevotionMap; - // Internal helper functions + QHash devotionPipCount; // W/U/B/R/G total symbols + QHash devotionCardCount; // how many cards provide devotion + + QHash productionPipCount; // mana produced by cards + QHash productionCardCount; // number of producers + + QHash typeCount; + QHash subtypeCount; + QHash colorCount; + QHash rarityCount; + QHash manaValueCount; + + QHash> manaCurveByType; + QHash> manaCurveBySubtype; + QHash> manaCurveByColor; + QHash> manaCurveByPower; + QHash> manaCurveByToughness; + + QHash> manaCurveCardsByType; + QHash> manaCurveCardsBySubtype; + QHash> manaCurveCardsByColor; + QHash> manaCurveCardsByPower; + QHash> manaCurveCardsByToughness; + + // Not storing card info — only numeric results. + QHash>> probabilityExact; + QHash>> probabilityAtLeast; + QHash determineManaProduction(const QString &); std::unordered_map countManaSymbols(const QString &); + double hypergeometric(int N, int K, int n, int k); + void clearData(); }; #endif // COCKATRICE_DECK_LIST_STATISTICS_ANALYZER_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp deleted file mode 100644 index 2e530db0b..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "mana_base_widget.h" - -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include -#include - -ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Base"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barContainer = new QWidget(this); - barLayout = new QHBoxLayout(barContainer); - layout->addWidget(barContainer); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaBaseWidget::updateDisplay); - - retranslateUi(); -} - -void ManaBaseWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Base")); -} - -void ManaBaseWidget::updateDisplay() -{ - // Clear the layout first - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - - auto manaBaseMap = deckStatAnalyzer->getManaBase(); - - int highestEntry = 0; - for (auto entry : manaBaseMap) { - if (entry > highestEntry) { - highestEntry = entry; - } - } - - // Define color mapping for mana types - QHash manaColors; - manaColors.insert("W", QColor(248, 231, 185)); - manaColors.insert("U", QColor(14, 104, 171)); - manaColors.insert("B", QColor(21, 11, 0)); - manaColors.insert("R", QColor(211, 32, 42)); - manaColors.insert("G", QColor(0, 115, 62)); - manaColors.insert("C", QColor(150, 150, 150)); - - for (auto manaColor : manaBaseMap.keys()) { - QColor barColor = manaColors.value(manaColor, Qt::gray); - BarWidget *barWidget = new BarWidget(QString(manaColor), manaBaseMap[manaColor], highestEntry, barColor, this); - barLayout->addWidget(barWidget); - } - - update(); -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h deleted file mode 100644 index 079449353..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file mana_base_widget.h - * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. - */ - -#ifndef MANA_BASE_WIDGET_H -#define MANA_BASE_WIDGET_H - -#include "../general/display/banner_widget.h" -#include "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include -#include - -class ManaBaseWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - BannerWidget *bannerWidget; - QVBoxLayout *layout; - QWidget *barContainer; - QHBoxLayout *barLayout; -}; - -#endif // MANA_BASE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp deleted file mode 100644 index c094eb590..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "mana_curve_widget.h" - -#include "../../../main.h" -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include - -ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Curve"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barContainer = new QWidget(this); - barLayout = new QHBoxLayout(barContainer); - layout->addWidget(barContainer); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay); - - retranslateUi(); -} - -void ManaCurveWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Curve")); -} - -void ManaCurveWidget::updateDisplay() -{ - // Clear the layout first - if (barLayout != nullptr) { - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - } - - auto manaCurveMap = deckStatAnalyzer->getManaCurve(); - - int highestEntry = 0; - for (const auto &entry : manaCurveMap) { - if (entry.second > highestEntry) { - highestEntry = entry.second; - } - } - - // Convert unordered_map to ordered map to ensure sorting by CMC - std::map sortedManaCurve(manaCurveMap.begin(), manaCurveMap.end()); - - // Add new widgets to the layout in sorted order - for (const auto &entry : sortedManaCurve) { - BarWidget *barWidget = - new BarWidget(QString::number(entry.first), entry.second, highestEntry, QColor(122, 122, 122), this); - barLayout->addWidget(barWidget); - } - - update(); // Update the widget display -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h deleted file mode 100644 index fad1fb0f8..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file mana_curve_widget.h - * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. - */ - -#ifndef MANA_CURVE_WIDGET_H -#define MANA_CURVE_WIDGET_H - -#include "../general/display/banner_widget.h" -#include "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include - -class ManaCurveWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - QVBoxLayout *layout; - BannerWidget *bannerWidget; - QWidget *barContainer; - QHBoxLayout *barLayout; -}; - -#endif // MANA_CURVE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp deleted file mode 100644 index 476bb8077..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "mana_devotion_widget.h" - -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include -#include - -ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Devotion"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barLayout = new QHBoxLayout(); - layout->addLayout(barLayout); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaDevotionWidget::updateDisplay); - - retranslateUi(); -} - -void ManaDevotionWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Devotion")); -} - -void ManaDevotionWidget::updateDisplay() -{ - // Clear the layout first - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - - auto manaDevotionMap = deckStatAnalyzer->getDevotion(); - - int highestEntry = 0; - for (auto entry : manaDevotionMap) { - if (highestEntry < entry.second) { - highestEntry = entry.second; - } - } - - // Define color mapping for devotion bars - std::unordered_map manaColors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)}, - {'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)}, - {'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}}; - - for (auto entry : manaDevotionMap) { - QColor barColor = manaColors.count(entry.first) ? manaColors[entry.first] : Qt::gray; - BarWidget *barWidget = new BarWidget(QString(entry.first), entry.second, highestEntry, barColor, this); - barLayout->addWidget(barWidget); - } - - update(); // Update the widget display -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h deleted file mode 100644 index ff2e86159..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file mana_devotion_widget.h - * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. - */ - -#ifndef MANA_DEVOTION_WIDGET_H -#define MANA_DEVOTION_WIDGET_H - -#include "../general/display/banner_widget.h" -#include "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include -#include - -class ManaDevotionWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - BannerWidget *bannerWidget; - QVBoxLayout *layout; - QHBoxLayout *barLayout; -}; - -#endif // MANA_DEVOTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp new file mode 100644 index 000000000..a0c971a75 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp @@ -0,0 +1,367 @@ +#include "resizable_panel.h" + +#include "libcockatrice/utility/qt_utils.h" + +#include +#include +#include +#include + +ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent) + : QWidget(parent), panel(analyticsPanel), typeId(_typeId) +{ + setAcceptDrops(true); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // Frame for selection highlight + frame = new QFrame(this); + frame->setFrameShape(QFrame::Box); + frame->setLineWidth(2); + frame->setStyleSheet("border: none;"); + + auto *frameLayout = new QVBoxLayout(frame); + frameLayout->setContentsMargins(0, 0, 0, 0); + frameLayout->setSpacing(0); + + // Add the analytics panel + frameLayout->addWidget(analyticsPanel); + + dropIndicator = new QFrame(frame); + dropIndicator->setStyleSheet("background-color: #3daee9;"); + dropIndicator->setFixedHeight(3); + dropIndicator->hide(); // hidden by default + dropIndicator->raise(); // make sure it's above children + + selectionOverlay = new QFrame(frame); + selectionOverlay->setStyleSheet("background-color: rgba(61,174,233,50);"); // semi-transparent blue + selectionOverlay->hide(); // hidden by default + selectionOverlay->raise(); // make sure it is above children + selectionOverlay->setAttribute(Qt::WA_TransparentForMouseEvents); + + // Bottom bar with drag button and resize handle + auto *bottomBar = new QWidget(frame); + auto *bottomLayout = new QHBoxLayout(bottomBar); + bottomLayout->setContentsMargins(0, 0, 0, 0); + bottomLayout->setSpacing(0); + + // Drag button on the left + dragButton = new QPushButton("☰", bottomBar); + dragButton->setFixedSize(40, 8); + dragButton->setCursor(Qt::OpenHandCursor); + dragButton->setStyleSheet("QPushButton { " + "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a4a4a, stop:1 #3a3a3a); " + "border: none; color: #888; font-size: 10px; }" + "QPushButton:hover { background: #5a5a5a; }"); + bottomLayout->addWidget(dragButton); + + // Resize handle fills the rest + resizeHandle = new QWidget(bottomBar); + resizeHandle->setFixedHeight(8); + resizeHandle->setCursor(Qt::SizeVerCursor); + resizeHandle->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:0, y2:1, " + "stop:0 #3a3a3a, stop:1 #2a2a2a);"); + bottomLayout->addWidget(resizeHandle, 1); + + frameLayout->addWidget(bottomBar); + + mainLayout->addWidget(frame); + + // Set size policy + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + // Calculate initial height - use panel's size hint if available + int panelHint = analyticsPanel->sizeHint().height(); + int panelMin = analyticsPanel->minimumSizeHint().height(); + + // Start with the larger of panel's hint and panel's minimum hint + currentHeight = qMax(panelHint + 8, panelMin + 8); + updateSizeConstraints(); + + // Install event filters + dragButton->installEventFilter(this); + resizeHandle->installEventFilter(this); + + // Timer for auto-scroll during drag + autoScrollTimer = new QTimer(this); + autoScrollTimer->setInterval(50); + connect(autoScrollTimer, &QTimer::timeout, this, &ResizablePanel::performAutoScroll); +} + +void ResizablePanel::setSelected(bool selected) +{ + if (selected) { + selectionOverlay->setGeometry(0, 0, width(), height()); + selectionOverlay->show(); + } else { + selectionOverlay->hide(); + } +} + +void ResizablePanel::setHeightFromSaved(int h) +{ + if (h > 0) { + currentHeight = qMax(h, getMinimumAllowedHeight()); + updateSizeConstraints(); + } +} + +int ResizablePanel::getCurrentHeight() const +{ + return currentHeight; +} + +QSize ResizablePanel::sizeHint() const +{ + return QSize(width(), currentHeight); +} + +QSize ResizablePanel::minimumSizeHint() const +{ + return QSize(0, getMinimumAllowedHeight()); +} + +// ===================================================================================================================== +// Event Handling +// ===================================================================================================================== + +bool ResizablePanel::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == dragButton) { + if (event->type() == QEvent::MouseButtonPress) { + auto *mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + dragStartPos = mouseEvent->globalPosition().toPoint(); +#else + dragStartPos = mouseEvent->globalPos(); +#endif + isDraggingPanel = false; + dragButton->setCursor(Qt::ClosedHandCursor); + } + return false; + } else if (event->type() == QEvent::MouseMove) { + auto *mouseEvent = static_cast(event); + if (mouseEvent->buttons() & Qt::LeftButton) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint currentPos = mouseEvent->globalPosition().toPoint(); +#else + QPoint currentPos = mouseEvent->globalPos(); +#endif + int distance = (currentPos - dragStartPos).manhattanLength(); + if (distance >= 5 && !isDraggingPanel) { + isDraggingPanel = true; + startDrag(); + return true; + } + } + return false; + } else if (event->type() == QEvent::MouseButtonRelease) { + dragButton->setCursor(Qt::OpenHandCursor); + isDraggingPanel = false; + return false; + } + } + + if (obj == resizeHandle) { + if (event->type() == QEvent::MouseButtonPress) { + auto *mouseEvent = static_cast(event); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + resizeStartY = mouseEvent->globalPosition().y(); +#else + resizeStartY = mouseEvent->globalPos().y(); +#endif + isResizing = true; + resizeStartHeight = currentHeight; + resizeHandle->grabMouse(); + return true; + } else if (event->type() == QEvent::MouseMove && isResizing) { + auto *mouseEvent = static_cast(event); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + int deltaY = mouseEvent->globalPosition().y() - resizeStartY; +#else + int deltaY = mouseEvent->globalPos().y() - resizeStartY; +#endif + int newHeight = resizeStartHeight + deltaY; + + int minAllowed = getMinimumAllowedHeight(); + newHeight = qMax(newHeight, minAllowed); + + currentHeight = newHeight; + updateSizeConstraints(); + + return true; + } else if (event->type() == QEvent::MouseButtonRelease) { + isResizing = false; + resizeHandle->releaseMouse(); + return true; + } + } + + return QWidget::eventFilter(obj, event); +} + +void ResizablePanel::dragEnterEvent(QDragEnterEvent *event) +{ + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + event->acceptProposedAction(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + showDropIndicator(event->position().y()); +#else + showDropIndicator(event->pos().y()); +#endif + } +} + +void ResizablePanel::dragMoveEvent(QDragMoveEvent *event) +{ + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + event->acceptProposedAction(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + showDropIndicator(event->position().y()); + lastDragPos = mapToGlobal(event->position().toPoint()); +#else + showDropIndicator(event->pos().y()); + lastDragPos = mapToGlobal(event->pos()); +#endif + + if (!autoScrollTimer->isActive()) { + autoScrollTimer->start(); + } + } +} + +void ResizablePanel::dragLeaveEvent(QDragLeaveEvent *event) +{ + Q_UNUSED(event); + hideDropIndicator(); + autoScrollTimer->stop(); +} + +void ResizablePanel::dropEvent(QDropEvent *event) +{ + hideDropIndicator(); + autoScrollTimer->stop(); + + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + QByteArray data = event->mimeData()->data("application/x-resizablepanel"); + quintptr ptr = *reinterpret_cast(data.constData()); + ResizablePanel *draggedPanel = reinterpret_cast(ptr); + + if (draggedPanel && draggedPanel != this) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + bool insertBefore = (event->position().y() < height() / 2); +#else + bool insertBefore = (event->pos().y() < height() / 2); +#endif + emit dropRequested(draggedPanel, this, insertBefore); + event->acceptProposedAction(); + } + } +} + +void ResizablePanel::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + if (selectionOverlay->isVisible()) { + selectionOverlay->setGeometry(0, 0, width(), height()); + } + + if (dropIndicator->isVisible()) { + dropIndicator->setGeometry(0, dropIndicator->y(), width(), dropIndicator->height()); + } +} + +// ===================================================================================================================== +// Private Helpers +// ===================================================================================================================== + +int ResizablePanel::getMinimumAllowedHeight() const +{ + QSize panelMin = panel->minimumSizeHint(); + int panelMinHeight = (panelMin.isValid() && panelMin.height() > 0) ? panelMin.height() : 100; + return panelMinHeight + 8; +} + +void ResizablePanel::updateSizeConstraints() +{ + setMinimumHeight(currentHeight); + setMaximumHeight(currentHeight); + updateGeometry(); +} + +void ResizablePanel::startDrag() +{ + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + + quintptr ptr = reinterpret_cast(this); + QByteArray data(reinterpret_cast(&ptr), sizeof(ptr)); + mimeData->setData("application/x-resizablepanel", data); + + drag->setMimeData(mimeData); + + QPixmap pixmap(width(), 40); + pixmap.fill(QColor(58, 58, 58, 200)); + drag->setPixmap(pixmap); + drag->setHotSpot(QPoint(width() / 2, 20)); + + emit dragStarted(this); + + autoScrollTimer->start(); + + Qt::DropAction result = drag->exec(Qt::MoveAction); + Q_UNUSED(result); + + autoScrollTimer->stop(); + dragButton->setCursor(Qt::OpenHandCursor); + isDraggingPanel = false; +} + +void ResizablePanel::performAutoScroll() +{ + QScrollArea *scrollArea = QtUtils::findParentOfType(this); + + if (!scrollArea) { + return; + } + + QScrollBar *scrollBar = scrollArea->verticalScrollBar(); + if (!scrollBar) { + return; + } + + QRect scrollRect = scrollArea->viewport()->rect(); + QPoint scrollTopLeft = scrollArea->viewport()->mapToGlobal(scrollRect.topLeft()); + QRect globalScrollRect(scrollTopLeft, scrollRect.size()); + + const int scrollMargin = 50; + int scrollSpeed = 0; + + if (lastDragPos.y() < globalScrollRect.top() + scrollMargin) { + scrollSpeed = -15; + } else if (lastDragPos.y() > globalScrollRect.bottom() - scrollMargin) { + scrollSpeed = 15; + } + + if (scrollSpeed != 0) { + int newValue = scrollBar->value() + scrollSpeed; + newValue = qBound(scrollBar->minimum(), newValue, scrollBar->maximum()); + scrollBar->setValue(newValue); + } +} + +void ResizablePanel::showDropIndicator(double y) +{ + bool before = (y < height() / 2); + dropIndicator->setGeometry(0, before ? 0 : height() - 3, width(), 3); + dropIndicator->show(); +} + +void ResizablePanel::hideDropIndicator() +{ + dropIndicator->hide(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h new file mode 100644 index 000000000..d958ec64d --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h @@ -0,0 +1,79 @@ +#ifndef COCKATRICE_RESIZABLE_PANEL_H +#define COCKATRICE_RESIZABLE_PANEL_H + +#include "abstract_analytics_panel_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ResizablePanel : public QWidget +{ + Q_OBJECT +public: + explicit ResizablePanel(const QString &typeId, + AbstractAnalyticsPanelWidget *analyticsPanel, + QWidget *parent = nullptr); + + void setSelected(bool selected); + void setHeightFromSaved(int h); + int getCurrentHeight() const; + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + QString getTypeId() const + { + return typeId; + } + + AbstractAnalyticsPanelWidget *panel; + +signals: + void dragStarted(ResizablePanel *panel); + void dropRequested(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private: + int getMinimumAllowedHeight() const; + void updateSizeConstraints(); + void startDrag(); + void performAutoScroll(); + void showDropIndicator(double y); + void hideDropIndicator(); + + QString typeId; + + QFrame *frame; + QFrame *selectionOverlay; + QFrame *dropIndicator; + QPushButton *dragButton; + QWidget *resizeHandle; + + int currentHeight; + bool isResizing = false; + bool isDraggingPanel = false; + double resizeStartY = 0; + int resizeStartHeight = 0; + + QPoint dragStartPos; + QPoint lastDragPos; + QTimer *autoScrollTimer; +}; + +#endif // COCKATRICE_RESIZABLE_PANEL_H diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 62195974b..014f121e4 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -64,6 +64,8 @@ void DeckEditorDeckDockWidget::createDeckDock() connect(deckStateManager, &DeckStateManager::focusIndexChanged, this, &DeckEditorDeckDockWidget::setSelectedIndex); connect(deckStateManager, &DeckStateManager::deckReplaced, this, &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); + connect(deckStateManager, &DeckStateManager::deckReplaced, this, + &DeckEditorDeckDockWidget::applyActiveGroupCriteria); deckView = new QTreeView(); deckView->setObjectName("deckView"); @@ -174,11 +176,8 @@ void DeckEditorDeckDockWidget::createDeckDock() activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE); activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST); activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR); - connect(activeGroupCriteriaComboBox, QOverload::of(&QComboBox::currentIndexChanged), [this]() { - getModel()->setActiveGroupCriteria(static_cast( - activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); - getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - }); + connect(activeGroupCriteriaComboBox, qOverload(&QComboBox::currentIndexChanged), this, + &DeckEditorDeckDockWidget::applyActiveGroupCriteria); aIncrement = new QAction(QString(), this); aIncrement->setIcon(QPixmap("theme:icons/increment")); @@ -297,7 +296,6 @@ void DeckEditorDeckDockWidget::initializeFormats() QString format = deckStateManager->getMetadata().gameFormat; if (!format.isEmpty()) { - getModel()->setActiveFormat(format); formatComboBox->setCurrentIndex(formatComboBox->findData(format)); } else { // Ensure no selection is visible initially @@ -429,6 +427,13 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index) deckStateManager->setBannerCard(bannerCard); } +void DeckEditorDeckDockWidget::applyActiveGroupCriteria() +{ + getModel()->setActiveGroupCriteria( + static_cast(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); + getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); +} + void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible) { bannerCardLabel->setHidden(!visible); @@ -477,16 +482,14 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() updateBannerCardComboBox(); bannerCardComboBox->blockSignals(false); updateHash(); - sortDeckModelToDeckView(); - deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); -} - -void DeckEditorDeckDockWidget::sortDeckModelToDeckView() -{ - getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - getModel()->setActiveFormat(deckStateManager->getMetadata().gameFormat); + formatComboBox->blockSignals(true); formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); + formatComboBox->blockSignals(false); + + deckTagsDisplayWidget->blockSignals(true); + deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); + deckTagsDisplayWidget->blockSignals(false); } /** @@ -575,18 +578,19 @@ void DeckEditorDeckDockWidget::expandAll() } /** - * Gets the index of all the currently selected card nodes in the decklist table. + * Gets the source index of all the currently selected card nodes in the decklist table. * The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them. * - * @return A model index list containing all selected card nodes + * @return A list containing the source indices of all selected card nodes */ -QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const +QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodeSourceIndices() const { auto selectedRows = deckView->selectionModel()->selectedRows(); - const auto notLeafNode = [this](const QModelIndex &index) { - return getModel()->hasChildren(proxy->mapToSource(index)); - }; + const auto mapToSource = [this](const QModelIndex &index) { return proxy->mapToSource(index); }; + std::transform(selectedRows.begin(), selectedRows.end(), selectedRows.begin(), mapToSource); + + const auto notLeafNode = [this](const QModelIndex &sourceIndex) { return getModel()->hasChildren(sourceIndex); }; selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end()); std::reverse(selectedRows.begin(), selectedRows.end()); @@ -605,10 +609,10 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString & void DeckEditorDeckDockWidget::actIncrementSelection() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); - for (const auto &index : selectedRows) { - offsetCountAtIndex(index, true); + for (const auto &sourceIndex : selectedRows) { + offsetCountAtIndex(sourceIndex, true); } } @@ -627,7 +631,7 @@ void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString void DeckEditorDeckDockWidget::actSwapSelection() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted @@ -635,8 +639,8 @@ void DeckEditorDeckDockWidget::actSwapSelection() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - for (const auto ¤tIndex : selectedRows) { - deckStateManager->swapCardAtIndex(currentIndex); + for (const auto &sourceIndex : selectedRows) { + deckStateManager->swapCardAtIndex(sourceIndex); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -656,7 +660,7 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z void DeckEditorDeckDockWidget::actDecrementSelection() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted @@ -664,8 +668,8 @@ void DeckEditorDeckDockWidget::actDecrementSelection() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - for (const auto &index : selectedRows) { - offsetCountAtIndex(index, false); + for (const auto &sourceIndex : selectedRows) { + offsetCountAtIndex(sourceIndex, false); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -673,7 +677,7 @@ void DeckEditorDeckDockWidget::actDecrementSelection() void DeckEditorDeckDockWidget::actRemoveCard() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted @@ -681,8 +685,8 @@ void DeckEditorDeckDockWidget::actRemoveCard() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - for (const auto &row : selectedRows) { - deckStateManager->removeCardAtIndex(row); + for (const auto &sourceIndex : selectedRows) { + deckStateManager->removeCardAtIndex(sourceIndex); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -690,7 +694,7 @@ void DeckEditorDeckDockWidget::actRemoveCard() /** * @brief Increments or decrements the amount of the card node at the index by 1. - * @param idx The proxy index + * @param idx The source index * @param isIncrement If true, increments the count. If false, decrements the count */ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement) @@ -699,12 +703,10 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i return; } - QModelIndex sourceIndex = proxy->mapToSource(idx); - if (isIncrement) { - deckStateManager->incrementCountAtIndex(sourceIndex); + deckStateManager->incrementCountAtIndex(idx); } else { - deckStateManager->decrementCountAtIndex(sourceIndex); + deckStateManager->decrementCountAtIndex(idx); } } diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 1a82b00d1..39b298550 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -51,7 +51,6 @@ public slots: void selectNextCard(); void updateBannerCardComboBox(); void syncDisplayWidgetsToModel(); - void sortDeckModelToDeckView(); void actAddCard(const ExactCard &card, const QString &zoneName); void actIncrementSelection(); void actDecrementCard(const ExactCard &card, QString zoneName); @@ -91,7 +90,7 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; DeckListModel *getModel() const; - [[nodiscard]] QModelIndexList getSelectedCardNodes() const; + [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); private slots: @@ -100,6 +99,7 @@ private slots: void writeName(); void writeComments(); void writeBannerCard(int); + void applyActiveGroupCriteria(); void setSelectedIndex(const QModelIndex &newCardIndex); void updateHash(); void refreshShortcuts(); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 191cc5d0b..fe8840a41 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -462,8 +462,13 @@ AppearanceSettingsPage::AppearanceSettingsPage() showShortcutsCheckBox.setChecked(settings.getShowShortcuts()); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); + showGameSelectorFilterToolbarCheckBox.setChecked(settings.getShowGameSelectorFilterToolbar()); + connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings, + &SettingsCache::setShowGameSelectorFilterToolbar); + auto *menuGrid = new QGridLayout; menuGrid->addWidget(&showShortcutsCheckBox, 0, 0); + menuGrid->addWidget(&showGameSelectorFilterToolbarCheckBox, 1, 0); menuGroupBox = new QGroupBox; menuGroupBox->setLayout(menuGrid); @@ -727,6 +732,7 @@ void AppearanceSettingsPage::retranslateUi() menuGroupBox->setTitle(tr("Menu settings")); showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus")); + showGameSelectorFilterToolbarCheckBox.setText(tr("Show game filter toolbar above list in room tab")); cardsGroupBox->setTitle(tr("Card rendering")); displayCardNamesCheckBox.setText(tr("Display card names on cards having a picture")); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 10134f3a8..06ad3601c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -120,6 +120,7 @@ private: QLabel minPlayersForMultiColumnLayoutLabel; QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; + QCheckBox showGameSelectorFilterToolbarCheckBox; QCheckBox displayCardNamesCheckBox; QCheckBox autoRotateSidewaysLayoutCardsCheckBox; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp new file mode 100644 index 000000000..f7ca669f3 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp @@ -0,0 +1,43 @@ +#include "bar_chart_background_widget.h" + +BarChartBackgroundWidget::BarChartBackgroundWidget(QWidget *parent) : QWidget(parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +QSize BarChartBackgroundWidget::sizeHint() const +{ + return QSize(100, 150); +} + +void BarChartBackgroundWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + + int left = 46; // axis space + internal padding + int right = width() - PAD; + int top = PAD; + int bottom = height() - PAD - LABEL_H; + + int barAreaHeight = bottom - top; + int barAreaWidth = right - left; + + p.fillRect(QRect(left, top, barAreaWidth, barAreaHeight), QColor(250, 250, 250)); + + int ticks = 5; + for (int i = 0; i <= ticks; i++) { + float r = float(i) / ticks; + int y = bottom - r * barAreaHeight; + + p.setPen(QPen(QColor(180, 180, 180, 120), 1)); + p.drawLine(left, y, right, y); + + p.setPen(Qt::black); + p.drawText(left - 35, y - 6, 32, 12, Qt::AlignRight | Qt::AlignVCenter, QString::number(int(r * highest))); + } +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h new file mode 100644 index 000000000..06a17c7c6 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h @@ -0,0 +1,23 @@ +#ifndef COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H +#define COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H + +#include +#include + +class BarChartBackgroundWidget : public QWidget +{ + Q_OBJECT +public: + int highest = 0; // global maximum (shared across bars) + int barCount = 0; // number of CMC columns + int labelHeight = 20; // reserved for CMC numbers + + explicit BarChartBackgroundWidget(QWidget *parent); +public slots: + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +#endif // COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp new file mode 100644 index 000000000..998808307 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp @@ -0,0 +1,215 @@ +#include "bar_chart_widget.h" + +#include +#include +#include +#include + +BarChartWidget::BarChartWidget(QWidget *parent) : QWidget(parent) +{ + setMouseTracking(true); +} + +void BarChartWidget::setBars(const QVector &newBars) +{ + bars = newBars; + update(); +} + +void BarChartWidget::setHighest(int h) +{ + highest = qMax(1, h); + update(); +} + +QSize BarChartWidget::sizeHint() const +{ + return QSize(300, 200); +} + +QSize BarChartWidget::minimumSizeHint() const +{ + return QSize(300, 50); +} + +void BarChartWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + + int w = width(); + int h = height(); + + int left = 46; + int right = w - PAD; + int top = PAD; + int bottom = h - PAD - LABEL_H; + + int barAreaHeight = bottom - top; + int barAreaWidth = right - left; + + int barCount = bars.size(); + if (barCount == 0) + return; + + int spacing = 6; + int barWidth = (barAreaWidth - (barCount - 1) * spacing) / barCount; + + // background + p.fillRect(QRect(left, top, barAreaWidth, barAreaHeight), QColor(250, 250, 250)); + + // y-axis ticks + int ticks = 5; + // qInfo() << "Tick Positions "; + for (int i = 0; i <= ticks; i++) { + float r = float(i) / ticks; + int tickVal = i * highest / ticks; // integer value of tick + int y = bottom - (tickVal * barAreaHeight / highest); + + // qInfo() << "Tick" << i << "value" << int(r * highest) << "y" << y; + + p.setPen(QPen(QColor(180, 180, 180, 120), 1)); + p.drawLine(left, y, right, y); + + p.setPen(Qt::black); + p.drawText(left - 35, y - 6, 32, 12, Qt::AlignRight | Qt::AlignVCenter, QString::number(int(r * highest))); + } + + // draw bars + // qInfo() << "Bar Segments"; + int drawWidth = barWidth / 4; // 1/4 of allocated width + int xOffset = (barWidth - drawWidth) / 2; // center the narrow bar + + for (int i = 0; i < barCount; i++) { + const BarData &bar = bars[i]; + int x = left + i * (barWidth + spacing) + xOffset; // shift to center + int yCurrent = bottom; + + for (int j = 0; j < bar.segments.size(); j++) { + const auto &seg = bar.segments[j]; + int segHeight = (seg.value * barAreaHeight / highest); + if (segHeight < 2 && seg.value > 0) + segHeight = 2; + + int topY = yCurrent - segHeight; + + QRect r(x, topY, drawWidth, segHeight); // use drawWidth instead of barWidth + bool isTop = (j == bar.segments.size() - 1); + + QLinearGradient g(r.topLeft(), r.bottomLeft()); + g.setColorAt(0, seg.color.lighter(120)); + g.setColorAt(1, seg.color.darker(110)); + p.setBrush(g); + p.setPen(Qt::NoPen); + + if (isTop) { + QPainterPath path; + int radius = 6; + + int bx = r.x(); + int by = r.y(); + int bw = r.width(); + int bh = r.height(); + + path.moveTo(bx, by + bh); + path.lineTo(bx, by + radius); + path.quadTo(bx, by, bx + radius, by); + path.lineTo(bx + bw - radius, by); + path.quadTo(bx + bw, by, bx + bw, by + radius); + path.lineTo(bx + bw, by + bh); + path.lineTo(bx, by + bh); + path.closeSubpath(); + + p.drawPath(path); + } else { + p.drawRect(r); + } + + yCurrent -= segHeight; + } + + // draw label below bar + QRect labelRect(left + i * (barWidth + spacing), bottom, barWidth, LABEL_H); + QFont f = p.font(); + f.setBold(true); + p.setFont(f); + p.setPen(Qt::black); + p.drawText(labelRect, Qt::AlignCenter, bar.label); + } +} + +void BarChartWidget::leaveEvent(QEvent *) +{ + hoveredBar = -1; + hoveredSegment = -1; + QToolTip::hideText(); +} + +void BarChartWidget::mouseMoveEvent(QMouseEvent *e) +{ + if (bars.isEmpty()) { + return; + } + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + int w = width(); + int h = height(); + int left = 46; + int right = w - PAD; + int top = PAD; + int bottom = h - PAD - LABEL_H; + int barAreaHeight = bottom - top; + + int barCount = bars.size(); + int spacing = 6; + int barWidth = (right - left - (barCount - 1) * spacing) / barCount; + + // find hovered bar + int mx = e->pos().x(); + hoveredBar = -1; + for (int i = 0; i < barCount; i++) { + int x0 = left + i * (barWidth + spacing); + if (mx >= x0 && mx <= x0 + barWidth) { + hoveredBar = i; + break; + } + } + if (hoveredBar < 0) { + return; + } + + // find hovered segment + int yCurrent = bottom; + const auto &segments = bars[hoveredBar].segments; + hoveredSegment = -1; + for (int i = 0; i < segments.size(); i++) { + const auto &seg = segments[i]; + int segHeight = (seg.value * barAreaHeight / highest); + if (segHeight < 2 && seg.value > 0) + segHeight = 2; + + int topY = yCurrent - segHeight; + int bottomY = yCurrent; + if (e->pos().y() >= topY && e->pos().y() <= bottomY) { + hoveredSegment = i; + break; + } + yCurrent -= segHeight; + } + + if (hoveredSegment >= 0) { + const auto &s = segments[hoveredSegment]; + QString text = QString("%1: %2 cards\n\n%3").arg(s.category).arg(s.value).arg(s.cards.join("\n")); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QToolTip::showText(e->globalPosition().toPoint(), text, this); +#else + QToolTip::showText(e->globalPos(), text, this); +#endif + } else { + QToolTip::hideText(); + } +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h new file mode 100644 index 000000000..e80a3f8e8 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h @@ -0,0 +1,52 @@ +#ifndef COCKATRICE_BAR_CHART_WIDGET_H +#define COCKATRICE_BAR_CHART_WIDGET_H + +#include +#include +#include +#include + +struct BarSegment +{ + QString category; + int value; + QStringList cards; + QColor color; +}; + +struct BarData +{ + QString label; + QVector segments; +}; + +class BarChartWidget : public QWidget +{ + Q_OBJECT +public: + explicit BarChartWidget(QWidget *parent = nullptr); + + void setBars(const QVector &bars); + void setHighest(int h); // global max for scaling + int barCount() const + { + return bars.size(); + } + +protected: + void paintEvent(QPaintEvent *event) override; + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + void mouseMoveEvent(QMouseEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + QVector bars; + int highest = 1; // global maximum value + + int hoveredBar = -1; + int hoveredSegment = -1; +}; + +#endif // COCKATRICE_BAR_CHART_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.cpp similarity index 100% rename from cockatrice/src/interface/widgets/general/display/bar_widget.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.cpp diff --git a/cockatrice/src/interface/widgets/general/display/bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/bar_widget.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.h diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp similarity index 99% rename from cockatrice/src/interface/widgets/general/display/color_bar.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp index d1eb7ef4c..94e2420b5 100644 --- a/cockatrice/src/interface/widgets/general/display/color_bar.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp @@ -1,4 +1,3 @@ - #include "color_bar.h" #include diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.h b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/color_bar.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h diff --git a/cockatrice/src/interface/widgets/general/display/percent_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp similarity index 100% rename from cockatrice/src/interface/widgets/general/display/percent_bar_widget.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp diff --git a/cockatrice/src/interface/widgets/general/display/percent_bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/percent_bar_widget.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.h diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp new file mode 100644 index 000000000..e027aabdd --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp @@ -0,0 +1,140 @@ +#include "segmented_bar_widget.h" + +#include +#include +#include +#include + +SegmentedBarWidget::SegmentedBarWidget(QString label, QVector segments, int total, QWidget *parent) + : QWidget(parent), label(std::move(label)), segments(std::move(segments)), total(total) +{ + setMouseTracking(true); + setMinimumWidth(36); + setMaximumWidth(50); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); +} + +QSize SegmentedBarWidget::sizeHint() const +{ + return QSize(50, 150); +} + +void SegmentedBarWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + + int w = width(); + int h = height(); + + int barX = PAD; + int barWidth = w - PAD * 2; + + int barTop = PAD; + int barBottom = h - PAD - LABEL_H; + int barHeight = barBottom - barTop; + + int yCurrent = barBottom; + + // draw stacked segments + for (int i = 0; i < segments.size(); i++) { + const auto &seg = segments[i]; + + int segHeight = total > 0 ? (seg.value * barHeight / total) : 0; + if (segHeight < 2) + segHeight = 2; + + QRect r(barX, yCurrent - segHeight, barWidth, segHeight); + bool isTop = (i == segments.size() - 1); + + QLinearGradient g(r.topLeft(), r.bottomLeft()); + g.setColorAt(0, seg.color.lighter(120)); + g.setColorAt(1, seg.color.darker(110)); + p.setBrush(g); + p.setPen(Qt::NoPen); + + if (isTop) { + QPainterPath path; + int radius = 6; + + int x = r.x(); + int y = r.y(); + int w = r.width(); + int h = r.height(); + + path.moveTo(x, y + h); + path.lineTo(x, y + radius); + path.quadTo(x, y, x + radius, y); + path.lineTo(x + w - radius, y); + path.quadTo(x + w, y, x + w, y + radius); + path.lineTo(x + w, y + h); + path.lineTo(x, y + h); + path.closeSubpath(); + + p.drawPath(path); + } else { + p.drawRect(r); + } + + yCurrent -= segHeight; + } + + // draw label + QRect labelRect(0, h - LABEL_H, w, LABEL_H); + QFont f = p.font(); + f.setBold(true); + p.setFont(f); + p.setPen(Qt::black); + p.drawText(labelRect, Qt::AlignCenter, label); +} + +int SegmentedBarWidget::segmentAt(int y) const +{ + int padding = 4; + int labelHeight = 20; + int barHeight = height() - padding * 2 - labelHeight; + int barTop = padding; + int barBottom = barTop + barHeight; + + int currentTop = barBottom; + + for (int i = 0; i < segments.size(); i++) { + int segHeight = total > 0 ? (segments[i].value * barHeight / total) : 0; + if (segHeight < 1) { + segHeight = 1; + } + + int top = currentTop - segHeight; + int bottom = currentTop; + + if (y >= top && y <= bottom) + return i; + + currentTop -= segHeight; + } + return -1; +} + +void SegmentedBarWidget::mouseMoveEvent(QMouseEvent *e) +{ + if (!hovered) { + return; + } + + int idx = segmentAt(e->pos().y()); + if (idx < 0) { + return; + } + + const Segment &s = segments[idx]; + QString text = QString("%1: %2 cards\n%3").arg(s.category).arg(s.value).arg(s.cards.join(", ")); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QToolTip::showText(e->globalPosition().toPoint(), text, this); +#else + QToolTip::showText(e->globalPos(), text, this); +#endif +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h new file mode 100644 index 000000000..4df54e42e --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h @@ -0,0 +1,38 @@ +#ifndef COCKATRICE_SEGMENTED_BAR_WIDGET_H +#define COCKATRICE_SEGMENTED_BAR_WIDGET_H + +#include +#include +#include + +class SegmentedBarWidget : public QWidget +{ + Q_OBJECT + +public: + struct Segment + { + QString category; + int value = 0; + QStringList cards; + QColor color; + }; + + QString label; + QVector segments; + float total = 1.0; + + explicit SegmentedBarWidget(QString label, QVector segments, int total, QWidget *parent = nullptr); + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + void mouseMoveEvent(QMouseEvent *e) override; + + int segmentAt(int y) const; + +private: + bool hovered = true; +}; + +#endif // COCKATRICE_SEGMENTED_BAR_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp new file mode 100644 index 000000000..e86793083 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp @@ -0,0 +1,205 @@ +#include "color_pie.h" + +#include +#include +#include +#include + +ColorPie::ColorPie(const QMap &_colors, QWidget *parent) : QWidget(parent), colors(_colors) +{ + setMouseTracking(true); +} + +void ColorPie::setColors(const QMap &_colors) +{ + colors = _colors; + update(); +} + +QSize ColorPie::minimumSizeHint() const +{ + return QSize(200, 200); +} + +void ColorPie::paintEvent(QPaintEvent *) +{ + if (colors.isEmpty()) { + return; + } + + int total = 0; + for (int v : colors.values()) { + total += v; + } + + if (total == 0) { + return; + } + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + int w = width(); + int h = height(); + int size = qMin(w, h) - 40; // leave space for labels + QRectF rect((w - size) / 2.0, (h - size) / 2.0, size, size); + + // Draw border + p.setPen(QPen(Qt::black, 1)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(rect); + + // Sorted keys for predictable order + QList sortedKeys = colors.keys(); + std::sort(sortedKeys.begin(), sortedKeys.end()); + + double startAngle = 0.0; + + for (const QString &key : sortedKeys) { + int value = colors[key]; + double ratio = double(value) / total; + + if (ratio <= minRatioThreshold) { + continue; + } + + double spanAngle = ratio * 360.0; + + QColor base = colorFromName(key); + + // Gradient + QRadialGradient grad(rect.center(), size / 2); + grad.setColorAt(0, base.lighter(130)); + grad.setColorAt(1, base.darker(130)); + p.setBrush(grad); + p.setPen(Qt::NoPen); + + // Draw slice + p.drawPie(rect, int(startAngle * 16), int(spanAngle * 16)); + + // Draw percent label + double midAngle = startAngle + spanAngle / 2; + double rad = qDegreesToRadians(midAngle); + double labelRadius = size / 2 + 15; // slightly outside the pie + QPointF center = rect.center(); + QPointF labelPos(center.x() + labelRadius * qCos(rad), center.y() - labelRadius * qSin(rad)); + + QString label = QString("%1%").arg(int(ratio * 100 + 0.5)); + + QFontMetrics fm(p.font()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) + int labelWidth = fm.horizontalAdvance(label); +#else + int labelWidth = fm.width(label); +#endif + QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height()); + + p.setPen(Qt::black); + p.drawText(textRect, Qt::AlignCenter, label); + + startAngle += spanAngle; + } +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void ColorPie::enterEvent(QEnterEvent *event) +{ + Q_UNUSED(event); + isHovered = true; +} +#else +void ColorPie::enterEvent(QEvent *event) +{ + Q_UNUSED(event); + isHovered = true; +} +#endif + +void ColorPie::leaveEvent(QEvent *) +{ + isHovered = false; +} + +void ColorPie::mouseMoveEvent(QMouseEvent *event) +{ + if (!isHovered || colors.isEmpty()) { + return; + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint p = event->position().toPoint(); + QPoint gp = event->globalPosition().toPoint(); +#else + QPoint p = event->pos(); + QPoint gp = event->globalPos(); +#endif + + QString text = tooltipForPoint(p); + if (!text.isEmpty()) { + QToolTip::showText(gp, text, this); + } +} + +QString ColorPie::tooltipForPoint(const QPoint &pt) const +{ + if (colors.isEmpty()) { + return {}; + } + + int total = 0; + for (int v : colors.values()) + total += v; + if (total == 0) + return {}; + + int w = width(); + int h = height(); + int size = qMin(w, h) - 40; + QPointF center(w / 2.0, h / 2.0); + + QPointF v = pt - center; + double distance = std::sqrt(v.x() * v.x() + v.y() * v.y()); + if (distance > size / 2.0) + return {}; // outside pie + + double angle = std::atan2(-v.y(), v.x()) * 180.0 / M_PI; + if (angle < 0) { + angle += 360.0; + } + + double acc = 0.0; + + QList keys = colors.keys(); + std::sort(keys.begin(), keys.end()); + + for (const QString &key : keys) { + double span = (double(colors[key]) / total) * 360.0; + + if (angle >= acc && angle < acc + span) { + double percent = (100.0 * colors[key]) / total; + return QString("%1: %2 cards (%3%)").arg(key).arg(colors[key]).arg(QString::number(percent, 'f', 1)); + } + acc += span; + } + + return {}; +} + +QColor ColorPie::colorFromName(const QString &name) const +{ + static QMap map = { + {"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)}, + {"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)}, + }; + + if (map.contains(name)) { + return map[name]; + } + + QColor c(name); + if (!c.isValid()) { + c = Qt::gray; + } + + return c; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h new file mode 100644 index 000000000..a8fe784a3 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_COLOR_PIE_H +#define COCKATRICE_COLOR_PIE_H + +#ifndef COLOR_PIE_H +#define COLOR_PIE_H + +#include +#include +#include + +class ColorPie : public QWidget +{ + Q_OBJECT + +public: + explicit ColorPie(const QMap &_colors = {}, QWidget *parent = nullptr); + + void setColors(const QMap &_colors); + + QSize minimumSizeHint() const override; + +protected: + void paintEvent(QPaintEvent *) override; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *event) override; +#else + void enterEvent(QEvent *event) override; +#endif + void leaveEvent(QEvent *) override; + void mouseMoveEvent(QMouseEvent *event) override; + +private: + QMap colors; + bool isHovered = false; + const double minRatioThreshold = 0.01; // skip tiny slices + + QColor colorFromName(const QString &name) const; + QString tooltipForPoint(const QPoint &pt) const; +}; + +#endif // COLOR_PIE_H + +#endif // COCKATRICE_COLOR_PIE_H diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 48a12d5b3..f14cc6d82 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -73,6 +73,12 @@ GameSelector::GameSelector(AbstractClient *_client, if (showFilters && restoresettings) { quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap); + quickFilterToolBar->setVisible(showFilters && restoresettings && + SettingsCache::instance().getShowGameSelectorFilterToolbar()); + + connect(&SettingsCache::instance(), &SettingsCache::showGameSelectorFilterToolbarChanged, this, [this] { + quickFilterToolBar->setVisible(SettingsCache::instance().getShowGameSelectorFilterToolbar()); + }); } else { quickFilterToolBar = nullptr; } diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp index 2998a03bc..736b69ea2 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp @@ -3,7 +3,7 @@ #include "../../../../../card_picture_loader/card_picture_loader.h" #include "../../../../cards/card_info_picture_with_text_overlay_widget.h" #include "../../../../general/display/background_plate_widget.h" -#include "../../../../general/display/color_bar.h" +#include "../../../../general/display/charts/bars/color_bar.h" #include "archidekt_deck_preview_image_display_widget.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h index 0174016f7..43baddb4c 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h @@ -7,7 +7,7 @@ #ifndef EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H #define EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H -#include "../../../../../general/display/percent_bar_widget.h" +#include "../../../../../general/display/charts/bars/percent_bar_widget.h" #include "../../api_response/cards/edhrec_api_response_card_details.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h index 39d26a409..c2e1c018c 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h @@ -7,7 +7,7 @@ #ifndef EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H #define EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H -#include "../../../../../general/display/percent_bar_widget.h" +#include "../../../../../general/display/charts/bars/percent_bar_widget.h" #include "../../api_response/cards/edhrec_api_response_card_details.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index f3d573d27..82f96860d 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -85,7 +85,7 @@ void TabDeckEditorVisual::onDeckChanged() { AbstractTabDeckEditor::onDeckModified(); tabContainer->visualDeckView->constructZoneWidgetsFromDeckListModel(); - tabContainer->deckAnalytics->refreshDisplays(); + tabContainer->deckAnalytics->updateDisplays(); tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel()); } diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index 98b6aff00..82aeb05a6 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -45,10 +45,13 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay); - deckAnalytics = new DeckAnalyticsWidget(this, deckModel); + statsAnalyzer = new DeckListStatisticsAnalyzer(this, deckModel); + statsAnalyzer->analyze(); + + deckAnalytics = new DeckAnalyticsWidget(this, statsAnalyzer); deckAnalytics->setObjectName("deckAnalytics"); - sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel); + sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel, statsAnalyzer); this->addNewTab(visualDeckView, tr("Visual Deck View")); this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display")); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h index 59c577024..9468df425 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -78,7 +78,8 @@ public: /// Get the total number of tabs. [[nodiscard]] int getTabCount() const; - VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget. + VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget. + DeckListStatisticsAnalyzer *statsAnalyzer; DeckAnalyticsWidget *deckAnalytics; ///< Deck analytics widget. VisualDatabaseDisplayWidget *visualDatabaseDisplay; ///< Database display widget. PrintingSelector *printingSelector; ///< Printing selector widget. diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 0badb76ff..24f521760 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -3,12 +3,16 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" #include "../cards/card_info_picture_widget.h" +#include "../deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h" +#include "../deck_analytics/deck_list_statistics_analyzer.h" #include #include -VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *_deckListModel) - : QWidget(parent), deckListModel(_deckListModel) +VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, + DeckListModel *_deckListModel, + DeckListStatisticsAnalyzer *_statsAnalyzer) + : QWidget(parent), deckListModel(_deckListModel), statsAnalyzer(_statsAnalyzer) { layout = new QVBoxLayout(this); setLayout(layout); @@ -35,6 +39,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); + drawProbabilityWidget = new DrawProbabilityWidget(this, statsAnalyzer); + layout->addWidget(drawProbabilityWidget); + cardSizeWidget = new CardSizeWidget(this, flowWidget); layout->addWidget(cardSizeWidget); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h index 751e16a3c..c63c74a4d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h @@ -8,6 +8,7 @@ #define VISUAL_DECK_EDITOR_SAMPLE_HAND_WIDGET_H #include "../cards/card_size_widget.h" +#include "../deck_analytics/deck_list_statistics_analyzer.h" #include "../general/layout_containers/flow_widget.h" #include @@ -15,11 +16,14 @@ #include #include +class DrawProbabilityWidget; class VisualDeckEditorSampleHandWidget : public QWidget { Q_OBJECT public: - VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *deckListModel); + VisualDeckEditorSampleHandWidget(QWidget *parent, + DeckListModel *deckListModel, + DeckListStatisticsAnalyzer *statsAnalyzer); QList getRandomCards(int amountToGet); public slots: @@ -29,12 +33,14 @@ public slots: private: DeckListModel *deckListModel; + DeckListStatisticsAnalyzer *statsAnalyzer; QVBoxLayout *layout; QWidget *resetAndHandSizeContainerWidget; QHBoxLayout *resetAndHandSizeLayout; QPushButton *resetButton; QSpinBox *handSizeSpinBox; FlowWidget *flowWidget; + DrawProbabilityWidget *drawProbabilityWidget; CardSizeWidget *cardSizeWidget; }; diff --git a/cockatrice/translations/cockatrice_fr.ts b/cockatrice/translations/cockatrice_fr.ts index 5cd205a48..3bd264358 100644 --- a/cockatrice/translations/cockatrice_fr.ts +++ b/cockatrice/translations/cockatrice_fr.ts @@ -133,7 +133,7 @@ Vérifiez que le répertoire ne soit pas en lecture seule et réessayez. seconds - + secondes @@ -154,7 +154,7 @@ You will not be able to manage printing preferences on a per-deck basis, or see You will have to use the Set Manager, available through Card Database -> Manage Sets. Are you sure you would like to enable this feature? - + L'activation de cette fonction désactivera l'utilisation du sélecteur d'impression.Vous ne pourrez plus gérer les préférences d'impression pour chaque deck ni voir les impressions sélectionnées par d'autres joueurs.Vous devrez utiliser le Gestionnaire d'extensions, accessible via Base de données de cartes -> Gérer les extensions.Êtes-vous sûr de vouloir activer cette fonction ? @@ -165,12 +165,12 @@ You can now choose printings on a per-deck basis in the Deck Editor and configur You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). Are you sure you would like to disable this feature? - + Désactiver cette fonction activera le sélecteur d'impression.Vous pouvez désormais choisir les impressions pour chaque deck dans l'éditeur de deck et configurer l'impression ajoutée par défaut en l'épinglant dans le sélecteur d'impression.Vous pouvez également utiliser le gestionnaire de d'extension pour personnaliser l'ordre de tri des impressions dans le sélecteur d'impression (d'autres ordres de tri, comme l'ordre alphabétique ou la date de parution, sont disponibles).Êtes-vous sûr de vouloir désactiver cette fonction ? Confirm Change - + Confirmer le changement @@ -190,17 +190,17 @@ Are you sure you would like to disable this feature? Home tab background source: - + Source d'arrière-plan de l'onglet Accueil: Home tab background shuffle frequency: - + Fréquence de mélange de l'arrière-plan de l'onglet Accueil: Disabled - + Désactivé @@ -230,7 +230,7 @@ Are you sure you would like to disable this feature? Override all card art with personal set preference (Pre-ProviderID change behavior) - + Remplacer toutes les illustrations de cartes par les préférences personnelles (comportement avant modification du ProviderID) @@ -319,17 +319,17 @@ Are you sure you would like to disable this feature? Theme - + Thème Art crop of random card - + Recadrage d'image d'une carte aléatoire Art crop of background.cod deck file - + Recadrage d'image du fichier background.cod @@ -667,124 +667,124 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Re&veal to... - + Révéler à... &All players - + &Tous les joueurs View related cards - + Voir les cartes associées Token: - + Jeton : All tokens - + Tout les jetons &Select All - + &Sélectionner Tout S&elect Row - + Sél&ectionne une ligne S&elect Column - + Sél&ectionne une colonne &Play - + &Jouer &Hide - + &Cacher Play &Face Down - + Jouer &Face Cachée &Tap / Untap Turn sideways or back again - + &Engager / Dégager Toggle &normal untapping - + Activer dégagement &normal T&urn Over Turn face up/face down - + Reto&urner &Peek at card face - + &Regarder furtivement la carte face cachée &Clone - + &Cloner Attac&h to card... - + Attac&her à une carte... Unattac&h - + Détac&her &Draw arrow... - + &Tracer une flèche... &Set annotation... - + &Annoter... Ca&rd counters - + Ma&rqueurs sur la carte &Add counter (%1) - + &Ajouter un marqueur (%1) &Remove counter (%1) - + &Retirer un marqueur (%1) &Set counters (%1)... - + &Changer le nombre de marqueurs (%1)... @@ -801,133 +801,133 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa their hand nominative - + sa main %1's hand nominative - + main de %1 their library look at zone - + sa bibliothèque %1's library look at zone - + bibliothèque de %1 of their library top cards of zone, - + de sa bibliothèque of %1's library top cards of zone - + de la bibliothèque de %1 their library reveal zone - + sa bibliothèque %1's library reveal zone - + bibliothèque de %1 their library shuffle - + sa bibliothèque %1's library shuffle - + bibliothèque de %1 their library nominative - + sa bibliothèque %1's library nominative - + bibliothèque de %1 their graveyard nominative - + son cimetière %1's graveyard nominative - + le cimetière de %1 their exile nominative - + sa zone d'exil %1's exile nominative - + la zone d'exil de %1 their sideboard look at zone - + sa réserve %1's sideboard look at zone - + la réserve de %1 their sideboard nominative - + sa réserve %1's sideboard nominative - + la réserve de %1 their custom zone '%1' nominative - + sa zone personnalisée '%1' %1's custom zone '%2' nominative - + La zone personnalisée '%2' de %1 @@ -951,13 +951,13 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa C&ustom Zones - + Zones pe&rsonnalisés View custom zone '%1' - + Voir la zone personnalisée '%1' @@ -993,12 +993,12 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Show on EDHRec (Commander) - + Afficher sur EDHRec (Commandant) Show on EDHRec (Card) - + Afficher sur EDHRec (carte) @@ -1204,7 +1204,7 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Load deck from online service... - + Charger un deck depuis un service en ligne... @@ -1671,7 +1671,7 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Load from website... - + Charger depuis un site web... @@ -1983,7 +1983,7 @@ Voulez-vous convertir le deck au format .cod ? Open decklists in lobby - + Ouvrir les listes de deck dans le lobby @@ -2455,7 +2455,7 @@ Assurez-vous d'activer l'édition « Fausse édition contenant les je Hide games with forced open decklists - + Masquer les parties avec les listes de decks ouvertes forcées @@ -2743,7 +2743,7 @@ Assurez-vous d'activer l'édition « Fausse édition contenant les je Paste a link to a decklist site here to import it. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - + Collez ici un lien vers un site de liste de deck pour l'importer.(Archidekt, Deckstats, Moxfield et TappedOut sont supportés.) @@ -2752,28 +2752,28 @@ Assurez-vous d'activer l'édition « Fausse édition contenant les je Load Deck from Website - + Charger un deck depuis un site web No parser available for this deck provider. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - + Aucun analyseur disponible pour ce fournisseur de deck.(Archidekt, Deckstats, Moxfield et TappedOut sont supportés.) Network error: %1 - + Erreur réseau : %1 Received empty deck data. - + Données de deck vide reçues. Failed to parse deck data: %1 - + Échec de l'analyse des données du deck : %1 @@ -2784,7 +2784,7 @@ https://archidekt.com/decks/9999999 https://deckstats.net/decks/99999/9999999-your-deck-name/en https://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzz https://tappedout.net/mtg-decks/your-deck-name/ - + L'URL fournie n'est pas reconnue comme une URL de deck valide.Les URL de deck valides ressemblent à ceci :https://archidekt.com/decks/9999999https://deckstats.net/decks/99999/9999999-your-deck-name/enhttps://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzzhttps://tappedout.net/mtg-decks/your-deck-name/ @@ -3495,22 +3495,22 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. kicked by game host or moderator - + Éjecté par l'hôte de la partie ou un modérateur player left the game - + joueur a quitté la partie player disconnected from server - + joueur déconnecté du serveur reason unknown - + raison inconnue @@ -3676,7 +3676,7 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. open decklists - + Ouvrir les listes de deck @@ -3889,47 +3889,47 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. &Graveyard - + &Cimetière &View graveyard - + &Voir le cimetière &Move graveyard to... - + &Déplacer le cimetière vers... &Top of library - + &Dessus de la bibliothèque &Bottom of library - + &Dessous de la bibliothèque &All players - + &Tous les joueurs &Hand - + &Main &Exile - + &Exil Reveal random card to... - + Révéler une carte au hasard à... @@ -3937,63 +3937,63 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. &Hand - + &Main &View hand - + &Voir la main &Sort hand - + &Trier la main Take &mulligan - + Faire un &mulligan &Move hand to... - + &Déplacer la main vers… &Top of library - + &Dessus de la bibliothèque &Bottom of library - + &Dessous de la bibliothèque &Graveyard - + &Cimetière &Exile - + &Exil &Reveal hand to... - + &Révéler la main à... Reveal r&andom card to... - + Révéler une c&arte au hasard à... &All players - + &Tous les joueurs @@ -4001,47 +4001,47 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. Create New Deck - + Créer un Nouveau Deck Browse Decks - + Parcourir les Decks Browse Card Database - + Parcourir la Base de Carte Browse EDHRec - + Parcourir EDHRec View Replays - + Voir les replays Quit - + Quitter Connecting... - + Connection... Connect - + Se connecter Play - + Jouer @@ -4049,193 +4049,193 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. &Library - + &Bibliothèque &View library - + &Voir la bibliothèque View &top cards of library... - + Voir les cartes du &dessus de la bibliothèque... View bottom cards of library... - + Voir les cartes du dessous de la bibliothèque... Reveal &library to... - + Révéler la &bibliothèque à... Lend library to... - + Prêter la bibliothèque à... Reveal &top cards to... - + Révéler les cartes du &dessus à... &Top of library... - + &Dessus de la bibliothèque... &Bottom of library... - + &Dessous de la bibliothèque... &Always reveal top card - + &Toujours révéler la carte du dessus &Always look at top card - + &Toujours voir la carte du dessus &Open deck in deck editor - + &Ouvrir le deck dans l'éditeur &Draw card - + &Piocher une carte D&raw cards... - + P&iocher plusieurs cartes... &Undo last draw - + &Annuler la dernière pioche Shuffle - + Mélanger &Play top card - + &Jouer la carte du dessus Play top card &face down - + Jouer la carte du dessus &face cachée Put top card on &bottom - + Placer la carte du dessus au-&dessous Move top card to grave&yard - + Mettre la carte du dessus dans le cime&tière Move top card to e&xile - + E&xiler la carte du dessus Move top cards to &graveyard... - + Mettre les cartes du dessus dans le &cimetière... Move top cards to &exile... - + &Exiler les cartes du dessus... Put top cards on stack &until... - + Placer les cartes du dessus sur la pile &jusqu'à... Shuffle top cards... - + Mélanger les cartes du dessus... &Draw bottom card - + &Piocher la carte du dessous D&raw bottom cards... - + P&iocher les cartes du dessous... &Play bottom card - + &Jouer la carte du dessous Play bottom card &face down - + Jouer la carte du dessous &face cachée Move bottom card to grave&yard - + Mettre la carte du dessous dans le cime&tière Move bottom card to e&xile - + E&xiler la carte du dessous Move bottom cards to &graveyard... - + Mettre les cartes du dessus dans le &cimetière... Move bottom cards to &exile... - + &Exiler les cartes du dessous... Put bottom card on &top - + Placer la carte du dessous au-&dessus Shuffle bottom cards... - + Mélanger les cartes du dessous... &All players - + &Tous les joueurs Reveal top cards of library - + Révéler les cartes du dessus de la bibliothèque Number of cards: (max. %1) - + Nombre de cartes : (max. %1) @@ -5418,7 +5418,7 @@ Cockactrice va maintenant recharger la base de données de cartes. You are flooding the game. Please wait a couple of seconds. - + Vous floodez la partie. Veuillez patienter quelques secondes. @@ -5804,37 +5804,37 @@ Cockactrice va maintenant recharger la base de données de cartes. Move to - + Déplacer vers &Top of library in random order - + &Dessus de la bibliothèque dans un ordre aléatoire X cards from the top of library... - + X cartes du dessus de la bibliothèque... &Bottom of library in random order - + &Dessous de la bibliothèque dans un ordre aléatoire &Hand - + &Main &Graveyard - + &Cimetière &Exile - + &Exil @@ -6043,7 +6043,7 @@ Cockactrice va maintenant recharger la base de données de cartes. View top cards of library - + Voir les cartes du dessus de la bibliothèque @@ -6058,119 +6058,119 @@ Cockactrice va maintenant recharger la base de données de cartes. Number of cards: (max. %1) - + Nombre de cartes : (max. %1) View bottom cards of library - + Voir les cartes du dessous de la bibliothèque Shuffle top cards of library - + Mélanger les cartes du dessus de la bibliothèque Shuffle bottom cards of library - + Mélanger les cartes du dessous de la bibliothèque Draw hand - + Piocher une main 0 and lower are in comparison to current hand size - + 0 et moins sont en comparaison avec la taille actuelle de la main Draw cards - + Piocher des cartes Move top cards to grave - + Mettre les cartes du dessus dans le cimetière Move top cards to exile - + Exiler les cartes du dessus Move bottom cards to grave - + Mettre les cartes du dessous dans le cimetière Move bottom cards to exile - + Exiler les cartes du dessous Draw bottom cards - + Piocher les cartes du dessous C&reate another %1 token - + C&réer un autre jeton %1 Create tokens - + Créer des jetons Number: - + Nombre : Place card X cards from top of library - + Placer les X cartes depuis le dessus de la bibliothèque Which position should this card be placed: - + Dans quelle position cette carte doit-elle être placée : (max. %1) - + (max. %1) Change power/toughness - + Changer la force/l'endurance Change stats to: - + Changer les stats pour : Set annotation - + Annoter Please enter the new annotation: - + Veuillez entrer la nouvelle annotation : Set counters - + Définir les marqueurs @@ -6178,17 +6178,17 @@ Cockactrice va maintenant recharger la base de données de cartes. Player "%1" - + Joueur "%1" &Counters - + &Marqueurs S&ay - + D&ire @@ -6282,57 +6282,57 @@ Cockactrice va maintenant recharger la base de données de cartes. Power / toughness - + Force / Endurance &Increase power - + &Augmenter la force &Decrease power - + &Diminuer la force I&ncrease toughness - + A&ugmenter l'endurance D&ecrease toughness - + D&iminuer l'endurance In&crease power and toughness - + Au&gmenter la force et l'endurance Dec&rease power and toughness - + Dim&inuer la force et l'endurance Increase power and decrease toughness - + Augmenter la force et diminuer l'endurance Decrease power and increase toughness - + Diminuer la force et augmenter l'endurance Set &power and toughness... - + Changer la &force et l'endurance... Reset p&ower and toughness - + Réinitialiser la f&orce et l'endurance @@ -6565,37 +6565,37 @@ Cockactrice va maintenant recharger la base de données de cartes. &Exile - + &Exil &View exile - + &Voir la zone d'exil &Move exile to... - + &Déplacer la zone d'exil vers... &Top of library - + &Dessus de la bibliothèque &Bottom of library - + &Dessous de la bibliothèque &Hand - + &Main &Graveyard - + &Cimetière @@ -6803,12 +6803,12 @@ Please check your shortcut settings! &Sideboard - + &Réserve &View sideboard - + &Voir la réserve @@ -7382,7 +7382,7 @@ Veuillez entrer un nom: EDHRec: - + EDHRec : @@ -7509,7 +7509,7 @@ Veuillez entrer un nom: Selected cards - + Cartes sélectionnées @@ -7588,7 +7588,7 @@ Veuillez entrer un nom: Home - + Accueil @@ -7841,13 +7841,13 @@ Plus vous entrez d'informations, meilleurs seront les résultats. Get replay share code - + Obtenir le code de partage du replay Look up replay by share code - + Regarder un replay depuis un code de partage @@ -7897,71 +7897,71 @@ Plus vous entrez d'informations, meilleurs seront les résultats. Failed to get code - + Echec d'obtention du code Either this server does not support replay sharing, or does not permit replay sharing for you. - + Soit ce serveur ne prend pas en charge le partage de replay, soit il ne vous autorise pas à partager des replays. Failed - + Échec Could not get replay code - + Impossible d'obtenir le code de replay Replay Share Code - + Code de Partage de Replay Others can use this code to add the replay to their list of remote replays: %1 - + D'autres peuvent utiliser ce code pour ajouter la replay à leur liste de replay distantes :%1 Copy to clipboard - + Copier vers le presse-papier Replay share code - + Code de partage de replay Replay code found - + Code de replay trouvé Replay was added, or you already had access to it. - + Le replay a été ajoutée, ou vous y aviez déjà accès. Replay code not found - + Code de replay non trouvé Failed to submit code - + Échec de l'envoi du code Unexpected error - + Erreur inattendue @@ -8078,7 +8078,7 @@ Plus vous entrez d'informations, meilleurs seront les résultats. Home - + Accueil @@ -8378,22 +8378,22 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Error - + Erreur This user does not exist. - + Cet utilisateur n'existe pas. You are being ignored by %1 and can't see their games. - + Vous avez été ignoré par %1 et ne pouvez pas voir ses parties. Could not get %1's games. - + Impossible d'obtenir les parties de %1. @@ -8833,32 +8833,32 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Increment all card counters - + Incrémenter tous les marqueurs de la carte &Untap all permanents - + &Dégager tous les permanents R&oll die... - + L&ancer un dé... &Create token... - + &Créer un jeton... C&reate another token - + C&réer un autre jeton Cr&eate predefined token - + C&réer un jeton prédéfini @@ -9019,7 +9019,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Loading database ... - + Chargement de la base de données... @@ -10000,7 +10000,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Increment all card counters - + Incrémenter tous les marqueurs de la carte @@ -10256,7 +10256,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Sort Hand - + Trier la main @@ -10449,7 +10449,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Home - + Accueil diff --git a/doc/doxygen/extra-pages/index.md b/doc/doxygen/extra-pages/index.md index c370a6fc2..b5c2b61e5 100644 --- a/doc/doxygen/extra-pages/index.md +++ b/doc/doxygen/extra-pages/index.md @@ -7,4 +7,4 @@ This is the **main landing page** of the Cockatrice documentation. - Go to the @subpage user_reference page - Review the @subpage developer_reference -Or check out the [Cockatrice Webpage](https://cockatrice.github.io/). +Or check out the Cockatrice Webpage. diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index a7dd01702..5ccdb5f66 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -9,7 +9,9 @@ set(HEADERS libcockatrice/deck_list/tree/inner_deck_list_node.h libcockatrice/deck_list/deck_list.h libcockatrice/deck_list/deck_list_history_manager.h + libcockatrice/deck_list/deck_list_node_tree.h libcockatrice/deck_list/deck_list_memento.h + libcockatrice/deck_list/sideboard_plan.h ) if(Qt6_FOUND) @@ -28,7 +30,7 @@ add_library( libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/deck_list_history_manager.cpp libcockatrice/deck_list/deck_list_node_tree.cpp - libcockatrice/deck_list/deck_list_node_tree.h + libcockatrice/deck_list/sideboard_plan.cpp ) add_dependencies(libcockatrice_deck_list libcockatrice_protocol) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index b453a57ef..a294601fb 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -21,61 +21,7 @@ uint qHash(const QRegularExpression &key, uint seed) noexcept } #endif -SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) - : name(_name), moveList(_moveList) -{ -} - -void SideboardPlan::setMoveList(const QList &_moveList) -{ - moveList = _moveList; -} - -bool SideboardPlan::readElement(QXmlStreamReader *xml) -{ - while (!xml->atEnd()) { - xml->readNext(); - const QString childName = xml->name().toString(); - if (xml->isStartElement()) { - if (childName == "name") - name = xml->readElementText(); - else if (childName == "move_card_to_zone") { - MoveCard_ToZone m; - while (!xml->atEnd()) { - xml->readNext(); - const QString childName2 = xml->name().toString(); - if (xml->isStartElement()) { - if (childName2 == "card_name") - m.set_card_name(xml->readElementText().toStdString()); - else if (childName2 == "start_zone") - m.set_start_zone(xml->readElementText().toStdString()); - else if (childName2 == "target_zone") - m.set_target_zone(xml->readElementText().toStdString()); - } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { - moveList.append(m); - break; - } - } - } - } else if (xml->isEndElement() && (childName == "sideboard_plan")) - return true; - } - return false; -} - -void SideboardPlan::write(QXmlStreamWriter *xml) -{ - xml->writeStartElement("sideboard_plan"); - xml->writeTextElement("name", name); - for (auto &i : moveList) { - xml->writeStartElement("move_card_to_zone"); - xml->writeTextElement("card_name", QString::fromStdString(i.card_name())); - xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone())); - xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone())); - xml->writeEndElement(); - } - xml->writeEndElement(); -} +static const QString CURRENT_SIDEBOARD_PLAN_KEY = ""; bool DeckList::Metadata::isEmpty() const { @@ -93,36 +39,23 @@ DeckList::DeckList(const QString &nativeString) DeckList::DeckList(const Metadata &metadata, const DecklistNodeTree &tree, - const QMap &sideboardPlans) + const QMap &sideboardPlans) : metadata(metadata), sideboardPlans(sideboardPlans), tree(tree) { } -DeckList::~DeckList() +QList DeckList::getCurrentSideboardPlan() const { - QMapIterator i(sideboardPlans); - while (i.hasNext()) - delete i.next().value(); -} + if (!sideboardPlans.contains(CURRENT_SIDEBOARD_PLAN_KEY)) { + return {}; + } -QList DeckList::getCurrentSideboardPlan() -{ - SideboardPlan *current = sideboardPlans.value(QString(), 0); - if (!current) - return QList(); - else - return current->getMoveList(); + return sideboardPlans.value(CURRENT_SIDEBOARD_PLAN_KEY).getMoveList(); } void DeckList::setCurrentSideboardPlan(const QList &plan) { - SideboardPlan *current = sideboardPlans.value(QString(), 0); - if (!current) { - current = new SideboardPlan; - sideboardPlans.insert(QString(), current); - } - - current->setMoveList(plan); + sideboardPlans[CURRENT_SIDEBOARD_PLAN_KEY].setMoveList(plan); } bool DeckList::readElement(QXmlStreamReader *xml) @@ -151,11 +84,9 @@ bool DeckList::readElement(QXmlStreamReader *xml) } else if (childName == "zone") { tree.readZoneElement(xml); } else if (childName == "sideboard_plan") { - SideboardPlan *newSideboardPlan = new SideboardPlan; - if (newSideboardPlan->readElement(xml)) { - sideboardPlans.insert(newSideboardPlan->getName(), newSideboardPlan); - } else { - delete newSideboardPlan; + SideboardPlan newSideboardPlan; + if (newSideboardPlan.readElement(xml)) { + sideboardPlans.insert(newSideboardPlan.getName(), newSideboardPlan); } } } else if (xml->isEndElement() && (childName == "cockatrice_deck")) { @@ -194,9 +125,8 @@ void DeckList::write(QXmlStreamWriter *xml) const tree.write(xml); // Write sideboard plans - QMapIterator i(sideboardPlans); - while (i.hasNext()) { - i.next().value()->write(xml); + for (auto &sideboardPlan : sideboardPlans.values()) { + sideboardPlan.write(xml); } xml->writeEndElement(); // Close "cockatrice_deck" diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index e8c57be67..d0ca55342 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -1,6 +1,6 @@ /** * @file deck_list.h - * @brief Defines the DeckList class and supporting types for managing a full + * @brief Defines the DeckList class, which manages a full * deck structure including cards, zones, sideboard plans, and * serialization to/from multiple formats. This is a logic class which * does not care about Qt or user facing views. @@ -12,12 +12,12 @@ #include "deck_list_memento.h" #include "deck_list_node_tree.h" +#include "sideboard_plan.h" #include "tree/inner_deck_list_node.h" #include #include #include -#include #include class AbstractDecklistNode; @@ -27,67 +27,6 @@ class QIODevice; class QTextStream; class InnerDecklistNode; -/** - * @class SideboardPlan - * @ingroup Decks - * @brief Represents a predefined sideboarding strategy for a deck. - * - * Sideboard plans store a named list of card movements that should be applied - * between the mainboard and sideboard for a specific matchup. Each movement - * is expressed using a `MoveCard_ToZone` protobuf message. - * - * ### Responsibilities: - * - Store the plan name and list of moves. - * - Support XML serialization/deserialization. - * - * ### Typical usage: - * A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"), - * each describing how to transform the main deck into its intended configuration. - */ -class SideboardPlan -{ -private: - QString name; ///< Human-readable name of this plan. - QList moveList; ///< List of move instructions for this plan. - -public: - /** - * @brief Construct a new SideboardPlan. - * @param _name The plan name. - * @param _moveList Initial list of card move instructions. - */ - explicit SideboardPlan(const QString &_name = QString(), - const QList &_moveList = QList()); - - /** - * @brief Read a SideboardPlan from an XML stream. - * @param xml XML reader positioned at the plan element. - * @return true if parsing succeeded. - */ - bool readElement(QXmlStreamReader *xml); - - /** - * @brief Write this SideboardPlan to XML. - * @param xml Stream to append the serialized element to. - */ - void write(QXmlStreamWriter *xml); - - /// @return The plan name. - [[nodiscard]] QString getName() const - { - return name; - } - - /// @return Const reference to the move list. - [[nodiscard]] const QList &getMoveList() const - { - return moveList; - } - - /// @brief Replace the move list with a new one. - void setMoveList(const QList &_moveList); -}; - /** * @class DeckList * @ingroup Decks @@ -139,9 +78,9 @@ public: }; private: - Metadata metadata; ///< Deck metadata that is stored in the deck file - QMap sideboardPlans; ///< Named sideboard plans. - DecklistNodeTree tree; ///< The deck tree (zones + cards). + Metadata metadata; ///< Deck metadata that is stored in the deck file + QMap sideboardPlans; ///< Named sideboard plans. + DecklistNodeTree tree; ///< The deck tree (zones + cards). /** * @brief Cached deck hash, recalculated lazily. @@ -193,8 +132,7 @@ public: /// @brief Construct from components DeckList(const Metadata &metadata, const DecklistNodeTree &tree, - const QMap &sideboardPlans = {}); - virtual ~DeckList(); + const QMap &sideboardPlans = {}); /** * @brief Gets a pointer to the underlying node tree. @@ -247,9 +185,9 @@ public: /// @name Sideboard plans ///@{ - QList getCurrentSideboardPlan(); + QList getCurrentSideboardPlan() const; void setCurrentSideboardPlan(const QList &plan); - const QMap &getSideboardPlans() const + const QMap &getSideboardPlans() const { return sideboardPlans; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp new file mode 100644 index 000000000..d991ec98e --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp @@ -0,0 +1,59 @@ +#include "sideboard_plan.h" + +#include + +SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) + : name(_name), moveList(_moveList) +{ +} + +void SideboardPlan::setMoveList(const QList &_moveList) +{ + moveList = _moveList; +} + +bool SideboardPlan::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "name") + name = xml->readElementText(); + else if (childName == "move_card_to_zone") { + MoveCard_ToZone m; + while (!xml->atEnd()) { + xml->readNext(); + const QString childName2 = xml->name().toString(); + if (xml->isStartElement()) { + if (childName2 == "card_name") + m.set_card_name(xml->readElementText().toStdString()); + else if (childName2 == "start_zone") + m.set_start_zone(xml->readElementText().toStdString()); + else if (childName2 == "target_zone") + m.set_target_zone(xml->readElementText().toStdString()); + } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { + moveList.append(m); + break; + } + } + } + } else if (xml->isEndElement() && (childName == "sideboard_plan")) + return true; + } + return false; +} + +void SideboardPlan::write(QXmlStreamWriter *xml) const +{ + xml->writeStartElement("sideboard_plan"); + xml->writeTextElement("name", name); + for (auto &i : moveList) { + xml->writeStartElement("move_card_to_zone"); + xml->writeTextElement("card_name", QString::fromStdString(i.card_name())); + xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone())); + xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone())); + xml->writeEndElement(); + } + xml->writeEndElement(); +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h new file mode 100644 index 000000000..524217c2d --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h @@ -0,0 +1,70 @@ +#ifndef COCKATRICE_SIDEBOARD_PLAN_H +#define COCKATRICE_SIDEBOARD_PLAN_H + +#include +#include + +class QXmlStreamWriter; +class QXmlStreamReader; + +/** + * @class SideboardPlan + * @ingroup Decks + * @brief Represents a predefined sideboarding strategy for a deck. + * + * Sideboard plans store a named list of card movements that should be applied + * between the mainboard and sideboard for a specific matchup. Each movement + * is expressed using a `MoveCard_ToZone` protobuf message. + * + * ### Responsibilities: + * - Store the plan name and list of moves. + * - Support XML serialization/deserialization. + * + * ### Typical usage: + * A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"), + * each describing how to transform the main deck into its intended configuration. + */ +class SideboardPlan +{ +private: + QString name; ///< Human-readable name of this plan. + QList moveList; ///< List of move instructions for this plan. + +public: + /** + * @brief Construct a new SideboardPlan. + * @param _name The plan name. + * @param _moveList Initial list of card move instructions. + */ + explicit SideboardPlan(const QString &_name = "", const QList &_moveList = {}); + + /** + * @brief Read a SideboardPlan from an XML stream. + * @param xml XML reader positioned at the plan element. + * @return true if parsing succeeded. + */ + bool readElement(QXmlStreamReader *xml); + + /** + * @brief Write this SideboardPlan to XML. + * @param xml Stream to append the serialized element to. + */ + void write(QXmlStreamWriter *xml) const; + + /// @return The plan name. + [[nodiscard]] QString getName() const + { + return name; + } + + /// @return Const reference to the move list. + [[nodiscard]] const QList &getMoveList() const + { + return moveList; + } + + /// @brief Replace the move list with a new one. + void setMoveList(const QList &_moveList); +}; + +#endif // COCKATRICE_SIDEBOARD_PLAN_H diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index ece3bc2f8..ce1c79263 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -599,6 +599,7 @@ void DeckListModel::setDeckList(const QSharedPointer &_deck) deckList = _deck; } rebuildTree(); + refreshCardFormatLegalities(); emit deckReplaced(); } diff --git a/libcockatrice_utility/libcockatrice/utility/color.h b/libcockatrice_utility/libcockatrice/utility/color.h index 164c6ffd7..bf4759565 100644 --- a/libcockatrice_utility/libcockatrice/utility/color.h +++ b/libcockatrice_utility/libcockatrice/utility/color.h @@ -21,6 +21,43 @@ inline color convertQColorToColor(const QColor &c) result.set_b(c.blue()); return result; } + +namespace GameSpecificColors +{ +namespace MTG +{ +inline QColor colorHelper(const QString &name) +{ + static const QMap colorMap = { + {"W", QColor(245, 245, 220)}, + {"U", QColor(80, 140, 255)}, + {"B", QColor(60, 60, 60)}, + {"R", QColor(220, 60, 50)}, + {"G", QColor(70, 160, 70)}, + {"Creature", QColor(70, 130, 180)}, + {"Instant", QColor(138, 43, 226)}, + {"Sorcery", QColor(199, 21, 133)}, + {"Enchantment", QColor(218, 165, 32)}, + {"Artifact", QColor(169, 169, 169)}, + {"Planeswalker", QColor(210, 105, 30)}, + {"Land", QColor(110, 80, 50)}, + }; + + if (colorMap.contains(name)) + return colorMap[name]; + + if (name.length() == 1 && colorMap.contains(name.toUpper())) + return colorMap[name.toUpper()]; + + uint h = qHash(name); + int r = 100 + (h % 120); + int g = 100 + ((h >> 8) % 120); + int b = 100 + ((h >> 16) % 120); + + return QColor(r, g, b); +} +} // namespace MTG +} // namespace GameSpecificColors #endif inline color makeColor(int r, int g, int b) diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h index 855cc8b18..606947143 100644 --- a/libcockatrice_utility/libcockatrice/utility/qt_utils.h +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -15,6 +15,20 @@ template T *findParentOfType(const QObject *obj) } return nullptr; } + +static inline void clearLayoutRec(QLayout *l) +{ + if (!l) + return; + QLayoutItem *it; + while ((it = l->takeAt(0)) != nullptr) { + if (QWidget *w = it->widget()) + w->deleteLater(); + if (QLayout *sub = it->layout()) + clearLayoutRec(sub); + delete it; + } +} } // namespace QtUtils #endif // COCKATRICE_QT_UTILS_H