diff --git a/README.md b/README.md index 3d01e9f3d..c08aee0e7 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ Latest beta version: # Related Repositories -- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice -- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to use in Cockatrice +- [Magic-Token](https://github.com/Cockatrice/Magic-Token): File with MtG token data for use in Cockatrice +- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Code to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) for use in Cockatrice - [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage -- [Cockatrice @Flathub](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration for our Linux `flatpak` package +- [io.github.Cockatrice.cockatrice](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration of our Linux `flatpak` package hosted at [Flathub](https://flathub.org/en/apps/io.github.Cockatrice.cockatrice) # Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA) @@ -55,7 +55,6 @@ Latest beta version: Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out. - [Official Website](https://cockatrice.github.io) - [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) -- [Official Code Documentation](https://cockatrice.github.io/docs) - [Official Discord](https://discord.gg/3Z9yzmA) - [reddit r/Cockatrice](https://reddit.com/r/cockatrice) @@ -64,6 +63,23 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p # Contribute +

+ Code | + Documentation | + Translation +

+ +#### Repository Activity +![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats") + +
+Kudos to all our amazing contributors ❤️ +
+ + +
+ Made with contrib.rocks +
### Code @@ -79,21 +95,17 @@ We'll happily advice on how best to implement a feature, or we can show you wher You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code). +### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush) + +There are various places where useful information for different needs are maintained: +- [Official Code Documentation](https://cockatrice.github.io/docs/) +- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) `Community supported` +- [Official Webpage](https://cockatrice.github.io/) +- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file` + Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide. -#### Repository Activity -![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats") - -
-Kudos to all our amazing contributors ❤️ -
- - -
- Made with contrib.rocks -
- -### Translations [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/) +### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/) Cockatrice uses Transifex to manage translations. You can help us bring Cockatrice, Oracle and Webatrice to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://explore.transifex.com/cockatrice/cockatrice/).
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index b2d387391..8ebd177db 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -144,11 +144,31 @@ set(cockatrice_SOURCES src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/deck_card_zone_display_widget.cpp src/interface/widgets/cards/deck_preview_card_picture_widget.cpp + src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp + src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp src/interface/widgets/deck_analytics/deck_analytics_widget.cpp src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp - src/interface/widgets/deck_analytics/mana_base_widget.cpp - src/interface/widgets/deck_analytics/mana_curve_widget.cpp - src/interface/widgets/deck_analytics/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/resizable_panel.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -156,16 +176,21 @@ set(cockatrice_SOURCES src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp src/interface/widgets/deck_editor/deck_list_style_proxy.cpp + src/interface/widgets/deck_editor/deck_state_manager.cpp src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/banner_widget.cpp - src/interface/widgets/general/display/bar_widget.cpp - src/interface/widgets/general/display/color_bar.cpp src/interface/widgets/general/display/dynamic_font_size_label.cpp src/interface/widgets/general/display/dynamic_font_size_push_button.cpp src/interface/widgets/general/display/labeled_input.cpp - src/interface/widgets/general/display/percent_bar_widget.cpp src/interface/widgets/general/display/shadow_background_label.cpp + src/interface/widgets/general/display/charts/bars/bar_widget.cpp + src/interface/widgets/general/display/charts/bars/color_bar.cpp + src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp + src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp + src/interface/widgets/general/display/charts/pies/color_pie.cpp src/interface/widgets/general/home_styled_button.cpp src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp diff --git a/cockatrice/resources/help/deck_search.md b/cockatrice/resources/help/deck_search.md index c42e41c42..4e1c5c557 100644 --- a/cockatrice/resources/help/deck_search.md +++ b/cockatrice/resources/help/deck_search.md @@ -1,11 +1,13 @@ @page deck_search_syntax_help Deck Search Syntax Help ## Deck Search Syntax Help ------ + The search bar recognizes a set of special commands.
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive. +
+
Display Name (The deck name, or the filename if the deck name isn't set):
[red deck wins](#red deck wins) (Any deck with a display name containing the words red, deck, and wins)
["red deck wins"](#%22red deck wins%22) (Any deck with a display name containing the exact phrase "red deck wins")
diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md index 3b3f0cc93..6f25e4013 100644 --- a/cockatrice/resources/help/search.md +++ b/cockatrice/resources/help/search.md @@ -1,10 +1,12 @@ @page search_syntax_help Search Syntax Help ## Search Syntax Help ------ + The search bar recognizes a set of special commands similar to some other card databases.
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive. +
+
Name:
[birds of paradise](#birds of paradise) (Any card name containing the words birds, of, and paradise)
["birds of paradise"](#%22birds of paradise%22) (Any card name containing the exact phrase "birds of paradise")
diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 7a518df67..2ad1f1ece 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -2,6 +2,7 @@ #include "../network/update/client/release_channel.h" #include "card_counter_settings.h" +#include "version_string.h" #include #include @@ -198,7 +199,13 @@ SettingsCache::SettingsCache() mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).toBool(); - checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool(); + if (settings->contains("personal/startupUpdateCheck")) { + checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool(); + } else if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) { + checkUpdatesOnStartup = false; // do not run auto updater on custom version + } else { + checkUpdatesOnStartup = true; // default to run auto updater + } startupCardUpdateCheckPromptForUpdate = settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool(); startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool(); @@ -206,7 +213,15 @@ SettingsCache::SettingsCache() lastCardUpdateCheck = settings->value("personal/lastCardUpdateCheck", QDateTime::currentDateTime().date()).toDate(); notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool(); notifyAboutNewVersion = settings->value("personal/newversionnotification", true).toBool(); - updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt(); + + if (settings->contains("personal/updatereleasechannel")) { + updateReleaseChannel = settings->value("personal/updatereleasechannel").toInt(); + } else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) { + // default to beta if this is a beta release + updateReleaseChannel = 1; + } else { + updateReleaseChannel = 0; // stable + } lang = settings->value("personal/lang").toString(); keepalive = settings->value("personal/keepalive", 3).toInt(); @@ -273,6 +288,7 @@ SettingsCache::SettingsCache() focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool(); + showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); displayCardNames = settings->value("cards/displaycardnames", true).toBool(); roundCardCorners = settings->value("cards/roundcardcorners", true).toBool(); overrideAllCardArtWithPersonalPreference = @@ -700,6 +716,13 @@ void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts) settings->setValue("menu/showshortcuts", showShortcuts); } +void SettingsCache::setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar) +{ + showGameSelectorFilterToolbar = static_cast(_showGameSelectorFilterToolbar); + settings->setValue("menu/showgameselectorfiltertoolbar", showGameSelectorFilterToolbar); + emit showGameSelectorFilterToolbarChanged(showGameSelectorFilterToolbar); +} + void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames) { displayCardNames = static_cast(_displayCardNames); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index df68ce0c4..ca9a2a39b 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -145,6 +145,7 @@ signals: void homeTabBackgroundShuffleFrequencyChanged(); void picDownloadChanged(); void showStatusBarChanged(bool state); + void showGameSelectorFilterToolbarChanged(bool state); void displayCardNamesChanged(); void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference); void bumpSetsWithCardsInDeckToTopChanged(); @@ -236,6 +237,7 @@ private: bool annotateTokens; QByteArray tabGameSplitterSizes; bool showShortcuts; + bool showGameSelectorFilterToolbar; bool displayCardNames; bool overrideAllCardArtWithPersonalPreference; bool bumpSetsWithCardsInDeckToTop; @@ -553,6 +555,10 @@ public: { return showShortcuts; } + [[nodiscard]] bool getShowGameSelectorFilterToolbar() const + { + return showGameSelectorFilterToolbar; + } [[nodiscard]] bool getDisplayCardNames() const { return displayCardNames; @@ -1017,6 +1023,7 @@ public slots: void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens); void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes); void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts); + void setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar); void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames); void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt); void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop); diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 736f0c896..156ee82ea 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -660,6 +660,12 @@ private: {"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"), parseSequenceString("Ctrl+M"), ShortcutGroup::Drawing)}, + {"Player/aMulliganSame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Same hand size)"), + parseSequenceString("Ctrl+Shift+M"), + ShortcutGroup::Drawing)}, + {"Player/aMulliganMinusOne", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Hand size - 1)"), + parseSequenceString("Ctrl+Shift+Alt+M"), + ShortcutGroup::Drawing)}, {"Player/aDrawCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw a Card"), parseSequenceString("Ctrl+D"), ShortcutGroup::Drawing)}, diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game/player/menu/hand_menu.cpp index e193d2e4a..019ab925c 100644 --- a/cockatrice/src/game/player/menu/hand_menu.cpp +++ b/cockatrice/src/game/player/menu/hand_menu.cpp @@ -60,6 +60,16 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan); addAction(aMulligan); + // Mulligan same size + aMulliganSame = new QAction(this); + connect(aMulliganSame, &QAction::triggered, actions, &PlayerActions::actMulliganSameSize); + addAction(aMulliganSame); + + // Mulligan -1 + aMulliganMinusOne = new QAction(this); + connect(aMulliganMinusOne, &QAction::triggered, actions, &PlayerActions::actMulliganMinusOne); + addAction(aMulliganMinusOne); + addSeparator(); mMoveHandMenu = addTearOffMenu(QString()); @@ -104,7 +114,9 @@ void HandMenu::retranslateUi() aSortHandByType->setText(tr("Type")); aSortHandByManaValue->setText(tr("Mana Value")); - aMulligan->setText(tr("Take &mulligan")); + aMulligan->setText(tr("Take &mulligan (Choose hand size)")); + aMulliganSame->setText(tr("Take mulligan (Same hand size)")); + aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)")); mMoveHandMenu->setTitle(tr("&Move hand to...")); aMoveHandToTopLibrary->setText(tr("&Top of library")); @@ -128,6 +140,8 @@ void HandMenu::setShortcutsActive() aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType")); aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue")); aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan")); + aMulliganSame->setShortcuts(shortcuts.getShortcut("Player/aMulliganSame")); + aMulliganMinusOne->setShortcuts(shortcuts.getShortcut("Player/aMulliganMinusOne")); aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll")); aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll")); } diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game/player/menu/hand_menu.h index 2c776b857..51e071a62 100644 --- a/cockatrice/src/game/player/menu/hand_menu.h +++ b/cockatrice/src/game/player/menu/hand_menu.h @@ -46,6 +46,8 @@ private: QAction *aViewHand = nullptr; QAction *aMulligan = nullptr; + QAction *aMulliganSame = nullptr; + QAction *aMulliganMinusOne = nullptr; QMenu *mSortHand = nullptr; QAction *aSortHandByName = nullptr; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 444eecb0d..ed77808b0 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -310,28 +310,48 @@ void PlayerActions::actMulligan() { int startSize = SettingsCache::instance().getStartingHandSize(); int handSize = player->getHandZone()->getCards().size(); - int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck + int deckSize = player->getDeckZone()->getCards().size() + handSize; + bool ok; int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"), tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + tr("0 and lower are in comparison to current hand size"), startSize, -handSize, deckSize, 1, &ok); + if (!ok) { return; } - Command_Mulligan cmd; + if (number < 1) { - if (handSize == 0) { - return; - } - cmd.set_number(handSize + number); - } else { - cmd.set_number(number); + number = handSize + number; } + + doMulligan(number); + SettingsCache::instance().setStartingHandSize(number); +} + +void PlayerActions::actMulliganSameSize() +{ + int handSize = player->getHandZone()->getCards().size(); + doMulligan(handSize); +} + +void PlayerActions::actMulliganMinusOne() +{ + int handSize = player->getHandZone()->getCards().size(); + int targetSize = qMax(1, handSize - 1); + doMulligan(targetSize); +} + +void PlayerActions::doMulligan(int number) +{ + if (number < 1) { + return; + } + + Command_Mulligan cmd; + cmd.set_number(number); sendGameCommand(cmd); - if (startSize != number) { - SettingsCache::instance().setStartingHandSize(number); - } } void PlayerActions::actDrawCards() diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index e0f86e4fc..b294b5946 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -85,6 +85,9 @@ public slots: void actDrawCards(); void actUndoDraw(); void actMulligan(); + void actMulliganSameSize(); + void actMulliganMinusOne(); + void doMulligan(int number); void actPlay(); void actPlayFacedown(); diff --git a/cockatrice/src/game/zones/view_zone_widget.cpp b/cockatrice/src/game/zones/view_zone_widget.cpp index 91abb4663..3ea4eb119 100644 --- a/cockatrice/src/game/zones/view_zone_widget.cpp +++ b/cockatrice/src/game/zones/view_zone_widget.cpp @@ -13,12 +13,20 @@ #include #include #include +#include #include #include #include +#include #include #include +namespace +{ +constexpr qreal kTitleBarHeight = 24.0; +constexpr qreal kMinVisibleWidth = 100.0; +} // namespace + /** * @param _player the player the cards were revealed to. * @param _origZone the zone the cards were revealed from. @@ -241,33 +249,191 @@ void ZoneViewWidget::retranslateUi() pileViewCheckBox.setText(tr("pile view")); } -void ZoneViewWidget::moveEvent(QGraphicsSceneMoveEvent * /* event */) +void ZoneViewWidget::stopWindowDrag() { - if (!scene()) + if (!draggingWindow) return; - int titleBarHeight = 24; + draggingWindow = false; + ungrabMouse(); +} - QPointF scenePos = pos(); +void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event) +{ + draggingWindow = true; + dragStartItemPos = pos(); + dragStartScreenPos = event->screenPos(); + dragView = findDragView(event->widget()); - if (scenePos.x() < 0) { - scenePos.setX(0); - } else { - qreal maxw = scene()->sceneRect().width() - 100; - if (scenePos.x() > maxw) - scenePos.setX(maxw); + // need to grab mouse to receive events and not miss initial movement + grabMouse(); +} + +QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const +{ + const QRectF frameRectF = windowFrameRect(); + + // query the style for the close button position (handles macOS top-left placement) + // Title bar rect MUST be local (0,0-based) for QStyle + const QRect titleBarRect(0, 0, static_cast(frameRectF.width()), static_cast(kTitleBarHeight)); + + if (styleWidget) { + QStyleOptionTitleBar opt; + opt.initFrom(styleWidget); + opt.rect = titleBarRect; + opt.text = windowTitle(); + opt.icon = styleWidget->windowIcon(); + opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint; + + opt.subControls = QStyle::SC_TitleBarCloseButton; + opt.activeSubControls = QStyle::SC_TitleBarCloseButton; + opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState; + + if (styleWidget->isActiveWindow()) { + opt.state |= QStyle::State_Active; + } + + QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton, + styleWidget); + + if (r.isValid() && !r.isEmpty()) { + // Translate from local-titlebar coords → frame coords + r.translate(frameRectF.topLeft().toPoint()); + return QRectF(r); + } } - if (scenePos.y() < titleBarHeight) { - scenePos.setY(titleBarHeight); - } else { - qreal maxh = scene()->sceneRect().height() - titleBarHeight; - if (scenePos.y() > maxh) - scenePos.setY(maxh); + // Fallback: frame-relative top-right + return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight); +} + +QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const +{ + QWidget *current = eventWidget; + while (current) { + if (auto *view = qobject_cast(current)) + return view; + current = current->parentWidget(); } - if (scenePos != pos()) - setPos(scenePos); + if (scene() && !scene()->views().isEmpty()) + return scene()->views().constFirst(); + + return nullptr; +} + +QPointF ZoneViewWidget::calcDraggedWindowPos(const QPoint &screenPos, + const QPointF &scenePos, + const QPointF &buttonDownScenePos) const +{ + if (dragView && dragView->viewport()) { + const QPoint vpStart = dragView->viewport()->mapFromGlobal(dragStartScreenPos); + const QPoint vpNow = dragView->viewport()->mapFromGlobal(screenPos); + const QPointF sceneStart = dragView->mapToScene(vpStart); + const QPointF sceneNow = dragView->mapToScene(vpNow); + return dragStartItemPos + (sceneNow - sceneStart); + } + + return dragStartItemPos + (scenePos - buttonDownScenePos); +} + +bool ZoneViewWidget::windowFrameEvent(QEvent *event) +{ + if (event->type() == QEvent::UngrabMouse) { + stopWindowDrag(); + return QGraphicsWidget::windowFrameEvent(event); + } + + auto *me = dynamic_cast(event); + if (!me) + return QGraphicsWidget::windowFrameEvent(event); + + switch (event->type()) { + case QEvent::GraphicsSceneMousePress: + if (me->button() == Qt::LeftButton && windowFrameSectionAt(me->pos()) == Qt::TitleBarArea) { + // avoid drag on close button + if (closeButtonRect(me->widget()).contains(me->pos())) { + me->accept(); + close(); + return true; + } + + startWindowDrag(me); + me->accept(); + return true; + } + break; + + case QEvent::GraphicsSceneMouseMove: + if (draggingWindow) { + if (!(me->buttons() & Qt::LeftButton)) { + stopWindowDrag(); + } else { + setPos( + calcDraggedWindowPos(me->screenPos(), me->scenePos(), me->buttonDownScenePos(Qt::LeftButton))); + } + me->accept(); + return true; + } + break; + + case QEvent::GraphicsSceneMouseRelease: + if (draggingWindow && me->button() == Qt::LeftButton) { + stopWindowDrag(); + me->accept(); + return true; + } + break; + + default: + break; + } + + return QGraphicsWidget::windowFrameEvent(event); +} + +void ZoneViewWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + // move if the scene routes moves while dragging + if (draggingWindow && (event->buttons() & Qt::LeftButton)) { + setPos(calcDraggedWindowPos(event->screenPos(), event->scenePos(), event->buttonDownScenePos(Qt::LeftButton))); + event->accept(); + return; + } + + QGraphicsWidget::mouseMoveEvent(event); +} + +void ZoneViewWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (draggingWindow && event->button() == Qt::LeftButton) { + stopWindowDrag(); + event->accept(); + return; + } + + QGraphicsWidget::mouseReleaseEvent(event); +} + +QVariant ZoneViewWidget::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == QGraphicsItem::ItemPositionChange && scene()) { + // Keep grab area in main view + const QRectF sceneRect = scene()->sceneRect(); + const QPointF requestedPos = value.toPointF(); + QPointF desiredPos = requestedPos; + + const qreal minX = sceneRect.left(); + const qreal maxX = qMax(minX, sceneRect.right() - kMinVisibleWidth); + const qreal minY = sceneRect.top() + kTitleBarHeight; + const qreal maxY = qMax(minY, sceneRect.bottom() - kTitleBarHeight); + + desiredPos.setX(qBound(minX, desiredPos.x(), maxX)); + desiredPos.setY(qBound(minY, desiredPos.y(), maxY)); + return desiredPos; + } + + return QGraphicsWidget::itemChange(change, value); } void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event) @@ -350,6 +516,7 @@ void ZoneViewWidget::handleScrollBarChange(int value) void ZoneViewWidget::closeEvent(QCloseEvent *event) { + stopWindowDrag(); disconnect(zone, &ZoneViewZone::closed, this, 0); // manually call zone->close in order to remove it from the origZones views zone->close(); diff --git a/cockatrice/src/game/zones/view_zone_widget.h b/cockatrice/src/game/zones/view_zone_widget.h index 272ff5560..1246192b8 100644 --- a/cockatrice/src/game/zones/view_zone_widget.h +++ b/cockatrice/src/game/zones/view_zone_widget.h @@ -3,7 +3,6 @@ * @ingroup GameGraphicsZones * @brief TODO: Document this. */ - #ifndef ZONEVIEWWIDGET_H #define ZONEVIEWWIDGET_H @@ -14,6 +13,7 @@ #include #include #include +#include #include class QLabel; @@ -28,6 +28,8 @@ class ServerInfo_Card; class QGraphicsSceneMouseEvent; class QGraphicsSceneWheelEvent; class QStyleOption; +class QGraphicsView; +class QWidget; class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget { @@ -52,7 +54,6 @@ private: ZoneViewZone *zone; QGraphicsWidget *zoneContainer; - QPushButton *closeButton; QScrollBar *scrollBar; ScrollableGraphicsProxyWidget *scrollBarProxy; @@ -66,6 +67,33 @@ private: int extraHeight; Player *player; + bool draggingWindow = false; + QPoint dragStartScreenPos; + QPointF dragStartItemPos; + QPointer dragView; + + void stopWindowDrag(); + void startWindowDrag(QGraphicsSceneMouseEvent *event); + QRectF closeButtonRect(QWidget *styleWidget) const; + /** + * @brief Resolves the QGraphicsView to use for drag coordinate mapping + * + * @param eventWidget QWidget that originated the mouse event + * @return The resolved QGraphicsView + */ + QGraphicsView *findDragView(QWidget *eventWidget) const; + /** + * @brief Calculates the desired widget position while dragging + * + * @param screenPos Global screen coordinates of the current mouse position + * @param scenePos Scene coordinates of the current mouse position + * @param buttonDownScenePos Scene coordinates of the initial mouse press position + * + * @return The new widget position in scene coordinates + */ + QPointF + calcDraggedWindowPos(const QPoint &screenPos, const QPointF &scenePos, const QPointF &buttonDownScenePos) const; + void resizeScrollbar(qreal newZoneHeight); signals: void closePressed(ZoneViewWidget *zv); @@ -76,7 +104,6 @@ private slots: void resizeToZoneContents(bool forceInitialHeight = false); void handleScrollBarChange(int value); void zoneDeleted(); - void moveEvent(QGraphicsSceneMoveEvent * /* event */) override; void resizeEvent(QGraphicsSceneResizeEvent * /* event */) override; void expandWindow(); @@ -101,6 +128,10 @@ public: protected: void closeEvent(QCloseEvent *event) override; void initStyleOption(QStyleOption *option) const override; + bool windowFrameEvent(QEvent *event) override; + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; }; diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp new file mode 100644 index 000000000..bad883d27 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp @@ -0,0 +1,48 @@ +#include "abstract_analytics_panel_widget.h" + +#include "deck_list_statistics_analyzer.h" + +#include + +AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : QWidget(parent), analyzer(analyzer) +{ + layout = new QVBoxLayout(this); + + bannerAndSettingsContainer = new QWidget(this); + + bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer); + bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout); + bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100); + bannerWidget->setMaximumHeight(100); + + bannerAndSettingsLayout->addWidget(bannerWidget, 1); + + // config button + configureButton = new QPushButton(tr("Configure"), this); + configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); + bannerAndSettingsLayout->addWidget(configureButton, 0); + + layout->addWidget(bannerAndSettingsContainer); + + connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay); +} + +bool AbstractAnalyticsPanelWidget::applyConfigFromDialog() +{ + QDialog *dlg = createConfigDialog(this); + if (!dlg) { + return false; + } + + bool ok = dlg->exec() == QDialog::Accepted; + if (ok) { + // dialog must expose its final config as JSON + auto newCfg = extractConfigFromDialog(dlg); + loadConfig(newCfg); + updateDisplay(); + } + dlg->deleteLater(); + return ok; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h new file mode 100644 index 000000000..23374a9e1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h @@ -0,0 +1,61 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H + +#include "../general/display/banner_widget.h" + +#include +#include +#include +#include + +class DeckListStatisticsAnalyzer; + +class AbstractAnalyticsPanelWidget : public QWidget +{ + Q_OBJECT +public slots: + virtual void updateDisplay() = 0; + // Widgets must return a config dialog + virtual QDialog *createConfigDialog(QWidget *parent) = 0; + +public: + explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + void setDisplayTitle(const QString &title) + { + displayTitle = title; + if (bannerWidget) { + bannerWidget->setText(displayTitle); + } + } + + QString displayTitleText() const + { + return displayTitle; + } + + virtual QJsonObject saveConfig() const + { + return {}; + } + virtual void loadConfig(const QJsonObject &) + { + } + + // Unified helper to run config dialog and update widget + bool applyConfigFromDialog(); + + // Dialog → JSON must be supplied by each subclass + virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0; + +protected: + DeckListStatisticsAnalyzer *analyzer; + QVBoxLayout *layout; + QWidget *bannerAndSettingsContainer; + QHBoxLayout *bannerAndSettingsLayout; + QString displayTitle; + BannerWidget *bannerWidget; + QPushButton *configureButton; +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp new file mode 100644 index 000000000..c83e6d982 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp @@ -0,0 +1,32 @@ +#include "add_analytics_panel_dialog.h" + +#include "analytics_panel_widget_factory.h" + +#include +#include + +AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent) +{ + setWindowTitle(tr("Add Analytics Panel")); + + layout = new QVBoxLayout(this); + + typeCombo = new QComboBox(this); + + // Populate using descriptors + const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets(); + + for (const auto &desc : widgets) { + // Show translated title to user + typeCombo->addItem(desc.title, desc.type); + } + + layout->addWidget(typeCombo); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h new file mode 100644 index 000000000..aa44734c2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h @@ -0,0 +1,29 @@ + +#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H +#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H + +#include "analytics_panel_widget_factory.h" + +#include +#include +#include +#include + +class AddAnalyticsPanelDialog : public QDialog +{ + Q_OBJECT +public: + explicit AddAnalyticsPanelDialog(QWidget *parent); + + QString selectedType() const + { + return typeCombo->currentData().toString(); + } + +private: + QVBoxLayout *layout; + QComboBox *typeCombo; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp new file mode 100644 index 000000000..7af641689 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp @@ -0,0 +1,33 @@ +#include "analytics_panel_widget_factory.h" + +#include "abstract_analytics_panel_widget.h" + +AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance() +{ + static AnalyticsPanelWidgetFactory f; + return f; +} + +void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc) +{ + widgets.insert(desc.type, desc); +} + +AbstractAnalyticsPanelWidget * +AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const +{ + auto it = widgets.find(type); + if (it == widgets.end()) + return nullptr; + + auto w = it->creator(parent, analyzer); + + w->setDisplayTitle(it->title); + + return w; +} + +QList AnalyticsPanelWidgetFactory::availableWidgets() const +{ + return widgets.values(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h new file mode 100644 index 000000000..6c5856d70 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H + +#include +#include +#include +#include +#include + +class AbstractAnalyticsPanelWidget; +class DeckListStatisticsAnalyzer; + +class AnalyticsPanelWidgetFactory +{ +public: + using Creator = std::function; + + struct Descriptor + { + QString type; // stable ID ("manaProdDevotion") + QString title; // translated, user-facing + Creator creator; + }; + + static AnalyticsPanelWidgetFactory &instance(); + + // NEW: richer registration + void registerWidget(const Descriptor &desc); + + AbstractAnalyticsPanelWidget * + create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const; + + // NEW: expose widgets to UI + QList availableWidgets() const; + +private: + AnalyticsPanelWidgetFactory() = default; // Ensure private constructor + AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete; + AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete; + + QMap widgets; +}; + +#endif diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp new file mode 100644 index 000000000..d4129a3d0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp @@ -0,0 +1 @@ +#include "analytics_panel_widget_registrar.h" diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h new file mode 100644 index 000000000..70d6df94f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h @@ -0,0 +1,17 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H + +#include "analytics_panel_widget_factory.h" + +class AnalyticsPanelWidgetRegistrar +{ +public: + AnalyticsPanelWidgetRegistrar(const QString &type, + const QString &title, + AnalyticsPanelWidgetFactory::Creator creator) + { + AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator}); + } +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp new file mode 100644 index 000000000..51129a4b4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp @@ -0,0 +1,28 @@ +#include "draw_probability_config.h" + +QJsonObject DrawProbabilityConfig::toJson() const +{ + QJsonObject o; + o["criteria"] = criteria; + o["atLeast"] = atLeast; + o["quantity"] = quantity; + o["drawn"] = drawn; + return o; +} +DrawProbabilityConfig DrawProbabilityConfig::fromJson(const QJsonObject &o) +{ + DrawProbabilityConfig cfg; + if (o.contains("criteria")) { + cfg.criteria = o["criteria"].toString(); + } + if (o.contains("atLeast")) { + cfg.atLeast = o["atLeast"].toBool(true); + } + if (o.contains("quantity")) { + cfg.quantity = o["quantity"].toInt(1); + } + if (o.contains("drawn")) { + cfg.drawn = o["drawn"].toInt(7); + } + return cfg; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h new file mode 100644 index 000000000..bbe61a68e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H +#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H + +#include +#include + +struct DrawProbabilityConfig +{ + QString criteria = "name"; // name, type, subtype, cmc + bool atLeast = true; // true = at least, false = exactly + int quantity = 1; // N + int drawn = 7; // M + + QJsonObject toJson() const; + + static DrawProbabilityConfig fromJson(const QJsonObject &o); +}; + +#endif diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp new file mode 100644 index 000000000..71f88c0fc --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp @@ -0,0 +1,92 @@ +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include + +DrawProbabilityConfigDialog::DrawProbabilityConfigDialog(QWidget *parent) : QDialog(parent) +{ + form = new QFormLayout(this); + + // Criteria + labelCriteria = new QLabel(this); + criteria = new QComboBox(this); + criteria->addItem(QString(), "name"); + criteria->addItem(QString(), "type"); + criteria->addItem(QString(), "subtype"); + criteria->addItem(QString(), "cmc"); + form->addRow(labelCriteria, criteria); + + // Exactness + labelExactness = new QLabel(this); + exactness = new QComboBox(this); + exactness->addItem(QString(), true); + exactness->addItem(QString(), false); + form->addRow(labelExactness, exactness); + + // Quantity + labelQuantity = new QLabel(this); + quantity = new QSpinBox(this); + quantity->setRange(1, 60); + form->addRow(labelQuantity, quantity); + + // Drawn + labelDrawn = new QLabel(this); + drawn = new QSpinBox(this); + drawn->setRange(1, 60); + drawn->setValue(7); + form->addRow(labelDrawn, drawn); + + // Button box + auto *bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + form->addWidget(bb); + + connect(bb, &QDialogButtonBox::accepted, this, &DrawProbabilityConfigDialog::accept); + connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject); + + retranslateUi(); +} + +void DrawProbabilityConfigDialog::retranslateUi() +{ + setWindowTitle(tr("Draw Probability Settings")); + + labelCriteria->setText(tr("Criteria:")); + criteria->setItemText(0, tr("Card Name")); + criteria->setItemText(1, tr("Type")); + criteria->setItemText(2, tr("Subtype")); + criteria->setItemText(3, tr("Mana Value")); + + labelExactness->setText(tr("Exactness:")); + exactness->setItemText(0, tr("At least")); + exactness->setItemText(1, tr("Exactly")); + + labelQuantity->setText(tr("Quantity (N):")); + labelDrawn->setText(tr("Cards drawn (M):")); + + // i18n-friendly suffixes + quantity->setSuffix(tr(" cards")); + drawn->setSuffix(tr(" cards")); +} + +void DrawProbabilityConfigDialog::setFromConfig(const DrawProbabilityConfig &_config) +{ + cfg = _config; + + criteria->setCurrentIndex(criteria->findData(_config.criteria)); + exactness->setCurrentIndex(exactness->findData(_config.atLeast)); + quantity->setValue(_config.quantity); + drawn->setValue(_config.drawn); +} + +void DrawProbabilityConfigDialog::accept() +{ + cfg.criteria = criteria->currentData().toString(); + cfg.atLeast = exactness->currentData().toBool(); + cfg.quantity = quantity->value(); + cfg.drawn = drawn->value(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h new file mode 100644 index 000000000..44b1f0eec --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h @@ -0,0 +1,44 @@ +#pragma once + +#include "draw_probability_config.h" + +#include +#include + +class QComboBox; +class QSpinBox; +class QLabel; + +class DrawProbabilityConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit DrawProbabilityConfigDialog(QWidget *parent = nullptr); + + void retranslateUi(); + + void setFromConfig(const DrawProbabilityConfig &_config); + DrawProbabilityConfig result() const + { + return cfg; + } + +protected: + void accept() override; + +private: + DrawProbabilityConfig cfg; + + QFormLayout *form; + + // Widgets + QComboBox *criteria; + QComboBox *exactness; + QSpinBox *quantity; + QSpinBox *drawn; + + QLabel *labelCriteria; + QLabel *labelExactness; + QLabel *labelQuantity; + QLabel *labelDrawn; +}; diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp new file mode 100644 index 000000000..a8bec834f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp @@ -0,0 +1,236 @@ +#include "draw_probability_widget.h" + +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : AbstractAnalyticsPanelWidget(parent, analyzer) +{ + controls = new QWidget(this); + controlLayout = new QHBoxLayout(controls); + + labelPrefix = new QLabel(this); + controlLayout->addWidget(labelPrefix); + + criteriaCombo = new QComboBox(this); + // Give these things item-data so we can translate the actual user-facing strings + criteriaCombo->addItem(QString(), "name"); + criteriaCombo->addItem(QString(), "type"); + criteriaCombo->addItem(QString(), "subtype"); + criteriaCombo->addItem(QString(), "cmc"); + controlLayout->addWidget(criteriaCombo); + + exactnessCombo = new QComboBox(this); + exactnessCombo->addItem(QString(), true); // At least + exactnessCombo->addItem(QString(), false); // Exactly + controlLayout->addWidget(exactnessCombo); + + quantitySpin = new QSpinBox(this); + quantitySpin->setRange(1, 60); + controlLayout->addWidget(quantitySpin); + + labelMiddle = new QLabel(this); + controlLayout->addWidget(labelMiddle); + + drawnSpin = new QSpinBox(this); + drawnSpin->setRange(1, 60); + drawnSpin->setValue(7); + controlLayout->addWidget(drawnSpin); + + labelSuffix = new QLabel(this); + controlLayout->addWidget(labelSuffix); + + labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + controlLayout->addStretch(1); + layout->addWidget(controls); + + // Table + resultTable = new QTableWidget(this); + resultTable->setColumnCount(3); + resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + layout->addWidget(resultTable); + + // Connections + connect(criteriaCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + config.criteria = criteriaCombo->currentData().toString(); + updateDisplay(); + }); + + connect(exactnessCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + config.atLeast = exactnessCombo->currentData().toBool(); + updateDisplay(); + }); + + connect(quantitySpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { + config.quantity = v; + updateDisplay(); + }); + + connect(drawnSpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { + config.drawn = v; + updateDisplay(); + }); + + retranslateUi(); + applyConfigToToolbar(); + updateFilterOptions(); +} + +void DrawProbabilityWidget::retranslateUi() +{ + bannerWidget->setText(tr("Draw Probability")); + + labelPrefix->setText(tr("Probability of drawing")); + + criteriaCombo->setItemText(0, tr("Card Name")); + criteriaCombo->setItemText(1, tr("Type")); + criteriaCombo->setItemText(2, tr("Subtype")); + criteriaCombo->setItemText(3, tr("Mana Value")); + + exactnessCombo->setItemText(0, tr("At least")); + exactnessCombo->setItemText(1, tr("Exactly")); + + labelMiddle->setText(tr("card(s) having drawn at least")); + labelSuffix->setText(tr("cards")); + + resultTable->setHorizontalHeaderLabels({tr("Category"), tr("Qty"), tr("Odds (%)")}); +} + +QDialog *DrawProbabilityWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new DrawProbabilityConfigDialog(parent); + dlg->setFromConfig(config); + return dlg; +} + +QJsonObject DrawProbabilityWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *dp = qobject_cast(dlg); + return dp ? dp->result().toJson() : QJsonObject{}; +} + +void DrawProbabilityWidget::applyConfigToToolbar() +{ + auto setComboByData = [](QComboBox *combo, const QVariant &value) { + int idx = combo->findData(value); + if (idx >= 0) { + combo->setCurrentIndex(idx); + } + }; + + setComboByData(criteriaCombo, config.criteria); + setComboByData(exactnessCombo, config.atLeast); + + quantitySpin->setValue(config.quantity); + drawnSpin->setValue(config.drawn); +} + +void DrawProbabilityWidget::updateDisplay() +{ + updateFilterOptions(); +} + +void DrawProbabilityWidget::loadConfig(const QJsonObject &cfg) +{ + config = DrawProbabilityConfig::fromJson(cfg); + applyConfigToToolbar(); + updateFilterOptions(); +} + +void DrawProbabilityWidget::updateFilterOptions() +{ + if (!analyzer->getModel()->getDeckList()) { + return; + } + + const QString criteria = config.criteria; + const bool atLeast = config.atLeast; + const int quantity = config.quantity; + const int drawn = config.drawn; + + QMap categoryCounts; + int totalDeckCards = 0; + + const auto nodes = analyzer->getModel()->getDeckList()->getCardNodes(); + for (auto *node : nodes) { + CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr(); + if (!info) { + continue; + } + + totalDeckCards += node->getNumber(); + + QStringList categories; + if (criteria == "name") { + categories << info->getName(); + } else if (criteria == "type") { + categories = info->getMainCardType().split(' ', Qt::SkipEmptyParts); + } else if (criteria == "subtype") { + categories = info->getCardType().split(' ', Qt::SkipEmptyParts); + } else if (criteria == "cmc") { + categories << QString::number(info->getCmc().toInt()); + } + + for (const QString &cat : categories) { + categoryCounts[cat] += node->getNumber(); + } + } + + resultTable->setRowCount(categoryCounts.size()); + + int row = 0; + for (auto it = categoryCounts.cbegin(); it != categoryCounts.cend(); ++it, ++row) { + const QString &cat = it.key(); + const int copies = it.value(); + + double probability = 0.0; + if (atLeast) { + for (int k = quantity; k <= drawn && k <= copies; ++k) { + probability += hypergeometricProbability(totalDeckCards, copies, drawn, k); + } + } else { + probability = hypergeometricProbability(totalDeckCards, copies, drawn, quantity); + } + + resultTable->setItem(row, 0, new QTableWidgetItem(cat)); + resultTable->setItem(row, 1, new QTableWidgetItem(QString::number(copies))); + resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2))); + } +} + +double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k) +{ + if (k < 0 || k > n || K > N || n > N) { + return 0.0; + } + + double logP = 0.0; + for (int i = 1; i <= k; ++i) { + logP += qLn(double(K - k + i) / i); + } + for (int i = 1; i <= n - k; ++i) { + logP += qLn(double(N - K - (n - k) + i) / i); + } + for (int i = 1; i <= n; ++i) { + logP -= qLn(double(N - n + i) / i); + } + + return qExp(logP); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h new file mode 100644 index 000000000..80015999f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H +#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H + +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "draw_probability_config.h" + +#include +#include +#include +#include + +class DrawProbabilityWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT +public: + DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + QDialog *createConfigDialog(QWidget *parent) override; + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + void applyConfigToToolbar(); + +public slots: + void updateDisplay() override; + void loadConfig(const QJsonObject &cfg) override; + void retranslateUi(); + +private slots: + void updateFilterOptions(); + +private: + DrawProbabilityConfig config; + + QWidget *controls; + QHBoxLayout *controlLayout; + QLabel *labelPrefix; + QLabel *labelMiddle; + QLabel *labelSuffix; + QLineEdit *cardNameEdit; + QComboBox *criteriaCombo; // Card Name / Type / Subtype / Mana Value + QComboBox *filterCombo; // The actual value + QComboBox *exactnessCombo; // At least / Exactly + QSpinBox *quantitySpin; // N + QSpinBox *drawnSpin; // M + + QSpinBox *manaValueSpin; + + QTableWidget *resultTable; + + double hypergeometricProbability(int N, int K, int n, int k); + double calculateProbability(int totalCards, int copies, int drawn, bool atLeast); +}; + +#endif // COCKATRICE_DRAW_PROBABILITY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp new file mode 100644 index 000000000..2f9f60752 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp @@ -0,0 +1,32 @@ +#include "mana_base_config.h" + +QJsonObject ManaBaseConfig::toJson() const +{ + QJsonObject jsonObject; + QJsonArray jsonArray; + jsonObject["displayType"] = displayType; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + return jsonObject; +} + +ManaBaseConfig ManaBaseConfig::fromJson(const QJsonObject &o) + +{ + ManaBaseConfig config; + + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h new file mode 100644 index 000000000..d01f88b8b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h @@ -0,0 +1,19 @@ + +#ifndef COCKATRICE_MANA_BASE_CONFIG_H +#define COCKATRICE_MANA_BASE_CONFIG_H + +#include +#include +#include + +struct ManaBaseConfig +{ + QString displayType; // "pie" or "bar" or "combinedBar" + QStringList filters; // which colors to show, empty = all + + QJsonObject toJson() const; + + static ManaBaseConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_BASE_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp new file mode 100644 index 000000000..3317486ea --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp @@ -0,0 +1,67 @@ +#include "mana_base_config_dialog.h" + +#include + +ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaBaseConfig initial, + QWidget *parent) + : QDialog(parent), config(initial) +{ + layout = new QVBoxLayout(this); + + displayTypeLabel = new QLabel(this); + layout->addWidget(displayTypeLabel); + + displayType = new QComboBox(this); + layout->addWidget(displayType); + + filterLabel = new QLabel(this); + layout->addWidget(filterLabel); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + layout->addWidget(filterList); + + QStringList colors = analyzer->getManaBase().keys(); + colors.sort(); + filterList->addItems(colors); + + // select initial filters + for (int i = 0; i < filterList->count(); ++i) { + if (config.filters.contains(filterList->item(i)->text())) + filterList->item(i)->setSelected(true); + } + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &ManaBaseConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaBaseConfigDialog::reject); + + retranslateUi(); +} + +void ManaBaseConfigDialog::retranslateUi() +{ + setWindowTitle(tr("Mana Base Configuration")); + + displayTypeLabel->setText(tr("Display type:")); + + displayType->clear(); + displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")}); + + filterLabel->setText(tr("Filter Colors (optional):")); + + buttons->button(QDialogButtonBox::Ok)->setText(tr("OK")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); +} + +void ManaBaseConfigDialog::accept() +{ + config.displayType = displayType->currentText(); + config.filters.clear(); + for (auto *item : filterList->selectedItems()) { + config.filters << item->text(); + } + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h new file mode 100644 index 000000000..a816f154f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h @@ -0,0 +1,42 @@ + +#ifndef COCKATRICE_MANA_BASE_ADD_DIALOG_H +#define COCKATRICE_MANA_BASE_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_base_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ManaBaseConfigDialog : public QDialog +{ + Q_OBJECT +public: + ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig initial = {}, QWidget *parent = nullptr); + void retranslateUi(); + + void accept() override; + + ManaBaseConfig result() const + { + return config; + } + +private: + ManaBaseConfig config; + QVBoxLayout *layout; + QLabel *displayTypeLabel; + QComboBox *displayType; + QLabel *filterLabel; + QListWidget *filterList; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_MANA_BASE_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp new file mode 100644 index 000000000..d38314b8c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp @@ -0,0 +1,115 @@ +#include "mana_base_widget.h" + +#include "../../../general/display/charts/bars/bar_widget.h" +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../analytics_panel_widget_registrar.h" +#include "mana_base_config_dialog.h" + +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaBase{ + "manaBase", ManaBaseWidget::tr("Mana Base"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg)) +{ + barContainer = new QWidget(this); + barLayout = new QHBoxLayout(barContainer); + layout->addWidget(barContainer); + + updateDisplay(); +} + +void ManaBaseWidget::updateDisplay() +{ + // Clear previous widgets + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + auto &pipCount = analyzer->getProductionPipCount(); + auto &cardCount = analyzer->getProductionCardCount(); + + QHash manaMap; + for (auto key : pipCount.keys()) { + manaMap[key] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash filtered; + for (auto f : config.filters) { + if (manaMap.contains(f)) { + filtered[f] = manaMap[f]; + } + } + manaMap = filtered; + } + + // Determine maximum for bar charts + int highest = 1; + for (auto val : manaMap) { + highest = std::max(highest, val); + } + + // Convert to QMap for ColorBar / ColorPie (sorted) + QMap mapSorted; + for (auto it = manaMap.begin(); it != manaMap.end(); ++it) { + mapSorted.insert(it.key(), it.value()); + } + + // Choose display mode + if (config.displayType == "bar") { + QHash colors = {{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)}, + {"B", QColor(21, 11, 0)}, {"R", QColor(211, 32, 42)}, + {"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)}}; + + for (auto color : manaMap.keys()) { + QString label = QString("%1 %2 (%3)").arg(color).arg(manaMap[color]).arg(cardCount.value(color)); + + BarWidget *bar = new BarWidget(label, manaMap[color], highest, colors.value(color, Qt::gray), this); + + barLayout->addWidget(bar); + } + } else if (config.displayType == "combinedBar") { + ColorBar *cb = new ColorBar(mapSorted, this); + cb->setMinimumHeight(30); + barLayout->addWidget(cb); + } else if (config.displayType == "pie") { + ColorPie *pie = new ColorPie(mapSorted, this); + pie->setMinimumSize(200, 200); + barLayout->addWidget(pie); + } + + update(); +} +QSize ManaBaseWidget::sizeHint() const +{ + return QSize(800, 150); +} + +QDialog *ManaBaseWidget::createConfigDialog(QWidget *parent) +{ + ManaBaseConfigDialog *dlg = new ManaBaseConfigDialog(analyzer, config, parent); + return dlg; +} + +QJsonObject ManaBaseWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h new file mode 100644 index 000000000..39380c07e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h @@ -0,0 +1,51 @@ +/** + * @file mana_base_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_BASE_WIDGET_H +#define MANA_BASE_WIDGET_H + +#include "../../../general/display/banner_widget.h" +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_base_config.h" + +#include +#include +#include +#include +#include + +class ManaBaseWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaBaseConfig::fromJson(o); + updateDisplay(); + } + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaBaseConfig config; + QWidget *barContainer; + QHBoxLayout *barLayout; +}; + +#endif // MANA_BASE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp new file mode 100644 index 000000000..a0ccedc74 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp @@ -0,0 +1,121 @@ +#include "mana_curve_category_widget.h" + +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config.h" +#include "mana_curve_total_widget.h" + +constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row + +ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent) +{ + layout = new QVBoxLayout(this); + layout->setSpacing(4); + layout->setContentsMargins(0, 0, 0, 0); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +// Same as minimum for now +QSize ManaCurveCategoryWidget::sizeHint() const +{ + if (layout->isEmpty()) { + return QSize(0, 0); + } + + // Calculate exact height needed for all rows + int rowCount = layout->count(); + + int totalHeight = rowCount * MIN_ROW_HEIGHT; + totalHeight += (rowCount - 1) * layout->spacing(); + + return QSize(0, totalHeight); +} + +QSize ManaCurveCategoryWidget::minimumSizeHint() const +{ + if (layout->isEmpty()) { + return QSize(0, 0); + } + + // Calculate actual minimum based on number of rows + int rowCount = layout->count(); + + int totalHeight = rowCount * MIN_ROW_HEIGHT; + totalHeight += (rowCount - 1) * layout->spacing(); + + return QSize(0, totalHeight); +} + +void ManaCurveCategoryWidget::updateDisplay(int minCmc, + int maxCmc, + int highest, + QHash> qCategoryCounts, + QHash> qCategoryCards, + const ManaCurveConfig &config) +{ + // Clear previous content + QtUtils::clearLayoutRec(layout); + + if (!config.showCategoryRows) { + return; // nothing to show + } + + // Collect categories + QStringList categories = qCategoryCounts.keys(); + + // Apply filters + if (!config.filters.isEmpty()) { + QStringList filtered; + for (const QString &cat : categories) { + if (config.filters.contains(cat)) { + filtered.append(cat); + } + } + categories = filtered; + } + + std::sort(categories.begin(), categories.end()); + + for (const QString &cat : categories) { + QWidget *row = new QWidget(this); + row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + row->setFixedHeight(MIN_ROW_HEIGHT); + + QHBoxLayout *rowLayout = new QHBoxLayout(row); + rowLayout->setContentsMargins(0, 0, 0, 0); + rowLayout->setSpacing(4); + + QLabel *categoryLabel = new QLabel(cat, row); + categoryLabel->setFixedWidth(80); + rowLayout->addWidget(categoryLabel); + + QVector catBars; + const auto cmcCounts = qCategoryCounts.value(cat); + const auto cmcCards = qCategoryCards.value(cat); + + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + int val = cmcCounts.value(cmc, 0); + QStringList cards = cmcCards.value(cmc); + + QVector segments; + if (val > 0) { + segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)}); + } + + catBars.push_back({QString::number(cmc), segments}); + } + + auto *catChart = new BarChartWidget(row); + catChart->setHighest(highest); + catChart->setBars(catBars); + catChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + rowLayout->addWidget(catChart); + layout->addWidget(row); + } + + // Update geometry after adding all widgets + updateGeometry(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h new file mode 100644 index 000000000..c6aa6f1f0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H +#define COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H + +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class ManaCurveCategoryWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveCategoryWidget(QWidget *parent); + void updateDisplay(int minCmc, + int maxCmc, + int highest, + QHash> qCategoryCounts, + QHash> qCategoryCards, + const ManaCurveConfig &config); + +public slots: + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +private: + QVBoxLayout *layout; +}; + +#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp new file mode 100644 index 000000000..0dfef1b66 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp @@ -0,0 +1,41 @@ +#include "mana_curve_config.h" + +QJsonObject ManaCurveConfig::toJson() const +{ + QJsonObject jsonObject; + jsonObject["groupBy"] = groupBy; + QJsonArray jsonArray; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + jsonObject["showMain"] = showMain; + jsonObject["showCategoryRows"] = showCategoryRows; + return jsonObject; +} + +ManaCurveConfig ManaCurveConfig::fromJson(const QJsonObject &o) +{ + ManaCurveConfig config; + + if (o.contains("groupBy")) { + config.groupBy = o["groupBy"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + if (o.contains("showMain")) { + config.showMain = o["showMain"].toBool(true); + } + + if (o.contains("showCategoryRows")) { + config.showCategoryRows = o["showCategoryRows"].toBool(true); + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h new file mode 100644 index 000000000..55222dd98 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h @@ -0,0 +1,21 @@ + +#ifndef COCKATRICE_MANA_CURVE_CONFIG_H +#define COCKATRICE_MANA_CURVE_CONFIG_H + +#include +#include +#include + +struct ManaCurveConfig +{ + QString groupBy = "type"; // "type", "color", "subtype", etc. + QStringList filters; // empty = all + bool showMain = true; + bool showCategoryRows = true; + + QJsonObject toJson() const; + + static ManaCurveConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_CURVE_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp new file mode 100644 index 000000000..38199656c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp @@ -0,0 +1,91 @@ +#include "mana_curve_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +ManaCurveConfigDialog::ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent) + : QDialog(parent), analyzer(analyzer) +{ + auto *lay = new QVBoxLayout(this); + + labelGroupBy = new QLabel(this); + lay->addWidget(labelGroupBy); + + groupBy = new QComboBox(this); + lay->addWidget(groupBy); + + labelFilters = new QLabel(this); + lay->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + lay->addWidget(filterList); + + showMain = new QCheckBox(this); + showMain->setChecked(true); + lay->addWidget(showMain); + + showCatRows = new QCheckBox(this); + showCatRows->setChecked(true); + lay->addWidget(showCatRows); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + lay->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaCurveConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaCurveConfigDialog::reject); + + // populate dynamic data + QStringList cats = analyzer->getManaCurveByType().keys(); + cats.append(analyzer->getManaCurveByColor().keys()); + cats.removeDuplicates(); + cats.sort(); + filterList->addItems(cats); + + groupBy->addItems({"type", "color", "subtype", "power", "toughness"}); + + retranslateUi(); +} + +void ManaCurveConfigDialog::retranslateUi() +{ + labelGroupBy->setText(tr("Group By:")); + groupBy->setItemText(0, tr("type")); + groupBy->setItemText(1, tr("color")); + groupBy->setItemText(2, tr("subtype")); + groupBy->setItemText(3, tr("power")); + groupBy->setItemText(4, tr("toughness")); + + labelFilters->setText(tr("Filters (optional):")); + + showMain->setText(tr("Show main bar row")); + showCatRows->setText(tr("Show per-category rows")); +} + +void ManaCurveConfigDialog::setFromConfig(const ManaCurveConfig &cfg) +{ + groupBy->setCurrentText(cfg.groupBy); + // restore filters + for (int i = 0; i < filterList->count(); ++i) + filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text())); + + showMain->setChecked(cfg.showMain); + showCatRows->setChecked(cfg.showCategoryRows); +} + +void ManaCurveConfigDialog::accept() +{ + cfg.groupBy = groupBy->currentText(); + + cfg.filters.clear(); + for (auto *item : filterList->selectedItems()) + cfg.filters << item->text(); + + cfg.showMain = showMain->isChecked(); + cfg.showCategoryRows = showCatRows->isChecked(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h new file mode 100644 index 000000000..17e6776e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_MANA_CURVE_ADD_DIALOG_H +#define COCKATRICE_MANA_CURVE_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class QListWidget; +class QCheckBox; +class QComboBox; + +class ManaCurveConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr); + void retranslateUi(); + void setFromConfig(const ManaCurveConfig &cfg); + + ManaCurveConfig result() const + { + return cfg; + } + +private: + ManaCurveConfig cfg; + DeckListStatisticsAnalyzer *analyzer; + + QLabel *labelGroupBy; + QComboBox *groupBy; + QLabel *labelFilters; + QListWidget *filterList; + QDialogButtonBox *buttons; + QCheckBox *showMain; + QCheckBox *showCatRows; + +private slots: + void accept() override; +}; + +#endif // COCKATRICE_MANA_CURVE_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp new file mode 100644 index 000000000..f059ff873 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp @@ -0,0 +1,78 @@ +#include "mana_curve_total_widget.h" + +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config.h" + +#include + +ManaCurveTotalWidget::ManaCurveTotalWidget(QWidget *parent) : QWidget(parent) +{ + layout = new QHBoxLayout(this); + + label = new QLabel(this); + label->setFixedWidth(80); + layout->addWidget(label); + + barChart = new BarChartWidget(this); + layout->addWidget(barChart, 1); + + setMinimumHeight(200); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +QSize ManaCurveTotalWidget::sizeHint() const +{ + return {0, 280}; +} + +QSize ManaCurveTotalWidget::minimumSizeHint() const +{ + return {0, 200}; +} + +void ManaCurveTotalWidget::updateDisplay(const QString &categoryName, + int minCmc, + int maxCmc, + int highest, + const QMap> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config) +{ + QVector mainBars; + mainBars.reserve(maxCmc - minCmc + 1); + + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + QVector segments; + + const auto cmcIt = cmcMap.constFind(cmc); + if (cmcIt != cmcMap.cend()) { + for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) { + const QString &category = it.key(); + + if (!config.filters.isEmpty() && !config.filters.contains(category)) + continue; + + const int value = it.value(); + + QStringList cards; + const auto catIt = cardsMap.constFind(category); + if (catIt != cardsMap.cend()) + cards = catIt->value(cmc); + + segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)}); + } + } + + std::sort(segments.begin(), segments.end(), + [](const BarSegment &a, const BarSegment &b) { return a.category < b.category; }); + + mainBars.push_back({QString::number(cmc), segments}); + } + + label->setText(categoryName); + + barChart->setHighest(highest); + barChart->setBars(mainBars); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h new file mode 100644 index 000000000..203f75345 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H +#define COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class ManaCurveTotalWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveTotalWidget(QWidget *parent); + QSize sizeHint() const; + QSize minimumSizeHint() const; + void updateDisplay(const QString &categoryName, + int minCmc, + int maxCmc, + int highest, + const QMap> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config); + +private: + QHBoxLayout *layout; + QLabel *label; + BarChartWidget *barChart; +}; + +#endif // COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp new file mode 100644 index 000000000..e09ecfe87 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp @@ -0,0 +1,148 @@ +#include "mana_curve_widget.h" + +#include "../../../general/display/charts/bars/bar_chart_background_widget.h" +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "../../../general/display/charts/bars/segmented_bar_widget.h" +#include "../../analytics_panel_widget_registrar.h" +#include "../../deck_list_statistics_analyzer.h" +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config_dialog.h" + +#include +#include +#include +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaCurve{ + "manaCurve", ManaCurveWidget::tr("Mana Curve"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(cfg) +{ + setLayout(layout); + + totalWidget = new ManaCurveTotalWidget(this); + totalWidget->setHidden(true); + layout->addWidget(totalWidget); + + categoryWidget = new ManaCurveCategoryWidget(this); + categoryWidget->setHidden(true); + layout->addWidget(categoryWidget); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay); + + updateDisplay(); +} + +QDialog *ManaCurveWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new ManaCurveConfigDialog(analyzer, parent); + dlg->setFromConfig(config); + return dlg; +} + +QJsonObject ManaCurveWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + return mc ? mc->result().toJson() : QJsonObject{}; +} + +static void buildMapsByCategory(const QHash> &categoryCounts, + const QHash> &categoryCards, + QMap> &outCmcMap, + QMap> &outCardsMap) +{ + outCmcMap.clear(); + outCardsMap.clear(); + + for (auto catIt = categoryCounts.cbegin(); catIt != categoryCounts.cend(); ++catIt) { + const QString &category = catIt.key(); + const auto &countsByCmc = catIt.value(); + + for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it) + outCmcMap[it.key()][category] = it.value(); + } + + for (auto catIt = categoryCards.cbegin(); catIt != categoryCards.cend(); ++catIt) { + const QString &category = catIt.key(); + const auto &cardsByCmc = catIt.value(); + + for (auto it = cardsByCmc.cbegin(); it != cardsByCmc.cend(); ++it) + outCardsMap[category][it.key()] = it.value(); + } +} + +static void findGlobalCmcRange(const QHash> &categoryCounts, int &minCmc, int &maxCmc) +{ + minCmc = 0; + maxCmc = 0; + + for (const auto &countsByCmc : categoryCounts) { + for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it) + maxCmc = qMax(maxCmc, it.key()); + } +} + +void ManaCurveWidget::updateDisplay() +{ + QHash> categoryCounts; + QHash> categoryCards; + + if (config.groupBy == "color") { + categoryCounts = analyzer->getManaCurveByColor(); + categoryCards = analyzer->getManaCurveCardsByColor(); + } else if (config.groupBy == "subtype") { + categoryCounts = analyzer->getManaCurveBySubtype(); + categoryCards = analyzer->getManaCurveCardsBySubtype(); + } else if (config.groupBy == "power") { + categoryCounts = analyzer->getManaCurveByPower(); + categoryCards = analyzer->getManaCurveCardsByPower(); + } else { + categoryCounts = analyzer->getManaCurveByType(); + categoryCards = analyzer->getManaCurveCardsByType(); + } + + QMap> cmcMap; + QMap> cardsMap; + buildMapsByCategory(categoryCounts, categoryCards, cmcMap, cardsMap); + + int minCmc = 0; + int maxCmc = 0; + findGlobalCmcRange(categoryCounts, minCmc, maxCmc); + + int highest = 1; + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + int sum = 0; + + const auto cmcIt = cmcMap.constFind(cmc); + if (cmcIt != cmcMap.cend()) { + for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) { + if (!config.filters.isEmpty() && !config.filters.contains(it.key())) { + continue; + } + + sum += it.value(); + } + } + + highest = qMax(highest, sum); + } + + totalWidget->updateDisplay(config.groupBy, minCmc, maxCmc, highest, cmcMap, cardsMap, config); + + totalWidget->setVisible(config.showMain); + + categoryWidget->updateDisplay(minCmc, maxCmc, highest, categoryCounts, categoryCards, config); + + categoryWidget->setVisible(config.showCategoryRows); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h new file mode 100644 index 000000000..da59da9a8 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h @@ -0,0 +1,50 @@ +/** + * @file mana_curve_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_CURVE_WIDGET_H +#define MANA_CURVE_WIDGET_H + +#include "../../abstract_analytics_panel_widget.h" +#include "mana_curve_category_widget.h" +#include "mana_curve_config.h" +#include "mana_curve_total_widget.h" + +#include + +class SegmentedBarWidget; +class DeckListStatisticsAnalyzer; + +class ManaCurveWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + // QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaCurveConfig::fromJson(o); + updateDisplay(); + }; + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaCurveConfig config; + ManaCurveTotalWidget *totalWidget; + ManaCurveCategoryWidget *categoryWidget; +}; + +#endif // MANA_CURVE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp new file mode 100644 index 000000000..ee57cb1e2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp @@ -0,0 +1,31 @@ +#include "mana_devotion_config.h" + +QJsonObject ManaDevotionConfig::toJson() const +{ + QJsonObject jsonObject; + QJsonArray jsonArray; + jsonObject["displayType"] = displayType; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + return jsonObject; +} + +ManaDevotionConfig ManaDevotionConfig::fromJson(const QJsonObject &o) +{ + ManaDevotionConfig config; + + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h new file mode 100644 index 000000000..08be146e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h @@ -0,0 +1,18 @@ +#ifndef COCKATRICE_MANA_DEVOTION_CONFIG_H +#define COCKATRICE_MANA_DEVOTION_CONFIG_H + +#include +#include +#include + +struct ManaDevotionConfig +{ + QString displayType; // "pie" or "bar" or "combinedBar" + QStringList filters; // which colors to show, empty = all + + QJsonObject toJson() const; + + static ManaDevotionConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_DEVOTION_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp new file mode 100644 index 000000000..80fd03928 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp @@ -0,0 +1,62 @@ +#include "mana_devotion_config_dialog.h" + +ManaDevotionConfigDialog::ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaDevotionConfig initial, + QWidget *parent) + : QDialog(parent), config(initial) +{ + layout = new QVBoxLayout(this); + + labelDisplayType = new QLabel(this); + layout->addWidget(labelDisplayType); + + displayType = new QComboBox(this); + layout->addWidget(displayType); + + labelFilters = new QLabel(this); + layout->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + + QStringList colors = analyzer->getDevotionPipCount().keys(); + colors.sort(); + filterList->addItems(colors); + layout->addWidget(filterList); + + // select initial filters + for (int i = 0; i < filterList->count(); ++i) { + if (config.filters.contains(filterList->item(i)->text())) + filterList->item(i)->setSelected(true); + } + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaDevotionConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaDevotionConfigDialog::reject); + + // populate combo box items + displayType->addItems({"pie", "bar", "combinedBar"}); + + retranslateUi(); +} + +void ManaDevotionConfigDialog::retranslateUi() +{ + labelDisplayType->setText(tr("Display type:")); + displayType->setItemText(0, tr("pie")); + displayType->setItemText(1, tr("bar")); + displayType->setItemText(2, tr("combinedBar")); + + labelFilters->setText(tr("Filter Colors (optional):")); +} + +void ManaDevotionConfigDialog::accept() +{ + config.displayType = displayType->currentText(); + config.filters.clear(); + for (auto *item : filterList->selectedItems()) { + config.filters << item->text(); + } + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h new file mode 100644 index 000000000..fd804e2af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h @@ -0,0 +1,42 @@ + +#ifndef COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H +#define COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_devotion_config.h" + +#include +#include +#include +#include +#include +#include +#include + +class ManaDevotionConfigDialog : public QDialog +{ + Q_OBJECT +public: + ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaDevotionConfig initial = {}, + QWidget *parent = nullptr); + void retranslateUi(); + + void accept() override; + + ManaDevotionConfig result() const + { + return config; + } + +private: + ManaDevotionConfig config; + QVBoxLayout *layout; + QLabel *labelDisplayType; + QComboBox *displayType; + QLabel *labelFilters; + QListWidget *filterList; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp new file mode 100644 index 000000000..709577ff2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp @@ -0,0 +1,123 @@ +#include "mana_devotion_widget.h" + +#include "../../../general/display/charts/bars/bar_widget.h" +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../analytics_panel_widget_registrar.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_devotion_config_dialog.h" + +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaDevotion{ + "manaDevotion", ManaDevotionWidget::tr("Mana Devotion"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg)) +{ + barContainer = new QWidget(this); + barLayout = new QHBoxLayout(barContainer); + barContainer->setLayout(barLayout); + layout->addWidget(barContainer); + + updateDisplay(); +} + +void ManaDevotionWidget::updateDisplay() +{ + // Clear previous widgets + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + auto &pipCount = analyzer->getDevotionPipCount(); + auto &cardCount = analyzer->getDevotionCardCount(); + + // Convert keys to single QChar form + QHash devoMap; + for (auto key : pipCount.keys()) { + devoMap[key[0]] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash filtered; + for (auto f : config.filters) { + if (devoMap.contains(f[0])) { + filtered[f[0]] = devoMap[f[0]]; + } + } + devoMap = filtered; + } + + // Determine maximum for bar charts + int highest = 1; + for (auto val : devoMap) { + highest = std::max(highest, val); + } + + // Convert to QMap for ColorBar / ColorPie + QMap mapSorted; + for (auto it = devoMap.begin(); it != devoMap.end(); ++it) { + mapSorted.insert(QString(it.key()), it.value()); + } + + // Color map + QHash colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)}, + {'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)}, + {'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}}; + + // Choose display mode + if (config.displayType == "bar") { + // One BarWidget per devotion color + for (auto c : devoMap.keys()) { + QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c))); + + BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this); + + barLayout->addWidget(bar); + } + } else if (config.displayType == "combinedBar") { + // Stacked devotion bar + ColorBar *cb = new ColorBar(mapSorted, this); + cb->setMinimumHeight(30); + barLayout->addWidget(cb); + } else if (config.displayType == "pie") { + // Devotion pie chart + ColorPie *pie = new ColorPie(mapSorted, this); + pie->setMinimumSize(200, 200); + barLayout->addWidget(pie); + } + + update(); +} + +QDialog *ManaDevotionWidget::createConfigDialog(QWidget *parent) +{ + ManaDevotionConfigDialog *dlg = new ManaDevotionConfigDialog(analyzer, config, parent); + return dlg; +} + +QJsonObject ManaDevotionWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} + +QSize ManaDevotionWidget::sizeHint() const +{ + return QSize(800, 150); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h new file mode 100644 index 000000000..833f12938 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h @@ -0,0 +1,45 @@ +/** + * @file mana_devotion_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_DEVOTION_WIDGET_H +#define MANA_DEVOTION_WIDGET_H +#include "../../../general/display/banner_widget.h" +#include "../../abstract_analytics_panel_widget.h" +#include "mana_devotion_config.h" + +#include + +class ManaDevotionWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaDevotionConfig::fromJson(o); + updateDisplay(); + } + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaDevotionConfig config; + QWidget *barContainer; + QHBoxLayout *barLayout; +}; + +#endif // MANA_DEVOTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp new file mode 100644 index 000000000..f70f32d5b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp @@ -0,0 +1,36 @@ +#include "mana_distribution_config.h" + +QJsonObject ManaDistributionConfig::toJson() const +{ + QJsonObject o; + o["displayType"] = displayType; + + QJsonArray jsonArray; + for (auto &s : filters) { + jsonArray.append(s); + } + o["filters"] = jsonArray; + + o["showColorRows"] = showColorRows; + return o; +} + +ManaDistributionConfig ManaDistributionConfig::fromJson(const QJsonObject &o) +{ + ManaDistributionConfig config; + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) + config.filters << v.toString(); + } + + if (o.contains("showColorRows")) { + config.showColorRows = o["showColorRows"].toBool(true); + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h new file mode 100644 index 000000000..d1aa0f48e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_CONFIG_H +#define COCKATRICE_MANA_DISTRIBUTION_CONFIG_H + +#include +#include +#include +#include + +struct ManaDistributionConfig +{ + QString displayType = "pie"; // "pie" or "bar" + QStringList filters; // empty = all colors + bool showColorRows = true; + + QJsonObject toJson() const; + + static ManaDistributionConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp new file mode 100644 index 000000000..7fe4d94e4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp @@ -0,0 +1,83 @@ +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +static const QStringList kColors = {"W", "U", "B", "R", "G", "C"}; + +ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent) + : QDialog(parent), analyzer(analyzer) +{ + auto *lay = new QVBoxLayout(this); + + // Labels + labelDisplayType = new QLabel(this); + lay->addWidget(labelDisplayType); + + displayType = new QComboBox(this); + lay->addWidget(displayType); + + labelFilters = new QLabel(this); + lay->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + filterList->addItems(kColors); // dynamic/fixed, no translation needed + lay->addWidget(filterList); + + showColorRows = new QCheckBox(this); + showColorRows->setChecked(true); + lay->addWidget(showColorRows); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + lay->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaDistributionConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaDistributionConfigDialog::reject); + + displayType->addItems({"pie", "bar"}); // combo items + + retranslateUi(); +} + +void ManaDistributionConfigDialog::retranslateUi() +{ + labelDisplayType->setText(tr("Top display type:")); + displayType->setItemText(0, tr("pie")); + displayType->setItemText(1, tr("bar")); + + labelFilters->setText(tr("Colors:")); + + showColorRows->setText(tr("Show per-color rows")); + + // QDialogButtonBox buttons are automatically translated +} + +void ManaDistributionConfigDialog::setFromConfig(const ManaDistributionConfig &cfgIn) +{ + cfg = cfgIn; + + displayType->setCurrentText(cfg.displayType); + + for (int i = 0; i < filterList->count(); ++i) + filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text())); + + showColorRows->setChecked(cfg.showColorRows); +} + +void ManaDistributionConfigDialog::accept() +{ + cfg.displayType = displayType->currentText(); + + // Filters + cfg.filters.clear(); + for (auto *item : filterList->selectedItems()) + cfg.filters << item->text(); + + cfg.showColorRows = showColorRows->isChecked(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h new file mode 100644 index 000000000..57c88e29d --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H +#define COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H + +#include "mana_distribution_config.h" + +#include +#include +#include +#include + +class QComboBox; +class QListWidget; +class QCheckBox; +class DeckListStatisticsAnalyzer; + +class ManaDistributionConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr); + void retranslateUi(); + + void setFromConfig(const ManaDistributionConfig &cfg); + const ManaDistributionConfig &config() const + { + return cfg; + } + +public slots: + void accept() override; + +private: + DeckListStatisticsAnalyzer *analyzer; + + QLabel *labelDisplayType; + QComboBox *displayType; + QLabel *labelFilters; + QListWidget *filterList; + QCheckBox *showColorRows; + QDialogButtonBox *buttons; + + ManaDistributionConfig cfg; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp new file mode 100644 index 000000000..8a5e14858 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp @@ -0,0 +1,49 @@ +#include "mana_distribution_single_display_widget.h" + +#include "../../../cards/additional_info/mana_symbol_widget.h" + +#include + +ManaDistributionSingleDisplayWidget::ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent) + : QWidget(parent) +{ + auto layout = new QVBoxLayout(this); + layout->setAlignment(Qt::AlignHCenter); + + symbolLabel = new ManaSymbolWidget(this, colorSymbol, true, false); + symbolLabel->setFixedSize(40, 40); + + devotionBar = new QProgressBar(this); + devotionBar->setRange(0, 100); + devotionBar->setTextVisible(false); + + devotionLabel = new QLabel(this); + devotionLabel->setAlignment(Qt::AlignCenter); + + productionBar = new QProgressBar(this); + productionBar->setRange(0, 100); + productionBar->setTextVisible(false); + + productionLabel = new QLabel(this); + productionLabel->setAlignment(Qt::AlignCenter); + + layout->addWidget(symbolLabel); + layout->addWidget(devotionBar); + layout->addWidget(devotionLabel); + layout->addWidget(productionBar); + layout->addWidget(productionLabel); + + setLayout(layout); +} + +void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int percent) +{ + devotionBar->setValue(percent); + devotionLabel->setText(QString(tr("%1 pips (%2 cards)")).arg(pips).arg(cards)); +} + +void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent) +{ + productionBar->setValue(percent); + productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards)); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h new file mode 100644 index 000000000..e33d0cec1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H +#define COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H + +#include +#include +#include +#include + +class ManaDistributionSingleDisplayWidget : public QWidget +{ + Q_OBJECT +public: + explicit ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent = nullptr); + + void setDevotion(int pips, int cards, int percent); + void setProduction(int pips, int cards, int percent); + +private: + QLabel *symbolLabel; + + QProgressBar *devotionBar; + QLabel *devotionLabel; + + QProgressBar *productionBar; + QLabel *productionLabel; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp new file mode 100644 index 000000000..8eaea1426 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp @@ -0,0 +1,129 @@ +#include "mana_distribution_widget.h" + +#include "../../analytics_panel_widget_registrar.h" +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include + +namespace +{ +AnalyticsPanelWidgetRegistrar registerManaDistribution{ + "manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }}; + +} // anonymous namespace + +static const QStringList kColors = {"W", "U", "B", "R", "G", "C"}; + +ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : AbstractAnalyticsPanelWidget(parent, analyzer) +{ + container = new QWidget(this); + containerLayout = new QVBoxLayout(container); + + devotionBarTop = new ColorBar({}, this); + devotionPieTop = new ColorPie({}, this); + productionBarTop = new ColorBar({}, this); + productionPieTop = new ColorPie({}, this); + + containerLayout->addWidget(devotionBarTop); + containerLayout->addWidget(devotionPieTop); + containerLayout->addWidget(productionBarTop); + containerLayout->addWidget(productionPieTop); + + devotionPieTop->hide(); + productionPieTop->hide(); + + row = new QHBoxLayout(); + containerLayout->addLayout(row); + + for (const QString &c : kColors) { + auto *w = new ManaDistributionSingleDisplayWidget(c, this); + row->addWidget(w); + rows[c] = w; + } + + layout->addWidget(container); +} + +void ManaDistributionWidget::updateDisplay() +{ + const auto &devPips = analyzer->getDevotionPipCount(); + const auto &devCards = analyzer->getDevotionCardCount(); + const auto &prodPips = analyzer->getProductionPipCount(); + const auto &prodCards = analyzer->getProductionCardCount(); + + QStringList filtered = config.filters.isEmpty() ? kColors : config.filters; + + QMap devMap, prodMap; + for (const QString &c : filtered) { + devMap[c] = devPips.value(c, 0); + prodMap[c] = prodPips.value(c, 0); + } + + bool showPie = (config.displayType == "pie"); + + devotionBarTop->setVisible(!showPie); + productionBarTop->setVisible(!showPie); + + devotionPieTop->setVisible(showPie); + productionPieTop->setVisible(showPie); + + if (showPie) { + devotionPieTop->setColors(devMap); + productionPieTop->setColors(prodMap); + } else { + devotionBarTop->setColors(devMap); + productionBarTop->setColors(prodMap); + } + + for (const QString &c : kColors) { + auto *w = rows.value(c); + + if (!w) { + continue; + } + + bool visible = config.showColorRows && filtered.contains(c); + w->setVisible(visible); + if (!visible) { + continue; + } + + int dp = devPips.value(c, 0); + int dc = devCards.value(c, 0); + int pp = prodPips.value(c, 0); + int pc = prodCards.value(c, 0); + + // Compute percentages + int totalDev = 0; + int totalProd = 0; + for (const QString &cc : filtered) { + totalDev += devPips.value(cc, 0); + totalProd += prodPips.value(cc, 0); + } + + int devPct = (totalDev > 0) ? int(100.0 * dp / totalDev) : 0; + int prodPct = (totalProd > 0) ? int(100.0 * pp / totalProd) : 0; + + w->setDevotion(dp, dc, devPct); + w->setProduction(pp, pc, prodPct); + } +} + +QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new ManaDistributionConfigDialog(analyzer, parent); + dlg->setWindowTitle(tr("Mana Distribution Settings")); + dlg->setFromConfig(config); + + connect(dlg, &QDialog::accepted, [this, dlg]() { + config = dlg->config(); + updateDisplay(); + }); + + return dlg; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h new file mode 100644 index 000000000..2d834f3af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_WIDGET_H +#define COCKATRICE_MANA_DISTRIBUTION_WIDGET_H + +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_distribution_config.h" +#include "mana_distribution_single_display_widget.h" + +#include +#include +#include +#include + +class ManaDistributionWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT +public: + explicit ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + QJsonObject extractConfigFromDialog(QDialog *) const override + { + return {}; + } + +private: + ManaDistributionConfig config; + + QWidget *container; + QVBoxLayout *containerLayout; + + QVBoxLayout *topLayout; + ColorBar *devotionBarTop; + ColorPie *devotionPieTop; + ColorBar *productionBarTop; + ColorPie *productionPieTop; + + QHBoxLayout *row; + QMap rows; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp index 45393bb5d..ea61302f0 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp @@ -1,35 +1,298 @@ #include "deck_analytics_widget.h" -DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel) - : QWidget(parent), deckListModel(_deckListModel) -{ - mainLayout = new QVBoxLayout(); - setLayout(mainLayout); +#include "abstract_analytics_panel_widget.h" +#include "add_analytics_panel_dialog.h" +#include "analytics_panel_widget_factory.h" +#include "analyzer_modules/mana_base/mana_base_config.h" +#include "analyzer_modules/mana_curve/mana_curve_config.h" +#include "analyzer_modules/mana_devotion/mana_devotion_config.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" +#include +#include +#include +#include +#include +#include +#include + +DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer) + : QWidget(parent), statsAnalyzer(_statsAnalyzer) +{ + layout = new QVBoxLayout(this); + + // Controls + controlContainer = new QWidget(this); + controlLayout = new QHBoxLayout(controlContainer); + addButton = new QPushButton(this); + removeButton = new QPushButton(this); + saveButton = new QPushButton(this); + loadButton = new QPushButton(this); + controlLayout->addWidget(addButton); + controlLayout->addWidget(removeButton); + controlLayout->addWidget(saveButton); + controlLayout->addWidget(loadButton); + + layout->addWidget(controlContainer); + + connect(addButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onAddPanel); + connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected); + connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout); + connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout); + + // Scroll area and container scrollArea = new QScrollArea(this); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setWidgetResizable(true); - mainLayout->addWidget(scrollArea); + scrollArea->setFrameShape(QFrame::NoFrame); - container = new QWidget(scrollArea); - containerLayout = new QVBoxLayout(container); - container->setLayout(containerLayout); - scrollArea->setWidget(container); + panelContainer = new QWidget(scrollArea); + panelLayout = new QVBoxLayout(panelContainer); + panelLayout->setSpacing(8); + panelLayout->setContentsMargins(4, 4, 4, 4); + panelLayout->addStretch(1); // push panels up - deckListStatisticsAnalyzer = new DeckListStatisticsAnalyzer(this, deckListModel); + scrollArea->setWidget(panelContainer); + layout->addWidget(scrollArea); - manaCurveWidget = new ManaCurveWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaCurveWidget); + loadLayout(); - manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaDevotionWidget); - - manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaBaseWidget); + retranslateUi(); } -void DeckAnalyticsWidget::refreshDisplays() +void DeckAnalyticsWidget::retranslateUi() { - deckListStatisticsAnalyzer->update(); + addButton->setText(tr("Add Panel")); + removeButton->setText(tr("Remove Panel")); + saveButton->setText(tr("Save Layout")); + loadButton->setText(tr("Load Layout")); } + +void DeckAnalyticsWidget::updateDisplays() +{ + statsAnalyzer->analyze(); +} + +void DeckAnalyticsWidget::onAddPanel() +{ + AddAnalyticsPanelDialog dlg(this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + QString selection = dlg.selectedType(); + if (selection.isEmpty()) { + return; + } + + AbstractAnalyticsPanelWidget *analyticsWidget = + AnalyticsPanelWidgetFactory::instance().create(selection, this, statsAnalyzer); + if (!analyticsWidget) { + return; + } + + if (!analyticsWidget->applyConfigFromDialog()) { + analyticsWidget->deleteLater(); + return; + } + + addPanelInstance(selection, analyticsWidget, analyticsWidget->saveConfig()); +} + +void DeckAnalyticsWidget::addPanelInstance(const QString &typeId, + AbstractAnalyticsPanelWidget *panel, + const QJsonObject &cfg) +{ + panel->loadConfig(cfg); + panel->updateDisplay(); + + auto *resPanel = new ResizablePanel(typeId, panel, panelContainer); + panelWrappers.push_back(resPanel); + + panelLayout->insertWidget(panelLayout->count() - 1, resPanel); + + // Event filter for selection + resPanel->installEventFilter(this); + panel->installEventFilter(this); + + // Connect drag-drop signals + connect(resPanel, &ResizablePanel::dropRequested, this, &DeckAnalyticsWidget::onPanelDropped); +} + +void DeckAnalyticsWidget::onRemoveSelected() +{ + int idx = indexOfSelectedWrapper(); + if (idx < 0) { + return; + } + + ResizablePanel *panel = panelWrappers.takeAt(idx); + selectWrapper(nullptr); + + panel->deleteLater(); +} + +void DeckAnalyticsWidget::saveLayout() +{ + QJsonArray arr; + + for (auto *wrapper : panelWrappers) { + QJsonObject entry; + entry["type"] = wrapper->getTypeId(); + entry["config"] = wrapper->panel->saveConfig(); + entry["height"] = wrapper->getCurrentHeight(); + arr.append(entry); + } + + QSettings s; + s.setValue("deckAnalytics/layout", QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact))); +} + +void DeckAnalyticsWidget::loadLayout() +{ + if (!loadLayoutInternal()) { + addDefaultPanels(); + } +} + +void DeckAnalyticsWidget::addDefaultPanels() +{ + struct DefaultPanel + { + QString type; + QJsonObject cfg; + }; + + // Prepare configs + QJsonObject manaCurveCfg = ManaCurveConfig{}.toJson(); + QJsonObject manaBaseCfg = ManaBaseConfig{"combinedBar", {}}.toJson(); + QJsonObject manaDevotionCfg = ManaDevotionConfig{"combinedBar", {}}.toJson(); + QVector defaults = { + {"manaCurve", manaCurveCfg}, {"manaBase", manaBaseCfg}, {"manaDevotion", manaDevotionCfg}}; + + for (auto &d : defaults) { + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(d.type, this, statsAnalyzer); + if (!w) { + continue; + } + + w->loadConfig(d.cfg); + addPanelInstance(d.type, w, d.cfg); + } +} + +bool DeckAnalyticsWidget::loadLayoutInternal() +{ + QSettings s; + QString layoutData = s.value("deckAnalytics/layout").toString(); + if (layoutData.isEmpty()) { + return false; + } + + QJsonDocument doc = QJsonDocument::fromJson(layoutData.toUtf8()); + if (!doc.isArray()) { + return false; + } + + clearPanels(); + + for (auto v : doc.array()) { + if (!v.isObject()) { + continue; + } + QJsonObject o = v.toObject(); + QString type = o["type"].toString(); + QJsonObject cfg = o["config"].toObject(); + + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(type, this, statsAnalyzer); + if (!w) { + continue; + } + + addPanelInstance(type, w, cfg); + + // Restore height AFTER adding the panel + if (o.contains("height")) { + panelWrappers.last()->setHeightFromSaved(o["height"].toInt()); + } + } + + return true; +} + +void DeckAnalyticsWidget::clearPanels() +{ + selectWrapper(nullptr); + while (!panelWrappers.isEmpty()) { + ResizablePanel *p = panelWrappers.takeLast(); + p->deleteLater(); + } +} + +bool DeckAnalyticsWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress) { + for (auto *p : panelWrappers) { + if (obj == p || obj == p->panel) { + selectWrapper(p); + break; + } + } + } + return QWidget::eventFilter(obj, event); +} + +void DeckAnalyticsWidget::selectWrapper(ResizablePanel *w) +{ + // Same wrapper + if (selectedWrapper == w) { + return; + } + // Deselect the old one + if (selectedWrapper) { + selectedWrapper->setSelected(false); + } + // Set current + selectedWrapper = w; + // Finally, select new + if (selectedWrapper) { + selectedWrapper->setSelected(true); + } +} + +int DeckAnalyticsWidget::indexOfSelectedWrapper() const +{ + if (!selectedWrapper) { + return -1; + } + return panelWrappers.indexOf(selectedWrapper); +} + +void DeckAnalyticsWidget::onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore) +{ + int draggedIdx = panelWrappers.indexOf(dragged); + int targetIdx = panelWrappers.indexOf(target); + + if (draggedIdx == -1 || targetIdx == -1 || draggedIdx == targetIdx) { + return; + } + + // Remove dragged panel from list and layout + panelWrappers.removeAt(draggedIdx); + panelLayout->removeWidget(dragged); + + // Adjust target index if needed + if (draggedIdx < targetIdx) { + targetIdx--; + } + + // Calculate insertion position + int insertIdx = insertBefore ? targetIdx : targetIdx + 1; + + // Insert back into list and layout + panelWrappers.insert(insertIdx, dragged); + panelLayout->insertWidget(insertIdx, dragged); + + // Clear selection + selectWrapper(nullptr); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h index 524362aed..31ee36fbb 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h @@ -1,44 +1,71 @@ /** * @file deck_analytics_widget.h * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. + * @brief Main analytics widget container with resizable panels for deck statistics. */ #ifndef DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H -#include "mana_base_widget.h" -#include "mana_curve_widget.h" -#include "mana_devotion_widget.h" +#include "abstract_analytics_panel_widget.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" -#include +#include #include +#include +#include #include -#include + +class LayoutInspector; class DeckAnalyticsWidget : public QWidget { Q_OBJECT +public slots: + void updateDisplays(); + public: - explicit DeckAnalyticsWidget(QWidget *parent, DeckListModel *deckListModel); - void setDeckList(const DeckList &_deckListModel); - std::map analyzeManaCurve(); - void refreshDisplays(); + explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + void retranslateUi(); + +private slots: + void onAddPanel(); + void onRemoveSelected(); + void onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore); + void saveLayout(); + void loadLayout(); + void addDefaultPanels(); + bool loadLayoutInternal(); + void clearPanels(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void selectWrapper(ResizablePanel *panel); + int indexOfSelectedWrapper() const; private: - DeckListModel *deckListModel; - DeckListStatisticsAnalyzer *deckListStatisticsAnalyzer; - QVBoxLayout *mainLayout; + void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {}); - QWidget *container; - QVBoxLayout *containerLayout; + QVBoxLayout *layout; + QWidget *controlContainer; + QHBoxLayout *controlLayout; + + QPushButton *addButton; + QPushButton *removeButton; + QPushButton *saveButton; + QPushButton *loadButton; QScrollArea *scrollArea; + QWidget *panelContainer; + QVBoxLayout *panelLayout; - ManaCurveWidget *manaCurveWidget; - ManaDevotionWidget *manaDevotionWidget; - ManaBaseWidget *manaBaseWidget; + QVector panelWrappers; + ResizablePanel *selectedWrapper = nullptr; + + DeckListStatisticsAnalyzer *statsAnalyzer; + LayoutInspector *insp = nullptr; }; #endif // DECK_ANALYTICS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp index 3ca18d21c..7d0259c72 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp @@ -9,38 +9,93 @@ DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *_model, - DeckListStatisticsAnalyzerConfig cfg) - : QObject(parent), model(_model), config(cfg) + DeckListStatisticsAnalyzerConfig _config) + : QObject(parent), model(_model), config(_config) { - connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::update); + connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::analyze); } -void DeckListStatisticsAnalyzer::update() +void DeckListStatisticsAnalyzer::analyze() { - manaBaseMap.clear(); - manaCurveMap.clear(); - manaDevotionMap.clear(); + clearData(); QList cards = model->getCards(); - for (const ExactCard &card : cards) { - // ---- Mana curve ---- + for (auto card : cards) { + auto info = card.getInfo(); + const int cmc = info.getCmc().toInt(); + + // Convert once + QStringList types = info.getMainCardType().split(' '); + QStringList subtypes = info.getCardType().split('-').last().split(" "); + QString colors = info.getColors(); + int power = info.getPowTough().split("/").first().toInt(); + int toughness = info.getPowTough().split("/").last().toInt(); + + // For each copy of card + // ---------------- Mana Curve ---------------- if (config.computeManaCurve) { - manaCurveMap[card.getInfo().getCmc().toInt()]++; + manaCurveMap[cmc]++; } - // ---- Mana base ---- + // per-type curve + for (auto &t : types) { + manaCurveByType[t][cmc]++; + manaCurveCardsByType[t][cmc].append(info.getName()); + } + + // Per-subtype curve + for (auto &st : subtypes) { + manaCurveBySubtype[st][cmc]++; + manaCurveCardsBySubtype[st][cmc].append(info.getName()); + } + + // per-color curve + for (auto &c : colors) { + manaCurveByColor[c][cmc]++; + manaCurveCardsByColor[c][cmc].append(info.getName()); + } + + // Power/toughness + manaCurveByPower[QString::number(power)][cmc]++; + manaCurveCardsByPower[QString::number(power)][cmc].append(info.getName()); + manaCurveByToughness[QString::number(toughness)][cmc]++; + manaCurveCardsByToughness[QString::number(toughness)][cmc].append(info.getName()); + + // ========== Category Counts =========== + for (auto &t : types) { + typeCount[t]++; + } + for (auto &st : subtypes) { + subtypeCount[st]++; + } + for (auto &c : colors) { + colorCount[c]++; + } + manaValueCount[cmc]++; + + // ---------------- Mana Base ---------------- if (config.computeManaBase) { - auto mana = determineManaProduction(card.getInfo().getText()); - for (auto it = mana.begin(); it != mana.end(); ++it) + auto prod = determineManaProduction(info.getText()); + for (auto it = prod.begin(); it != prod.end(); ++it) { + if (it.value() > 0) { + productionPipCount[it.key()] += it.value(); + productionCardCount[it.key()]++; + } manaBaseMap[it.key()] += it.value(); + } } - // ---- Devotion ---- + // ---------------- Devotion ---------------- if (config.computeDevotion) { - auto devo = countManaSymbols(card.getInfo().getManaCost()); - for (auto &d : devo) + auto devo = countManaSymbols(info.getManaCost()); + for (auto &d : devo) { + if (d.second > 0) { + devotionPipCount[QString(d.first)] += d.second; + devotionCardCount[QString(d.first)]++; + } manaDevotionMap[d.first] += d.second; + } } } @@ -112,3 +167,57 @@ std::unordered_map DeckListStatisticsAnalyzer::countManaSymbols(const return manaCounts; } + +// Hypergeometric probability: P(X=k) +double DeckListStatisticsAnalyzer::hypergeometric(int N, int K, int n, int k) +{ + if (k < 0 || k > n || K > N) { + return 0.0; + } + + auto choose = [](int n, int r) -> double { + if (r > n) + return 0.0; + if (r == 0 || r == n) + return 1.0; + double res = 1.0; + for (int i = 1; i <= r; ++i) { + res *= (n - r + i); + res /= i; + } + return res; + }; + + return choose(K, k) * choose(N - K, n - k) / choose(N, n); +} + +void DeckListStatisticsAnalyzer::clearData() +{ + manaBaseMap.clear(); + manaCurveMap.clear(); + manaDevotionMap.clear(); + + devotionPipCount.clear(); + devotionCardCount.clear(); + + productionPipCount.clear(); + productionCardCount.clear(); + + manaCurveByType.clear(); + manaCurveBySubtype.clear(); + manaCurveByColor.clear(); + manaCurveByPower.clear(); + manaCurveByToughness.clear(); + + manaCurveCardsByType.clear(); + manaCurveCardsBySubtype.clear(); + manaCurveCardsByColor.clear(); + manaCurveCardsByPower.clear(); + manaCurveCardsByToughness.clear(); + + typeCount.clear(); + subtypeCount.clear(); + colorCount.clear(); + rarityCount.clear(); + manaValueCount.clear(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h index 8fa033971..946bb0117 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h @@ -14,6 +14,9 @@ struct DeckListStatisticsAnalyzerConfig bool computeManaBase = true; bool computeManaCurve = true; bool computeDevotion = true; + bool computeCategories = true; + bool computeCurveBreakdowns = true; + bool computeProbabilities = true; }; class DeckListStatisticsAnalyzer : public QObject @@ -23,9 +26,9 @@ class DeckListStatisticsAnalyzer : public QObject public: explicit DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *model, - DeckListStatisticsAnalyzerConfig cfg = DeckListStatisticsAnalyzerConfig()); + DeckListStatisticsAnalyzerConfig _config = DeckListStatisticsAnalyzerConfig()); - void update(); + void analyze(); [[nodiscard]] const QHash &getManaBase() const { @@ -40,6 +43,96 @@ public: return manaDevotionMap; } + const QHash &getDevotionPipCount() const + { + return devotionPipCount; + } + const QHash &getDevotionCardCount() const + { + return devotionCardCount; + } + + const QHash &getProductionPipCount() const + { + return productionPipCount; + } + const QHash &getProductionCardCount() const + { + return productionCardCount; + } + + const QHash &getTypeCount() const + { + return typeCount; + } + const QHash &getSubtypeCount() const + { + return subtypeCount; + } + const QHash &getColorCount() const + { + return colorCount; + } + const QHash &getRarityCount() const + { + return rarityCount; + } + const QHash &getManaValueCount() const + { + return manaValueCount; + } + + const QHash> &getManaCurveByType() const + { + return manaCurveByType; + } + const QHash> &getManaCurveBySubtype() const + { + return manaCurveBySubtype; + } + const QHash> &getManaCurveByColor() const + { + return manaCurveByColor; + } + const QHash> &getManaCurveByPower() const + { + return manaCurveByPower; + } + const QHash> &getManaCurveByToughness() const + { + return manaCurveByToughness; + } + + const QHash> &getManaCurveCardsByType() const + { + return manaCurveCardsByType; + } + + const QHash> &getManaCurveCardsBySubtype() const + { + return manaCurveCardsBySubtype; + } + + const QHash> &getManaCurveCardsByColor() const + { + return manaCurveCardsByColor; + } + + const QHash> &getManaCurveCardsByPower() const + { + return manaCurveCardsByPower; + } + + const QHash> &getManaCurveCardsByToughness() const + { + return manaCurveCardsByToughness; + } + + DeckListModel *getModel() const + { + return model; + } + signals: void statsUpdated(); @@ -47,14 +140,42 @@ private: DeckListModel *model; DeckListStatisticsAnalyzerConfig config; - // Internal result containers QHash manaBaseMap; std::unordered_map manaCurveMap; std::unordered_map manaDevotionMap; - // Internal helper functions + QHash devotionPipCount; // W/U/B/R/G total symbols + QHash devotionCardCount; // how many cards provide devotion + + QHash productionPipCount; // mana produced by cards + QHash productionCardCount; // number of producers + + QHash typeCount; + QHash subtypeCount; + QHash colorCount; + QHash rarityCount; + QHash manaValueCount; + + QHash> manaCurveByType; + QHash> manaCurveBySubtype; + QHash> manaCurveByColor; + QHash> 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 3ca171a15..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 @@ -1,8 +1,8 @@ #include "deck_editor_deck_dock_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../deck_loader/deck_loader.h" #include "deck_list_style_proxy.h" +#include "deck_state_manager.h" #include #include @@ -38,7 +38,7 @@ static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo) } DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent) - : QDockWidget(parent), deckEditor(parent) + : QDockWidget(parent), deckEditor(parent), deckStateManager(parent->deckStateManager) { setObjectName("deckDock"); @@ -52,19 +52,21 @@ DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent void DeckEditorDeckDockWidget::createDeckDock() { - deckModel = new DeckListModel(this); - deckModel->setObjectName("deckModel"); - connect(deckModel, &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash); - - deckLoader = new DeckLoader(this); + connect(getModel(), &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash); proxy = new DeckListStyleProxy(this); - proxy->setSourceModel(deckModel); + proxy->setSourceModel(getModel()); - historyManagerWidget = new DeckListHistoryManagerWidget(deckModel, proxy, deckEditor->getHistoryManager(), this); + historyManagerWidget = new DeckListHistoryManagerWidget(deckStateManager, proxy, this); connect(historyManagerWidget, &DeckListHistoryManagerWidget::requestDisplayWidgetSync, this, &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); + 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"); deckView->setModel(proxy); @@ -97,7 +99,7 @@ void DeckEditorDeckDockWidget::createDeckDock() nameDebounceTimer = new QTimer(this); nameDebounceTimer->setSingleShot(true); nameDebounceTimer->setInterval(300); // debounce duration in ms - connect(nameDebounceTimer, &QTimer::timeout, this, [this]() { updateName(nameEdit->text()); }); + connect(nameDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeName); connect(nameEdit, &LineEditUnfocusable::textChanged, this, [this]() { nameDebounceTimer->start(); // restart debounce timer @@ -141,7 +143,7 @@ void DeckEditorDeckDockWidget::createDeckDock() commentsDebounceTimer = new QTimer(this); commentsDebounceTimer->setSingleShot(true); commentsDebounceTimer->setInterval(400); // longer debounce for multi-line - connect(commentsDebounceTimer, &QTimer::timeout, this, [this]() { updateComments(); }); + connect(commentsDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeComments); connect(commentsEdit, &QTextEdit::textChanged, this, [this]() { commentsDebounceTimer->start(); // restart debounce timer @@ -152,21 +154,21 @@ void DeckEditorDeckDockWidget::createDeckDock() bannerCardLabel->setText(tr("Banner Card")); bannerCardLabel->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); bannerCardComboBox = new QComboBox(this); - connect(deckModel, &DeckListModel::dataChanged, this, [this]() { + connect(getModel(), &DeckListModel::dataChanged, this, [this]() { // Delay the update to avoid race conditions QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox); }); - connect(deckModel, &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand); - connect(deckModel, &DeckListModel::modelReset, this, &DeckEditorDeckDockWidget::expandAll); + connect(getModel(), &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand); + connect(getModel(), &DeckListModel::modelReset, this, &DeckEditorDeckDockWidget::expandAll); connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &DeckEditorDeckDockWidget::setBannerCard); + &DeckEditorDeckDockWidget::writeBannerCard); bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); - deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()->getTags()); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {}); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible()); - connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, - &DeckEditorDeckDockWidget::setTags); + connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager, + &DeckStateManager::setTags); activeGroupCriteriaLabel = new QLabel(this); @@ -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]() { - deckModel->setActiveGroupCriteria(static_cast( - activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); - deckModel->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")); @@ -295,9 +294,9 @@ void DeckEditorDeckDockWidget::initializeFormats() formatComboBox->addItem(formatName, formatName); // store the raw key in itemData } - if (!deckModel->getDeckList()->getGameFormat().isEmpty()) { - deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat()); - formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat())); + QString format = deckStateManager->getMetadata().gameFormat; + if (!format.isEmpty()) { + formatComboBox->setCurrentIndex(formatComboBox->findData(format)); } else { // Ensure no selection is visible initially formatComboBox->setCurrentIndex(-1); @@ -306,11 +305,10 @@ void DeckEditorDeckDockWidget::initializeFormats() connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { if (index >= 0) { QString formatKey = formatComboBox->itemData(index).toString(); - deckModel->setActiveFormat(formatKey); + deckStateManager->setFormat(formatKey); } else { - deckModel->setActiveFormat(QString()); // clear format if deselected + deckStateManager->setFormat(""); // clear format if deselected } - emit deckModified(); }); } @@ -340,43 +338,37 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard() void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*¤t*/, const QModelIndex & /*previous*/) { if (ExactCard card = getCurrentCard()) { - emit cardChanged(card); + emit selectedCardChanged(card); } } -void DeckEditorDeckDockWidget::updateName(const QString &name) +/** + * @brief Writes the contents of the name textBox to the DeckStateManager + */ +void DeckEditorDeckDockWidget::writeName() { - emit requestDeckHistorySave( - QString(tr("Rename deck to \"%1\" from \"%2\"")).arg(name).arg(deckLoader->getDeck().deckList.getName())); - deckModel->getDeckList()->setName(name); - deckEditor->setModified(name.isEmpty()); - emit nameChanged(); - emit deckModified(); + QString name = nameEdit->text(); + deckStateManager->setName(name); } -void DeckEditorDeckDockWidget::updateComments() +/** + * @brief Writes the contents of the comments textBox to the DeckStateManager + */ +void DeckEditorDeckDockWidget::writeComments() { - emit requestDeckHistorySave(tr("Updated comments (was %1 chars, now %2 chars)") - .arg(deckLoader->getDeck().deckList.getComments().size()) - .arg(commentsEdit->toPlainText().size())); - - deckModel->getDeckList()->setComments(commentsEdit->toPlainText()); - deckEditor->setModified(commentsEdit->toPlainText().isEmpty()); - emit commentsChanged(); - emit deckModified(); + QString comments = commentsEdit->toPlainText(); + deckStateManager->setComments(comments); } void DeckEditorDeckDockWidget::updateHash() { - hashLabel->setText(deckModel->getDeckList()->getDeckHash()); - emit hashChanged(); - emit deckModified(); + hashLabel->setText(deckStateManager->getDeckHash()); } void DeckEditorDeckDockWidget::updateBannerCardComboBox() { // Store current banner card identity - CardRef wanted = deckModel->getDeckList()->getBannerCard(); + CardRef wanted = deckStateManager->getMetadata().bannerCard; // Block signals temporarily bool wasBlocked = bannerCardComboBox->blockSignals(true); @@ -386,7 +378,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() // Collect unique (name, providerId) pairs QSet> bannerCardSet; - QList cardsInDeck = deckModel->getCardRefs(); + QList cardsInDeck = getModel()->getCardRefs(); for (auto cardRef : cardsInDeck) { if (!CardDatabaseManager::query()->getCard(cardRef)) { @@ -415,7 +407,6 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() // Handle results if (restoreIndex != -1) { bannerCardComboBox->setCurrentIndex(restoreIndex); - syncDeckListBannerCardWithComboBox(); } else { // Add a placeholder "-" and set it as the current selection bannerCardComboBox->insertItem(0, "-"); @@ -426,25 +417,21 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() bannerCardComboBox->blockSignals(wasBlocked); } -void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */) +/** + * @brief Writes the selected bannerCard to the DeckStateManager + */ +void DeckEditorDeckDockWidget::writeBannerCard(int index) { - emit requestDeckHistorySave(tr("Banner card changed")); - syncDeckListBannerCardWithComboBox(); - deckEditor->setModified(true); - emit deckModified(); + auto [name, id] = bannerCardComboBox->itemData(index).value>(); + CardRef bannerCard = {name, id}; + deckStateManager->setBannerCard(bannerCard); } -void DeckEditorDeckDockWidget::setTags(const QStringList &tags) +void DeckEditorDeckDockWidget::applyActiveGroupCriteria() { - deckModel->getDeckList()->setTags(tags); - deckEditor->setModified(true); - emit deckModified(); -} - -void DeckEditorDeckDockWidget::syncDeckListBannerCardWithComboBox() -{ - auto [name, id] = bannerCardComboBox->currentData().value>(); - deckModel->getDeckList()->setBannerCard({name, id}); + getModel()->setActiveGroupCriteria( + static_cast(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); + getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); } void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible) @@ -460,7 +447,7 @@ void DeckEditorDeckDockWidget::updateShowTagsWidget(const bool visible) void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() { - if (deckModel->getDeckList()->getBannerCard().name == "") { + if (deckStateManager->getMetadata().bannerCard.name == "") { if (bannerCardComboBox->findText("-") != -1) { bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText("-")); } else { @@ -468,36 +455,26 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() bannerCardComboBox->setCurrentIndex(0); } } else { - bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().name); + bannerCardComboBox->setCurrentText(deckStateManager->getMetadata().bannerCard.name); } } -/** - * Sets the currently active deck for this tab - * @param _deck The deck. - */ -void DeckEditorDeckDockWidget::setDeck(const LoadedDeck &_deck) +void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex) { - deckLoader->setDeck(_deck); - deckModel->setDeckList(&deckLoader->getDeck().deckList); - connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree); - - emit requestDeckHistoryClear(); - historyManagerWidget->setDeckListModel(deckModel); - - syncDisplayWidgetsToModel(); - - emit deckChanged(); + deckView->clearSelection(); + deckView->setCurrentIndex(newCardIndex); + recursiveExpand(newCardIndex); + deckView->setFocus(Qt::FocusReason::MouseFocusReason); } void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() { nameEdit->blockSignals(true); - nameEdit->setText(deckModel->getDeckList()->getName()); + nameEdit->setText(deckStateManager->getMetadata().name); nameEdit->blockSignals(false); commentsEdit->blockSignals(true); - commentsEdit->setText(deckModel->getDeckList()->getComments()); + commentsEdit->setText(deckStateManager->getMetadata().comments); commentsEdit->blockSignals(false); bannerCardComboBox->blockSignals(true); @@ -505,46 +482,22 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() updateBannerCardComboBox(); bannerCardComboBox->blockSignals(false); updateHash(); - sortDeckModelToDeckView(); - deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); -} + formatComboBox->blockSignals(true); + formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); + formatComboBox->blockSignals(false); -void DeckEditorDeckDockWidget::sortDeckModelToDeckView() -{ - deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat()); - formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat())); - - emit deckChanged(); -} - -DeckLoader *DeckEditorDeckDockWidget::getDeckLoader() -{ - return deckLoader; -} - -const DeckList &DeckEditorDeckDockWidget::getDeckList() const -{ - return *deckModel->getDeckList(); + deckTagsDisplayWidget->blockSignals(true); + deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); + deckTagsDisplayWidget->blockSignals(false); } /** - * Resets the tab to the state for a blank new tab. + * @brief Convenience method to get the underlying model instance from the DeckStateManager */ -void DeckEditorDeckDockWidget::cleanDeck() +DeckListModel *DeckEditorDeckDockWidget::getModel() const { - deckModel->cleanList(); - nameEdit->setText(QString()); - emit nameChanged(); - commentsEdit->setText(QString()); - emit commentsChanged(); - hashLabel->setText(QString()); - emit hashChanged(); - emit deckModified(); - emit deckChanged(); - updateBannerCardComboBox(); - deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); + return deckStateManager->getModel(); } void DeckEditorDeckDockWidget::selectPrevCard() @@ -571,6 +524,15 @@ void DeckEditorDeckDockWidget::changeSelectedCard(int changeBy) // Get the current index of the selected item auto deckViewCurrentIndex = deckView->currentIndex(); + // For some reason, if the deckModel is modified but the view is not manually reselected, + // currentIndex will return an index for the underlying deckModel instead of the proxy. + // That index will return an invalid index when indexBelow/indexAbove crosses a header node, + // causing the selection to fail to move down. + /// \todo Figure out why it's happening so we can do a proper fix instead of a hacky workaround + if (deckViewCurrentIndex.model() == proxy->sourceModel()) { + deckViewCurrentIndex = proxy->mapFromSource(deckViewCurrentIndex); + } + auto nextIndex = deckViewCurrentIndex.siblingAtRow(deckViewCurrentIndex.row() + changeBy); if (!nextIndex.isValid()) { nextIndex = deckViewCurrentIndex; @@ -616,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 deckModel->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()); @@ -641,29 +604,15 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString & } QString zoneName = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : _zoneName; - - emit requestDeckHistorySave(tr("Added (%1): %2 (%3) %4") - .arg(zoneName, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), - card.getPrinting().getProperty("num"))); - - QModelIndex newCardIndex = deckModel->addCard(card, zoneName); - - if (!newCardIndex.isValid()) { - return; - } - - deckView->clearSelection(); - deckView->setCurrentIndex(newCardIndex); - - emit deckModified(); + deckStateManager->addCard(card, zoneName); } 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); } } @@ -672,17 +621,17 @@ void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString QString providerId = card.getPrinting().getUuid(); QString collectorNumber = card.getPrinting().getProperty("num"); - QModelIndex foundCard = deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber); + QModelIndex foundCard = getModel()->findCard(card.getName(), zoneName, providerId, collectorNumber); if (!foundCard.isValid()) { - foundCard = deckModel->findCard(card.getName(), zoneName); + foundCard = getModel()->findCard(card.getName(), zoneName); } - swapCard(foundCard); + deckStateManager->swapCardAtIndex(foundCard); } 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 @@ -690,54 +639,15 @@ void DeckEditorDeckDockWidget::actSwapSelection() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - bool isModified = false; - for (const auto ¤tIndex : selectedRows) { - if (swapCard(currentIndex)) { - isModified = true; - } + for (const auto &sourceIndex : selectedRows) { + deckStateManager->swapCardAtIndex(sourceIndex); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); - if (isModified) { - emit deckModified(); - } - update(); } -/** - * Swaps the card at the index between the maindeck and sideboard - * - * @param currentIndex The index to swap. - * @return True if the swap was successful - */ -bool DeckEditorDeckDockWidget::swapCard(const QModelIndex ¤tIndex) -{ - if (!currentIndex.isValid()) - return false; - const QString cardName = currentIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); - const QString cardProviderID = - currentIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); - const QModelIndex gparent = currentIndex.parent().parent(); - - if (!gparent.isValid()) - return false; - - const QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - offsetCountAtIndex(currentIndex, false); - const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; - - if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, cardProviderID})) { - deckModel->addCard(card, otherZoneName); - } else { - // Third argument (true) says create the card no matter what, even if not in DB - deckModel->addPreferredPrintingCard(cardName, otherZoneName, true); - } - - return true; -} - void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString zoneName) { if (!card) @@ -745,22 +655,12 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z if (card.getInfo().getIsToken()) zoneName = DECK_ZONE_TOKENS; - QString providerId = card.getPrinting().getUuid(); - QString collectorNumber = card.getPrinting().getProperty("num"); - - QModelIndex idx = deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber); - if (!idx.isValid()) { - return; - } - - deckView->clearSelection(); - deckView->setCurrentIndex(proxy->mapToSource(idx)); - offsetCountAtIndex(idx, false); + deckStateManager->decrementCard(card, zoneName); } 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 @@ -768,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); @@ -777,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 @@ -785,56 +685,29 @@ void DeckEditorDeckDockWidget::actRemoveCard() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - bool isModified = false; - for (const auto &index : selectedRows) { - if (!index.isValid() || deckModel->hasChildren(index)) { - continue; - } - QModelIndex sourceIndex = proxy->mapToSource(index); - QString cardName = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); - - emit requestDeckHistorySave(QString(tr("Removed \"%1\" (all copies)")).arg(cardName)); - - deckModel->removeRow(sourceIndex.row(), sourceIndex.parent()); - isModified = true; + for (const auto &sourceIndex : selectedRows) { + deckStateManager->removeCardAtIndex(sourceIndex); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); - - if (isModified) { - emit deckModified(); - } } /** * @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) { - if (!idx.isValid() || deckModel->hasChildren(idx)) { + if (!idx.isValid() || getModel()->hasChildren(idx)) { return; } - QModelIndex sourceIndex = proxy->mapToSource(idx); - - QString cardName = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - QString providerId = - sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - - const auto reason = QString(tr("%1 %2 × \"%3\" (%4)")) - .arg(isIncrement ? tr("Added") : tr("Removed")) - .arg(1) - .arg(cardName) - .arg(providerId); - - emit requestDeckHistorySave(reason); - - int offset = isIncrement ? 1 : -1; - deckModel->offsetCountAtIndex(sourceIndex, offset); - - emit deckModified(); + if (isIncrement) { + deckStateManager->incrementCountAtIndex(idx); + } else { + deckStateManager->decrementCountAtIndex(idx); + } } void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point) 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 a50434f0b..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 @@ -28,22 +28,14 @@ class DeckEditorDeckDockWidget : public QDockWidget Q_OBJECT public: explicit DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent); - DeckLoader *deckLoader; + DeckListStyleProxy *proxy; - DeckListModel *deckModel; QTreeView *deckView; QComboBox *bannerCardComboBox; void createDeckDock(); ExactCard getCurrentCard(); void retranslateUi(); - QString getDeckName() - { - return nameEdit->text(); - } - QString getSimpleDeckName() - { - return nameEdit->text().simplified(); - } + QComboBox *getGroupByComboBox() { return activeGroupCriteriaComboBox; @@ -55,15 +47,10 @@ public: } public slots: - void cleanDeck(); void selectPrevCard(); void selectNextCard(); void updateBannerCardComboBox(); - void setDeck(const LoadedDeck &_deck); void syncDisplayWidgetsToModel(); - void sortDeckModelToDeckView(); - DeckLoader *getDeckLoader(); - const DeckList &getDeckList() const; void actAddCard(const ExactCard &card, const QString &zoneName); void actIncrementSelection(); void actDecrementCard(const ExactCard &card, QString zoneName); @@ -74,17 +61,12 @@ public slots: void initializeFormats(); signals: - void nameChanged(); - void commentsChanged(); - void hashChanged(); - void deckChanged(); - void deckModified(); - void requestDeckHistorySave(const QString &modificationReason); - void requestDeckHistoryClear(); - void cardChanged(const ExactCard &_card); + void selectedCardChanged(const ExactCard &card); private: AbstractTabDeckEditor *deckEditor; + DeckStateManager *deckStateManager; + DeckListHistoryManagerWidget *historyManagerWidget; KeySignals deckViewKeySignals; QLabel *nameLabel; @@ -107,18 +89,18 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; - [[nodiscard]] QModelIndexList getSelectedCardNodes() const; + DeckListModel *getModel() const; + [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); private slots: void decklistCustomMenu(QPoint point); - bool swapCard(const QModelIndex ¤tIndex); void updateCard(QModelIndex, const QModelIndex ¤t); - void updateName(const QString &name); - void updateComments(); - void setBannerCard(int); - void setTags(const QStringList &tags); - void syncDeckListBannerCardWithComboBox(); + void writeName(); + void writeComments(); + void writeBannerCard(int); + void applyActiveGroupCriteria(); + void setSelectedIndex(const QModelIndex &newCardIndex); void updateHash(); void refreshShortcuts(); void updateShowBannerCardComboBox(bool visible); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp index c68934ea6..d2d748753 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp @@ -1,10 +1,11 @@ #include "deck_list_history_manager_widget.h" -DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckListModel, +#include "deck_state_manager.h" + +DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager, DeckListStyleProxy *_styleProxy, - DeckListHistoryManager *manager, QWidget *parent) - : QWidget(parent), deckListModel(_deckListModel), styleProxy(_styleProxy), historyManager(manager) + : QWidget(parent), deckStateManager(_deckStateManager), styleProxy(_styleProxy) { layout = new QHBoxLayout(this); @@ -43,8 +44,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckL connect(historyList, &QListWidget::itemClicked, this, &DeckListHistoryManagerWidget::onListClicked); - connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, - &DeckListHistoryManagerWidget::refreshList); + connect(deckStateManager, &DeckStateManager::historyChanged, this, &DeckListHistoryManagerWidget::refreshList); refreshList(); retranslateUi(); @@ -58,15 +58,12 @@ void DeckListHistoryManagerWidget::retranslateUi() historyLabel->setText(tr("Click on an entry to revert to that point in the history.")); } -void DeckListHistoryManagerWidget::setDeckListModel(DeckListModel *_deckListModel) -{ - deckListModel = _deckListModel; -} - void DeckListHistoryManagerWidget::refreshList() { historyList->clear(); + DeckListHistoryManager *historyManager = deckStateManager->getHistoryManager(); + // Fill redo section first (oldest redo at top, newest redo closest to divider) const auto redoStack = historyManager->getRedoStack(); for (int i = 0; i < redoStack.size(); ++i) { // iterate forward @@ -98,36 +95,7 @@ void DeckListHistoryManagerWidget::refreshList() redoButton->setEnabled(historyManager->canRedo()); } -void DeckListHistoryManagerWidget::doUndo() -{ - if (!historyManager->canUndo()) { - return; - } - - historyManager->undo(deckListModel->getDeckList()); - deckListModel->rebuildTree(); - emit deckListModel->layoutChanged(); - emit requestDisplayWidgetSync(); - - refreshList(); -} - -void DeckListHistoryManagerWidget::doRedo() -{ - if (!historyManager->canRedo()) { - return; - } - - historyManager->redo(deckListModel->getDeckList()); - deckListModel->rebuildTree(); - - emit deckListModel->layoutChanged(); - emit requestDisplayWidgetSync(); - - refreshList(); -} - -void DeckListHistoryManagerWidget::onListClicked(QListWidgetItem *item) +void DeckListHistoryManagerWidget::onListClicked(const QListWidgetItem *item) { // Ignore non-selectable items (like divider) if (!(item->flags() & Qt::ItemIsSelectable)) { @@ -138,23 +106,24 @@ void DeckListHistoryManagerWidget::onListClicked(QListWidgetItem *item) int index = item->data(Qt::UserRole + 1).toInt(); if (mode == "redo") { - const auto redoStack = historyManager->getRedoStack(); + const auto redoStack = deckStateManager->getHistoryManager()->getRedoStack(); int steps = redoStack.size() - index; - for (int i = 0; i < steps; ++i) { - historyManager->redo(deckListModel->getDeckList()); - } + deckStateManager->redo(steps); } else if (mode == "undo") { - const auto undoStack = historyManager->getUndoStack(); - int steps = undoStack.size() - 1 - index; - for (int i = 0; i < steps + 1; ++i) { - historyManager->undo(deckListModel->getDeckList()); - } + const auto undoStack = deckStateManager->getHistoryManager()->getUndoStack(); + int steps = undoStack.size() - index; + deckStateManager->undo(steps); } - deckListModel->rebuildTree(); - - emit deckListModel->layoutChanged(); - emit requestDisplayWidgetSync(); - refreshList(); +} + +void DeckListHistoryManagerWidget::doUndo() +{ + deckStateManager->undo(); +} + +void DeckListHistoryManagerWidget::doRedo() +{ + deckStateManager->redo(); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h index 0e208ad2b..ab53912e2 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h @@ -14,6 +14,8 @@ #include #include +class DeckStateManager; + class DeckListHistoryManagerWidget : public QWidget { Q_OBJECT @@ -25,22 +27,19 @@ public slots: void retranslateUi(); public: - explicit DeckListHistoryManagerWidget(DeckListModel *deckListModel, + explicit DeckListHistoryManagerWidget(DeckStateManager *deckStateManager, DeckListStyleProxy *styleProxy, - DeckListHistoryManager *manager, QWidget *parent = nullptr); - void setDeckListModel(DeckListModel *_deckListModel); private slots: void refreshList(); - void onListClicked(QListWidgetItem *item); + void onListClicked(const QListWidgetItem *item); void doUndo(); void doRedo(); private: - DeckListModel *deckListModel; + DeckStateManager *deckStateManager; DeckListStyleProxy *styleProxy; - DeckListHistoryManager *historyManager; QHBoxLayout *layout; QAction *aUndo; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp new file mode 100644 index 000000000..628d33d51 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -0,0 +1,361 @@ +#include "deck_state_manager.h" + +#include +#include + +DeckStateManager::DeckStateManager(QObject *parent) + : QObject(parent), deckList(QSharedPointer(new DeckList)), + deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this)) +{ + connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] { + setModified(true); + emit historyChanged(); + }); + connect(deckListModel, &DeckListModel::rowsInserted, this, &DeckStateManager::uniqueCardsChanged); + connect(deckListModel, &DeckListModel::rowsRemoved, this, &DeckStateManager::uniqueCardsChanged); +} + +const DeckList &DeckStateManager::getDeckList() const +{ + return *deckList.get(); +} + +LoadedDeck DeckStateManager::toLoadedDeck() const +{ + return {getDeckList(), lastLoadInfo}; +} + +DeckList::Metadata const &DeckStateManager::getMetadata() const +{ + return deckList->getMetadata(); +} + +QString DeckStateManager::getSimpleDeckName() const +{ + return deckList->getMetadata().name.simplified(); +} + +QString DeckStateManager::getDeckHash() const +{ + return deckList->getDeckHash(); +} + +bool DeckStateManager::isModified() const +{ + return modified; +} + +void DeckStateManager::setModified(bool state) +{ + if (state == modified) { + return; + } + + modified = state; + emit isModifiedChanged(modified); +} + +bool DeckStateManager::isBlankNewDeck() const +{ + return !isModified() && deckList->isBlankDeck(); +} + +void DeckStateManager::replaceDeck(const LoadedDeck &deck) +{ + lastLoadInfo = deck.lastLoadInfo; + deckList = QSharedPointer(new DeckList(deck.deckList)); + deckListModel->setDeckList(deckList); + + historyManager->clear(); + + setModified(false); + emit deckReplaced(); +} + +void DeckStateManager::clearDeck() +{ + replaceDeck(LoadedDeck()); +} + +bool DeckStateManager::modifyDeck(const QString &reason, const std::function &operation) +{ + DeckListMemento memento = deckList->createMemento(reason); + bool success = operation(deckListModel); + + if (success) { + historyManager->save(memento); + doCardModified(); + } + + return success; +} + +QModelIndex DeckStateManager::modifyDeck(const QString &reason, + const std::function &operation) +{ + DeckListMemento memento = deckList->createMemento(reason); + QModelIndex idx = operation(deckListModel); + + if (idx.isValid()) { + historyManager->save(memento); + doCardModified(); + } + + return idx; +} + +void DeckStateManager::setName(const QString &name) +{ + QString previous = deckList->getName(); + if (previous == name) { + return; + } + + requestHistorySave(tr("Rename deck to \"%1\" from \"%2\"").arg(name).arg(previous)); + deckList->setName(name); + + doMetadataModified(); +} + +void DeckStateManager::setComments(const QString &comments) +{ + QString previous = deckList->getComments(); + if (previous == comments) { + return; + } + + requestHistorySave(tr("Updated comments (was %1 chars, now %2 chars)").arg(previous.size()).arg(comments.size())); + deckList->setComments(comments); + + doMetadataModified(); +} + +void DeckStateManager::setBannerCard(const CardRef &bannerCard) +{ + CardRef previous = deckList->getBannerCard(); + if (previous == bannerCard) { + return; + } + + requestHistorySave(tr("Set banner card to %1 (%2)").arg(bannerCard.name).arg(bannerCard.providerId)); + deckList->setBannerCard(bannerCard); + + doMetadataModified(); +} + +void DeckStateManager::setTags(const QStringList &tags) +{ + QStringList previous = deckList->getTags(); + if (previous == tags) { + return; + } + + requestHistorySave(tr("Tags changed")); + deckList->setTags(tags); + + doMetadataModified(); +} + +void DeckStateManager::setFormat(const QString &format) +{ + if (deckList->getMetadata().gameFormat == format) { + return; + } + + requestHistorySave(tr("Set format to %1").arg(format)); + deckListModel->setActiveFormat(format); + + doMetadataModified(); +} + +QModelIndex DeckStateManager::addCard(const ExactCard &card, const QString &zoneName) +{ + if (!card) { + return {}; + } + + QString reason = tr("Added (%1): %2 (%3) %4") + .arg(zoneName, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), + card.getPrinting().getProperty("num")); + + QModelIndex idx = modifyDeck(reason, [&card, &zoneName](auto model) { return model->addCard(card, zoneName); }); + + if (idx.isValid()) { + emit focusIndexChanged(idx); + } + + return idx; +} + +QModelIndex DeckStateManager::decrementCard(const ExactCard &card, const QString &zoneName) +{ + if (!card) + return {}; + + QString providerId = card.getPrinting().getUuid(); + QString collectorNumber = card.getPrinting().getProperty("num"); + + QModelIndex idx = deckListModel->findCard(card.getName(), zoneName, providerId, collectorNumber); + if (!idx.isValid()) { + return {}; + } + + bool success = offsetCountAtIndex(idx, false); + + if (!success) { + return {}; + } + + if (idx.isValid()) { + emit focusIndexChanged(idx); + } + + return idx; +} + +static bool doSwapCard(DeckListModel *model, + const QModelIndex &idx, + const QString &cardName, + const QString &providerId, + const QString &otherZone) +{ + bool success = model->offsetCountAtIndex(idx, -1); + if (!success) { + return false; + } + + if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId})) { + model->addCard(card, otherZone); + } else { + // Third argument (true) says create the card no matter what, even if not in DB + model->addPreferredPrintingCard(cardName, otherZone, true); + } + + return true; +} + +bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) +{ + if (!idx.isValid()) + return false; + + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); + QModelIndex gparent = idx.parent().parent(); + + if (!gparent.isValid()) + return false; + + QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; + + QString reason = tr("Moved to %1 1 × \"%2\" (%3)") // + .arg(otherZoneName) + .arg(cardName) + .arg(providerId); + + return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) { + return doSwapCard(model, idx, cardName, providerId, otherZoneName); + }); +} + +bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx) +{ + if (!idx.isValid() || deckListModel->hasChildren(idx)) { + return false; + } + + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + + QString reason = tr("Removed \"%1\" (all copies)").arg(cardName); + + return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); }); +} + +bool DeckStateManager::incrementCountAtIndex(const QModelIndex &idx) +{ + return offsetCountAtIndex(idx, 1); +} + +bool DeckStateManager::decrementCountAtIndex(const QModelIndex &idx) +{ + return offsetCountAtIndex(idx, -1); +} + +bool DeckStateManager::offsetCountAtIndex(const QModelIndex &idx, int offset) +{ + if (!idx.isValid()) { + return false; + } + + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); + + QString reason = tr("%1 1 × \"%2\" (%3)") // + .arg(offset > 0 ? tr("Added") : tr("Removed")) + .arg(cardName) + .arg(providerId); + + return modifyDeck(reason, [&idx, &offset](auto model) { return model->offsetCountAtIndex(idx, offset); }); +} + +void DeckStateManager::undo(int steps) +{ + if (!historyManager->canUndo()) { + return; + } + + for (int i = 0; i < steps; i++) { + if (!historyManager->canUndo()) { + continue; + } + historyManager->undo(deckList.get()); + } + + deckListModel->rebuildTree(); + + emit deckListModel->layoutChanged(); +} + +void DeckStateManager::redo(int steps) +{ + if (!historyManager->canRedo()) { + return; + } + + for (int i = 0; i < steps; i++) { + if (!historyManager->canRedo()) { + continue; + } + historyManager->redo(deckList.get()); + } + + deckListModel->rebuildTree(); + + emit deckListModel->layoutChanged(); +} + +void DeckStateManager::requestHistorySave(const QString &reason) +{ + historyManager->save(deckList->createMemento(reason)); +} + +/** + * @brief Handles updating state and emitting signals whenever the cards are modified + */ +void DeckStateManager::doCardModified() +{ + setModified(true); + emit cardModified(); + emit deckModified(); +} + +/** + * @brief Handles updating state and emitting signals whenever the metadata is modified + */ +void DeckStateManager::doMetadataModified() +{ + setModified(true); + emit metadataModified(); + emit deckModified(); +} diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h new file mode 100644 index 000000000..0f3ba3255 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -0,0 +1,297 @@ +#ifndef COCKATRICE_DECK_STATE_MANAGER_H +#define COCKATRICE_DECK_STATE_MANAGER_H + +#include "../../deck_loader/loaded_deck.h" +#include "deck_list_model.h" + +#include +#include + +class DeckListHistoryManager; + +/** + * @brief This class centralizes the management of the state of the deck in the deck editor tab. + * It is responsible for owning and managing the DeckListModel, underlying DeckList, load info, and edit history. + * + * Although this class provides getters for the underlying DeckListModel, you should generally refrain from directly + * modifying the returned model. Outside modifications to the deck state should be done through @link + * DeckStateManager::modifyDeck and the metadata setters. + * Those methods ensure that the history is recorded and correct signals are emitted. + */ +class DeckStateManager : public QObject +{ + Q_OBJECT + + LoadedDeck::LoadInfo lastLoadInfo; + QSharedPointer deckList; + DeckListModel *deckListModel; + DeckListHistoryManager *historyManager; + + bool modified = false; + +public: + explicit DeckStateManager(QObject *parent = nullptr); + + /** + * Gets the underlying HistoryManager. + * @return The DeckListHistoryManager instance + */ + DeckListHistoryManager *getHistoryManager() const + { + return historyManager; + } + + /** + * @brief Gets the underlying DeckListModel. + * You should generally refrain modifying the returned model directly. + * However, it's fine (and intended) to perform queries on the returned model. + * @return The DeckListModel instance + */ + DeckListModel *getModel() const + { + return deckListModel; + } + + /** + * @brief Gets a view of the current deck. + */ + const DeckList &getDeckList() const; + + /** + * @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo. + * + * @return A new LoadedDeck instance. + */ + LoadedDeck toLoadedDeck() const; + + /** + * @brief Gets a view of the metadata in the DeckList + */ + DeckList::Metadata const &getMetadata() const; + + /** + * @brief Gets the deck's simplified name. + */ + QString getSimpleDeckName() const; + + /** + * @brief Gets the deck hash. + */ + QString getDeckHash() const; + + /** + * @brief Checks if the deck has been modified since it was last saved + */ + bool isModified() const; + + /** + * @brief Sets the new isModified state, emitting a signal if the state changed. + * This class will automatically update its isModified state, but you may need to set it manually to handle, for + * example, saving. + * @param state The state + */ + void setModified(bool state); + + /** + * @brief Checks if the deck state is as if it was a new deck + */ + bool isBlankNewDeck() const; + + /** + * @brief Overwrites the current deck with a new deck, resetting all history + * @param deck The new deck. + */ + void replaceDeck(const LoadedDeck &deck); + + /** + * @brief Resets the deck to a blank new deck, resetting all history. + */ + void clearDeck(); + + /** + * @brief Sets the lastLoadInfo. + * @param loadInfo The lastLoadInfo + */ + void setLastLoadInfo(const LoadedDeck::LoadInfo &loadInfo) + { + lastLoadInfo = loadInfo; + } + + /** + * @brief Modifies the cards in the deck, in a wrapped operation that is saved to the history. + * + * The operation is a function that accepts a DeckListModel that it operates upon, and returns a bool. + * + * This method will pass the underlying DeckListModel into the operation function. The function can call methods on + * the model to modify the deck. + * The function should return a bool to indicate success/failure. + * + * If the operation returns true, the state of the deck before the operation is ran is saved to the history, and the + * isModified state is updated. + * If the operation returns false, the history and isModified state is not updated. + * + * Note that even if the operation fails, any modifications to the model will already have been made. + * It's recommended for the operation to always return true if any modification has already been made to the model, + * as not doing that may cause the state to become desynced. + * + * @param reason The reason to display in the history + * @param operation The modification operation. + * @return The bool returned from the operation + */ + bool modifyDeck(const QString &reason, const std::function &operation); + + /** + * @brief Modifies the cards in the deck, in a wrapped operation that is saved to the history. + * + * The operation is a function that accepts a DeckListModel that it operates upon, and returns a QModelIndex. + * If the index is invalid, then the operation is considered to be a failure. + * + * See the other @link DeckStateManager::modifyDeck for more info about the behavior of this method. + * + * @param reason The reason to display in the history + * @param operation The modification operation. + * @return The QModelIndex returned from the operation + */ + QModelIndex modifyDeck(const QString &reason, const std::function &operation); + + /// @name Metadata setters + /// @brief These methods set the metadata. Will no-op if the new value is the same as the current value. + /// Saves the operation to history if successful. + ///@{ + void setName(const QString &name); + void setComments(const QString &comments); + void setBannerCard(const CardRef &bannerCard); + void setTags(const QStringList &tags); + void setFormat(const QString &format); + ///@} + + /** + * @brief Adds the given card to the given zone. + * Saves the operation to history if successful. + * + * @param card The card to add + * @param zoneName The zone to add the card to + * @return The index of the added card + */ + QModelIndex addCard(const ExactCard &card, const QString &zoneName); + + /** + * @brief Removes 1 copy of the given card from the given zone. + * Saves the operation to history if successful. + * + * @param card The card to remove + * @param zoneName The zone to remove the card from + * @return The index of the removed card. Will be invalid if the last copy was removed. + */ + QModelIndex decrementCard(const ExactCard &card, const QString &zoneName); + + /** + * @brief Swaps one copy of the card at the given index between the maindeck and sideboard. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool swapCardAtIndex(const QModelIndex &idx); + + /** + * @brief Removes all copies of the card at the given index. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool removeCardAtIndex(const QModelIndex &idx); + + /** + * @brief Increments the number of copies of the card at the given index by 1. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool incrementCountAtIndex(const QModelIndex &idx); + + /** + * @brief Decrements the number of copies of the card at the given index by 1. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool decrementCountAtIndex(const QModelIndex &idx); + + /** + * Undoes n steps of the history, setting the decklist state and updating the current step in the historyManager. + * @param steps Number of steps to undo. + */ + void undo(int steps = 1); + + /** + * Redoes n steps of the history, setting the decklist state and updating the current step in the historyManager. + * @param steps Number of steps to redo. + */ + void redo(int steps = 1); + +public slots: + /** + * Saves the current decklist state to history. + * @param reason The reason that is shown in the history. + */ + void requestHistorySave(const QString &reason); + +private: + bool offsetCountAtIndex(const QModelIndex &idx, int offset); + void doCardModified(); + void doMetadataModified(); + +signals: + /** + * A modification has been made to the cards in the deck + */ + void cardModified(); + + /** + * A card that wasn't previously in the deck was added to the deck, or the last copy of a card was removed from the + * deck. + */ + void uniqueCardsChanged(); + + /** + * A modification has been made to the metadata in the deck + */ + void metadataModified(); + + /** + * A modification has been made to the cards or metadata in the deck + */ + void deckModified(); + + /** + * The history has been greatly changed and needs to be reloaded. + */ + void historyChanged(); + + /** + * The deck has been completely changed. + */ + void deckReplaced(); + + /** + * The isModified state of the deck has changed + * @param isModified the new state + */ + void isModifiedChanged(bool isModified); + + /** + * The selected card on any views connected to this deck should be changed to this index. + * @param index The model index + */ + void focusIndexChanged(QModelIndex index); +}; + +#endif // COCKATRICE_DECK_STATE_MANAGER_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp index 587407065..6ed6b67a4 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp @@ -2,6 +2,7 @@ #include "../../deck_loader/card_node_function.h" #include "../../deck_loader/deck_loader.h" +#include "../deck_editor/deck_state_manager.h" #include "../interface/widgets/cards/card_info_picture_widget.h" #include "../interface/widgets/general/layout_containers/flow_widget.h" @@ -21,7 +22,8 @@ #include #include -DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckListModel *_model) : QDialog(parent), model(_model) +DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *deckStateManger) + : QDialog(parent), deckStateManager(deckStateManger) { setMinimumSize(500, 500); setAcceptDrops(true); @@ -165,36 +167,39 @@ void DlgSelectSetForCards::actOK() if (modifiedSetsAndCardsMap.isEmpty()) { accept(); // Nothing to do - } else { - emit deckAboutToBeModified(tr("Bulk modified printings.")); + return; } - for (QString modifiedSet : modifiedSetsAndCardsMap.keys()) { - for (QString card : modifiedSetsAndCardsMap.value(modifiedSet)) { - swapPrinting(model, modifiedSet, card); + auto bulkModify = [&modifiedSetsAndCardsMap](DeckListModel *model) { + for (QString modifiedSet : modifiedSetsAndCardsMap.keys()) { + for (QString card : modifiedSetsAndCardsMap.value(modifiedSet)) { + swapPrinting(model, modifiedSet, card); + } } - } + return true; + }; + + deckStateManager->modifyDeck(tr("Bulk modified printings."), bulkModify); - if (!modifiedSetsAndCardsMap.isEmpty()) { - emit deckModified(); - } accept(); } void DlgSelectSetForCards::actClear() { - emit deckAboutToBeModified(tr("Cleared all printing information.")); - model->forEachCard(CardNodeFunction::ClearPrintingData()); - emit deckModified(); + deckStateManager->modifyDeck(tr("Cleared all printing information."), [](auto model) { + model->forEachCard(CardNodeFunction::ClearPrintingData()); + return true; + }); accept(); } void DlgSelectSetForCards::actSetAllToPreferred() { - emit deckAboutToBeModified(tr("Set all printings to preferred.")); - model->forEachCard(CardNodeFunction::ClearPrintingData()); - model->forEachCard(CardNodeFunction::SetProviderIdToPreferred()); - emit deckModified(); + deckStateManager->modifyDeck(tr("Set all printings to preferred."), [](auto model) { + model->forEachCard(CardNodeFunction::ClearPrintingData()); + model->forEachCard(CardNodeFunction::SetProviderIdToPreferred()); + return true; + }); accept(); } @@ -227,10 +232,8 @@ void DlgSelectSetForCards::sortSetsByCount() QMap DlgSelectSetForCards::getSetsForCards() { QMap setCounts; - if (!model) - return setCounts; - QList cardNames = model->getCardNames(); + QList cardNames = deckStateManager->getModel()->getCardNames(); for (auto cardName : cardNames) { CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(cardName); @@ -269,7 +272,7 @@ void DlgSelectSetForCards::updateCardLists() } } - QList cardNames = model->getCardNames(); + QList cardNames = deckStateManager->getModel()->getCardNames(); for (auto cardName : cardNames) { bool found = false; @@ -351,10 +354,8 @@ void DlgSelectSetForCards::dropEvent(QDropEvent *event) QMap DlgSelectSetForCards::getCardsForSets() { QMap setCards; - if (!model) - return setCards; - QList cardNames = model->getCardNames(); + QList cardNames = deckStateManager->getModel()->getCardNames(); for (auto cardName : cardNames) { CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(cardName); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h index 5cdef5a30..795366b57 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h @@ -18,6 +18,7 @@ #include #include +class DeckStateManager; class SetEntryWidget; // Forward declaration class DlgSelectSetForCards : public QDialog @@ -25,7 +26,7 @@ class DlgSelectSetForCards : public QDialog Q_OBJECT public: - explicit DlgSelectSetForCards(QWidget *parent, DeckListModel *_model); + explicit DlgSelectSetForCards(QWidget *parent, DeckStateManager *deckStateManager); void retranslateUi(); void sortSetsByCount(); QMap getCardsForSets(); @@ -37,7 +38,6 @@ public: signals: void widgetOrderChanged(); void orderChanged(); - void deckAboutToBeModified(const QString &reason); void deckModified(); public slots: @@ -61,7 +61,7 @@ private: QLabel *modifiedCardsLabel; QWidget *listContainer; QListWidget *listWidget; - DeckListModel *model; + DeckStateManager *deckStateManager; QMap setEntries; QPushButton *clearButton; QPushButton *setAllToPreferredButton; 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/printing_selector/all_zones_card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp index d8bd88b37..b1346e4fd 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp @@ -11,16 +11,12 @@ * UI elements for managing card counts in both the mainboard and sideboard zones. * * @param parent The parent widget. - * @param deckEditor Pointer to the TabDeckEditor. - * @param deckModel Pointer to the DeckListModel. - * @param deckView Pointer to the QTreeView for the deck display. + * @param deckStateManager Pointer to the DeckStateManager * @param cardSizeSlider Pointer to the QSlider used for dynamic font resizing. * @param rootCard The root card for the widget. */ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard) : QWidget(parent), cardSizeSlider(cardSizeSlider) @@ -32,11 +28,9 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, setContentsMargins(5, 5, 5, 5); // Padding around the text zoneLabelMainboard = new ShadowBackgroundLabel(this, tr("Mainboard")); - buttonBoxMainboard = - new CardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard, DECK_ZONE_MAIN); + buttonBoxMainboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_MAIN); zoneLabelSideboard = new ShadowBackgroundLabel(this, tr("Sideboard")); - buttonBoxSideboard = - new CardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard, DECK_ZONE_SIDE); + buttonBoxSideboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_SIDE); layout->addWidget(zoneLabelMainboard, 0, Qt::AlignHCenter | Qt::AlignBottom); layout->addWidget(buttonBoxMainboard, 0, Qt::AlignHCenter | Qt::AlignTop); diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h index 6ce10cf2e..5a03c5f4a 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h @@ -18,9 +18,7 @@ class AllZonesCardAmountWidget : public QWidget Q_OBJECT public: explicit AllZonesCardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard); int getMainboardAmount(); diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp index 623b797a7..62c8e2d60 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -1,5 +1,7 @@ #include "card_amount_widget.h" +#include "../deck_editor/deck_state_manager.h" + #include #include @@ -7,22 +9,17 @@ * @brief Constructs a widget for displaying and controlling the card count in a specific zone. * * @param parent The parent widget. - * @param deckEditor Pointer to the TabDeckEditor instance. - * @param deckModel Pointer to the DeckListModel instance. - * @param deckView Pointer to the QTreeView displaying the deck. * @param cardSizeSlider Pointer to the QSlider for adjusting font size. * @param rootCard The root card to manage within the widget. * @param zoneName The zone name (e.g., DECK_ZONE_MAIN or DECK_ZONE_SIDE). */ CardAmountWidget::CardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard, const QString &zoneName) - : QWidget(parent), deckEditor(deckEditor), deckModel(deckModel), deckView(deckView), cardSizeSlider(cardSizeSlider), - rootCard(rootCard), zoneName(zoneName), hovered(false) + : QWidget(parent), deckStateManager(deckStateManager), cardSizeSlider(cardSizeSlider), rootCard(rootCard), + zoneName(zoneName), hovered(false) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -56,15 +53,10 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, layout->addWidget(incrementButton); // React to model changes - connect(deckModel, &DeckListModel::dataChanged, this, &CardAmountWidget::updateCardCount); - connect(deckModel, &QAbstractItemModel::rowsRemoved, this, &CardAmountWidget::updateCardCount); + connect(deckStateManager, &DeckStateManager::cardModified, this, &CardAmountWidget::updateCardCount); // Connect slider for dynamic font size adjustment connect(cardSizeSlider, &QSlider::valueChanged, this, &CardAmountWidget::adjustFontSize); - - if (deckEditor) { - connect(this, &CardAmountWidget::deckModified, deckEditor, &AbstractTabDeckEditor::onDeckHistorySaveRequested); - } } /** @@ -151,9 +143,7 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model, // Check if a card without a providerId already exists in the deckModel and replace it, if so. if (existing.isValid() && existing != newCardIndex && replaceProviderless) { - for (int i = 0; i < extraCopies; i++) { - model->addCard(rootCard, zone); - } + model->offsetCountAtIndex(newCardIndex, extraCopies); model->removeRow(existing.row(), existing.parent()); } @@ -170,7 +160,7 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model, void CardAmountWidget::addPrinting(const QString &zone) { // Check if we will need to add extra copies due to replacing copies without providerIds - QModelIndex existing = deckModel->findCard(rootCard.getName(), zone); + QModelIndex existing = deckStateManager->getModel()->findCard(rootCard.getName(), zone); int extraCopies = 0; bool replacingProviderless = false; @@ -194,15 +184,13 @@ void CardAmountWidget::addPrinting(const QString &zone) .arg(rootCard.getPrinting().getUuid()) .arg(replacingProviderless ? " (replaced providerless printings)" : ""); - emit deckModified(reason); - // Add the card and expand the list UI - auto newCardIndex = addAndReplacePrintings(deckModel, existing, rootCard, zone, extraCopies, replacingProviderless); + QModelIndex newCardIndex = deckStateManager->modifyDeck(reason, [&](auto model) { + return addAndReplacePrintings(model, existing, rootCard, zone, extraCopies, replacingProviderless); + }); if (newCardIndex.isValid()) { - deckView->setCurrentIndex(newCardIndex); - deckView->setFocus(Qt::FocusReason::MouseFocusReason); - deckEditor->setModified(true); + emit deckStateManager->focusIndexChanged(newCardIndex); } } @@ -252,13 +240,11 @@ void CardAmountWidget::decrementCardHelper(const QString &zone) .arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard") .arg(rootCard.getPrinting().getUuid()); - emit deckModified(reason); - - QModelIndex idx = deckModel->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), + deckStateManager->modifyDeck(reason, [this, &zone](auto model) { + QModelIndex idx = model->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), rootCard.getPrinting().getProperty("num")); - - deckModel->offsetCountAtIndex(idx, -1); - deckEditor->setModified(true); + return model->offsetCountAtIndex(idx, -1); + }); } /** @@ -275,7 +261,7 @@ int CardAmountWidget::countCardsInZone(const QString &deckZone) return 0; // Cards without uuids/providerIds CANNOT match another card, they are undefined for us. } - QList cards = deckModel->getCardsForZone(deckZone); + QList cards = deckStateManager->getModel()->getCardsForZone(deckZone); return std::count_if(cards.cbegin(), cards.cend(), [&uuid](const ExactCard &card) { return card.getPrinting().getUuid() == uuid; }); diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h index b4704cede..983416782 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -27,9 +27,7 @@ signals: public: explicit CardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard, const QString &zoneName); @@ -44,9 +42,7 @@ protected: void showEvent(QShowEvent *event) override; private: - AbstractTabDeckEditor *deckEditor; - DeckListModel *deckModel; - QTreeView *deckView; + DeckStateManager *deckStateManager; QSlider *cardSizeSlider; ExactCard rootCard; QString zoneName; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index e64d6a009..95d6b2cdf 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -3,6 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/widgets/dialogs/dlg_select_set_for_cards.h" +#include "../deck_editor/deck_state_manager.h" #include "printing_selector_card_display_widget.h" #include "printing_selector_card_search_widget.h" #include "printing_selector_card_selection_widget.h" @@ -21,12 +22,9 @@ * * @param parent The parent widget for the PrintingSelector. * @param deckEditor The TabDeckEditor instance used for managing the deck. - * @param deckModel The DeckListModel instance that provides data for the deck's contents. - * @param deckView The QTreeView instance used to display the deck and its contents. */ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deckEditor) - : QWidget(parent), deckEditor(_deckEditor), deckModel(deckEditor->deckDockWidget->deckModel), - deckView(deckEditor->deckDockWidget->deckView) + : QWidget(parent), deckEditor(_deckEditor), deckStateManager(_deckEditor->deckStateManager) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout = new QVBoxLayout(this); @@ -74,13 +72,12 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck layout->addWidget(flowWidget); - cardSelectionBar = new PrintingSelectorCardSelectionWidget(this); + cardSelectionBar = new PrintingSelectorCardSelectionWidget(this, deckStateManager); cardSelectionBar->setVisible(SettingsCache::instance().getPrintingSelectorNavigationButtonsVisible()); layout->addWidget(cardSelectionBar); // Connect deck model data change signal to update display - connect(deckModel, &DeckListModel::rowsInserted, this, &PrintingSelector::printingsInDeckChanged); - connect(deckModel, &DeckListModel::rowsRemoved, this, &PrintingSelector::printingsInDeckChanged); + connect(deckStateManager, &DeckStateManager::uniqueCardsChanged, this, &PrintingSelector::printingsInDeckChanged); retranslateUi(); } @@ -152,7 +149,8 @@ void PrintingSelector::getAllSetsForCurrentCard() QList printingsToUse; if (SettingsCache::instance().getBumpSetsWithCardsInDeckToTop()) { - printingsToUse = sortToolBar->prependPrintingsInDeck(filteredPrintings, selectedCard, deckModel); + printingsToUse = + sortToolBar->prependPrintingsInDeck(filteredPrintings, selectedCard, deckStateManager->getModel()); } else { printingsToUse = filteredPrintings; } @@ -164,7 +162,7 @@ void PrintingSelector::getAllSetsForCurrentCard() connect(widgetLoadingBufferTimer, &QTimer::timeout, this, [=, this]() mutable { for (int i = 0; i < BATCH_SIZE && currentIndex < printingsToUse.size(); ++i, ++currentIndex) { auto card = ExactCard(selectedCard, printingsToUse[currentIndex]); - auto *cardDisplayWidget = new PrintingSelectorCardDisplayWidget(this, deckEditor, deckModel, deckView, + auto *cardDisplayWidget = new PrintingSelectorCardDisplayWidget(this, deckEditor, deckStateManager, cardSizeWidget->getSlider(), card); flowWidget->addWidget(cardDisplayWidget); cardDisplayWidget->clampSetNameToPicture(); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index e34ce3fe6..e1c07addf 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -21,6 +21,7 @@ #define BATCH_SIZE 10 +class DeckStateManager; class PrintingSelectorCardSearchWidget; class PrintingSelectorCardSelectionWidget; class PrintingSelectorCardSortingWidget; @@ -35,15 +36,6 @@ public: void setCard(const CardInfoPtr &newCard); void getAllSetsForCurrentCard(); - [[nodiscard]] DeckListModel *getDeckModel() const - { - return deckModel; - } - - [[nodiscard]] AbstractTabDeckEditor *getDeckEditor() const - { - return deckEditor; - } public slots: void retranslateUi(); @@ -75,8 +67,7 @@ private: CardSizeWidget *cardSizeWidget; PrintingSelectorCardSelectionWidget *cardSelectionBar; AbstractTabDeckEditor *deckEditor; - DeckListModel *deckModel; - QTreeView *deckView; + DeckStateManager *deckStateManager; CardInfoPtr selectedCard; QTimer *widgetLoadingBufferTimer; int currentIndex = 0; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp index 86d6659a8..92cf2437c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp @@ -18,15 +18,13 @@ * * @param parent The parent widget for this display. * @param deckEditor The TabDeckEditor instance for deck management. - * @param deckModel The DeckListModel instance providing deck data. - * @param deckView The QTreeView instance displaying the deck. + * @param deckStateManager The DeckStateManager instance providing deck data. * @param cardSizeSlider The slider controlling the size of the displayed card. * @param rootCard The root card object, representing the card to be displayed. */ PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard) : QWidget(parent) @@ -36,8 +34,7 @@ PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *pa setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // Create the overlay widget for the card display - overlayWidget = - new PrintingSelectorCardOverlayWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard); + overlayWidget = new PrintingSelectorCardOverlayWidget(this, deckEditor, deckStateManager, cardSizeSlider, rootCard); connect(overlayWidget, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this, [this]() { emit cardPreferenceChanged(); }); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h index 608c2df5c..2637b0e57 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h @@ -21,8 +21,7 @@ class PrintingSelectorCardDisplayWidget : public QWidget public: PrintingSelectorCardDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index 04ab07a59..ac36f2cf4 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -22,15 +22,13 @@ * * @param parent The parent widget for this overlay. * @param _deckEditor The TabDeckEditor instance for deck management. - * @param deckModel The DeckListModel instance providing deck data. - * @param deckView The QTreeView instance displaying the deck. + * @param deckStateManager The DeckStateManager instance providing deck data. * @param cardSizeSlider The slider controlling the size of the card. * @param _rootCard The root card object that contains information about the card. */ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &_rootCard) : QWidget(parent), deckEditor(_deckEditor), rootCard(_rootCard) @@ -58,8 +56,7 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa updatePinBadgeVisibility(); // Add AllZonesCardAmountWidget - allZonesCardAmountWidget = - new AllZonesCardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, _rootCard); + allZonesCardAmountWidget = new AllZonesCardAmountWidget(this, deckStateManager, cardSizeSlider, _rootCard); allZonesCardAmountWidget->raise(); // Ensure it's on top of the picture // Set initial visibility based on amounts diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h index 3bd5ce247..8550612bd 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h @@ -20,8 +20,7 @@ class PrintingSelectorCardOverlayWidget : public QWidget public: explicit PrintingSelectorCardOverlayWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, - DeckListModel *_deckModel, - QTreeView *_deckView, + DeckStateManager *_deckStateManager, QSlider *_cardSizeSlider, const ExactCard &_rootCard); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp index 317ce83b6..2eb2ef245 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp @@ -11,7 +11,9 @@ * * @param parent The parent PrintingSelector widget responsible for managing card selection. */ -PrintingSelectorCardSelectionWidget::PrintingSelectorCardSelectionWidget(PrintingSelector *parent) : parent(parent) +PrintingSelectorCardSelectionWidget::PrintingSelectorCardSelectionWidget(PrintingSelector *parent, + DeckStateManager *deckStateManager) + : parent(parent), deckStateManager(deckStateManager) { cardSelectionBarLayout = new QHBoxLayout(this); cardSelectionBarLayout->setContentsMargins(9, 0, 9, 0); @@ -48,12 +50,6 @@ void PrintingSelectorCardSelectionWidget::connectSignals() void PrintingSelectorCardSelectionWidget::selectSetForCards() { - auto *setSelectionDialog = new DlgSelectSetForCards(nullptr, parent->getDeckModel()); - connect(setSelectionDialog, &DlgSelectSetForCards::deckAboutToBeModified, parent->getDeckEditor(), - &AbstractTabDeckEditor::onDeckHistorySaveRequested); - connect(setSelectionDialog, &DlgSelectSetForCards::deckModified, parent->getDeckEditor(), - &AbstractTabDeckEditor::onDeckModified); - if (!setSelectionDialog->exec()) { - return; - } + auto *setSelectionDialog = new DlgSelectSetForCards(nullptr, deckStateManager); + setSelectionDialog->exec(); } diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h index a1176a76c..ecd5c83e3 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h @@ -18,7 +18,7 @@ class PrintingSelectorCardSelectionWidget : public QWidget Q_OBJECT public: - explicit PrintingSelectorCardSelectionWidget(PrintingSelector *parent); + explicit PrintingSelectorCardSelectionWidget(PrintingSelector *parent, DeckStateManager *deckStateManager); void connectSignals(); @@ -27,6 +27,7 @@ public slots: private: PrintingSelector *parent; + DeckStateManager *deckStateManager; QHBoxLayout *cardSelectionBarLayout; QPushButton *previousCardButton; QPushButton *selectSetForCardsButton; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp index af7cedbeb..725e5df90 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp @@ -183,7 +183,7 @@ QList PrintingSelectorCardSortingWidget::prependPinnedPrintings(co */ QList PrintingSelectorCardSortingWidget::prependPrintingsInDeck(const QList &printings, const CardInfoPtr &selectedCard, - DeckListModel *deckModel) + const DeckListModel *deckModel) { if (!selectedCard) { return {}; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h index d0faea4ac..b5a00b81e 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h @@ -23,7 +23,7 @@ public: QList prependPinnedPrintings(const QList &printings, const QString &cardName); QList prependPrintingsInDeck(const QList &printings, const CardInfoPtr &selectedCard, - DeckListModel *deckModel); + const DeckListModel *deckModel); public slots: void updateSortOrder(); 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/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index cb2002199..29bcc1ab1 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -12,6 +12,7 @@ #include "../../../client/settings/cache_settings.h" #include "../client/network/interfaces/deck_stats_interface.h" #include "../client/network/interfaces/tapped_out_interface.h" +#include "../deck_editor/deck_state_manager.h" #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/pixel_map_generator.h" #include "../interface/widgets/dialogs/dlg_load_deck.h" @@ -52,7 +53,7 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta { setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); - historyManager = new DeckListHistoryManager(this); + deckStateManager = new DeckStateManager(this); databaseDisplayDockWidget = new DeckEditorDatabaseDisplayWidget(this); deckDockWidget = new DeckEditorDeckDockWidget(this); @@ -64,14 +65,8 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta }); // Connect deck signals to this tab - connect(deckDockWidget, &DeckEditorDeckDockWidget::deckChanged, this, &AbstractTabDeckEditor::onDeckChanged); - connect(deckDockWidget, &DeckEditorDeckDockWidget::deckModified, this, &AbstractTabDeckEditor::onDeckModified); - connect(deckDockWidget, &DeckEditorDeckDockWidget::requestDeckHistorySave, this, - &AbstractTabDeckEditor::onDeckHistorySaveRequested); - connect(deckDockWidget, &DeckEditorDeckDockWidget::requestDeckHistoryClear, this, - &AbstractTabDeckEditor::onDeckHistoryClearRequested); - connect(deckDockWidget, &DeckEditorDeckDockWidget::cardChanged, this, &AbstractTabDeckEditor::updateCard); - connect(this, &AbstractTabDeckEditor::decrementCard, deckDockWidget, &DeckEditorDeckDockWidget::actDecrementCard); + connect(deckStateManager, &DeckStateManager::isModifiedChanged, this, &AbstractTabDeckEditor::onDeckModified); + connect(deckDockWidget, &DeckEditorDeckDockWidget::selectedCardChanged, this, &AbstractTabDeckEditor::updateCard); // Connect database display signals to this tab connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, this, @@ -107,7 +102,6 @@ void AbstractTabDeckEditor::updateCard(const ExactCard &card) /** @brief Placeholder: called when the deck changes. */ void AbstractTabDeckEditor::onDeckChanged() { - historyManager->clear(); } /** @@ -115,24 +109,8 @@ void AbstractTabDeckEditor::onDeckChanged() */ void AbstractTabDeckEditor::onDeckModified() { - setModified(!isBlankNewDeck()); - deckMenu->setSaveStatus(!isBlankNewDeck()); -} - -/** - * @brief Marks the tab as modified and updates the save menu status. - */ -void AbstractTabDeckEditor::onDeckHistorySaveRequested(const QString &modificationReason) -{ - historyManager->save(deckDockWidget->getDeckList().createMemento(modificationReason)); -} - -/** - * @brief Marks the tab as modified and updates the save menu status. - */ -void AbstractTabDeckEditor::onDeckHistoryClearRequested() -{ - historyManager->clear(); + deckMenu->setSaveStatus(!deckStateManager->isBlankNewDeck()); + emit tabTextChanged(this, getTabText()); } /** @@ -142,7 +120,7 @@ void AbstractTabDeckEditor::onDeckHistoryClearRequested() */ void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName) { - deckDockWidget->actAddCard(card, zoneName); + deckStateManager->addCard(card, zoneName); databaseDisplayDockWidget->searchEdit->setSelection(0, databaseDisplayDockWidget->searchEdit->text().length()); } @@ -170,13 +148,13 @@ void AbstractTabDeckEditor::actAddCardToSideboard(const ExactCard &card) /** @brief Decrements a card from the main deck. */ void AbstractTabDeckEditor::actDecrementCard(const ExactCard &card) { - emit decrementCard(card, DECK_ZONE_MAIN); + deckStateManager->decrementCard(card, DECK_ZONE_MAIN); } /** @brief Decrements a card from the sideboard. */ void AbstractTabDeckEditor::actDecrementCardFromSideboard(const ExactCard &card) { - emit decrementCard(card, DECK_ZONE_SIDE); + deckStateManager->decrementCard(card, DECK_ZONE_SIDE); } /** @@ -198,45 +176,13 @@ void AbstractTabDeckEditor::openDeck(const LoadedDeck &deck) */ void AbstractTabDeckEditor::setDeck(const LoadedDeck &_deck) { - deckDockWidget->setDeck(_deck); - CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(getDeckList().getCardRefList())); - setModified(false); + deckStateManager->replaceDeck(_deck); + CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(_deck.deckList.getCardRefList())); aDeckDockVisible->setChecked(true); deckDockWidget->setVisible(aDeckDockVisible->isChecked()); } -/** @brief Returns the currently loaded deck. */ -DeckLoader *AbstractTabDeckEditor::getDeckLoader() const -{ - return deckDockWidget->getDeckLoader(); -} - -/** @brief Returns the currently loaded deck list. */ -const DeckList &AbstractTabDeckEditor::getDeckList() const -{ - return deckDockWidget->getDeckList(); -} - -/** - * @brief Sets the modified state of the tab. - * @param _modified True if tab is modified, false otherwise. - */ -void AbstractTabDeckEditor::setModified(bool _modified) -{ - modified = _modified; - emit tabTextChanged(this, getTabText()); -} - -/** - * @brief Returns true if the tab is a blank newly created deck. - */ -bool AbstractTabDeckEditor::isBlankNewDeck() const -{ - const LoadedDeck &loadedDeck = deckDockWidget->getDeckLoader()->getDeck(); - return !modified && loadedDeck.isEmpty(); -} - /** @brief Creates a new deck. Handles opening in new tab if needed. */ void AbstractTabDeckEditor::actNewDeck() { @@ -255,9 +201,8 @@ void AbstractTabDeckEditor::actNewDeck() /** @brief Clears the current deck and resets modified flag. */ void AbstractTabDeckEditor::cleanDeckAndResetModified() { + deckStateManager->clearDeck(); deckMenu->setSaveStatus(false); - deckDockWidget->cleanDeck(); - setModified(false); } /** @@ -268,13 +213,13 @@ void AbstractTabDeckEditor::cleanDeckAndResetModified() AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank) { if (SettingsCache::instance().getOpenDeckInNewTab()) { - if (openInSameTabIfBlank && isBlankNewDeck()) + if (openInSameTabIfBlank && deckStateManager->isBlankNewDeck()) return SAME_TAB; else return NEW_TAB; } - if (!modified) + if (!deckStateManager->isModified()) return SAME_TAB; tabSupervisor->setCurrentWidget(this); @@ -325,7 +270,6 @@ void AbstractTabDeckEditor::actLoadDeck() QString fileName = dialog.selectedFiles().at(0); openDeckFromFile(fileName, deckOpenLocation); - deckDockWidget->updateBannerCardComboBox(); } /** @@ -371,7 +315,7 @@ void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLo */ bool AbstractTabDeckEditor::actSaveDeck() { - const LoadedDeck &loadedDeck = getDeckLoader()->getDeck(); + const auto loadedDeck = deckStateManager->toLoadedDeck(); if (loadedDeck.lastLoadInfo.remoteDeckId != LoadedDeck::LoadInfo::NON_REMOTE_ID) { QString deckString = loadedDeck.deckList.writeToString_Native(); if (deckString.length() > MAX_FILE_LENGTH) { @@ -392,8 +336,10 @@ bool AbstractTabDeckEditor::actSaveDeck() if (loadedDeck.lastLoadInfo.fileName.isEmpty()) return actSaveDeckAs(); - if (getDeckLoader()->saveToFile(loadedDeck.lastLoadInfo.fileName, loadedDeck.lastLoadInfo.fileFormat)) { - setModified(false); + auto deckLoader = DeckLoader(this); + deckLoader.setDeck(loadedDeck); + if (deckLoader.saveToFile(loadedDeck.lastLoadInfo.fileName, loadedDeck.lastLoadInfo.fileFormat)) { + deckStateManager->setModified(false); return true; } @@ -409,12 +355,14 @@ bool AbstractTabDeckEditor::actSaveDeck() */ bool AbstractTabDeckEditor::actSaveDeckAs() { + LoadedDeck loadedDeck = deckStateManager->toLoadedDeck(); + QFileDialog dialog(this, tr("Save deck")); dialog.setDirectory(SettingsCache::instance().getDeckPath()); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setDefaultSuffix("cod"); dialog.setNameFilters(DeckLoader::FILE_NAME_FILTERS); - dialog.selectFile(getDeckList().getName().trimmed()); + dialog.selectFile(loadedDeck.deckList.getName().trimmed()); if (!dialog.exec()) return false; @@ -422,14 +370,18 @@ bool AbstractTabDeckEditor::actSaveDeckAs() QString fileName = dialog.selectedFiles().at(0); DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(fileName); - if (!getDeckLoader()->saveToFile(fileName, fmt)) { + DeckLoader deckLoader = DeckLoader(this); + deckLoader.setDeck(loadedDeck); + if (!deckLoader.saveToFile(fileName, fmt)) { QMessageBox::critical( this, tr("Error"), tr("The deck could not be saved.\nPlease check that the directory is writable and try again.")); return false; } - setModified(false); + deckStateManager->setLastLoadInfo({.fileName = fileName, .fileFormat = fmt}); + + deckStateManager->setModified(false); SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName); return true; } @@ -443,7 +395,7 @@ void AbstractTabDeckEditor::saveDeckRemoteFinished(const Response &response) if (response.response_code() != Response::RespOk) QMessageBox::critical(this, tr("Error"), tr("The deck could not be saved.")); else - setModified(false); + deckStateManager->setModified(false); } /** @@ -464,7 +416,7 @@ void AbstractTabDeckEditor::actLoadDeckFromClipboard() emit openDeckEditor({.deckList = dlg.getDeckList()}); } else { setDeck({.deckList = dlg.getDeckList()}); - setModified(true); + deckStateManager->setModified(true); } deckMenu->setSaveStatus(true); @@ -476,12 +428,13 @@ void AbstractTabDeckEditor::actLoadDeckFromClipboard() */ void AbstractTabDeckEditor::editDeckInClipboard(bool annotated) { - DlgEditDeckInClipboard dlg(getDeckLoader()->getDeck().deckList, annotated, this); + LoadedDeck loadedDeck = deckStateManager->toLoadedDeck(); + DlgEditDeckInClipboard dlg(loadedDeck.deckList, annotated, this); if (!dlg.exec()) return; - setDeck({dlg.getDeckList(), getDeckLoader()->getDeck().lastLoadInfo}); - setModified(true); + setDeck({dlg.getDeckList(), loadedDeck.lastLoadInfo}); + deckStateManager->setModified(true); deckMenu->setSaveStatus(true); } @@ -500,25 +453,25 @@ void AbstractTabDeckEditor::actEditDeckInClipboardRaw() /** @brief Saves deck to clipboard with set info and annotation. */ void AbstractTabDeckEditor::actSaveDeckToClipboard() { - DeckLoader::saveToClipboard(getDeckList(), true, true); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), true, true); } /** @brief Saves deck to clipboard with annotation, without set info. */ void AbstractTabDeckEditor::actSaveDeckToClipboardNoSetInfo() { - DeckLoader::saveToClipboard(getDeckList(), true, false); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), true, false); } /** @brief Saves deck to clipboard without annotations, with set info. */ void AbstractTabDeckEditor::actSaveDeckToClipboardRaw() { - DeckLoader::saveToClipboard(getDeckList(), false, true); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), false, true); } /** @brief Saves deck to clipboard without annotations or set info. */ void AbstractTabDeckEditor::actSaveDeckToClipboardRawNoSetInfo() { - DeckLoader::saveToClipboard(getDeckList(), false, false); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), false, false); } /** @brief Prints the deck using a QPrintPreviewDialog. */ @@ -526,7 +479,7 @@ void AbstractTabDeckEditor::actPrintDeck() { auto *dlg = new QPrintPreviewDialog(this); connect(dlg, &QPrintPreviewDialog::paintRequested, this, - [this](QPrinter *printer) { DeckLoader::printDeckList(printer, getDeckList()); }); + [this](QPrinter *printer) { DeckLoader::printDeckList(printer, deckStateManager->getDeckList()); }); dlg->exec(); } @@ -547,7 +500,7 @@ void AbstractTabDeckEditor::actLoadDeckFromWebsite() emit openDeckEditor({.deckList = dlg.getDeck()}); } else { setDeck({.deckList = dlg.getDeck()}); - setModified(true); + deckStateManager->setModified(true); } deckMenu->setSaveStatus(true); @@ -559,7 +512,7 @@ void AbstractTabDeckEditor::actLoadDeckFromWebsite() */ void AbstractTabDeckEditor::exportToDecklistWebsite(DeckLoader::DecklistWebsite website) { - QString decklistUrlString = DeckLoader::exportDeckToDecklist(getDeckList(), website); + QString decklistUrlString = DeckLoader::exportDeckToDecklist(deckStateManager->getDeckList(), website); // Check to make sure the string isn't empty. if (decklistUrlString.isEmpty()) { // Show an error if the deck is empty, and return. @@ -592,14 +545,14 @@ void AbstractTabDeckEditor::actExportDeckDecklistXyz() void AbstractTabDeckEditor::actAnalyzeDeckDeckstats() { auto *interface = new DeckStatsInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(), this); - interface->analyzeDeck(getDeckList()); + interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Analyzes the deck using TappedOut. */ void AbstractTabDeckEditor::actAnalyzeDeckTappedout() { auto *interface = new TappedOutInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(), this); - interface->analyzeDeck(getDeckList()); + interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Applies a new filter tree to the database display. */ @@ -658,7 +611,7 @@ bool AbstractTabDeckEditor::eventFilter(QObject *o, QEvent *e) /** @brief Shows a confirmation dialog before closing. */ bool AbstractTabDeckEditor::confirmClose() { - if (modified) { + if (deckStateManager->isModified()) { tabSupervisor->setCurrentWidget(this); int ret = createSaveConfirmationWindow()->exec(); if (ret == QMessageBox::Save) diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index bfbda778c..69b2b4a1c 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -19,6 +19,7 @@ #include +class DeckStateManager; class CardDatabaseModel; class CardDatabaseDisplayModel; @@ -117,30 +118,13 @@ public: */ void openDeck(const LoadedDeck &deck); - /** @brief Returns the currently active deck loader. */ - DeckLoader *getDeckLoader() const; - - /** @brief Returns the currently active deck list. */ - const DeckList &getDeckList() const; - - /** @brief Sets the modified state of the tab. - * @param _windowModified Whether the tab is modified. - */ - void setModified(bool _windowModified); - DeckEditorDeckDockWidget *getDeckDockWidget() const { return deckDockWidget; } - DeckListHistoryManager *getHistoryManager() const - { - return historyManager; - } - - DeckListHistoryManager *historyManager; - // UI Elements + DeckStateManager *deckStateManager; DeckEditorMenu *deckMenu; ///< Menu for deck operations DeckEditorDatabaseDisplayWidget *databaseDisplayDockWidget; ///< Database dock DeckEditorCardInfoDockWidget *cardInfoDockWidget; ///< Card info dock @@ -155,14 +139,6 @@ public slots: /** @brief Called when the deck is modified. */ virtual void onDeckModified(); - /** @brief Called when a widget is about to modify the state of the DeckList. - * @param modificationReason The reason for the state modification - */ - virtual void onDeckHistorySaveRequested(const QString &modificationReason); - - /** @brief Called when a widget would like to clear the history. */ - virtual void onDeckHistoryClearRequested(); - /** @brief Updates the card info panel. * @param card The card to display. */ @@ -202,9 +178,6 @@ signals: /** @brief Emitted before the tab is closed. */ void deckEditorClosing(AbstractTabDeckEditor *tab); - /** @brief Emitted when a card should be decremented. */ - void decrementCard(const ExactCard &card, QString zoneName); - protected slots: /** @brief Starts a new deck in this tab. */ virtual void actNewDeck(); @@ -315,9 +288,6 @@ protected: */ QMessageBox *createSaveConfirmationWindow(); - /** @brief Returns true if the tab is a blank newly created deck. */ - bool isBlankNewDeck() const; - /** @brief Helper function to add a card to a specific deck zone. */ void addCardHelper(const ExactCard &card, const QString &zoneName); @@ -330,8 +300,6 @@ protected: QAction *aResetLayout; QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating; QAction *aFilterDockVisible, *aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating; - - bool modified = false; ///< Whether the deck/tab has unsaved changes }; #endif // TAB_GENERIC_DECK_EDITOR_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 1547cca74..3a2468368 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -68,7 +68,10 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi model = new DeckListModel(this); connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); - model->getDeckList()->loadFromStream_Plain(deckStream, false); + + auto decklist = QSharedPointer(new DeckList); + decklist->loadFromStream_Plain(deckStream, false); + model->setDeckList(decklist); model->forEachCard(CardNodeFunction::ResolveProviderId()); 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/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 4e3d0c57d..286252e13 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -1,6 +1,7 @@ #include "tab_deck_editor.h" #include "../../../client/settings/cache_settings.h" +#include "../deck_editor/deck_state_manager.h" #include "../filters/filter_builder.h" #include "../interface/pixel_map_generator.h" #include "../interface/widgets/cards/card_info_frame_widget.h" @@ -114,8 +115,8 @@ void TabDeckEditor::createMenus() */ QString TabDeckEditor::getTabText() const { - QString result = tr("Deck: %1").arg(deckDockWidget->getSimpleDeckName()); - if (modified) + QString result = tr("Deck: %1").arg(deckStateManager->getSimpleDeckName()); + if (deckStateManager->isModified()) result.prepend("* "); return result; } 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 a124eaa01..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 @@ -1,6 +1,7 @@ #include "tab_deck_editor_visual.h" #include "../../../../client/settings/cache_settings.h" +#include "../../deck_editor/deck_state_manager.h" #include "../../filters/filter_builder.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/widgets/cards/card_info_frame_widget.h" @@ -61,7 +62,7 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame = new QVBoxLayout; centralWidget->setLayout(centralFrame); - tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckDockWidget->deckModel, + tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseDisplayDockWidget->databaseModel, databaseDisplayDockWidget->databaseDisplayModel); @@ -84,8 +85,8 @@ void TabDeckEditorVisual::onDeckChanged() { AbstractTabDeckEditor::onDeckModified(); tabContainer->visualDeckView->constructZoneWidgetsFromDeckListModel(); - tabContainer->deckAnalytics->refreshDisplays(); - tabContainer->sampleHandWidget->setDeckModel(deckDockWidget->deckModel); + tabContainer->deckAnalytics->updateDisplays(); + tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel()); } /** @brief Creates menus for deck editing and view options, including dock actions. */ @@ -149,8 +150,8 @@ void TabDeckEditorVisual::createMenus() /** @brief Returns the tab text, prepending a mark if the deck has unsaved changes. */ QString TabDeckEditorVisual::getTabText() const { - QString result = tr("Visual Deck: %1").arg(deckDockWidget->getSimpleDeckName()); - if (modified) + QString result = tr("Visual Deck: %1").arg(deckStateManager->getSimpleDeckName()); + if (deckStateManager->isModified()) result.prepend("* "); return result; } @@ -166,9 +167,9 @@ void TabDeckEditorVisual::changeModelIndexAndCardInfo(const ExactCard &activeCar void TabDeckEditorVisual::changeModelIndexToCard(const ExactCard &activeCard) { QString cardName = activeCard.getName(); - QModelIndex index = deckDockWidget->deckModel->findCard(cardName, DECK_ZONE_MAIN); + QModelIndex index = deckStateManager->getModel()->findCard(cardName, DECK_ZONE_MAIN); if (!index.isValid()) { - index = deckDockWidget->deckModel->findCard(cardName, DECK_ZONE_SIDE); + index = deckStateManager->getModel()->findCard(cardName, DECK_ZONE_SIDE); } if (!deckDockWidget->getSelectionModel()->hasSelection()) { deckDockWidget->getSelectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); @@ -182,7 +183,7 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, auto card = instance->getCard(); // Get the model index for the card - QModelIndex idx = deckDockWidget->deckModel->findCard(card.getName(), zoneName); + QModelIndex idx = deckStateManager->getModel()->findCard(card.getName(), zoneName); if (!idx.isValid()) { return; } @@ -191,8 +192,8 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, // Double click = swap if (event->type() == QEvent::MouseButtonDblClick && event->button() == Qt::LeftButton) { - deckDockWidget->actSwapCard(card, zoneName); - idx = deckDockWidget->deckModel->findCard(card.getName(), zoneName); + deckStateManager->swapCardAtIndex(idx); + idx = deckStateManager->getModel()->findCard(card.getName(), zoneName); sel->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect); return; } 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_database_display/visual_database_display_name_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp index 7551954f3..5098696dd 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp @@ -2,6 +2,7 @@ #include "../../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" +#include "../deck_editor/deck_state_manager.h" #include @@ -60,7 +61,7 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi() void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck() { - DeckListModel *deckListModel = deckEditor->deckDockWidget->deckModel; + DeckListModel *deckListModel = deckEditor->deckStateManager->getModel(); if (!deckListModel) return; 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/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 41113185c..2e135d170 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -938,9 +938,25 @@ void MainWindow::startupConfigCheck() const auto reloadOk0 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); }); } - qCInfo(WindowMainStartupShortcutsLog) << "[MainWindow] Migrating shortcuts after update detected."; + qCInfo(WindowMainStartupShortcutsLog) << "Migrating shortcuts after update detected."; SettingsCache::instance().shortcuts().migrateShortcuts(); + if (SettingsCache::instance().getCheckUpdatesOnStartup()) { + if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) { + qCInfo(WindowMainStartupShortcutsLog) << "Update has changed to custom version, disabling auto update"; + SettingsCache::instance().setCheckUpdatesOnStartup(Qt::Unchecked); + } else { + int channel = 0; + if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) { + channel = 1; + } + if (SettingsCache::instance().getUpdateReleaseChannelIndex() != channel) { + qCInfo(WindowMainStartupShortcutsLog) << "Update has changed beta state, updating release channel."; + SettingsCache::instance().setUpdateReleaseChannelIndex(channel); + } + } + } + SettingsCache::instance().setClientVersion(VERSION_STRING); } else { // previous config from this version found 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/developer_documentation/card_database_schema_and_parsing.md b/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md index 1de6fda4f..7b4bdafc3 100644 --- a/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md +++ b/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md @@ -1,3 +1,16 @@ @page card_database_schema_and_parsing Card Database Schema and Parsing +# Card Database Schemas + +Cockatrice uses `XML files` to store information of available cards to be used in the app (`cards.xml`). +The token file follows the schema of the card database (`tokens.xml`), too. + +Saved decks (`.xml`) use a different schema. + +- [XSD Schema for `Card Databases`](https://github.com/Cockatrice/Cockatrice/blob/master/doc/carddatabase_v4/cards.xsd) v4 +- [XSD Schema for `Decks`](https://github.com/Cockatrice/Cockatrice/blob/master/doc/deck.xsd) v1 + + +# Card Database Parsing + TODO 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/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md b/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md index bfe578afd..153320c3d 100644 --- a/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md +++ b/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md @@ -8,10 +8,8 @@ by selecting "Deck Editor" or "Visual Deck Editor" under the "Tabs" application # Further References -See @ref editing_decks for information on how to modify the attributes and contents of a deck in the Deck Editor +- See @ref editing_decks for information on how to modify the attributes and contents of a deck in the Deck Editor widgets. - -See @ref exporting_decks for information on how to store and persist your deck either in-client or to external +- See @ref exporting_decks for information on how to store and persist your deck either in-client or to external services. - -See @ref importing_decks for information on how to import existing decks either in-client or from external services \ No newline at end of file +- See @ref importing_decks for information on how to import existing decks either in-client or from external services diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 3054a10cf..ae73ef2db 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -75,7 +75,17 @@ QString CardInfo::getCorrectedName() const return result.remove(rmrx).replace(spacerx, space); } -void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo _info) +bool CardInfo::isLegalInFormat(const QString &format) const +{ + if (format.isEmpty()) { + return true; + } + + QString formatLegality = getProperty("format-" + format); + return formatLegality == "legal" || formatLegality == "restricted"; +} + +void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) { if (!_set->contains(smartThis)) { _set->append(smartThis); @@ -156,7 +166,7 @@ QString CardInfo::simplifyName(const QString &name) return simpleName; } -const QChar CardInfo::getColorChar() const +QChar CardInfo::getColorChar() const { QString colors = getColors(); switch (colors.size()) { @@ -178,7 +188,7 @@ void CardInfo::resetReverseRelatedCards2Me() } // Back-compatibility methods. Remove ASAP -const QString CardInfo::getCardType() const +QString CardInfo::getCardType() const { return getProperty(Mtg::CardType); } @@ -186,11 +196,11 @@ void CardInfo::setCardType(const QString &value) { setProperty(Mtg::CardType, value); } -const QString CardInfo::getCmc() const +QString CardInfo::getCmc() const { return getProperty(Mtg::ConvertedManaCost); } -const QString CardInfo::getColors() const +QString CardInfo::getColors() const { return getProperty(Mtg::Colors); } @@ -198,19 +208,19 @@ void CardInfo::setColors(const QString &value) { setProperty(Mtg::Colors, value); } -const QString CardInfo::getLoyalty() const +QString CardInfo::getLoyalty() const { return getProperty(Mtg::Loyalty); } -const QString CardInfo::getMainCardType() const +QString CardInfo::getMainCardType() const { return getProperty(Mtg::MainCardType); } -const QString CardInfo::getManaCost() const +QString CardInfo::getManaCost() const { return getProperty(Mtg::ManaCost); } -const QString CardInfo::getPowTough() const +QString CardInfo::getPowTough() const { return getProperty(Mtg::PowTough); } diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 00e8fec37..b81cf51dc 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -267,18 +267,18 @@ public: } //@} - [[nodiscard]] const QChar getColorChar() const; + [[nodiscard]] QChar getColorChar() const; /** @name Legacy/Convenience Property Accessors */ //@{ - [[nodiscard]] const QString getCardType() const; + [[nodiscard]] QString getCardType() const; void setCardType(const QString &value); - [[nodiscard]] const QString getCmc() const; - [[nodiscard]] const QString getColors() const; + [[nodiscard]] QString getCmc() const; + [[nodiscard]] QString getColors() const; void setColors(const QString &value); - [[nodiscard]] const QString getLoyalty() const; - [[nodiscard]] const QString getMainCardType() const; - [[nodiscard]] const QString getManaCost() const; - [[nodiscard]] const QString getPowTough() const; + [[nodiscard]] QString getLoyalty() const; + [[nodiscard]] QString getMainCardType() const; + [[nodiscard]] QString getManaCost() const; + [[nodiscard]] QString getPowTough() const; void setPowTough(const QString &value); //@} @@ -291,6 +291,15 @@ public: */ [[nodiscard]] QString getCorrectedName() const; + /** + * @brief Checks if the card is legal in the given format. + * A card is considered legal in a format if its properties map contains an entry for "format-", with value + * "legal" or "restricted". + * @param format The format's name. If empty, will always return true. + * @return Whether the card is legal in the given format. + */ + [[nodiscard]] bool isLegalInFormat(const QString &format) const; + /** * @brief Adds a printing to a specific set. * @@ -299,7 +308,7 @@ public: * @param _set The set to which the card should be added. * @param _info Optional printing information. */ - void addToSet(const CardSetPtr &_set, PrintingInfo _info = PrintingInfo()); + void addToSet(const CardSetPtr &_set, const PrintingInfo &_info = PrintingInfo()); /** * @brief Combines legality properties from a provided map. 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 2ccb95690..ce1c79263 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -5,26 +5,33 @@ DeckListModel::DeckListModel(QObject *parent) : QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder) { - // This class will leak the decklist object. We cannot safely delete it in the dtor because the deckList field is a - // non-owning pointer and another deckList might have been assigned to it. - // `DeckListModel::cleanList` also leaks for the same reason. - // TODO: fix the leak - deckList = new DeckList; + deckList = QSharedPointer(new DeckList()); root = new InnerDecklistNode; } +DeckListModel::DeckListModel(QObject *parent, const QSharedPointer &deckList) : DeckListModel(parent) +{ + setDeckList(deckList); +} + DeckListModel::~DeckListModel() { delete root; } -QString DeckListModel::getGroupCriteriaForCard(CardInfoPtr info) const +/** + * @brief Extract the value from the card that is used for the group criteria. + * @param info Pointer to card information. + * @param criteria The group criteria + * @return String representing the value of the criteria. + */ +static QString extractGroupCriteriaValue(const CardInfoPtr &info, DeckListModelGroupCriteria::Type criteria) { if (!info) { return "unknown"; } - switch (activeGroupCriteria) { + switch (criteria) { case DeckListModelGroupCriteria::MAIN_TYPE: return info->getMainCardType(); case DeckListModelGroupCriteria::MANA_COST: @@ -56,7 +63,7 @@ void DeckListModel::rebuildTree() } CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(currentCard->getName()); - QString groupCriteria = getGroupCriteriaForCard(info); + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); auto *groupNode = dynamic_cast(node->findChild(groupCriteria)); @@ -353,7 +360,7 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, return nullptr; } - QString groupCriteria = getGroupCriteriaForCard(info); + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); InnerDecklistNode *groupNode = dynamic_cast(zoneNode->findChild(groupCriteria)); if (!groupNode) { return nullptr; @@ -406,7 +413,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam CardInfoPtr cardInfo = card.getCardPtr(); PrintingInfo printingInfo = card.getPrinting(); - QString groupCriteria = getGroupCriteriaForCard(cardInfo); + QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode); const QModelIndex parentIndex = nodeToIndex(groupNode); @@ -420,7 +427,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"), - printingInfo.getProperty("uuid"), isCardLegalForCurrentFormat(cardInfo)); + printingInfo.getProperty("uuid"), cardInfo->isLegalInFormat(deckList->getGameFormat())); beginInsertRows(parentIndex, insertRow, insertRow); cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); @@ -472,7 +479,7 @@ bool DeckListModel::offsetCountAtIndex(const QModelIndex &idx, int offset) return true; } -int DeckListModel::findSortedInsertRow(InnerDecklistNode *parent, CardInfoPtr cardInfo) const +int DeckListModel::findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const { if (!cardInfo) { return parent->size(); // fallback: append at end @@ -580,18 +587,19 @@ void DeckListModel::setActiveFormat(const QString &_format) void DeckListModel::cleanList() { - setDeckList(new DeckList); + setDeckList(QSharedPointer(new DeckList())); } /** * @param _deck The deck. */ -void DeckListModel::setDeckList(DeckList *_deck) +void DeckListModel::setDeckList(const QSharedPointer &_deck) { if (deckList != _deck) { deckList = _deck; } rebuildTree(); + refreshCardFormatLegalities(); emit deckReplaced(); } @@ -661,18 +669,6 @@ QList DeckListModel::getZones() const return zones; } -bool DeckListModel::isCardLegalForCurrentFormat(const CardInfoPtr cardInfo) -{ - if (!deckList->getGameFormat().isEmpty()) { - if (cardInfo->getProperties().contains("format-" + deckList->getGameFormat())) { - QString formatLegality = cardInfo->getProperty("format-" + deckList->getGameFormat()); - return formatLegality == "legal" || formatLegality == "restricted"; - } - return false; - } - return true; -} - static int maxAllowedForLegality(const FormatRules &format, const QString &legality) { for (const AllowedCount &c : format.allowedCounts) { @@ -683,25 +679,29 @@ static int maxAllowedForLegality(const FormatRules &format, const QString &legal return -1; // unknown legality → treat as illegal } -bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardInfo, int quantity) +static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, int quantity) { - auto formatRules = CardDatabaseManager::query()->getFormat(deckList->getGameFormat()); + if (format.isEmpty()) { + return true; + } + + auto formatRules = CardDatabaseManager::query()->getFormat(format); if (!formatRules) { return true; } // Exceptions always win - if (cardHasAnyException(*cardInfo, *formatRules)) { + if (cardHasAnyException(cardInfo, *formatRules)) { return true; } - const QString legalityProp = "format-" + deckList->getGameFormat(); - if (!cardInfo->getProperties().contains(legalityProp)) { + const QString legalityProp = "format-" + format; + if (!cardInfo.getProperties().contains(legalityProp)) { return false; } - const QString legality = cardInfo->getProperty(legalityProp); + const QString legality = cardInfo.getProperty(legalityProp); int maxAllowed = maxAllowedForLegality(*formatRules, legality); @@ -735,10 +735,11 @@ void DeckListModel::refreshCardFormatLegalities() continue; } - bool legal = isCardLegalForCurrentFormat(exactCard.getCardPtr()); + QString format = deckList->getGameFormat(); + bool legal = exactCard.getInfo().isLegalInFormat(format); if (legal) { - legal = isCardQuantityLegalForCurrentFormat(exactCard.getCardPtr(), currentCard->getNumber()); + legal = isCardQuantityLegalForFormat(format, exactCard.getInfo(), currentCard->getNumber()); } currentCard->setFormatLegality(legal); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 724a4d9d8..e6f10c072 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -217,7 +217,12 @@ public slots: */ void rebuildTree(); -public slots: + /** + * @brief Sets the criteria used to group cards in the model. + * @param newCriteria The new grouping criteria. + */ + void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria); + void setActiveFormat(const QString &_format); signals: @@ -240,6 +245,7 @@ signals: public: explicit DeckListModel(QObject *parent = nullptr); + explicit DeckListModel(QObject *parent, const QSharedPointer &deckList); ~DeckListModel() override; /** @@ -251,14 +257,8 @@ public: return nodeToIndex(root); } - /** - * @brief Returns the value of the grouping category for a card based on the current criteria. - * @param info Pointer to card information. - * @return String representing the value of the current grouping criteria for the card. - */ - [[nodiscard]] QString getGroupCriteriaForCard(CardInfoPtr info) const; - - // Qt model overrides + /// @name Qt model overrides + ///@{ [[nodiscard]] int rowCount(const QModelIndex &parent) const override; [[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; @@ -268,6 +268,8 @@ public: [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; bool removeRows(int row, int count, const QModelIndex &parent) override; + void sort(int column, Qt::SortOrder order) override; + ///@} /** * @brief Finds a card by name, zone, and optional identifiers. @@ -309,25 +311,16 @@ public: */ bool offsetCountAtIndex(const QModelIndex &idx, int offset); - /** - * @brief Determines the sorted insertion row for a card. - * @param parent The parent node where the card will be inserted. - * @param cardInfo The card info to insert. - * @return Row index where the card should be inserted to maintain sort order. - */ - int findSortedInsertRow(InnerDecklistNode *parent, CardInfoPtr cardInfo) const; - - void sort(int column, Qt::SortOrder order) override; - /** * @brief Removes all cards and resets the model. */ void cleanList(); - [[nodiscard]] DeckList *getDeckList() const + + [[nodiscard]] QSharedPointer getDeckList() const { return deckList; } - void setDeckList(DeckList *_deck); + void setDeckList(const QSharedPointer &_deck); /** * @brief Apply a function to every card in the deck tree. @@ -359,19 +352,9 @@ public: */ [[nodiscard]] QList getZones() const; - bool isCardLegalForCurrentFormat(CardInfoPtr cardInfo); - bool isCardQuantityLegalForCurrentFormat(CardInfoPtr cardInfo, int quantity); - void refreshCardFormatLegalities(); - - /** - * @brief Sets the criteria used to group cards in the model. - * @param newCriteria The new grouping criteria. - */ - void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria); - private: - DeckList *deckList; /**< Pointer to the deck loader providing the underlying data. */ - InnerDecklistNode *root; /**< Root node of the model tree. */ + QSharedPointer deckList; /**< Pointer to the decklist providing the underlying data. */ + InnerDecklistNode *root; /**< Root node of the model tree. */ DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE; int lastKnownColumn; /**< Last column used for sorting. */ Qt::SortOrder lastKnownOrder; /**< Last known sort order. */ @@ -383,6 +366,14 @@ private: const QString &providerId = "", const QString &cardNumber = "") const; + /** + * @brief Determines the sorted insertion row for a card. + * @param parent The parent node where the card will be inserted. + * @param cardInfo The card info to insert. + * @return Row index where the card should be inserted to maintain sort order. + */ + int findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const; + /** * @brief Recursively emits the dataChanged signal with role as Qt::BackgroundRole for all indices that are children * of the given node. This is used to update the background color when changing formats. @@ -404,6 +395,8 @@ private: return dynamic_cast(root); return dynamic_cast(static_cast(index.internalPointer())); } + + void refreshCardFormatLegalities(); }; #endif 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