Compare commits

...

13 Commits

Author SHA1 Message Date
tooomm
4373ab5e89 typo 2026-01-03 14:09:48 +01:00
tooomm
b748009b52 colon + space 2026-01-03 13:43:15 +01:00
tooomm
0fef7c149b space 2026-01-03 13:31:20 +01:00
tooomm
69a3cecfe7 colon 2026-01-03 13:05:31 +01:00
tooomm
089342ee75 colon + space 2026-01-03 13:00:56 +01:00
RickyRister
4fbb9d9682 [PrintingSelector] optimize amount calculation (#6478) 2026-01-03 01:04:56 -08:00
RickyRister
84aefda486 [DeckListModel] add getCardNodes method (#6484)
* [DeckListModel] add getCardNodes method

* Update one usage
2026-01-02 18:55:27 -08:00
BruebachL
73cc0541f5 [Game] Add shortcuts for same size and hand size - 1 mulligans (#6483)
Took 21 minutes

Took 3 seconds

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-01-03 00:03:11 +01:00
transifex-integration[bot]
bcf3939fee Translate cockatrice/cockatrice_en@source.ts in fr (#6480) 2026-01-02 16:26:16 +01:00
tooomm
2e6f1128bb Docs: Fix search help rendering and open external link in new tab (#6440)
* Use HTML link to open webpage in new tab

* Fix rendering for doxygen
2026-01-02 14:38:25 +01:00
RickyRister
bbd8671e6e [DeckDockWidget] Fix VDE crash due to not mapping proxy index (#6479) 2026-01-02 14:32:22 +01:00
RickyRister
84e6907fa9 [DeckList] Store sideboardPlans by value to fix crash (#6475) 2026-01-02 09:10:41 +01:00
RickyRister
93a4647b04 [DeckList] move SideboardPlan into separate file (#6474) 2026-01-01 16:24:47 -08:00
33 changed files with 610 additions and 489 deletions

View File

@@ -1,11 +1,13 @@
@page deck_search_syntax_help Deck Search Syntax Help
## Deck Search Syntax Help
-----
The search bar recognizes a set of special commands.<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,10 +1,12 @@
@page search_syntax_help Search Syntax Help
## Search Syntax Help
-----
The search bar recognizes a set of special commands similar to some other card databases.<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

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

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

View File

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

View File

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

View File

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

View File

@@ -168,7 +168,7 @@ void DrawProbabilityWidget::updateFilterOptions()
QMap<QString, int> categoryCounts;
int totalDeckCards = 0;
const auto nodes = analyzer->getModel()->getDeckList()->getCardNodes();
const auto nodes = analyzer->getModel()->getCardNodes();
for (auto *node : nodes) {
CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr();
if (!info) {

View File

@@ -578,18 +578,19 @@ void DeckEditorDeckDockWidget::expandAll()
}
/**
* Gets the index of all the currently selected card nodes in the decklist table.
* Gets the source index of all the currently selected card nodes in the decklist table.
* The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them.
*
* @return A model index list containing all selected card nodes
* @return A list containing the source indices of all selected card nodes
*/
QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const
QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodeSourceIndices() const
{
auto selectedRows = deckView->selectionModel()->selectedRows();
const auto notLeafNode = [this](const QModelIndex &index) {
return getModel()->hasChildren(proxy->mapToSource(index));
};
const auto mapToSource = [this](const QModelIndex &index) { return proxy->mapToSource(index); };
std::transform(selectedRows.begin(), selectedRows.end(), selectedRows.begin(), mapToSource);
const auto notLeafNode = [this](const QModelIndex &sourceIndex) { return getModel()->hasChildren(sourceIndex); };
selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end());
std::reverse(selectedRows.begin(), selectedRows.end());
@@ -608,10 +609,10 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString &
void DeckEditorDeckDockWidget::actIncrementSelection()
{
auto selectedRows = getSelectedCardNodes();
auto selectedRows = getSelectedCardNodeSourceIndices();
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, true);
for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(sourceIndex, true);
}
}
@@ -630,7 +631,7 @@ void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString
void DeckEditorDeckDockWidget::actSwapSelection()
{
auto selectedRows = getSelectedCardNodes();
auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@@ -638,8 +639,8 @@ void DeckEditorDeckDockWidget::actSwapSelection()
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &currentIndex : selectedRows) {
deckStateManager->swapCardAtIndex(currentIndex);
for (const auto &sourceIndex : selectedRows) {
deckStateManager->swapCardAtIndex(sourceIndex);
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -659,7 +660,7 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z
void DeckEditorDeckDockWidget::actDecrementSelection()
{
auto selectedRows = getSelectedCardNodes();
auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@@ -667,8 +668,8 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, false);
for (const auto &sourceIndex : selectedRows) {
offsetCountAtIndex(sourceIndex, false);
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -676,7 +677,7 @@ void DeckEditorDeckDockWidget::actDecrementSelection()
void DeckEditorDeckDockWidget::actRemoveCard()
{
auto selectedRows = getSelectedCardNodes();
auto selectedRows = getSelectedCardNodeSourceIndices();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
@@ -684,8 +685,8 @@ void DeckEditorDeckDockWidget::actRemoveCard()
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &row : selectedRows) {
deckStateManager->removeCardAtIndex(row);
for (const auto &sourceIndex : selectedRows) {
deckStateManager->removeCardAtIndex(sourceIndex);
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -693,7 +694,7 @@ void DeckEditorDeckDockWidget::actRemoveCard()
/**
* @brief Increments or decrements the amount of the card node at the index by 1.
* @param idx The proxy index
* @param idx The source index
* @param isIncrement If true, increments the count. If false, decrements the count
*/
void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement)
@@ -702,12 +703,10 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i
return;
}
QModelIndex sourceIndex = proxy->mapToSource(idx);
if (isIncrement) {
deckStateManager->incrementCountAtIndex(sourceIndex);
deckStateManager->incrementCountAtIndex(idx);
} else {
deckStateManager->decrementCountAtIndex(sourceIndex);
deckStateManager->decrementCountAtIndex(idx);
}
}

View File

@@ -90,7 +90,7 @@ private:
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
DeckListModel *getModel() const;
[[nodiscard]] QModelIndexList getSelectedCardNodes() const;
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
private slots:

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

@@ -441,7 +441,7 @@ AppearanceSettingsPage::AppearanceSettingsPage()
});
homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600);
homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds"));
homeTabBackgroundShuffleFrequencySpinBox.setSuffix(QString(" ") + tr("seconds"));
homeTabBackgroundShuffleFrequencySpinBox.setValue(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency());
connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload<int>(&QSpinBox::valueChanged),
&SettingsCache::instance(), &SettingsCache::setHomeTabBackgroundShuffleFrequency);

View File

@@ -72,6 +72,12 @@ 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.
*
@@ -79,7 +85,7 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage)
*/
int AllZonesCardAmountWidget::getMainboardAmount()
{
return buttonBoxMainboard->countCardsInZone(DECK_ZONE_MAIN);
return buttonBoxMainboard->getAmount();
}
/**
@@ -89,7 +95,15 @@ int AllZonesCardAmountWidget::getMainboardAmount()
*/
int AllZonesCardAmountWidget::getSideboardAmount()
{
return buttonBoxSideboard->countCardsInZone(DECK_ZONE_SIDE);
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;
}
/**

View File

@@ -23,6 +23,8 @@ public:
const ExactCard &rootCard);
int getMainboardAmount();
int getSideboardAmount();
bool isNonZero();
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void enterEvent(QEnterEvent *event) override;
#else
@@ -31,6 +33,7 @@ public:
public slots:
void adjustFontSize(int scalePercentage);
void setAmounts(int mainboardAmount, int sideboardAmount);
private:
QVBoxLayout *layout;

View File

@@ -45,20 +45,28 @@ CardAmountWidget::CardAmountWidget(QWidget *parent,
connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingSideboard);
}
cardCountInZone = new QLabel(QString::number(countCardsInZone(zoneName)), this);
cardCountInZone = new QLabel(QString::number(amount), this);
cardCountInZone->setAlignment(Qt::AlignCenter);
layout->addWidget(decrementButton);
layout->addWidget(cardCountInZone);
layout->addWidget(incrementButton);
// React to model changes
connect(deckStateManager, &DeckStateManager::cardModified, 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();
}
/**
* @brief Handles the painting of the widget, drawing a semi-transparent background.
*
@@ -124,7 +132,7 @@ void CardAmountWidget::adjustFontSize(int scalePercentage)
*/
void CardAmountWidget::updateCardCount()
{
cardCountInZone->setText("<font color='white'>" + QString::number(countCardsInZone(zoneName)) + "</font>");
cardCountInZone->setText("<font color='white'>" + QString::number(amount) + "</font>");
layout->invalidate();
layout->activate();
}
@@ -169,8 +177,8 @@ void CardAmountWidget::addPrinting(const QString &zone)
QString foundProviderId =
existing.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString();
if (foundProviderId.isEmpty()) {
int amount = existing.data(Qt::DisplayRole).toInt();
extraCopies = amount - 1; // One less because we *always* add one
int existingAmount = existing.data(Qt::DisplayRole).toInt();
extraCopies = existingAmount - 1; // One less because we *always* add one
replacingProviderless = true;
}
}
@@ -246,23 +254,3 @@ void CardAmountWidget::decrementCardHelper(const QString &zone)
return model->offsetCountAtIndex(idx, -1);
});
}
/**
* @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 = deckStateManager->getModel()->getCardsForZone(deckZone);
return std::count_if(cards.cbegin(), cards.cend(),
[&uuid](const ExactCard &card) { return card.getPrinting().getUuid() == uuid; });
}

View File

@@ -31,9 +31,10 @@ public:
QSlider *cardSizeSlider,
const ExactCard &rootCard,
const QString &zoneName);
int countCardsInZone(const QString &deckZone);
int getAmount();
public slots:
void setAmount(int _amount);
void updateCardCount();
void addPrinting(const QString &zone);
@@ -52,6 +53,7 @@ private:
QLabel *cardCountInZone;
bool hovered;
int amount = 0;
void decrementCardHelper(const QString &zoneName);

View File

@@ -78,6 +78,7 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck
// Connect deck model data change signal to update display
connect(deckStateManager, &DeckStateManager::uniqueCardsChanged, this, &PrintingSelector::printingsInDeckChanged);
connect(deckStateManager, &DeckStateManager::cardModified, this, &PrintingSelector::updateCardAmounts);
retranslateUi();
}
@@ -93,6 +94,36 @@ void PrintingSelector::printingsInDeckChanged()
QTimer::singleShot(100, this, &PrintingSelector::updateDisplay);
}
/**
* @return A map of uuid to amounts (main, side).
*/
static QMap<QString, QPair<int, int>> tallyUuidCounts(const DeckListModel *model, const QString &cardName)
{
QMap<QString, QPair<int, int>> map;
auto mainNodes = model->getCardNodesForZone(DECK_ZONE_MAIN);
for (auto &node : mainNodes) {
if (node->getName() == cardName) {
map[node->getCardProviderId()].first += node->getNumber();
}
}
auto sideNodes = model->getCardNodesForZone(DECK_ZONE_SIDE);
for (auto &node : sideNodes) {
if (node->getName() == cardName) {
map[node->getCardProviderId()].second += node->getNumber();
}
}
return map;
}
void PrintingSelector::updateCardAmounts()
{
auto map = tallyUuidCounts(deckStateManager->getModel(), selectedCard->getName());
emit cardAmountsChanged(map);
}
/**
* @brief Updates the display by clearing the layout and loading new sets for the current card.
*/
@@ -156,6 +187,8 @@ void PrintingSelector::getAllSetsForCurrentCard()
}
printingsToUse = sortToolBar->prependPinnedPrintings(printingsToUse, selectedCard->getName());
auto uuidToAmounts = tallyUuidCounts(deckStateManager->getModel(), selectedCard->getName());
// Defer widget creation
currentIndex = 0;
@@ -166,8 +199,11 @@ void PrintingSelector::getAllSetsForCurrentCard()
cardSizeWidget->getSlider(), card);
flowWidget->addWidget(cardDisplayWidget);
cardDisplayWidget->clampSetNameToPicture();
cardDisplayWidget->updateCardAmounts(uuidToAmounts);
connect(cardDisplayWidget, &PrintingSelectorCardDisplayWidget::cardPreferenceChanged, this,
&PrintingSelector::updateDisplay);
connect(this, &PrintingSelector::cardAmountsChanged, cardDisplayWidget,
&PrintingSelectorCardDisplayWidget::updateCardAmounts);
}
// Stop timer when done

View File

@@ -44,6 +44,7 @@ public slots:
private slots:
void printingsInDeckChanged();
void updateCardAmounts();
signals:
/**
@@ -55,6 +56,12 @@ signals:
*/
void nextCardRequested();
/**
* The amounts of the printings in the deck has changed
* @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard) in the deck
*/
void cardAmountsChanged(const QMap<QString, QPair<int, int>> &uuidToAmounts);
private:
QVBoxLayout *layout;
SettingsButtonWidget *displayOptionsWidget;

View File

@@ -27,7 +27,7 @@ PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *pa
DeckStateManager *deckStateManager,
QSlider *cardSizeSlider,
const ExactCard &rootCard)
: QWidget(parent)
: QWidget(parent), rootCard(rootCard)
{
layout = new QVBoxLayout(this);
setLayout(layout);
@@ -64,3 +64,9 @@ void PrintingSelectorCardDisplayWidget::clampSetNameToPicture()
}
update();
}
void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap<QString, QPair<int, int>> &uuidToAmounts)
{
auto [main, side] = uuidToAmounts.value(rootCard.getPrinting().getUuid());
overlayWidget->updateCardAmounts(main, side);
}

View File

@@ -27,11 +27,13 @@ public:
public slots:
void clampSetNameToPicture();
void updateCardAmounts(const QMap<QString, QPair<int, int>> &uuidToAmounts);
signals:
void cardPreferenceChanged();
private:
ExactCard rootCard;
QVBoxLayout *layout;
SetNameAndCollectorsNumberDisplayWidget *setNameAndCollectorsNumberDisplayWidget;
PrintingSelectorCardOverlayWidget *overlayWidget;

View File

@@ -59,12 +59,6 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
allZonesCardAmountWidget = new AllZonesCardAmountWidget(this, deckStateManager, cardSizeSlider, _rootCard);
allZonesCardAmountWidget->raise(); // Ensure it's on top of the picture
// Set initial visibility based on amounts
if (allZonesCardAmountWidget->getMainboardAmount() > 0 || allZonesCardAmountWidget->getSideboardAmount() > 0) {
allZonesCardAmountWidget->setVisible(true);
} else {
allZonesCardAmountWidget->setVisible(false);
}
// Attempt to cast the parent to PrintingSelectorCardDisplayWidget
if (const auto *parentWidget = qobject_cast<PrintingSelectorCardDisplayWidget *>(parent)) {
@@ -113,8 +107,7 @@ void PrintingSelectorCardOverlayWidget::resizeEvent(QResizeEvent *event)
/**
* @brief Handles the mouse enter event when the cursor enters the overlay widget area.
*
* When the cursor enters the widget, the card information is updated, and the card amount widget
* is displayed if the amounts are zero for both the mainboard and sideboard.
* When the cursor enters the widget, the card amount widget becomes visible regardless of whether the amounts are zero.
*
* @param event The event triggered when the mouse enters the widget.
*/
@@ -126,16 +119,27 @@ void PrintingSelectorCardOverlayWidget::enterEvent(QEvent *event)
{
QWidget::enterEvent(event);
deckEditor->updateCard(rootCard);
// Check if either mainboard or sideboard amount is greater than 0
if (allZonesCardAmountWidget->getMainboardAmount() > 0 || allZonesCardAmountWidget->getSideboardAmount() > 0) {
// Don't change visibility if amounts are greater than 0
return;
}
// Show the widget if amounts are 0
allZonesCardAmountWidget->setVisible(true);
updateVisibility();
}
void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount, int sideboardAmount)
{
allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount);
updateVisibility();
}
/**
* @brief Sets the visibility of the widgets depending on the amounts and whether the mouse is hovering over.
*/
void PrintingSelectorCardOverlayWidget::updateVisibility()
{
if (allZonesCardAmountWidget->isNonZero() || underMouse()) {
allZonesCardAmountWidget->setVisible(true);
} else {
allZonesCardAmountWidget->setVisible(false);
}
}
/**
* @brief Updates the pin badge visibility and position based on the card's pinned state.
*
@@ -182,15 +186,7 @@ void PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility()
void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event)
{
QWidget::leaveEvent(event);
// Check if either mainboard or sideboard amount is greater than 0
if (allZonesCardAmountWidget->getMainboardAmount() > 0 || allZonesCardAmountWidget->getSideboardAmount() > 0) {
// Don't hide the widget if amounts are greater than 0
return;
}
// Hide the widget if amounts are 0
allZonesCardAmountWidget->setVisible(false);
updateVisibility();
}
/**

View File

@@ -38,7 +38,11 @@ protected:
signals:
void cardPreferenceChanged();
public slots:
void updateCardAmounts(int mainboardAmount, int sideboardAmount);
private slots:
void updateVisibility();
void updatePinBadgeVisibility();
private:

View File

@@ -29,7 +29,7 @@ public:
[[nodiscard]] QString getTabText() const override
{
auto cardName = cardToQuery.isNull() ? QString() : cardToQuery->getName();
return tr("EDHRec: ") + cardName;
return tr("EDHRec") + ": " + cardName;
}
CardSizeWidget *getCardSizeSlider() const

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
#include "sideboard_plan.h"
#include <QXmlStreamReader>
SideboardPlan::SideboardPlan(const QString &_name, const QList<MoveCard_ToZone> &_moveList)
: name(_name), moveList(_moveList)
{
}
void SideboardPlan::setMoveList(const QList<MoveCard_ToZone> &_moveList)
{
moveList = _moveList;
}
bool SideboardPlan::readElement(QXmlStreamReader *xml)
{
while (!xml->atEnd()) {
xml->readNext();
const QString childName = xml->name().toString();
if (xml->isStartElement()) {
if (childName == "name")
name = xml->readElementText();
else if (childName == "move_card_to_zone") {
MoveCard_ToZone m;
while (!xml->atEnd()) {
xml->readNext();
const QString childName2 = xml->name().toString();
if (xml->isStartElement()) {
if (childName2 == "card_name")
m.set_card_name(xml->readElementText().toStdString());
else if (childName2 == "start_zone")
m.set_start_zone(xml->readElementText().toStdString());
else if (childName2 == "target_zone")
m.set_target_zone(xml->readElementText().toStdString());
} else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) {
moveList.append(m);
break;
}
}
}
} else if (xml->isEndElement() && (childName == "sideboard_plan"))
return true;
}
return false;
}
void SideboardPlan::write(QXmlStreamWriter *xml) const
{
xml->writeStartElement("sideboard_plan");
xml->writeTextElement("name", name);
for (auto &i : moveList) {
xml->writeStartElement("move_card_to_zone");
xml->writeTextElement("card_name", QString::fromStdString(i.card_name()));
xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone()));
xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone()));
xml->writeEndElement();
}
xml->writeEndElement();
}

View File

@@ -0,0 +1,70 @@
#ifndef COCKATRICE_SIDEBOARD_PLAN_H
#define COCKATRICE_SIDEBOARD_PLAN_H
#include <QList>
#include <libcockatrice/protocol/pb/move_card_to_zone.pb.h>
class QXmlStreamWriter;
class QXmlStreamReader;
/**
* @class SideboardPlan
* @ingroup Decks
* @brief Represents a predefined sideboarding strategy for a deck.
*
* Sideboard plans store a named list of card movements that should be applied
* between the mainboard and sideboard for a specific matchup. Each movement
* is expressed using a `MoveCard_ToZone` protobuf message.
*
* ### Responsibilities:
* - Store the plan name and list of moves.
* - Support XML serialization/deserialization.
*
* ### Typical usage:
* A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"),
* each describing how to transform the main deck into its intended configuration.
*/
class SideboardPlan
{
private:
QString name; ///< Human-readable name of this plan.
QList<MoveCard_ToZone> moveList; ///< List of move instructions for this plan.
public:
/**
* @brief Construct a new SideboardPlan.
* @param _name The plan name.
* @param _moveList Initial list of card move instructions.
*/
explicit SideboardPlan(const QString &_name = "", const QList<MoveCard_ToZone> &_moveList = {});
/**
* @brief Read a SideboardPlan from an XML stream.
* @param xml XML reader positioned at the plan element.
* @return true if parsing succeeded.
*/
bool readElement(QXmlStreamReader *xml);
/**
* @brief Write this SideboardPlan to XML.
* @param xml Stream to append the serialized element to.
*/
void write(QXmlStreamWriter *xml) const;
/// @return The plan name.
[[nodiscard]] QString getName() const
{
return name;
}
/// @return Const reference to the move list.
[[nodiscard]] const QList<MoveCard_ToZone> &getMoveList() const
{
return moveList;
}
/// @brief Replace the move list with a new one.
void setMoveList(const QList<MoveCard_ToZone> &_moveList);
};
#endif // COCKATRICE_SIDEBOARD_PLAN_H

View File

@@ -637,6 +637,16 @@ QList<ExactCard> DeckListModel::getCardsForZone(const QString &zoneName) const
return cardNodesToExactCards(nodes);
}
QList<const DecklistCardNode *> DeckListModel::getCardNodes() const
{
return deckList->getCardNodes();
}
QList<const DecklistCardNode *> DeckListModel::getCardNodesForZone(const QString &zoneName) const
{
return deckList->getCardNodes({zoneName});
}
QList<QString> DeckListModel::getCardNames() const
{
auto nodes = deckList->getCardNodes();

View File

@@ -339,6 +339,12 @@ public:
[[nodiscard]] QList<ExactCard> getCards() const;
[[nodiscard]] QList<ExactCard> getCardsForZone(const QString &zoneName) const;
/**
* @brief Gets a list of all card nodes in the deck.
*/
[[nodiscard]] QList<const DecklistCardNode *> getCardNodes() const;
[[nodiscard]] QList<const DecklistCardNode *> getCardNodesForZone(const QString &zoneName) const;
/**
* @brief Gets a deduplicated list of all card names that appear in the model
*/