diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 1ca3c77c2..fbf593669 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -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 diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 51615745b..d98c2cda1 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -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)}, diff --git a/cockatrice/src/game/dialogs/dlg_select_all_matching.cpp b/cockatrice/src/game/dialogs/dlg_select_all_matching.cpp new file mode 100644 index 000000000..0eb4a65b6 --- /dev/null +++ b/cockatrice/src/game/dialogs/dlg_select_all_matching.cpp @@ -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 +#include +#include +#include +#include + +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; +} \ No newline at end of file diff --git a/cockatrice/src/game/dialogs/dlg_select_all_matching.h b/cockatrice/src/game/dialogs/dlg_select_all_matching.h new file mode 100644 index 000000000..606e2fa2f --- /dev/null +++ b/cockatrice/src/game/dialogs/dlg_select_all_matching.h @@ -0,0 +1,42 @@ +#ifndef COCKATRICE_DLG_SELECT_ALL_MATCHING_H +#define COCKATRICE_DLG_SELECT_ALL_MATCHING_H + +#include + +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 diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index cd77c2968..0fa6b2e16 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -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(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++) { diff --git a/cockatrice/src/game/player/menu/card_menu.h b/cockatrice/src/game/player/menu/card_menu.h index b7f2f8241..50ee10eed 100644 --- a/cockatrice/src/game/player/menu/card_menu.h +++ b/cockatrice/src/game/player/menu/card_menu.h @@ -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; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 287231402..f43af5728 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -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 filter = [](const CardItem *) { return true; }) + std::function 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()) { diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index d4c6daacf..aeab881b8 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -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);