Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
e675162797 Bump node-forge from 1.3.1 to 1.3.3 in /webclient
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.3.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.3)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 16:23:21 +00:00
143 changed files with 1607 additions and 6366 deletions

View File

@@ -142,6 +142,7 @@ jobs:
- distro: Ubuntu
version: 22.04
package: DEB
test: skip # Running tests on all distros is superfluous
- distro: Ubuntu
version: 24.04

View File

@@ -44,10 +44,10 @@ Latest <kbd>beta</kbd> version:
# Related Repositories
- [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
- [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
- [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage
- [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)
- [Cockatrice @Flathub](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration for our Linux `flatpak` package
# Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA)
@@ -55,6 +55,7 @@ Latest <kbd>beta</kbd> 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)
@@ -63,23 +64,6 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
# Contribute
<p>
<a href="#code">Code</a> <b>|</b>
<a href="#documentation-">Documentation</a> <b>|</b>
<a href="#translation-">Translation</a>
</p>
#### Repository Activity
![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats")
<details>
<summary><b>Kudos to all our amazing contributors ❤️</b></summary>
<br>
<a href="https://github.com/Cockatrice/Cockatrice/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Cockatrice/Cockatrice" />
</a><br>
<sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub>
</details>
### Code
@@ -95,17 +79,21 @@ 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.
### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/)
#### Repository Activity
![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats")
<details>
<summary><b>Kudos to all our amazing contributors ❤️</b></summary>
<br>
<a href="https://github.com/Cockatrice/Cockatrice/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Cockatrice/Cockatrice" />
</a><br>
<sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub>
</details>
### Translations [![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 <kbd>Cockatrice</kbd>, <kbd>Oracle</kbd> and <kbd>Webatrice</kbd> 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/).<br>

View File

@@ -144,31 +144,11 @@ 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/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_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_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
@@ -176,21 +156,16 @@ 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

View File

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

View File

@@ -1,12 +1,10 @@
@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.<br>
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive.
<dl>
<dt>Name:</dt>
<dd>[birds of paradise](#birds of paradise) <small>(Any card name containing the words birds, of, and paradise)</small></dd>
<dd>["birds of paradise"](#%22birds of paradise%22) <small>(Any card name containing the exact phrase "birds of paradise")</small></dd>

View File

@@ -2,7 +2,6 @@
#include "../network/update/client/release_channel.h"
#include "card_counter_settings.h"
#include "version_string.h"
#include <QAbstractListModel>
#include <QApplication>
@@ -199,13 +198,7 @@ SettingsCache::SettingsCache()
mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).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
}
checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool();
startupCardUpdateCheckPromptForUpdate =
settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool();
startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool();
@@ -213,15 +206,7 @@ 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();
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
}
updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt();
lang = settings->value("personal/lang").toString();
keepalive = settings->value("personal/keepalive", 3).toInt();
@@ -288,7 +273,6 @@ 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 =
@@ -716,13 +700,6 @@ void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts)
settings->setValue("menu/showshortcuts", showShortcuts);
}
void SettingsCache::setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar)
{
showGameSelectorFilterToolbar = static_cast<bool>(_showGameSelectorFilterToolbar);
settings->setValue("menu/showgameselectorfiltertoolbar", showGameSelectorFilterToolbar);
emit showGameSelectorFilterToolbarChanged(showGameSelectorFilterToolbar);
}
void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames)
{
displayCardNames = static_cast<bool>(_displayCardNames);

View File

@@ -145,7 +145,6 @@ signals:
void homeTabBackgroundShuffleFrequencyChanged();
void picDownloadChanged();
void showStatusBarChanged(bool state);
void showGameSelectorFilterToolbarChanged(bool state);
void displayCardNamesChanged();
void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference);
void bumpSetsWithCardsInDeckToTopChanged();
@@ -237,7 +236,6 @@ private:
bool annotateTokens;
QByteArray tabGameSplitterSizes;
bool showShortcuts;
bool showGameSelectorFilterToolbar;
bool displayCardNames;
bool overrideAllCardArtWithPersonalPreference;
bool bumpSetsWithCardsInDeckToTop;
@@ -555,10 +553,6 @@ public:
{
return showShortcuts;
}
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const
{
return showGameSelectorFilterToolbar;
}
[[nodiscard]] bool getDisplayCardNames() const
{
return displayCardNames;
@@ -1023,7 +1017,6 @@ 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);

View File

@@ -660,12 +660,6 @@ 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)},

View File

@@ -396,7 +396,7 @@ void CardMenu::addRelatedCardActions()
relatedCardName = relatedCard.getName(); // "name"
}
QString text = tr("Token") + ": ";
QString text = tr("Token: ");
if (cardRelation->getDoesAttach()) {
text +=
tr(cardRelation->getDoesTransform() ? "Transform into " : "Attach to ") + "\"" + relatedCardName + "\"";
@@ -502,4 +502,4 @@ void CardMenu::setShortcutsActive()
aRemoveCounter[i]->setShortcuts(shortcuts.getShortcut("Player/aRC" + colorWords[i]));
aSetCounter[i]->setShortcuts(shortcuts.getShortcut("Player/aSC" + colorWords[i]));
}
}
}

View File

@@ -60,16 +60,6 @@ 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());
@@ -114,9 +104,7 @@ void HandMenu::retranslateUi()
aSortHandByType->setText(tr("Type"));
aSortHandByManaValue->setText(tr("Mana Value"));
aMulligan->setText(tr("Take &mulligan (Choose hand size)"));
aMulliganSame->setText(tr("Take mulligan (Same hand size)"));
aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)"));
aMulligan->setText(tr("Take &mulligan"));
mMoveHandMenu->setTitle(tr("&Move hand to..."));
aMoveHandToTopLibrary->setText(tr("&Top of library"));
@@ -140,8 +128,6 @@ 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"));
}

View File

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

View File

@@ -310,48 +310,28 @@ void PlayerActions::actMulligan()
{
int startSize = SettingsCache::instance().getStartingHandSize();
int handSize = player->getHandZone()->getCards().size();
int deckSize = player->getDeckZone()->getCards().size() + handSize;
int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck
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;
}
if (number < 1) {
number = handSize + number;
}
doMulligan(number);
SettingsCache::instance().setStartingHandSize(number);
}
void PlayerActions::actMulliganSameSize()
{
int handSize = player->getHandZone()->getCards().size();
doMulligan(handSize);
}
void PlayerActions::actMulliganMinusOne()
{
int handSize = player->getHandZone()->getCards().size();
int targetSize = qMax(1, handSize - 1);
doMulligan(targetSize);
}
void PlayerActions::doMulligan(int number)
{
if (number < 1) {
return;
}
Command_Mulligan cmd;
cmd.set_number(number);
if (number < 1) {
if (handSize == 0) {
return;
}
cmd.set_number(handSize + number);
} else {
cmd.set_number(number);
}
sendGameCommand(cmd);
if (startSize != number) {
SettingsCache::instance().setStartingHandSize(number);
}
}
void PlayerActions::actDrawCards()

View File

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

View File

@@ -13,20 +13,12 @@
#include <QGraphicsLinearLayout>
#include <QGraphicsProxyWidget>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QLabel>
#include <QPainter>
#include <QScrollBar>
#include <QStyle>
#include <QStyleOption>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
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.
@@ -249,191 +241,33 @@ void ZoneViewWidget::retranslateUi()
pileViewCheckBox.setText(tr("pile view"));
}
void ZoneViewWidget::stopWindowDrag()
void ZoneViewWidget::moveEvent(QGraphicsSceneMoveEvent * /* event */)
{
if (!draggingWindow)
if (!scene())
return;
draggingWindow = false;
ungrabMouse();
}
int titleBarHeight = 24;
void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event)
{
draggingWindow = true;
dragStartItemPos = pos();
dragStartScreenPos = event->screenPos();
dragView = findDragView(event->widget());
QPointF scenePos = pos();
// 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<int>(frameRectF.width()), static_cast<int>(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.x() < 0) {
scenePos.setX(0);
} else {
qreal maxw = scene()->sceneRect().width() - 100;
if (scenePos.x() > maxw)
scenePos.setX(maxw);
}
// 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<QGraphicsView *>(current))
return view;
current = current->parentWidget();
if (scenePos.y() < titleBarHeight) {
scenePos.setY(titleBarHeight);
} else {
qreal maxh = scene()->sceneRect().height() - titleBarHeight;
if (scenePos.y() > maxh)
scenePos.setY(maxh);
}
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<QGraphicsSceneMouseEvent *>(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);
if (scenePos != pos())
setPos(scenePos);
}
void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event)
@@ -516,7 +350,6 @@ 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();

View File

@@ -3,6 +3,7 @@
* @ingroup GameGraphicsZones
* @brief TODO: Document this.
*/
#ifndef ZONEVIEWWIDGET_H
#define ZONEVIEWWIDGET_H
@@ -13,7 +14,6 @@
#include <QGraphicsProxyWidget>
#include <QGraphicsWidget>
#include <QLineEdit>
#include <QPointer>
#include <libcockatrice/utility/macros.h>
class QLabel;
@@ -28,8 +28,6 @@ class ServerInfo_Card;
class QGraphicsSceneMouseEvent;
class QGraphicsSceneWheelEvent;
class QStyleOption;
class QGraphicsView;
class QWidget;
class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget
{
@@ -54,6 +52,7 @@ private:
ZoneViewZone *zone;
QGraphicsWidget *zoneContainer;
QPushButton *closeButton;
QScrollBar *scrollBar;
ScrollableGraphicsProxyWidget *scrollBarProxy;
@@ -67,33 +66,6 @@ private:
int extraHeight;
Player *player;
bool draggingWindow = false;
QPoint dragStartScreenPos;
QPointF dragStartItemPos;
QPointer<QGraphicsView> 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);
@@ -104,6 +76,7 @@ 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();
@@ -128,10 +101,6 @@ 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;
};

View File

@@ -27,7 +27,7 @@ CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *pa
layout->addWidget(text, 0, Qt::AlignCenter);
setLayout(layout);
setFrameStyle(static_cast<int>(QFrame::Panel) | QFrame::Raised);
setFrameStyle(QFrame::Panel | QFrame::Raised);
int pixmapHeight = QGuiApplication::primaryScreen()->geometry().height() / 3;
int pixmapWidth = static_cast<int>(pixmapHeight / aspectRatio);

View File

@@ -1,48 +0,0 @@
#include "abstract_analytics_panel_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QPushButton>
AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: QWidget(parent), analyzer(analyzer)
{
layout = new QVBoxLayout(this);
bannerAndSettingsContainer = new QWidget(this);
bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer);
bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout);
bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
bannerAndSettingsLayout->addWidget(bannerWidget, 1);
// config button
configureButton = new QPushButton(tr("Configure"), this);
configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog);
bannerAndSettingsLayout->addWidget(configureButton, 0);
layout->addWidget(bannerAndSettingsContainer);
connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay);
}
bool AbstractAnalyticsPanelWidget::applyConfigFromDialog()
{
QDialog *dlg = createConfigDialog(this);
if (!dlg) {
return false;
}
bool ok = dlg->exec() == QDialog::Accepted;
if (ok) {
// dialog must expose its final config as JSON
auto newCfg = extractConfigFromDialog(dlg);
loadConfig(newCfg);
updateDisplay();
}
dlg->deleteLater();
return ok;
}

View File

@@ -1,61 +0,0 @@
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H
#include "../general/display/banner_widget.h"
#include <QDialog>
#include <QJsonObject>
#include <QVBoxLayout>
#include <QWidget>
class DeckListStatisticsAnalyzer;
class AbstractAnalyticsPanelWidget : public QWidget
{
Q_OBJECT
public slots:
virtual void updateDisplay() = 0;
// Widgets must return a config dialog
virtual QDialog *createConfigDialog(QWidget *parent) = 0;
public:
explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void setDisplayTitle(const QString &title)
{
displayTitle = title;
if (bannerWidget) {
bannerWidget->setText(displayTitle);
}
}
QString displayTitleText() const
{
return displayTitle;
}
virtual QJsonObject saveConfig() const
{
return {};
}
virtual void loadConfig(const QJsonObject &)
{
}
// Unified helper to run config dialog and update widget
bool applyConfigFromDialog();
// Dialog → JSON must be supplied by each subclass
virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0;
protected:
DeckListStatisticsAnalyzer *analyzer;
QVBoxLayout *layout;
QWidget *bannerAndSettingsContainer;
QHBoxLayout *bannerAndSettingsLayout;
QString displayTitle;
BannerWidget *bannerWidget;
QPushButton *configureButton;
};
#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H

View File

@@ -1,32 +0,0 @@
#include "add_analytics_panel_dialog.h"
#include "analytics_panel_widget_factory.h"
#include <QDialogButtonBox>
#include <QVBoxLayout>
AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Add Analytics Panel"));
layout = new QVBoxLayout(this);
typeCombo = new QComboBox(this);
// Populate using descriptors
const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets();
for (const auto &desc : widgets) {
// Show translated title to user
typeCombo->addItem(desc.title, desc.type);
}
layout->addWidget(typeCombo);
buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
}

View File

@@ -1,29 +0,0 @@
#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H
#include "analytics_panel_widget_factory.h"
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QVBoxLayout>
class AddAnalyticsPanelDialog : public QDialog
{
Q_OBJECT
public:
explicit AddAnalyticsPanelDialog(QWidget *parent);
QString selectedType() const
{
return typeCombo->currentData().toString();
}
private:
QVBoxLayout *layout;
QComboBox *typeCombo;
QDialogButtonBox *buttons;
};
#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H

View File

@@ -1,33 +0,0 @@
#include "analytics_panel_widget_factory.h"
#include "abstract_analytics_panel_widget.h"
AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance()
{
static AnalyticsPanelWidgetFactory f;
return f;
}
void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc)
{
widgets.insert(desc.type, desc);
}
AbstractAnalyticsPanelWidget *
AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const
{
auto it = widgets.find(type);
if (it == widgets.end())
return nullptr;
auto w = it->creator(parent, analyzer);
w->setDisplayTitle(it->title);
return w;
}
QList<AnalyticsPanelWidgetFactory::Descriptor> AnalyticsPanelWidgetFactory::availableWidgets() const
{
return widgets.values();
}

View File

@@ -1,44 +0,0 @@
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H
#include <QMap>
#include <QString>
#include <QStringList>
#include <QWidget>
#include <functional>
class AbstractAnalyticsPanelWidget;
class DeckListStatisticsAnalyzer;
class AnalyticsPanelWidgetFactory
{
public:
using Creator = std::function<AbstractAnalyticsPanelWidget *(QWidget *, DeckListStatisticsAnalyzer *)>;
struct Descriptor
{
QString type; // stable ID ("manaProdDevotion")
QString title; // translated, user-facing
Creator creator;
};
static AnalyticsPanelWidgetFactory &instance();
// NEW: richer registration
void registerWidget(const Descriptor &desc);
AbstractAnalyticsPanelWidget *
create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const;
// NEW: expose widgets to UI
QList<Descriptor> availableWidgets() const;
private:
AnalyticsPanelWidgetFactory() = default; // Ensure private constructor
AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete;
AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete;
QMap<QString, Descriptor> widgets;
};
#endif

View File

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

View File

@@ -1,17 +0,0 @@
#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H
#include "analytics_panel_widget_factory.h"
class AnalyticsPanelWidgetRegistrar
{
public:
AnalyticsPanelWidgetRegistrar(const QString &type,
const QString &title,
AnalyticsPanelWidgetFactory::Creator creator)
{
AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator});
}
};
#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +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 "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h"
#include "mana_base_config.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
class ManaBaseWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaBaseConfig::fromJson(o);
updateDisplay();
}
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaBaseConfig config;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_BASE_WIDGET_H

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,45 +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 "../../abstract_analytics_panel_widget.h"
#include "mana_devotion_config.h"
#include <QHBoxLayout>
class ManaDevotionWidget : public AbstractAnalyticsPanelWidget
{
Q_OBJECT
public slots:
QSize sizeHint() const override;
void updateDisplay() override;
QDialog *createConfigDialog(QWidget *parent) override;
public:
ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg = {});
QJsonObject saveConfig() const override
{
return config.toJson();
}
void loadConfig(const QJsonObject &o) override
{
config = ManaDevotionConfig::fromJson(o);
updateDisplay();
}
QJsonObject extractConfigFromDialog(QDialog *dlg) const override;
private:
ManaDevotionConfig config;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_DEVOTION_WIDGET_H

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,298 +1,35 @@
#include "deck_analytics_widget.h"
#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 <QEvent>
#include <QJsonArray>
#include <QJsonDocument>
#include <QPushButton>
#include <QScrollArea>
#include <QSettings>
#include <QVBoxLayout>
DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer)
: QWidget(parent), statsAnalyzer(_statsAnalyzer)
DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel)
: QWidget(parent), deckListModel(_deckListModel)
{
layout = new QVBoxLayout(this);
mainLayout = new QVBoxLayout();
setLayout(mainLayout);
// 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);
scrollArea->setFrameShape(QFrame::NoFrame);
mainLayout->addWidget(scrollArea);
panelContainer = new QWidget(scrollArea);
panelLayout = new QVBoxLayout(panelContainer);
panelLayout->setSpacing(8);
panelLayout->setContentsMargins(4, 4, 4, 4);
panelLayout->addStretch(1); // push panels up
container = new QWidget(scrollArea);
containerLayout = new QVBoxLayout(container);
container->setLayout(containerLayout);
scrollArea->setWidget(container);
scrollArea->setWidget(panelContainer);
layout->addWidget(scrollArea);
deckListStatisticsAnalyzer = new DeckListStatisticsAnalyzer(this, deckListModel);
loadLayout();
manaCurveWidget = new ManaCurveWidget(this, deckListStatisticsAnalyzer);
containerLayout->addWidget(manaCurveWidget);
retranslateUi();
manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer);
containerLayout->addWidget(manaDevotionWidget);
manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer);
containerLayout->addWidget(manaBaseWidget);
}
void DeckAnalyticsWidget::retranslateUi()
void DeckAnalyticsWidget::refreshDisplays()
{
addButton->setText(tr("Add Panel"));
removeButton->setText(tr("Remove Panel"));
saveButton->setText(tr("Save Layout"));
loadButton->setText(tr("Load Layout"));
deckListStatisticsAnalyzer->update();
}
void DeckAnalyticsWidget::updateDisplays()
{
statsAnalyzer->analyze();
}
void DeckAnalyticsWidget::onAddPanel()
{
AddAnalyticsPanelDialog dlg(this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
QString selection = dlg.selectedType();
if (selection.isEmpty()) {
return;
}
AbstractAnalyticsPanelWidget *analyticsWidget =
AnalyticsPanelWidgetFactory::instance().create(selection, this, statsAnalyzer);
if (!analyticsWidget) {
return;
}
if (!analyticsWidget->applyConfigFromDialog()) {
analyticsWidget->deleteLater();
return;
}
addPanelInstance(selection, analyticsWidget, analyticsWidget->saveConfig());
}
void DeckAnalyticsWidget::addPanelInstance(const QString &typeId,
AbstractAnalyticsPanelWidget *panel,
const QJsonObject &cfg)
{
panel->loadConfig(cfg);
panel->updateDisplay();
auto *resPanel = new ResizablePanel(typeId, panel, panelContainer);
panelWrappers.push_back(resPanel);
panelLayout->insertWidget(panelLayout->count() - 1, resPanel);
// Event filter for selection
resPanel->installEventFilter(this);
panel->installEventFilter(this);
// Connect drag-drop signals
connect(resPanel, &ResizablePanel::dropRequested, this, &DeckAnalyticsWidget::onPanelDropped);
}
void DeckAnalyticsWidget::onRemoveSelected()
{
int idx = indexOfSelectedWrapper();
if (idx < 0) {
return;
}
ResizablePanel *panel = panelWrappers.takeAt(idx);
selectWrapper(nullptr);
panel->deleteLater();
}
void DeckAnalyticsWidget::saveLayout()
{
QJsonArray arr;
for (auto *wrapper : panelWrappers) {
QJsonObject entry;
entry["type"] = wrapper->getTypeId();
entry["config"] = wrapper->panel->saveConfig();
entry["height"] = wrapper->getCurrentHeight();
arr.append(entry);
}
QSettings s;
s.setValue("deckAnalytics/layout", QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact)));
}
void DeckAnalyticsWidget::loadLayout()
{
if (!loadLayoutInternal()) {
addDefaultPanels();
}
}
void DeckAnalyticsWidget::addDefaultPanels()
{
struct DefaultPanel
{
QString type;
QJsonObject cfg;
};
// Prepare configs
QJsonObject manaCurveCfg = ManaCurveConfig{}.toJson();
QJsonObject manaBaseCfg = ManaBaseConfig{"combinedBar", {}}.toJson();
QJsonObject manaDevotionCfg = ManaDevotionConfig{"combinedBar", {}}.toJson();
QVector<DefaultPanel> defaults = {
{"manaCurve", manaCurveCfg}, {"manaBase", manaBaseCfg}, {"manaDevotion", manaDevotionCfg}};
for (auto &d : defaults) {
AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(d.type, this, statsAnalyzer);
if (!w) {
continue;
}
w->loadConfig(d.cfg);
addPanelInstance(d.type, w, d.cfg);
}
}
bool DeckAnalyticsWidget::loadLayoutInternal()
{
QSettings s;
QString layoutData = s.value("deckAnalytics/layout").toString();
if (layoutData.isEmpty()) {
return false;
}
QJsonDocument doc = QJsonDocument::fromJson(layoutData.toUtf8());
if (!doc.isArray()) {
return false;
}
clearPanels();
for (auto v : doc.array()) {
if (!v.isObject()) {
continue;
}
QJsonObject o = v.toObject();
QString type = o["type"].toString();
QJsonObject cfg = o["config"].toObject();
AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(type, this, statsAnalyzer);
if (!w) {
continue;
}
addPanelInstance(type, w, cfg);
// Restore height AFTER adding the panel
if (o.contains("height")) {
panelWrappers.last()->setHeightFromSaved(o["height"].toInt());
}
}
return true;
}
void DeckAnalyticsWidget::clearPanels()
{
selectWrapper(nullptr);
while (!panelWrappers.isEmpty()) {
ResizablePanel *p = panelWrappers.takeLast();
p->deleteLater();
}
}
bool DeckAnalyticsWidget::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::MouseButtonPress) {
for (auto *p : panelWrappers) {
if (obj == p || obj == p->panel) {
selectWrapper(p);
break;
}
}
}
return QWidget::eventFilter(obj, event);
}
void DeckAnalyticsWidget::selectWrapper(ResizablePanel *w)
{
// Same wrapper
if (selectedWrapper == w) {
return;
}
// Deselect the old one
if (selectedWrapper) {
selectedWrapper->setSelected(false);
}
// Set current
selectedWrapper = w;
// Finally, select new
if (selectedWrapper) {
selectedWrapper->setSelected(true);
}
}
int DeckAnalyticsWidget::indexOfSelectedWrapper() const
{
if (!selectedWrapper) {
return -1;
}
return panelWrappers.indexOf(selectedWrapper);
}
void DeckAnalyticsWidget::onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore)
{
int draggedIdx = panelWrappers.indexOf(dragged);
int targetIdx = panelWrappers.indexOf(target);
if (draggedIdx == -1 || targetIdx == -1 || draggedIdx == targetIdx) {
return;
}
// Remove dragged panel from list and layout
panelWrappers.removeAt(draggedIdx);
panelLayout->removeWidget(dragged);
// Adjust target index if needed
if (draggedIdx < targetIdx) {
targetIdx--;
}
// Calculate insertion position
int insertIdx = insertBefore ? targetIdx : targetIdx + 1;
// Insert back into list and layout
panelWrappers.insert(insertIdx, dragged);
panelLayout->insertWidget(insertIdx, dragged);
// Clear selection
selectWrapper(nullptr);
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,71 @@
#include "mana_base_widget.h"
#include "../../deck_loader/deck_loader.h"
#include "../general/display/banner_widget.h"
#include "../general/display/bar_widget.h"
#include <QHash>
#include <QRegularExpression>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer)
: QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer)
{
layout = new QVBoxLayout(this);
setLayout(layout);
bannerWidget = new BannerWidget(this, tr("Mana Base"), Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
layout->addWidget(bannerWidget);
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
layout->addWidget(barContainer);
connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaBaseWidget::updateDisplay);
retranslateUi();
}
void ManaBaseWidget::retranslateUi()
{
bannerWidget->setText(tr("Mana Base"));
}
void ManaBaseWidget::updateDisplay()
{
// Clear the layout first
QLayoutItem *item;
while ((item = barLayout->takeAt(0)) != nullptr) {
item->widget()->deleteLater();
delete item;
}
auto manaBaseMap = deckStatAnalyzer->getManaBase();
int highestEntry = 0;
for (auto entry : manaBaseMap) {
if (entry > highestEntry) {
highestEntry = entry;
}
}
// Define color mapping for mana types
QHash<QString, QColor> manaColors;
manaColors.insert("W", QColor(248, 231, 185));
manaColors.insert("U", QColor(14, 104, 171));
manaColors.insert("B", QColor(21, 11, 0));
manaColors.insert("R", QColor(211, 32, 42));
manaColors.insert("G", QColor(0, 115, 62));
manaColors.insert("C", QColor(150, 150, 150));
for (auto manaColor : manaBaseMap.keys()) {
QColor barColor = manaColors.value(manaColor, Qt::gray);
BarWidget *barWidget = new BarWidget(QString(manaColor), manaBaseMap[manaColor], highestEntry, barColor, this);
barLayout->addWidget(barWidget);
}
update();
}

View File

@@ -0,0 +1,38 @@
/**
* @file mana_base_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_BASE_WIDGET_H
#define MANA_BASE_WIDGET_H
#include "../general/display/banner_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
class ManaBaseWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer);
void updateDisplay();
public slots:
void retranslateUi();
private:
DeckListStatisticsAnalyzer *deckStatAnalyzer;
BannerWidget *bannerWidget;
QVBoxLayout *layout;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_BASE_WIDGET_H

View File

@@ -0,0 +1,68 @@
#include "mana_curve_widget.h"
#include "../../../main.h"
#include "../../deck_loader/deck_loader.h"
#include "../general/display/banner_widget.h"
#include "../general/display/bar_widget.h"
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <unordered_map>
ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer)
: QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer)
{
layout = new QVBoxLayout(this);
setLayout(layout);
bannerWidget = new BannerWidget(this, tr("Mana Curve"), Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
layout->addWidget(bannerWidget);
barContainer = new QWidget(this);
barLayout = new QHBoxLayout(barContainer);
layout->addWidget(barContainer);
connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay);
retranslateUi();
}
void ManaCurveWidget::retranslateUi()
{
bannerWidget->setText(tr("Mana Curve"));
}
void ManaCurveWidget::updateDisplay()
{
// Clear the layout first
if (barLayout != nullptr) {
QLayoutItem *item;
while ((item = barLayout->takeAt(0)) != nullptr) {
item->widget()->deleteLater();
delete item;
}
}
auto manaCurveMap = deckStatAnalyzer->getManaCurve();
int highestEntry = 0;
for (const auto &entry : manaCurveMap) {
if (entry.second > highestEntry) {
highestEntry = entry.second;
}
}
// Convert unordered_map to ordered map to ensure sorting by CMC
std::map<int, int> sortedManaCurve(manaCurveMap.begin(), manaCurveMap.end());
// Add new widgets to the layout in sorted order
for (const auto &entry : sortedManaCurve) {
BarWidget *barWidget =
new BarWidget(QString::number(entry.first), entry.second, highestEntry, QColor(122, 122, 122), this);
barLayout->addWidget(barWidget);
}
update(); // Update the widget display
}

View File

@@ -0,0 +1,37 @@
/**
* @file mana_curve_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_CURVE_WIDGET_H
#define MANA_CURVE_WIDGET_H
#include "../general/display/banner_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <unordered_map>
class ManaCurveWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer);
void updateDisplay();
public slots:
void retranslateUi();
private:
DeckListStatisticsAnalyzer *deckStatAnalyzer;
QVBoxLayout *layout;
BannerWidget *bannerWidget;
QWidget *barContainer;
QHBoxLayout *barLayout;
};
#endif // MANA_CURVE_WIDGET_H

View File

@@ -0,0 +1,66 @@
#include "mana_devotion_widget.h"
#include "../../deck_loader/deck_loader.h"
#include "../general/display/banner_widget.h"
#include "../general/display/bar_widget.h"
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <regex>
#include <unordered_map>
ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer)
: QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer)
{
layout = new QVBoxLayout(this);
setLayout(layout);
bannerWidget = new BannerWidget(this, tr("Mana Devotion"), Qt::Vertical, 100);
bannerWidget->setMaximumHeight(100);
layout->addWidget(bannerWidget);
barLayout = new QHBoxLayout();
layout->addLayout(barLayout);
connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaDevotionWidget::updateDisplay);
retranslateUi();
}
void ManaDevotionWidget::retranslateUi()
{
bannerWidget->setText(tr("Mana Devotion"));
}
void ManaDevotionWidget::updateDisplay()
{
// Clear the layout first
QLayoutItem *item;
while ((item = barLayout->takeAt(0)) != nullptr) {
item->widget()->deleteLater();
delete item;
}
auto manaDevotionMap = deckStatAnalyzer->getDevotion();
int highestEntry = 0;
for (auto entry : manaDevotionMap) {
if (highestEntry < entry.second) {
highestEntry = entry.second;
}
}
// Define color mapping for devotion bars
std::unordered_map<char, QColor> manaColors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)},
{'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)},
{'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}};
for (auto entry : manaDevotionMap) {
QColor barColor = manaColors.count(entry.first) ? manaColors[entry.first] : Qt::gray;
BarWidget *barWidget = new BarWidget(QString(entry.first), entry.second, highestEntry, barColor, this);
barLayout->addWidget(barWidget);
}
update(); // Update the widget display
}

View File

@@ -0,0 +1,37 @@
/**
* @file mana_devotion_widget.h
* @ingroup DeckEditorAnalyticsWidgets
* @brief TODO: Document this.
*/
#ifndef MANA_DEVOTION_WIDGET_H
#define MANA_DEVOTION_WIDGET_H
#include "../general/display/banner_widget.h"
#include "deck_list_statistics_analyzer.h"
#include <QHBoxLayout>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <utility>
class ManaDevotionWidget : public QWidget
{
Q_OBJECT
public:
explicit ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer);
void updateDisplay();
public slots:
void retranslateUi();
private:
DeckListStatisticsAnalyzer *deckStatAnalyzer;
BannerWidget *bannerWidget;
QVBoxLayout *layout;
QHBoxLayout *barLayout;
};
#endif // MANA_DEVOTION_WIDGET_H

View File

@@ -1,367 +0,0 @@
#include "resizable_panel.h"
#include "libcockatrice/utility/qt_utils.h"
#include <QColor>
#include <QHBoxLayout>
#include <QPixmap>
#include <QtGlobal>
ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent)
: QWidget(parent), panel(analyticsPanel), typeId(_typeId)
{
setAcceptDrops(true);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Frame for selection highlight
frame = new QFrame(this);
frame->setFrameShape(QFrame::Box);
frame->setLineWidth(2);
frame->setStyleSheet("border: none;");
auto *frameLayout = new QVBoxLayout(frame);
frameLayout->setContentsMargins(0, 0, 0, 0);
frameLayout->setSpacing(0);
// Add the analytics panel
frameLayout->addWidget(analyticsPanel);
dropIndicator = new QFrame(frame);
dropIndicator->setStyleSheet("background-color: #3daee9;");
dropIndicator->setFixedHeight(3);
dropIndicator->hide(); // hidden by default
dropIndicator->raise(); // make sure it's above children
selectionOverlay = new QFrame(frame);
selectionOverlay->setStyleSheet("background-color: rgba(61,174,233,50);"); // semi-transparent blue
selectionOverlay->hide(); // hidden by default
selectionOverlay->raise(); // make sure it is above children
selectionOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
// Bottom bar with drag button and resize handle
auto *bottomBar = new QWidget(frame);
auto *bottomLayout = new QHBoxLayout(bottomBar);
bottomLayout->setContentsMargins(0, 0, 0, 0);
bottomLayout->setSpacing(0);
// Drag button on the left
dragButton = new QPushButton("", bottomBar);
dragButton->setFixedSize(40, 8);
dragButton->setCursor(Qt::OpenHandCursor);
dragButton->setStyleSheet("QPushButton { "
"background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a4a4a, stop:1 #3a3a3a); "
"border: none; color: #888; font-size: 10px; }"
"QPushButton:hover { background: #5a5a5a; }");
bottomLayout->addWidget(dragButton);
// Resize handle fills the rest
resizeHandle = new QWidget(bottomBar);
resizeHandle->setFixedHeight(8);
resizeHandle->setCursor(Qt::SizeVerCursor);
resizeHandle->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:0, y2:1, "
"stop:0 #3a3a3a, stop:1 #2a2a2a);");
bottomLayout->addWidget(resizeHandle, 1);
frameLayout->addWidget(bottomBar);
mainLayout->addWidget(frame);
// Set size policy
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
// Calculate initial height - use panel's size hint if available
int panelHint = analyticsPanel->sizeHint().height();
int panelMin = analyticsPanel->minimumSizeHint().height();
// Start with the larger of panel's hint and panel's minimum hint
currentHeight = qMax(panelHint + 8, panelMin + 8);
updateSizeConstraints();
// Install event filters
dragButton->installEventFilter(this);
resizeHandle->installEventFilter(this);
// Timer for auto-scroll during drag
autoScrollTimer = new QTimer(this);
autoScrollTimer->setInterval(50);
connect(autoScrollTimer, &QTimer::timeout, this, &ResizablePanel::performAutoScroll);
}
void ResizablePanel::setSelected(bool selected)
{
if (selected) {
selectionOverlay->setGeometry(0, 0, width(), height());
selectionOverlay->show();
} else {
selectionOverlay->hide();
}
}
void ResizablePanel::setHeightFromSaved(int h)
{
if (h > 0) {
currentHeight = qMax(h, getMinimumAllowedHeight());
updateSizeConstraints();
}
}
int ResizablePanel::getCurrentHeight() const
{
return currentHeight;
}
QSize ResizablePanel::sizeHint() const
{
return QSize(width(), currentHeight);
}
QSize ResizablePanel::minimumSizeHint() const
{
return QSize(0, getMinimumAllowedHeight());
}
// =====================================================================================================================
// Event Handling
// =====================================================================================================================
bool ResizablePanel::eventFilter(QObject *obj, QEvent *event)
{
if (obj == dragButton) {
if (event->type() == QEvent::MouseButtonPress) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::LeftButton) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
dragStartPos = mouseEvent->globalPosition().toPoint();
#else
dragStartPos = mouseEvent->globalPos();
#endif
isDraggingPanel = false;
dragButton->setCursor(Qt::ClosedHandCursor);
}
return false;
} else if (event->type() == QEvent::MouseMove) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->buttons() & Qt::LeftButton) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPoint currentPos = mouseEvent->globalPosition().toPoint();
#else
QPoint currentPos = mouseEvent->globalPos();
#endif
int distance = (currentPos - dragStartPos).manhattanLength();
if (distance >= 5 && !isDraggingPanel) {
isDraggingPanel = true;
startDrag();
return true;
}
}
return false;
} else if (event->type() == QEvent::MouseButtonRelease) {
dragButton->setCursor(Qt::OpenHandCursor);
isDraggingPanel = false;
return false;
}
}
if (obj == resizeHandle) {
if (event->type() == QEvent::MouseButtonPress) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
resizeStartY = mouseEvent->globalPosition().y();
#else
resizeStartY = mouseEvent->globalPos().y();
#endif
isResizing = true;
resizeStartHeight = currentHeight;
resizeHandle->grabMouse();
return true;
} else if (event->type() == QEvent::MouseMove && isResizing) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
int deltaY = mouseEvent->globalPosition().y() - resizeStartY;
#else
int deltaY = mouseEvent->globalPos().y() - resizeStartY;
#endif
int newHeight = resizeStartHeight + deltaY;
int minAllowed = getMinimumAllowedHeight();
newHeight = qMax(newHeight, minAllowed);
currentHeight = newHeight;
updateSizeConstraints();
return true;
} else if (event->type() == QEvent::MouseButtonRelease) {
isResizing = false;
resizeHandle->releaseMouse();
return true;
}
}
return QWidget::eventFilter(obj, event);
}
void ResizablePanel::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
event->acceptProposedAction();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
showDropIndicator(event->position().y());
#else
showDropIndicator(event->pos().y());
#endif
}
}
void ResizablePanel::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
event->acceptProposedAction();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
showDropIndicator(event->position().y());
lastDragPos = mapToGlobal(event->position().toPoint());
#else
showDropIndicator(event->pos().y());
lastDragPos = mapToGlobal(event->pos());
#endif
if (!autoScrollTimer->isActive()) {
autoScrollTimer->start();
}
}
}
void ResizablePanel::dragLeaveEvent(QDragLeaveEvent *event)
{
Q_UNUSED(event);
hideDropIndicator();
autoScrollTimer->stop();
}
void ResizablePanel::dropEvent(QDropEvent *event)
{
hideDropIndicator();
autoScrollTimer->stop();
if (event->mimeData()->hasFormat("application/x-resizablepanel")) {
QByteArray data = event->mimeData()->data("application/x-resizablepanel");
quintptr ptr = *reinterpret_cast<const quintptr *>(data.constData());
ResizablePanel *draggedPanel = reinterpret_cast<ResizablePanel *>(ptr);
if (draggedPanel && draggedPanel != this) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
bool insertBefore = (event->position().y() < height() / 2);
#else
bool insertBefore = (event->pos().y() < height() / 2);
#endif
emit dropRequested(draggedPanel, this, insertBefore);
event->acceptProposedAction();
}
}
}
void ResizablePanel::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (selectionOverlay->isVisible()) {
selectionOverlay->setGeometry(0, 0, width(), height());
}
if (dropIndicator->isVisible()) {
dropIndicator->setGeometry(0, dropIndicator->y(), width(), dropIndicator->height());
}
}
// =====================================================================================================================
// Private Helpers
// =====================================================================================================================
int ResizablePanel::getMinimumAllowedHeight() const
{
QSize panelMin = panel->minimumSizeHint();
int panelMinHeight = (panelMin.isValid() && panelMin.height() > 0) ? panelMin.height() : 100;
return panelMinHeight + 8;
}
void ResizablePanel::updateSizeConstraints()
{
setMinimumHeight(currentHeight);
setMaximumHeight(currentHeight);
updateGeometry();
}
void ResizablePanel::startDrag()
{
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
quintptr ptr = reinterpret_cast<quintptr>(this);
QByteArray data(reinterpret_cast<const char *>(&ptr), sizeof(ptr));
mimeData->setData("application/x-resizablepanel", data);
drag->setMimeData(mimeData);
QPixmap pixmap(width(), 40);
pixmap.fill(QColor(58, 58, 58, 200));
drag->setPixmap(pixmap);
drag->setHotSpot(QPoint(width() / 2, 20));
emit dragStarted(this);
autoScrollTimer->start();
Qt::DropAction result = drag->exec(Qt::MoveAction);
Q_UNUSED(result);
autoScrollTimer->stop();
dragButton->setCursor(Qt::OpenHandCursor);
isDraggingPanel = false;
}
void ResizablePanel::performAutoScroll()
{
QScrollArea *scrollArea = QtUtils::findParentOfType<QScrollArea>(this);
if (!scrollArea) {
return;
}
QScrollBar *scrollBar = scrollArea->verticalScrollBar();
if (!scrollBar) {
return;
}
QRect scrollRect = scrollArea->viewport()->rect();
QPoint scrollTopLeft = scrollArea->viewport()->mapToGlobal(scrollRect.topLeft());
QRect globalScrollRect(scrollTopLeft, scrollRect.size());
const int scrollMargin = 50;
int scrollSpeed = 0;
if (lastDragPos.y() < globalScrollRect.top() + scrollMargin) {
scrollSpeed = -15;
} else if (lastDragPos.y() > globalScrollRect.bottom() - scrollMargin) {
scrollSpeed = 15;
}
if (scrollSpeed != 0) {
int newValue = scrollBar->value() + scrollSpeed;
newValue = qBound(scrollBar->minimum(), newValue, scrollBar->maximum());
scrollBar->setValue(newValue);
}
}
void ResizablePanel::showDropIndicator(double y)
{
bool before = (y < height() / 2);
dropIndicator->setGeometry(0, before ? 0 : height() - 3, width(), 3);
dropIndicator->show();
}
void ResizablePanel::hideDropIndicator()
{
dropIndicator->hide();
}

View File

@@ -1,79 +0,0 @@
#ifndef COCKATRICE_RESIZABLE_PANEL_H
#define COCKATRICE_RESIZABLE_PANEL_H
#include "abstract_analytics_panel_widget.h"
#include <QApplication>
#include <QDrag>
#include <QFrame>
#include <QMimeData>
#include <QMouseEvent>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
class ResizablePanel : public QWidget
{
Q_OBJECT
public:
explicit ResizablePanel(const QString &typeId,
AbstractAnalyticsPanelWidget *analyticsPanel,
QWidget *parent = nullptr);
void setSelected(bool selected);
void setHeightFromSaved(int h);
int getCurrentHeight() const;
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
QString getTypeId() const
{
return typeId;
}
AbstractAnalyticsPanelWidget *panel;
signals:
void dragStarted(ResizablePanel *panel);
void dropRequested(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void dropEvent(QDropEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
int getMinimumAllowedHeight() const;
void updateSizeConstraints();
void startDrag();
void performAutoScroll();
void showDropIndicator(double y);
void hideDropIndicator();
QString typeId;
QFrame *frame;
QFrame *selectionOverlay;
QFrame *dropIndicator;
QPushButton *dragButton;
QWidget *resizeHandle;
int currentHeight;
bool isResizing = false;
bool isDraggingPanel = false;
double resizeStartY = 0;
int resizeStartHeight = 0;
QPoint dragStartPos;
QPoint lastDragPos;
QTimer *autoScrollTimer;
};
#endif // COCKATRICE_RESIZABLE_PANEL_H

View File

@@ -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 <QComboBox>
#include <QDockWidget>
@@ -38,7 +38,7 @@ static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo)
}
DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent)
: QDockWidget(parent), deckEditor(parent), deckStateManager(parent->deckStateManager)
: QDockWidget(parent), deckEditor(parent)
{
setObjectName("deckDock");
@@ -52,21 +52,19 @@ DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent
void DeckEditorDeckDockWidget::createDeckDock()
{
connect(getModel(), &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash);
deckModel = new DeckListModel(this);
deckModel->setObjectName("deckModel");
connect(deckModel, &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash);
deckLoader = new DeckLoader(this);
proxy = new DeckListStyleProxy(this);
proxy->setSourceModel(getModel());
proxy->setSourceModel(deckModel);
historyManagerWidget = new DeckListHistoryManagerWidget(deckStateManager, proxy, this);
historyManagerWidget = new DeckListHistoryManagerWidget(deckModel, proxy, deckEditor->getHistoryManager(), 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);
@@ -99,7 +97,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
nameDebounceTimer = new QTimer(this);
nameDebounceTimer->setSingleShot(true);
nameDebounceTimer->setInterval(300); // debounce duration in ms
connect(nameDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeName);
connect(nameDebounceTimer, &QTimer::timeout, this, [this]() { updateName(nameEdit->text()); });
connect(nameEdit, &LineEditUnfocusable::textChanged, this, [this]() {
nameDebounceTimer->start(); // restart debounce timer
@@ -143,7 +141,7 @@ void DeckEditorDeckDockWidget::createDeckDock()
commentsDebounceTimer = new QTimer(this);
commentsDebounceTimer->setSingleShot(true);
commentsDebounceTimer->setInterval(400); // longer debounce for multi-line
connect(commentsDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeComments);
connect(commentsDebounceTimer, &QTimer::timeout, this, [this]() { updateComments(); });
connect(commentsEdit, &QTextEdit::textChanged, this, [this]() {
commentsDebounceTimer->start(); // restart debounce timer
@@ -154,21 +152,21 @@ void DeckEditorDeckDockWidget::createDeckDock()
bannerCardLabel->setText(tr("Banner Card"));
bannerCardLabel->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
bannerCardComboBox = new QComboBox(this);
connect(getModel(), &DeckListModel::dataChanged, this, [this]() {
connect(deckModel, &DeckListModel::dataChanged, this, [this]() {
// Delay the update to avoid race conditions
QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox);
});
connect(getModel(), &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand);
connect(getModel(), &DeckListModel::modelReset, this, &DeckEditorDeckDockWidget::expandAll);
connect(deckModel, &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand);
connect(deckModel, &DeckListModel::deckReplaced, this, &DeckEditorDeckDockWidget::expandAll);
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&DeckEditorDeckDockWidget::writeBannerCard);
&DeckEditorDeckDockWidget::setBannerCard);
bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible());
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {});
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()->getTags());
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager,
&DeckStateManager::setTags);
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this,
&DeckEditorDeckDockWidget::setTags);
activeGroupCriteriaLabel = new QLabel(this);
@@ -176,8 +174,11 @@ 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<int>(&QComboBox::currentIndexChanged), this,
&DeckEditorDeckDockWidget::applyActiveGroupCriteria);
connect(activeGroupCriteriaComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() {
deckModel->setActiveGroupCriteria(static_cast<DeckListModelGroupCriteria::Type>(
activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
});
aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment"));
@@ -294,9 +295,9 @@ void DeckEditorDeckDockWidget::initializeFormats()
formatComboBox->addItem(formatName, formatName); // store the raw key in itemData
}
QString format = deckStateManager->getMetadata().gameFormat;
if (!format.isEmpty()) {
formatComboBox->setCurrentIndex(formatComboBox->findData(format));
if (!deckModel->getDeckList()->getGameFormat().isEmpty()) {
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
} else {
// Ensure no selection is visible initially
formatComboBox->setCurrentIndex(-1);
@@ -305,10 +306,11 @@ void DeckEditorDeckDockWidget::initializeFormats()
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString();
deckStateManager->setFormat(formatKey);
deckModel->setActiveFormat(formatKey);
} else {
deckStateManager->setFormat(""); // clear format if deselected
deckModel->setActiveFormat(QString()); // clear format if deselected
}
emit deckModified();
});
}
@@ -338,37 +340,43 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*&current*/, const QModelIndex & /*previous*/)
{
if (ExactCard card = getCurrentCard()) {
emit selectedCardChanged(card);
emit cardChanged(card);
}
}
/**
* @brief Writes the contents of the name textBox to the DeckStateManager
*/
void DeckEditorDeckDockWidget::writeName()
void DeckEditorDeckDockWidget::updateName(const QString &name)
{
QString name = nameEdit->text();
deckStateManager->setName(name);
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();
}
/**
* @brief Writes the contents of the comments textBox to the DeckStateManager
*/
void DeckEditorDeckDockWidget::writeComments()
void DeckEditorDeckDockWidget::updateComments()
{
QString comments = commentsEdit->toPlainText();
deckStateManager->setComments(comments);
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();
}
void DeckEditorDeckDockWidget::updateHash()
{
hashLabel->setText(deckStateManager->getDeckHash());
hashLabel->setText(deckModel->getDeckList()->getDeckHash());
emit hashChanged();
emit deckModified();
}
void DeckEditorDeckDockWidget::updateBannerCardComboBox()
{
// Store current banner card identity
CardRef wanted = deckStateManager->getMetadata().bannerCard;
CardRef wanted = deckModel->getDeckList()->getBannerCard();
// Block signals temporarily
bool wasBlocked = bannerCardComboBox->blockSignals(true);
@@ -378,7 +386,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
// Collect unique (name, providerId) pairs
QSet<QPair<QString, QString>> bannerCardSet;
QList<CardRef> cardsInDeck = getModel()->getCardRefs();
QList<CardRef> cardsInDeck = deckModel->getCardRefs();
for (auto cardRef : cardsInDeck) {
if (!CardDatabaseManager::query()->getCard(cardRef)) {
@@ -407,6 +415,7 @@ 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, "-");
@@ -417,21 +426,25 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
bannerCardComboBox->blockSignals(wasBlocked);
}
/**
* @brief Writes the selected bannerCard to the DeckStateManager
*/
void DeckEditorDeckDockWidget::writeBannerCard(int index)
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
{
auto [name, id] = bannerCardComboBox->itemData(index).value<QPair<QString, QString>>();
CardRef bannerCard = {name, id};
deckStateManager->setBannerCard(bannerCard);
emit requestDeckHistorySave(tr("Banner card changed"));
syncDeckListBannerCardWithComboBox();
deckEditor->setModified(true);
emit deckModified();
}
void DeckEditorDeckDockWidget::applyActiveGroupCriteria()
void DeckEditorDeckDockWidget::setTags(const QStringList &tags)
{
getModel()->setActiveGroupCriteria(
static_cast<DeckListModelGroupCriteria::Type>(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckModel->getDeckList()->setTags(tags);
deckEditor->setModified(true);
emit deckModified();
}
void DeckEditorDeckDockWidget::syncDeckListBannerCardWithComboBox()
{
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
deckModel->getDeckList()->setBannerCard({name, id});
}
void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible)
@@ -447,7 +460,7 @@ void DeckEditorDeckDockWidget::updateShowTagsWidget(const bool visible)
void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
{
if (deckStateManager->getMetadata().bannerCard.name == "") {
if (deckModel->getDeckList()->getBannerCard().name == "") {
if (bannerCardComboBox->findText("-") != -1) {
bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText("-"));
} else {
@@ -455,26 +468,36 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
bannerCardComboBox->setCurrentIndex(0);
}
} else {
bannerCardComboBox->setCurrentText(deckStateManager->getMetadata().bannerCard.name);
bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().name);
}
}
void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex)
/**
* Sets the currently active deck for this tab
* @param _deck The deck.
*/
void DeckEditorDeckDockWidget::setDeck(const LoadedDeck &_deck)
{
deckView->clearSelection();
deckView->setCurrentIndex(newCardIndex);
recursiveExpand(newCardIndex);
deckView->setFocus(Qt::FocusReason::MouseFocusReason);
deckLoader->setDeck(_deck);
deckModel->setDeckList(&deckLoader->getDeck().deckList);
connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree);
emit requestDeckHistoryClear();
historyManagerWidget->setDeckListModel(deckModel);
syncDisplayWidgetsToModel();
emit deckChanged();
}
void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
{
nameEdit->blockSignals(true);
nameEdit->setText(deckStateManager->getMetadata().name);
nameEdit->setText(deckModel->getDeckList()->getName());
nameEdit->blockSignals(false);
commentsEdit->blockSignals(true);
commentsEdit->setText(deckStateManager->getMetadata().comments);
commentsEdit->setText(deckModel->getDeckList()->getComments());
commentsEdit->blockSignals(false);
bannerCardComboBox->blockSignals(true);
@@ -482,22 +505,46 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
updateBannerCardComboBox();
bannerCardComboBox->blockSignals(false);
updateHash();
sortDeckModelToDeckView();
formatComboBox->blockSignals(true);
formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat));
formatComboBox->blockSignals(false);
deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags());
}
deckTagsDisplayWidget->blockSignals(true);
deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags);
deckTagsDisplayWidget->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();
}
/**
* @brief Convenience method to get the underlying model instance from the DeckStateManager
* Resets the tab to the state for a blank new tab.
*/
DeckListModel *DeckEditorDeckDockWidget::getModel() const
void DeckEditorDeckDockWidget::cleanDeck()
{
return deckStateManager->getModel();
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());
}
void DeckEditorDeckDockWidget::selectPrevCard()
@@ -524,15 +571,6 @@ 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;
@@ -578,19 +616,18 @@ void DeckEditorDeckDockWidget::expandAll()
}
/**
* Gets the source index of all the currently selected card nodes in the decklist table.
* Gets the 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 list containing the source indices of all selected card nodes
* @return A model index list containing all selected card nodes
*/
QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodeSourceIndices() const
QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const
{
auto selectedRows = deckView->selectionModel()->selectedRows();
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); };
const auto notLeafNode = [this](const QModelIndex &index) {
return deckModel->hasChildren(proxy->mapToSource(index));
};
selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end());
std::reverse(selectedRows.begin(), selectedRows.end());
@@ -604,15 +641,29 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString &
}
QString zoneName = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : _zoneName;
deckStateManager->addCard(card, 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();
}
void DeckEditorDeckDockWidget::actIncrementSelection()
{
auto selectedRows = getSelectedCardNodeSourceIndices();
auto selectedRows = getSelectedCardNodes();
for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(sourceIndex, true);
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, true);
}
}
@@ -621,17 +672,17 @@ void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString
QString providerId = card.getPrinting().getUuid();
QString collectorNumber = card.getPrinting().getProperty("num");
QModelIndex foundCard = getModel()->findCard(card.getName(), zoneName, providerId, collectorNumber);
QModelIndex foundCard = deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber);
if (!foundCard.isValid()) {
foundCard = getModel()->findCard(card.getName(), zoneName);
foundCard = deckModel->findCard(card.getName(), zoneName);
}
deckStateManager->swapCardAtIndex(foundCard);
swapCard(foundCard);
}
void DeckEditorDeckDockWidget::actSwapSelection()
{
auto selectedRows = getSelectedCardNodeSourceIndices();
auto selectedRows = getSelectedCardNodes();
// 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
@@ -639,15 +690,54 @@ void DeckEditorDeckDockWidget::actSwapSelection()
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &sourceIndex : selectedRows) {
deckStateManager->swapCardAtIndex(sourceIndex);
bool isModified = false;
for (const auto &currentIndex : selectedRows) {
if (swapCard(currentIndex)) {
isModified = true;
}
}
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 &currentIndex)
{
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)
@@ -655,12 +745,22 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z
if (card.getInfo().getIsToken())
zoneName = DECK_ZONE_TOKENS;
deckStateManager->decrementCard(card, zoneName);
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);
}
void DeckEditorDeckDockWidget::actDecrementSelection()
{
auto selectedRows = getSelectedCardNodeSourceIndices();
auto selectedRows = getSelectedCardNodes();
// 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
@@ -668,8 +768,8 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(sourceIndex, false);
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, false);
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -677,7 +777,7 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
void DeckEditorDeckDockWidget::actRemoveCard()
{
auto selectedRows = getSelectedCardNodeSourceIndices();
auto selectedRows = getSelectedCardNodes();
// 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
@@ -685,29 +785,56 @@ void DeckEditorDeckDockWidget::actRemoveCard()
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &sourceIndex : selectedRows) {
deckStateManager->removeCardAtIndex(sourceIndex);
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;
}
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 source index
* @param idx The proxy index
* @param isIncrement If true, increments the count. If false, decrements the count
*/
void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement)
{
if (!idx.isValid() || getModel()->hasChildren(idx)) {
if (!idx.isValid() || deckModel->hasChildren(idx)) {
return;
}
if (isIncrement) {
deckStateManager->incrementCountAtIndex(idx);
} else {
deckStateManager->decrementCountAtIndex(idx);
}
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();
}
void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point)

View File

@@ -28,14 +28,22 @@ 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;
@@ -47,10 +55,15 @@ 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);
@@ -61,12 +74,17 @@ public slots:
void initializeFormats();
signals:
void selectedCardChanged(const ExactCard &card);
void nameChanged();
void commentsChanged();
void hashChanged();
void deckChanged();
void deckModified();
void requestDeckHistorySave(const QString &modificationReason);
void requestDeckHistoryClear();
void cardChanged(const ExactCard &_card);
private:
AbstractTabDeckEditor *deckEditor;
DeckStateManager *deckStateManager;
DeckListHistoryManagerWidget *historyManagerWidget;
KeySignals deckViewKeySignals;
QLabel *nameLabel;
@@ -89,18 +107,18 @@ private:
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
DeckListModel *getModel() const;
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
[[nodiscard]] QModelIndexList getSelectedCardNodes() const;
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
private slots:
void decklistCustomMenu(QPoint point);
bool swapCard(const QModelIndex &currentIndex);
void updateCard(QModelIndex, const QModelIndex &current);
void writeName();
void writeComments();
void writeBannerCard(int);
void applyActiveGroupCriteria();
void setSelectedIndex(const QModelIndex &newCardIndex);
void updateName(const QString &name);
void updateComments();
void setBannerCard(int);
void setTags(const QStringList &tags);
void syncDeckListBannerCardWithComboBox();
void updateHash();
void refreshShortcuts();
void updateShowBannerCardComboBox(bool visible);

View File

@@ -1,11 +1,10 @@
#include "deck_list_history_manager_widget.h"
#include "deck_state_manager.h"
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager,
DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckListModel,
DeckListStyleProxy *_styleProxy,
DeckListHistoryManager *manager,
QWidget *parent)
: QWidget(parent), deckStateManager(_deckStateManager), styleProxy(_styleProxy)
: QWidget(parent), deckListModel(_deckListModel), styleProxy(_styleProxy), historyManager(manager)
{
layout = new QHBoxLayout(this);
@@ -44,7 +43,8 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_de
connect(historyList, &QListWidget::itemClicked, this, &DeckListHistoryManagerWidget::onListClicked);
connect(deckStateManager, &DeckStateManager::historyChanged, this, &DeckListHistoryManagerWidget::refreshList);
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this,
&DeckListHistoryManagerWidget::refreshList);
refreshList();
retranslateUi();
@@ -58,12 +58,15 @@ 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
@@ -95,7 +98,36 @@ void DeckListHistoryManagerWidget::refreshList()
redoButton->setEnabled(historyManager->canRedo());
}
void DeckListHistoryManagerWidget::onListClicked(const QListWidgetItem *item)
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)
{
// Ignore non-selectable items (like divider)
if (!(item->flags() & Qt::ItemIsSelectable)) {
@@ -106,24 +138,23 @@ void DeckListHistoryManagerWidget::onListClicked(const QListWidgetItem *item)
int index = item->data(Qt::UserRole + 1).toInt();
if (mode == "redo") {
const auto redoStack = deckStateManager->getHistoryManager()->getRedoStack();
const auto redoStack = historyManager->getRedoStack();
int steps = redoStack.size() - index;
deckStateManager->redo(steps);
for (int i = 0; i < steps; ++i) {
historyManager->redo(deckListModel->getDeckList());
}
} else if (mode == "undo") {
const auto undoStack = deckStateManager->getHistoryManager()->getUndoStack();
int steps = undoStack.size() - index;
deckStateManager->undo(steps);
const auto undoStack = historyManager->getUndoStack();
int steps = undoStack.size() - 1 - index;
for (int i = 0; i < steps + 1; ++i) {
historyManager->undo(deckListModel->getDeckList());
}
}
deckListModel->rebuildTree();
emit deckListModel->layoutChanged();
emit requestDisplayWidgetSync();
refreshList();
}
void DeckListHistoryManagerWidget::doUndo()
{
deckStateManager->undo();
}
void DeckListHistoryManagerWidget::doRedo()
{
deckStateManager->redo();
}

View File

@@ -14,8 +14,6 @@
#include <libcockatrice/deck_list/deck_list_history_manager.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
class DeckStateManager;
class DeckListHistoryManagerWidget : public QWidget
{
Q_OBJECT
@@ -27,19 +25,22 @@ public slots:
void retranslateUi();
public:
explicit DeckListHistoryManagerWidget(DeckStateManager *deckStateManager,
explicit DeckListHistoryManagerWidget(DeckListModel *deckListModel,
DeckListStyleProxy *styleProxy,
DeckListHistoryManager *manager,
QWidget *parent = nullptr);
void setDeckListModel(DeckListModel *_deckListModel);
private slots:
void refreshList();
void onListClicked(const QListWidgetItem *item);
void onListClicked(QListWidgetItem *item);
void doUndo();
void doRedo();
private:
DeckStateManager *deckStateManager;
DeckListModel *deckListModel;
DeckListStyleProxy *styleProxy;
DeckListHistoryManager *historyManager;
QHBoxLayout *layout;
QAction *aUndo;

View File

@@ -1,361 +0,0 @@
#include "deck_state_manager.h"
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list_history_manager.h>
DeckStateManager::DeckStateManager(QObject *parent)
: QObject(parent), deckList(QSharedPointer<DeckList>(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<DeckList>(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<bool(DeckListModel *)> &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<QModelIndex(DeckListModel *)> &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();
}

View File

@@ -1,297 +0,0 @@
#ifndef COCKATRICE_DECK_STATE_MANAGER_H
#define COCKATRICE_DECK_STATE_MANAGER_H
#include "../../deck_loader/loaded_deck.h"
#include "deck_list_model.h"
#include <QSharedPointer>
#include <libcockatrice/deck_list/deck_list.h>
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> 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<bool(DeckListModel *)> &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<QModelIndex(DeckListModel *)> &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

View File

@@ -49,7 +49,7 @@ DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent
playernameEdit->setMaxLength(MAX_NAME_LENGTH);
playernameLabel->setBuddy(playernameEdit);
tokenLabel = new QLabel(tr("Token") + ":");
tokenLabel = new QLabel(tr("Token:"));
tokenEdit = new QLineEdit();
tokenEdit->setMaxLength(MAX_NAME_LENGTH);
tokenLabel->setBuddy(tokenLabel);

View File

@@ -2,7 +2,6 @@
#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"
@@ -22,8 +21,7 @@
#include <qdrag.h>
#include <qevent.h>
DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *deckStateManger)
: QDialog(parent), deckStateManager(deckStateManger)
DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckListModel *_model) : QDialog(parent), model(_model)
{
setMinimumSize(500, 500);
setAcceptDrops(true);
@@ -167,39 +165,36 @@ void DlgSelectSetForCards::actOK()
if (modifiedSetsAndCardsMap.isEmpty()) {
accept(); // Nothing to do
return;
} else {
emit deckAboutToBeModified(tr("Bulk modified printings."));
}
auto bulkModify = [&modifiedSetsAndCardsMap](DeckListModel *model) {
for (QString modifiedSet : modifiedSetsAndCardsMap.keys()) {
for (QString card : modifiedSetsAndCardsMap.value(modifiedSet)) {
swapPrinting(model, modifiedSet, card);
}
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()
{
deckStateManager->modifyDeck(tr("Cleared all printing information."), [](auto model) {
model->forEachCard(CardNodeFunction::ClearPrintingData());
return true;
});
emit deckAboutToBeModified(tr("Cleared all printing information."));
model->forEachCard(CardNodeFunction::ClearPrintingData());
emit deckModified();
accept();
}
void DlgSelectSetForCards::actSetAllToPreferred()
{
deckStateManager->modifyDeck(tr("Set all printings to preferred."), [](auto model) {
model->forEachCard(CardNodeFunction::ClearPrintingData());
model->forEachCard(CardNodeFunction::SetProviderIdToPreferred());
return true;
});
emit deckAboutToBeModified(tr("Set all printings to preferred."));
model->forEachCard(CardNodeFunction::ClearPrintingData());
model->forEachCard(CardNodeFunction::SetProviderIdToPreferred());
emit deckModified();
accept();
}
@@ -232,8 +227,10 @@ void DlgSelectSetForCards::sortSetsByCount()
QMap<QString, int> DlgSelectSetForCards::getSetsForCards()
{
QMap<QString, int> setCounts;
if (!model)
return setCounts;
QList<QString> cardNames = deckStateManager->getModel()->getCardNames();
QList<QString> cardNames = model->getCardNames();
for (auto cardName : cardNames) {
CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(cardName);
@@ -272,7 +269,7 @@ void DlgSelectSetForCards::updateCardLists()
}
}
QList<QString> cardNames = deckStateManager->getModel()->getCardNames();
QList<QString> cardNames = model->getCardNames();
for (auto cardName : cardNames) {
bool found = false;
@@ -354,8 +351,10 @@ void DlgSelectSetForCards::dropEvent(QDropEvent *event)
QMap<QString, QStringList> DlgSelectSetForCards::getCardsForSets()
{
QMap<QString, QStringList> setCards;
if (!model)
return setCards;
QList<QString> cardNames = deckStateManager->getModel()->getCardNames();
QList<QString> cardNames = model->getCardNames();
for (auto cardName : cardNames) {
CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(cardName);

View File

@@ -18,7 +18,6 @@
#include <QVBoxLayout>
#include <libcockatrice/models/deck_list/deck_list_model.h>
class DeckStateManager;
class SetEntryWidget; // Forward declaration
class DlgSelectSetForCards : public QDialog
@@ -26,7 +25,7 @@ class DlgSelectSetForCards : public QDialog
Q_OBJECT
public:
explicit DlgSelectSetForCards(QWidget *parent, DeckStateManager *deckStateManager);
explicit DlgSelectSetForCards(QWidget *parent, DeckListModel *_model);
void retranslateUi();
void sortSetsByCount();
QMap<QString, QStringList> getCardsForSets();
@@ -38,6 +37,7 @@ 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;
DeckStateManager *deckStateManager;
DeckListModel *model;
QMap<QString, SetEntryWidget *> setEntries;
QPushButton *clearButton;
QPushButton *setAllToPreferredButton;

View File

@@ -441,7 +441,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
});
homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600);
homeTabBackgroundShuffleFrequencySpinBox.setSuffix(QString(" ") + tr("seconds"));
homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds"));
homeTabBackgroundShuffleFrequencySpinBox.setValue(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency());
connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload<int>(&QSpinBox::valueChanged),
&SettingsCache::instance(), &SettingsCache::setHomeTabBackgroundShuffleFrequency);
@@ -462,13 +462,8 @@ 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);
@@ -732,7 +727,6 @@ 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"));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,12 +11,16 @@
* UI elements for managing card counts in both the mainboard and sideboard zones.
*
* @param parent The parent widget.
* @param deckStateManager Pointer to the DeckStateManager
* @param deckEditor Pointer to the TabDeckEditor.
* @param deckModel Pointer to the DeckListModel.
* @param deckView Pointer to the QTreeView for the deck display.
* @param cardSizeSlider Pointer to the QSlider used for dynamic font resizing.
* @param rootCard The root card for the widget.
*/
AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent,
DeckStateManager *deckStateManager,
AbstractTabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView,
QSlider *cardSizeSlider,
const ExactCard &rootCard)
: QWidget(parent), cardSizeSlider(cardSizeSlider)
@@ -28,9 +32,11 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent,
setContentsMargins(5, 5, 5, 5); // Padding around the text
zoneLabelMainboard = new ShadowBackgroundLabel(this, tr("Mainboard"));
buttonBoxMainboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_MAIN);
buttonBoxMainboard =
new CardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard, DECK_ZONE_MAIN);
zoneLabelSideboard = new ShadowBackgroundLabel(this, tr("Sideboard"));
buttonBoxSideboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_SIDE);
buttonBoxSideboard =
new CardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard, DECK_ZONE_SIDE);
layout->addWidget(zoneLabelMainboard, 0, Qt::AlignHCenter | Qt::AlignBottom);
layout->addWidget(buttonBoxMainboard, 0, Qt::AlignHCenter | Qt::AlignTop);
@@ -72,12 +78,6 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage)
repaint();
}
void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount)
{
buttonBoxMainboard->setAmount(mainboardAmount);
buttonBoxSideboard->setAmount(sideboardAmount);
}
/**
* @brief Gets the card count in the mainboard zone.
*
@@ -85,7 +85,7 @@ void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmou
*/
int AllZonesCardAmountWidget::getMainboardAmount()
{
return buttonBoxMainboard->getAmount();
return buttonBoxMainboard->countCardsInZone(DECK_ZONE_MAIN);
}
/**
@@ -95,15 +95,7 @@ int AllZonesCardAmountWidget::getMainboardAmount()
*/
int AllZonesCardAmountWidget::getSideboardAmount()
{
return buttonBoxSideboard->getAmount();
}
/**
* @brief Checks if the amount is at least one in either the mainboard or sideboard.
*/
bool AllZonesCardAmountWidget::isNonZero()
{
return getMainboardAmount() > 0 || getSideboardAmount() > 0;
return buttonBoxSideboard->countCardsInZone(DECK_ZONE_SIDE);
}
/**

View File

@@ -18,13 +18,13 @@ class AllZonesCardAmountWidget : public QWidget
Q_OBJECT
public:
explicit AllZonesCardAmountWidget(QWidget *parent,
DeckStateManager *deckStateManager,
AbstractTabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView,
QSlider *cardSizeSlider,
const ExactCard &rootCard);
int getMainboardAmount();
int getSideboardAmount();
bool isNonZero();
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void enterEvent(QEnterEvent *event) override;
#else
@@ -33,7 +33,6 @@ public:
public slots:
void adjustFontSize(int scalePercentage);
void setAmounts(int mainboardAmount, int sideboardAmount);
private:
QVBoxLayout *layout;

View File

@@ -1,7 +1,5 @@
#include "card_amount_widget.h"
#include "../deck_editor/deck_state_manager.h"
#include <QPainter>
#include <QTimer>
@@ -9,17 +7,22 @@
* @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,
DeckStateManager *deckStateManager,
AbstractTabDeckEditor *deckEditor,
DeckListModel *deckModel,
QTreeView *deckView,
QSlider *cardSizeSlider,
const ExactCard &rootCard,
const QString &zoneName)
: QWidget(parent), deckStateManager(deckStateManager), cardSizeSlider(cardSizeSlider), rootCard(rootCard),
zoneName(zoneName), hovered(false)
: QWidget(parent), deckEditor(deckEditor), deckModel(deckModel), deckView(deckView), cardSizeSlider(cardSizeSlider),
rootCard(rootCard), zoneName(zoneName), hovered(false)
{
layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
@@ -45,26 +48,23 @@ CardAmountWidget::CardAmountWidget(QWidget *parent,
connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingSideboard);
}
cardCountInZone = new QLabel(QString::number(amount), this);
cardCountInZone = new QLabel(QString::number(countCardsInZone(zoneName)), this);
cardCountInZone->setAlignment(Qt::AlignCenter);
layout->addWidget(decrementButton);
layout->addWidget(cardCountInZone);
layout->addWidget(incrementButton);
// React to model changes
connect(deckModel, &DeckListModel::dataChanged, this, &CardAmountWidget::updateCardCount);
connect(deckModel, &QAbstractItemModel::rowsRemoved, this, &CardAmountWidget::updateCardCount);
// Connect slider for dynamic font size adjustment
connect(cardSizeSlider, &QSlider::valueChanged, this, &CardAmountWidget::adjustFontSize);
}
int CardAmountWidget::getAmount()
{
return amount;
}
void CardAmountWidget::setAmount(int _amount)
{
amount = _amount;
updateCardCount();
if (deckEditor) {
connect(this, &CardAmountWidget::deckModified, deckEditor, &AbstractTabDeckEditor::onDeckHistorySaveRequested);
}
}
/**
@@ -132,7 +132,7 @@ void CardAmountWidget::adjustFontSize(int scalePercentage)
*/
void CardAmountWidget::updateCardCount()
{
cardCountInZone->setText("<font color='white'>" + QString::number(amount) + "</font>");
cardCountInZone->setText("<font color='white'>" + QString::number(countCardsInZone(zoneName)) + "</font>");
layout->invalidate();
layout->activate();
}
@@ -151,7 +151,9 @@ 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) {
model->offsetCountAtIndex(newCardIndex, extraCopies);
for (int i = 0; i < extraCopies; i++) {
model->addCard(rootCard, zone);
}
model->removeRow(existing.row(), existing.parent());
}
@@ -168,7 +170,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 = deckStateManager->getModel()->findCard(rootCard.getName(), zone);
QModelIndex existing = deckModel->findCard(rootCard.getName(), zone);
int extraCopies = 0;
bool replacingProviderless = false;
@@ -177,8 +179,8 @@ void CardAmountWidget::addPrinting(const QString &zone)
QString foundProviderId =
existing.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString();
if (foundProviderId.isEmpty()) {
int existingAmount = existing.data(Qt::DisplayRole).toInt();
extraCopies = existingAmount - 1; // One less because we *always* add one
int amount = existing.data(Qt::DisplayRole).toInt();
extraCopies = amount - 1; // One less because we *always* add one
replacingProviderless = true;
}
}
@@ -192,13 +194,15 @@ 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
QModelIndex newCardIndex = deckStateManager->modifyDeck(reason, [&](auto model) {
return addAndReplacePrintings(model, existing, rootCard, zone, extraCopies, replacingProviderless);
});
auto newCardIndex = addAndReplacePrintings(deckModel, existing, rootCard, zone, extraCopies, replacingProviderless);
if (newCardIndex.isValid()) {
emit deckStateManager->focusIndexChanged(newCardIndex);
deckView->setCurrentIndex(newCardIndex);
deckView->setFocus(Qt::FocusReason::MouseFocusReason);
deckEditor->setModified(true);
}
}
@@ -248,9 +252,31 @@ void CardAmountWidget::decrementCardHelper(const QString &zone)
.arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard")
.arg(rootCard.getPrinting().getUuid());
deckStateManager->modifyDeck(reason, [this, &zone](auto model) {
QModelIndex idx = model->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(),
emit deckModified(reason);
QModelIndex idx = deckModel->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(),
rootCard.getPrinting().getProperty("num"));
return model->offsetCountAtIndex(idx, -1);
});
deckModel->offsetCountAtIndex(idx, -1);
deckEditor->setModified(true);
}
/**
* @brief Counts the number of cards in a specific zone (mainboard or sideboard).
*
* @param deckZone The name of the zone (e.g., DECK_ZONE_MAIN or DECK_ZONE_SIDE).
* @return The number of cards in the zone.
*/
int CardAmountWidget::countCardsInZone(const QString &deckZone)
{
QString uuid = rootCard.getPrinting().getUuid();
if (uuid.isEmpty()) {
return 0; // Cards without uuids/providerIds CANNOT match another card, they are undefined for us.
}
QList<ExactCard> cards = deckModel->getCardsForZone(deckZone);
return std::count_if(cards.cbegin(), cards.cend(),
[&uuid](const ExactCard &card) { return card.getPrinting().getUuid() == uuid; });
}

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