[Game] implement Select All Matching action

This commit is contained in:
RickyRister 2026-03-18 02:17:06 -07:00
parent 5cc21f591d
commit 57b4b2b73b
8 changed files with 220 additions and 3 deletions

View File

@ -71,6 +71,7 @@ set(cockatrice_SOURCES
src/game/dialogs/dlg_create_token.cpp
src/game/dialogs/dlg_move_top_cards_until.cpp
src/game/dialogs/dlg_roll_dice.cpp
src/game/dialogs/dlg_select_all_matching.cpp
src/game/game.cpp
src/game/game_event_handler.cpp
src/game/game_meta_info.cpp

View File

@ -546,6 +546,10 @@ private:
{"Player/aSelectColumn", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Select All Cards in Column"),
parseSequenceString("Ctrl+Shift+C"),
ShortcutGroup::Playing_Area)},
{"Player/aSelectAllMatching",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Select All Cards in Zone Matching Expression"),
parseSequenceString(""),
ShortcutGroup::Playing_Area)},
{"Player/aRevealToAll", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Reveal Selected Cards to All Players"),
parseSequenceString(""),
ShortcutGroup::Playing_Area)},

View File

@ -0,0 +1,119 @@
#include "dlg_select_all_matching.h"
#include "libcockatrice/card/card_info.h"
#include "libcockatrice/card/database/card_database_manager.h"
#include "libcockatrice/filters/filter_string.h"
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QMessageBox>
#include <QVBoxLayout>
DlgSelectAllMatching::DlgSelectAllMatching(QWidget *parent, const SelectAllMatchingOptions &options) : QDialog(parent)
{
exprLabel = new QLabel(tr("Card name (or search expressions):"));
exprComboBox = new QComboBox(this);
exprComboBox->setFocus();
exprComboBox->setEditable(true);
exprComboBox->setInsertPolicy(QComboBox::InsertAtTop);
exprComboBox->insertItems(0, options.exprs);
exprLabel->setBuddy(exprComboBox);
clearSelectionCheckBox = new QCheckBox(tr("Clear existing selection first"));
clearSelectionCheckBox->setChecked(options.clearSelection);
toggleSelectedCheckBox = new QCheckBox(tr("Toggle already selected matches"));
toggleSelectedCheckBox->setChecked(options.toggleSelected);
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgSelectAllMatching::validateAndAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(exprLabel);
mainLayout->addWidget(exprComboBox);
mainLayout->addWidget(clearSelectionCheckBox);
mainLayout->addWidget(toggleSelectedCheckBox);
mainLayout->addWidget(buttonBox);
setLayout(mainLayout);
setWindowTitle(tr("Select all cards matching..."));
}
/**
* @brief Checks if a card matching the expr exists in the card database.
*
* @returns true if a card matching the expression exists.
*/
static bool matchExistsInDb(const FilterString &filterString)
{
const auto *cardDatabase = CardDatabaseManager::getInstance();
const auto &allCards = cardDatabase->getCardList();
const auto it = std::find_if(allCards.begin(), allCards.end(),
[&filterString](const CardInfoPtr &card) { return filterString.check(card); });
return it != allCards.end();
}
bool DlgSelectAllMatching::validateMatchExists(const FilterString &filterString)
{
if (matchExistsInDb(filterString)) {
return true;
}
const auto msg = tr("No cards matching the search expression exists in the card database. Proceed anyways?");
const auto res =
QMessageBox::warning(this, tr("Cockatrice"), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (res == QMessageBox::No) {
return false;
}
return true;
}
void DlgSelectAllMatching::validateAndAccept()
{
auto movingCardsUntilFilter = FilterString(exprComboBox->currentText());
if (!movingCardsUntilFilter.valid()) {
QMessageBox::warning(this, tr("Invalid filter"), movingCardsUntilFilter.error(), QMessageBox::Ok);
return;
}
if (!validateMatchExists(movingCardsUntilFilter)) {
return;
}
// move currently selected text to top of history list
if (exprComboBox->currentIndex() != 0) {
QString currentExpr = exprComboBox->currentText();
exprComboBox->removeItem(exprComboBox->currentIndex());
exprComboBox->insertItem(0, currentExpr);
exprComboBox->setCurrentIndex(0);
}
accept();
}
QString DlgSelectAllMatching::getExpr() const
{
return exprComboBox->currentText();
}
SelectAllMatchingOptions DlgSelectAllMatching::getOptions() const
{
return {.exprs = getExprs(),
.clearSelection = clearSelectionCheckBox->isChecked(),
.toggleSelected = toggleSelectedCheckBox->isChecked()};
}
QStringList DlgSelectAllMatching::getExprs() const
{
QStringList exprs;
for (int i = 0; i < exprComboBox->count(); ++i) {
exprs.append(exprComboBox->itemText(i));
}
return exprs;
}

View File

@ -0,0 +1,42 @@
#ifndef COCKATRICE_DLG_SELECT_ALL_MATCHING_H
#define COCKATRICE_DLG_SELECT_ALL_MATCHING_H
#include <QDialog>
class FilterString;
class QCheckBox;
class QDialogButtonBox;
class QSpinBox;
class QComboBox;
class QLabel;
struct SelectAllMatchingOptions
{
QStringList exprs = {};
bool clearSelection = true;
bool toggleSelected = false;
};
class DlgSelectAllMatching : public QDialog
{
Q_OBJECT
QLabel *exprLabel;
QComboBox *exprComboBox;
QCheckBox *clearSelectionCheckBox;
QCheckBox *toggleSelectedCheckBox;
QDialogButtonBox *buttonBox;
void validateAndAccept();
bool validateMatchExists(const FilterString &filterString);
[[nodiscard]] QStringList getExprs() const;
public:
explicit DlgSelectAllMatching(QWidget *parent, const SelectAllMatchingOptions &options);
[[nodiscard]] QString getExpr() const;
[[nodiscard]] SelectAllMatchingOptions getOptions() const;
};
#endif // COCKATRICE_DLG_SELECT_ALL_MATCHING_H

View File

@ -59,6 +59,8 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive
connect(aSelectRow, &QAction::triggered, playerActions, &PlayerActions::actSelectRow);
aSelectColumn = new QAction(this);
connect(aSelectColumn, &QAction::triggered, playerActions, &PlayerActions::actSelectColumn);
aSelectAllMatching = new QAction(this);
connect(aSelectAllMatching, &QAction::triggered, playerActions, &PlayerActions::actSelectAllMatching);
aPlay = new QAction(this);
connect(aPlay, &QAction::triggered, playerActions, &PlayerActions::actPlay);
@ -111,6 +113,7 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
addAction(aSelectColumn);
addRelatedCardView();
} else {
@ -155,6 +158,7 @@ void CardMenu::createTableMenu(bool canModifyCard)
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
addAction(aSelectRow);
return;
}
@ -183,6 +187,7 @@ void CardMenu::createTableMenu(bool canModifyCard)
addMenu(new MoveMenu(player));
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
addAction(aSelectRow);
addSeparator();
@ -210,12 +215,14 @@ void CardMenu::createStackMenu(bool canModifyCard)
addMenu(new MoveMenu(player));
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
} else {
addAction(aDrawArrow);
addSeparator();
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
}
addRelatedCardView();
@ -234,6 +241,7 @@ void CardMenu::createGraveyardOrExileMenu(bool canModifyCard)
addMenu(new MoveMenu(player));
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
addAction(aSelectColumn);
addSeparator();
@ -243,6 +251,7 @@ void CardMenu::createGraveyardOrExileMenu(bool canModifyCard)
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
addAction(aSelectColumn);
addSeparator();
addAction(aDrawArrow);
@ -263,6 +272,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard)
addAction(aClone);
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
return;
}
@ -289,6 +299,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard)
addSeparator();
addAction(aSelectAll);
addAction(aSelectAllMatching);
if (qobject_cast<ZoneViewZoneLogic *>(card->getZone())) {
addAction(aSelectColumn);
}
@ -442,6 +453,7 @@ void CardMenu::retranslateUi()
aSelectAll->setText(tr("&Select All"));
aSelectRow->setText(tr("S&elect Row"));
aSelectColumn->setText(tr("S&elect Column"));
aSelectAllMatching->setText(tr("Select All Matching..."));
aPlay->setText(tr("&Play"));
aHide->setText(tr("&Hide"));
@ -498,6 +510,7 @@ void CardMenu::setShortcutsActive()
aSelectAll->setShortcuts(shortcuts.getShortcut("Player/aSelectAll"));
aSelectRow->setShortcuts(shortcuts.getShortcut("Player/aSelectRow"));
aSelectColumn->setShortcuts(shortcuts.getShortcut("Player/aSelectColumn"));
aSelectAllMatching->setShortcuts(shortcuts.getShortcut("Player/aSelectAllMatching"));
static const QStringList colorWords = {"Red", "Yellow", "Green", "Cyan", "Purple", "Magenta"};
for (int i = 0; i < aAddCounter.size(); i++) {

View File

@ -30,7 +30,7 @@ public:
QAction *aRevealToAll;
QAction *aHide;
QAction *aClone;
QAction *aSelectAll, *aSelectRow, *aSelectColumn;
QAction *aSelectAll, *aSelectRow, *aSelectColumn, *aSelectAllMatching;
QAction *aDrawArrow;
QAction *aTap, *aDoesntUntap;
QAction *aFlip, *aPeek;

View File

@ -5,6 +5,7 @@
#include "../board/card_item.h"
#include "../dialogs/dlg_move_top_cards_until.h"
#include "../dialogs/dlg_roll_dice.h"
#include "../dialogs/dlg_select_all_matching.h"
#include "../zones/hand_zone.h"
#include "../zones/logic/view_zone_logic.h"
#include "../zones/table_zone.h"
@ -696,10 +697,12 @@ void PlayerActions::actMoveBottomCardToTop()
*
* @param zone The zone to select from
* @param filter A predicate to filter which cards are selected. Defaults to always returning true.
* @param toggleSelected If true, toggles the selection state if card is already selected
*/
static void selectCardsInZone(
const CardZoneLogic *zone,
std::function<bool(const CardItem *)> filter = [](const CardItem *) { return true; })
std::function<bool(const CardItem *)> filter = [](const CardItem *) { return true; },
bool toggleSelected = false)
{
if (!zone) {
return;
@ -707,7 +710,11 @@ static void selectCardsInZone(
for (auto &cardItem : zone->getCards()) {
if (cardItem && filter(cardItem)) {
cardItem->setSelected(true);
if (toggleSelected) {
cardItem->setSelected(!cardItem->isSelected());
} else {
cardItem->setSelected(true);
}
}
}
}
@ -746,6 +753,33 @@ void PlayerActions::actSelectColumn()
selectCardsInZone(card->getZone(), isSameColumn);
}
void PlayerActions::actSelectAllMatching()
{
const CardItem *card = player->getGame()->getActiveCard();
if (!card) {
return;
}
auto dlg = DlgSelectAllMatching(player->getGame()->getTab(), lastSelectAllMatchingOptions);
if (!dlg.exec()) {
return;
}
QString expr = dlg.getExpr();
lastSelectAllMatchingOptions = dlg.getOptions();
if (lastSelectAllMatchingOptions.clearSelection) {
player->getGameScene()->clearSelection();
}
auto filterString = FilterString(expr);
auto matches = [filterString](const CardItem *cardItem) {
return filterString.check(cardItem->getCard().getCardPtr());
};
selectCardsInZone(card->getZone(), matches, lastSelectAllMatchingOptions.toggleSelected);
}
void PlayerActions::actDrawBottomCard()
{
if (player->getDeckZone()->getCards().empty()) {

View File

@ -9,6 +9,7 @@
#define COCKATRICE_PLAYER_ACTIONS_H
#include "../dialogs/dlg_create_token.h"
#include "../dialogs/dlg_move_top_cards_until.h"
#include "../dialogs/dlg_select_all_matching.h"
#include "event_processing_options.h"
#include "player.h"
@ -119,6 +120,7 @@ public slots:
void actSelectAll();
void actSelectRow();
void actSelectColumn();
void actSelectAllMatching();
void actViewLibrary();
void actViewHand();
@ -183,6 +185,8 @@ private:
int movingCardsUntilCounter = 0;
MoveTopCardsUntilOptions movingCardsUntilOptions;
SelectAllMatchingOptions lastSelectAllMatchingOptions;
void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown);
void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown);