From 70b41c20958c3a1e87195540756635ea4cdb4891 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Tue, 24 Mar 2026 15:31:34 -0400 Subject: [PATCH 1/4] refactor: extract AbstractPlayerComponent interface for polymorphic player component management. (#6696) Non-QObject polymorphic interface with setShortcutsActive(), setShortcutsInactive(), and retranslateUi(). Uses regular multiple inheritance to avoid diamond inheritance with Qt's MOC. All zone menus, SayMenu, and AbstractCounter implement this interface. PlayerMenu manages them via a managedComponents list with two template helpers (addManagedMenu/registerManagedComponent), replacing individual if-guarded lifecycle calls with a single polymorphic loop. SayMenu now owns its shortcut and translation lifecycle instead of having PlayerMenu manage its title and shortcuts externally. Counters are iterated via Player::getCounters() rather than managedComponents to avoid duplicating the authoritative owner's map. --- cockatrice/src/game/board/abstract_counter.h | 9 +- .../player/menu/abstract_player_component.h | 32 +++++++ .../src/game/player/menu/custom_zone_menu.h | 12 ++- cockatrice/src/game/player/menu/grave_menu.h | 9 +- cockatrice/src/game/player/menu/hand_menu.h | 9 +- .../src/game/player/menu/library_menu.h | 9 +- .../src/game/player/menu/player_menu.cpp | 89 ++++--------------- cockatrice/src/game/player/menu/player_menu.h | 27 +++++- cockatrice/src/game/player/menu/rfg_menu.h | 11 ++- cockatrice/src/game/player/menu/say_menu.cpp | 34 ++++++- cockatrice/src/game/player/menu/say_menu.h | 11 ++- .../src/game/player/menu/sideboard_menu.h | 10 ++- .../src/game/player/menu/utility_menu.h | 10 ++- 13 files changed, 164 insertions(+), 108 deletions(-) create mode 100644 cockatrice/src/game/player/menu/abstract_player_component.h diff --git a/cockatrice/src/game/board/abstract_counter.h b/cockatrice/src/game/board/abstract_counter.h index ea13cb00f..074650d54 100644 --- a/cockatrice/src/game/board/abstract_counter.h +++ b/cockatrice/src/game/board/abstract_counter.h @@ -8,6 +8,7 @@ #define COUNTER_H #include "../../interface/widgets/menus/tearoff_menu.h" +#include "../player/menu/abstract_player_component.h" #include #include @@ -18,7 +19,7 @@ class QKeyEvent; class QMenu; class QString; -class AbstractCounter : public QObject, public QGraphicsItem +class AbstractCounter : public QObject, public QGraphicsItem, public AbstractPlayerComponent { Q_OBJECT Q_INTERFACES(QGraphicsItem) @@ -56,10 +57,10 @@ public: QGraphicsItem *parent = nullptr); ~AbstractCounter() override; - void retranslateUi(); + void retranslateUi() override; void setValue(int _value); - void setShortcutsActive(); - void setShortcutsInactive(); + void setShortcutsActive() override; + void setShortcutsInactive() override; void delCounter(); QMenu *getMenu() const diff --git a/cockatrice/src/game/player/menu/abstract_player_component.h b/cockatrice/src/game/player/menu/abstract_player_component.h new file mode 100644 index 000000000..989300d41 --- /dev/null +++ b/cockatrice/src/game/player/menu/abstract_player_component.h @@ -0,0 +1,32 @@ +/** + * @file abstract_player_component.h + * @ingroup GameMenusPlayers + * @brief Polymorphic interface for player-bound UI components managed by PlayerMenu. + */ + +#ifndef COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H +#define COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H + +/** + * @brief Interface for player-bound UI components that need shortcut and translation lifecycle management. + * + * Not a QObject — avoids diamond inheritance with Qt's MOC. Each concrete component + * inherits QObject through its Qt base class (QMenu, TearOffMenu, QGraphicsItem, etc.) + * and this interface through regular multiple inheritance. + */ +class AbstractPlayerComponent +{ +public: + virtual ~AbstractPlayerComponent() = default; + + /// Bind keyboard shortcuts. Called when this player gains focus. + virtual void setShortcutsActive() = 0; + + /// Unbind keyboard shortcuts. Called when this player loses focus. + virtual void setShortcutsInactive() = 0; + + /// Retranslate all user-visible strings. Called on language change. + virtual void retranslateUi() = 0; +}; + +#endif // COCKATRICE_ABSTRACT_PLAYER_COMPONENT_H diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.h b/cockatrice/src/game/player/menu/custom_zone_menu.h index 0944029f4..c4e66754e 100644 --- a/cockatrice/src/game/player/menu/custom_zone_menu.h +++ b/cockatrice/src/game/player/menu/custom_zone_menu.h @@ -7,15 +7,23 @@ #ifndef COCKATRICE_CUSTOM_ZONE_MENU_H #define COCKATRICE_CUSTOM_ZONE_MENU_H +#include "abstract_player_component.h" + #include class Player; -class CustomZoneMenu : public QMenu +class CustomZoneMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit CustomZoneMenu(Player *player); - void retranslateUi(); + void retranslateUi() override; + void setShortcutsActive() override + { + } + void setShortcutsInactive() override + { + } private: Player *player; diff --git a/cockatrice/src/game/player/menu/grave_menu.h b/cockatrice/src/game/player/menu/grave_menu.h index faaf497b6..429173afa 100644 --- a/cockatrice/src/game/player/menu/grave_menu.h +++ b/cockatrice/src/game/player/menu/grave_menu.h @@ -8,12 +8,13 @@ #define COCKATRICE_GRAVE_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include class Player; -class GraveyardMenu : public TearOffMenu +class GraveyardMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT signals: @@ -25,9 +26,9 @@ public: void createViewActions(); void populateRevealRandomMenuWithActivePlayers(); void onRevealRandomTriggered(); - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; QMenu *mRevealRandomGraveyardCard = nullptr; QMenu *moveGraveMenu = nullptr; diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game/player/menu/hand_menu.h index 51e071a62..76434cc98 100644 --- a/cockatrice/src/game/player/menu/hand_menu.h +++ b/cockatrice/src/game/player/menu/hand_menu.h @@ -8,6 +8,7 @@ #define COCKATRICE_HAND_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include @@ -15,7 +16,7 @@ class Player; class PlayerActions; -class HandMenu : public TearOffMenu +class HandMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT @@ -31,9 +32,9 @@ public: return mRevealRandomHandCard; } - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; private slots: void populateRevealHandMenuWithActivePlayers(); diff --git a/cockatrice/src/game/player/menu/library_menu.h b/cockatrice/src/game/player/menu/library_menu.h index c0883107c..444e8f516 100644 --- a/cockatrice/src/game/player/menu/library_menu.h +++ b/cockatrice/src/game/player/menu/library_menu.h @@ -8,6 +8,7 @@ #define COCKATRICE_LIBRARY_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include @@ -15,7 +16,7 @@ class Player; class PlayerActions; -class LibraryMenu : public TearOffMenu +class LibraryMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT public slots: @@ -28,15 +29,15 @@ public: void createShuffleActions(); void createMoveActions(); void createViewActions(); - void retranslateUi(); + void retranslateUi() override; void populateRevealLibraryMenuWithActivePlayers(); void populateLendLibraryMenuWithActivePlayers(); void populateRevealTopCardMenuWithActivePlayers(); void onRevealLibraryTriggered(); void onLendLibraryTriggered(); void onRevealTopCardTriggered(); - void setShortcutsActive(); - void setShortcutsInactive(); + void setShortcutsActive() override; + void setShortcutsInactive() override; [[nodiscard]] bool isAlwaysRevealTopCardChecked() const { diff --git a/cockatrice/src/game/player/menu/player_menu.cpp b/cockatrice/src/game/player/menu/player_menu.cpp index 3016a727f..7786ec3fc 100644 --- a/cockatrice/src/game/player/menu/player_menu.cpp +++ b/cockatrice/src/game/player/menu/player_menu.cpp @@ -15,33 +15,24 @@ PlayerMenu::PlayerMenu(Player *_player) : player(_player) playerMenu = new TearOffMenu(); if (player->getPlayerInfo()->getLocalOrJudge()) { - handMenu = new HandMenu(player, player->getPlayerActions(), playerMenu); - playerMenu->addMenu(handMenu); - - libraryMenu = new LibraryMenu(player, playerMenu); - playerMenu->addMenu(libraryMenu); + handMenu = addManagedMenu(player, player->getPlayerActions(), playerMenu); + libraryMenu = addManagedMenu(player, playerMenu); } else { handMenu = nullptr; libraryMenu = nullptr; } - graveMenu = new GraveyardMenu(player, playerMenu); - playerMenu->addMenu(graveMenu); - - rfgMenu = new RfgMenu(player, playerMenu); - playerMenu->addMenu(rfgMenu); + graveMenu = addManagedMenu(player, playerMenu); + rfgMenu = addManagedMenu(player, playerMenu); if (player->getPlayerInfo()->getLocalOrJudge()) { - sideboardMenu = new SideboardMenu(player, playerMenu); - playerMenu->addMenu(sideboardMenu); - - customZonesMenu = new CustomZoneMenu(player); - playerMenu->addMenu(customZonesMenu); + sideboardMenu = addManagedMenu(player, playerMenu); + customZonesMenu = addManagedMenu(player); playerMenu->addSeparator(); countersMenu = playerMenu->addMenu(QString()); - utilityMenu = new UtilityMenu(player, playerMenu); + utilityMenu = createManagedComponent(player, playerMenu); } else { sideboardMenu = nullptr; customZonesMenu = nullptr; @@ -50,8 +41,7 @@ PlayerMenu::PlayerMenu(Player *_player) : player(_player) } if (player->getPlayerInfo()->getLocal()) { - sayMenu = new SayMenu(player); - playerMenu->addMenu(sayMenu); + sayMenu = addManagedMenu(player); } else { sayMenu = nullptr; } @@ -99,40 +89,18 @@ void PlayerMenu::retranslateUi() { playerMenu->setTitle(tr("Player \"%1\"").arg(player->getPlayerInfo()->getName())); - if (handMenu) { - handMenu->retranslateUi(); - } - if (libraryMenu) { - libraryMenu->retranslateUi(); - } - - graveMenu->retranslateUi(); - rfgMenu->retranslateUi(); - - if (sideboardMenu) { - sideboardMenu->retranslateUi(); + for (auto *component : managedComponents) { + component->retranslateUi(); } if (countersMenu) { countersMenu->setTitle(tr("&Counters")); } - if (customZonesMenu) { - customZonesMenu->retranslateUi(); - } - QMapIterator counterIterator(player->getCounters()); while (counterIterator.hasNext()) { counterIterator.next().value()->retranslateUi(); } - - if (utilityMenu) { - utilityMenu->retranslateUi(); - } - - if (sayMenu) { - sayMenu->setTitle(tr("S&ay")); - } } void PlayerMenu::refreshShortcuts() @@ -153,52 +121,29 @@ void PlayerMenu::setShortcutsActive() { shortcutsActive = true; - if (handMenu) { - handMenu->setShortcutsActive(); - } - if (libraryMenu) { - libraryMenu->setShortcutsActive(); - } - graveMenu->setShortcutsActive(); - // No shortcuts for RfgMenu yet - - if (sideboardMenu) { - sideboardMenu->setShortcutsActive(); + for (auto *component : managedComponents) { + component->setShortcutsActive(); } + // Counters implement AbstractPlayerComponent but are iterated via Player::counters + // (the authoritative source) rather than managedComponents to avoid a redundant + // list that must stay in sync with the map. QMapIterator counterIterator(player->getCounters()); while (counterIterator.hasNext()) { counterIterator.next().value()->setShortcutsActive(); } - - if (utilityMenu) { - utilityMenu->setShortcutsActive(); - } } void PlayerMenu::setShortcutsInactive() { shortcutsActive = false; - if (handMenu) { - handMenu->setShortcutsInactive(); - } - if (libraryMenu) { - libraryMenu->setShortcutsInactive(); - } - graveMenu->setShortcutsInactive(); - // No shortcuts for RfgMenu yet - - if (sideboardMenu) { - sideboardMenu->setShortcutsInactive(); + for (auto *component : managedComponents) { + component->setShortcutsInactive(); } QMapIterator counterIterator(player->getCounters()); while (counterIterator.hasNext()) { counterIterator.next().value()->setShortcutsInactive(); } - - if (utilityMenu) { - utilityMenu->setShortcutsInactive(); - } } \ No newline at end of file diff --git a/cockatrice/src/game/player/menu/player_menu.h b/cockatrice/src/game/player/menu/player_menu.h index 882bfedc5..5fce27158 100644 --- a/cockatrice/src/game/player/menu/player_menu.h +++ b/cockatrice/src/game/player/menu/player_menu.h @@ -1,7 +1,7 @@ /** * @file player_menu.h * @ingroup GameMenusPlayers - * @brief TODO: Document this. + * @brief Orchestrates lifecycle management for all player-bound UI components. */ #ifndef COCKATRICE_PLAYER_MENU_H @@ -18,6 +18,7 @@ #include "sideboard_menu.h" #include "utility_menu.h" +#include #include #include @@ -37,6 +38,7 @@ private slots: public: PlayerMenu(Player *player); + /// Lifecycle methods: delegate to all managedComponents, plus counters separately via player->getCounters(). void retranslateUi(); QMenu *updateCardMenu(const CardItem *card); @@ -66,7 +68,9 @@ public: return shortcutsActive; } + /// Delegates to all managedComponents, plus counters separately. void setShortcutsActive(); + /// Delegates to all managedComponents, plus counters separately. void setShortcutsInactive(); private: @@ -82,9 +86,26 @@ private: SayMenu *sayMenu; CustomZoneMenu *customZonesMenu; - bool shortcutsActive; + /// Drives AbstractPlayerComponent lifecycle delegation. Counters are iterated separately via player->getCounters(). + QList managedComponents; + bool shortcutsActive = false; - void initSayMenu(); + /// Creates component, adds it as a submenu of playerMenu, and registers in managedComponents. + template MenuT *addManagedMenu(Args &&...args) + { + auto *menu = new MenuT(std::forward(args)...); + playerMenu->addMenu(menu); + managedComponents.append(menu); + return menu; + } + + /// Creates component and registers in managedComponents, but does NOT add it as a submenu. + template ComponentT *createManagedComponent(Args &&...args) + { + auto *component = new ComponentT(std::forward(args)...); + managedComponents.append(component); + return component; + } }; #endif // COCKATRICE_PLAYER_MENU_H diff --git a/cockatrice/src/game/player/menu/rfg_menu.h b/cockatrice/src/game/player/menu/rfg_menu.h index 0b4623d2a..8f79b2f4a 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.h +++ b/cockatrice/src/game/player/menu/rfg_menu.h @@ -8,19 +8,26 @@ #define COCKATRICE_RFG_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" +#include "abstract_player_component.h" #include #include class Player; -class RfgMenu : public TearOffMenu +class RfgMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit RfgMenu(Player *player, QWidget *parent = nullptr); void createMoveActions(); void createViewActions(); - void retranslateUi(); + void retranslateUi() override; + void setShortcutsActive() override + { + } + void setShortcutsInactive() override + { + } QMenu *moveRfgMenu = nullptr; diff --git a/cockatrice/src/game/player/menu/say_menu.cpp b/cockatrice/src/game/player/menu/say_menu.cpp index 3c4802aa5..116fba49a 100644 --- a/cockatrice/src/game/player/menu/say_menu.cpp +++ b/cockatrice/src/game/player/menu/say_menu.cpp @@ -8,6 +8,31 @@ SayMenu::SayMenu(Player *_player) : player(_player) { connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu); initSayMenu(); + retranslateUi(); +} + +void SayMenu::retranslateUi() +{ + setTitle(tr("S&ay")); +} + +void SayMenu::setShortcutsActive() +{ + shortcutsActive = true; + + const auto menuActions = actions(); + for (int i = 0; i < menuActions.size() && i < 10; ++i) { + menuActions[i]->setShortcut(QKeySequence("Ctrl+" + QString::number((i + 1) % 10))); + } +} + +void SayMenu::setShortcutsInactive() +{ + shortcutsActive = false; + + for (auto *action : actions()) { + action->setShortcut(QKeySequence()); + } } void SayMenu::initSayMenu() @@ -19,10 +44,11 @@ void SayMenu::initSayMenu() for (int i = 0; i < count; ++i) { auto *newAction = new QAction(SettingsCache::instance().messages().getMessageAt(i), this); - if (i < 10) { - newAction->setShortcut(QKeySequence("Ctrl+" + QString::number((i + 1) % 10))); - } connect(newAction, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actSayMessage); addAction(newAction); } -} \ No newline at end of file + + if (shortcutsActive) { + setShortcutsActive(); + } +} diff --git a/cockatrice/src/game/player/menu/say_menu.h b/cockatrice/src/game/player/menu/say_menu.h index 5dbde2277..fadf5f368 100644 --- a/cockatrice/src/game/player/menu/say_menu.h +++ b/cockatrice/src/game/player/menu/say_menu.h @@ -7,18 +7,27 @@ #ifndef COCKATRICE_SAY_MENU_H #define COCKATRICE_SAY_MENU_H +#include "abstract_player_component.h" + #include class Player; -class SayMenu : public QMenu +class SayMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit SayMenu(Player *player); + + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; + +private slots: void initSayMenu(); private: Player *player; + bool shortcutsActive = false; }; #endif // COCKATRICE_SAY_MENU_H diff --git a/cockatrice/src/game/player/menu/sideboard_menu.h b/cockatrice/src/game/player/menu/sideboard_menu.h index 22d5a2d69..4a77d1b52 100644 --- a/cockatrice/src/game/player/menu/sideboard_menu.h +++ b/cockatrice/src/game/player/menu/sideboard_menu.h @@ -7,18 +7,20 @@ #ifndef COCKATRICE_SIDEBOARD_MENU_H #define COCKATRICE_SIDEBOARD_MENU_H +#include "abstract_player_component.h" + #include class Player; -class SideboardMenu : public QMenu +class SideboardMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: explicit SideboardMenu(Player *player, QMenu *playerMenu); - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; private: Player *player; diff --git a/cockatrice/src/game/player/menu/utility_menu.h b/cockatrice/src/game/player/menu/utility_menu.h index ff57e7252..f6577d7d1 100644 --- a/cockatrice/src/game/player/menu/utility_menu.h +++ b/cockatrice/src/game/player/menu/utility_menu.h @@ -7,17 +7,19 @@ #ifndef COCKATRICE_UTILITY_MENU_H #define COCKATRICE_UTILITY_MENU_H +#include "abstract_player_component.h" + #include class Player; -class UtilityMenu : public QMenu +class UtilityMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public slots: void populatePredefinedTokensMenu(); - void retranslateUi(); - void setShortcutsActive(); - void setShortcutsInactive(); + void retranslateUi() override; + void setShortcutsActive() override; + void setShortcutsInactive() override; public: explicit UtilityMenu(Player *player, QMenu *playerMenu); From 94ea574c76ea214d7d74910b71795f225b28b75e Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Tue, 24 Mar 2026 16:45:52 -0400 Subject: [PATCH 2/4] Add moveToTable context menu action and extract tableRowToGridY helper (#6738) Adds a Table option to the Move menu, allowing cards to be moved directly to the battlefield from any zone. Extracts the repeated tableRow-to-grid-Y conversion logic into TableZone::tableRowToGridY(), consolidating five call sites and fixing a latent bug where cards with tableRow > 2 could land on the wrong row. --- .../src/client/settings/shortcuts_settings.h | 3 ++ .../src/game/player/card_menu_action_type.h | 5 ++- cockatrice/src/game/player/menu/move_menu.cpp | 9 +++- cockatrice/src/game/player/menu/move_menu.h | 1 + cockatrice/src/game/player/player_actions.cpp | 45 ++++++++++++++----- cockatrice/src/game/zones/table_zone.cpp | 8 ++++ cockatrice/src/game/zones/table_zone.h | 7 +++ 7 files changed, 64 insertions(+), 14 deletions(-) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 51615745b..d0849042b 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -563,6 +563,9 @@ private: {"Player/aMoveToTopLibrary", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Top of Library"), parseSequenceString(""), ShortcutGroup::Move_selected)}, + {"Player/aMoveToTable", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Battlefield"), + parseSequenceString(""), + ShortcutGroup::Move_selected)}, {"Player/aViewHand", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Hand"), parseSequenceString(""), ShortcutGroup::View)}, {"Player/aViewGraveyard", diff --git a/cockatrice/src/game/player/card_menu_action_type.h b/cockatrice/src/game/player/card_menu_action_type.h index aec6d6397..1b63674fa 100644 --- a/cockatrice/src/game/player/card_menu_action_type.h +++ b/cockatrice/src/game/player/card_menu_action_type.h @@ -9,17 +9,20 @@ enum CardMenuActionType { + // Per-card attribute actions (must be <= cmClone for cardMenuAction() dispatch) cmTap, cmUntap, cmDoesntUntap, cmFlip, cmPeek, cmClone, + // Move actions (must be > cmClone for cardMenuAction() dispatch) cmMoveToTopLibrary, cmMoveToBottomLibrary, cmMoveToHand, cmMoveToGraveyard, - cmMoveToExile + cmMoveToExile, + cmMoveToTable }; #endif // COCKATRICE_CARD_MENU_ACTION_TYPE_H diff --git a/cockatrice/src/game/player/menu/move_menu.cpp b/cockatrice/src/game/player/menu/move_menu.cpp index d27e16009..91e2d8d10 100644 --- a/cockatrice/src/game/player/menu/move_menu.cpp +++ b/cockatrice/src/game/player/menu/move_menu.cpp @@ -11,6 +11,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to")) aMoveToBottomLibrary = new QAction(this); aMoveToBottomLibrary->setData(cmMoveToBottomLibrary); aMoveToXfromTopOfLibrary = new QAction(this); + aMoveToTable = new QAction(this); + aMoveToTable->setData(cmMoveToTable); aMoveToGraveyard = new QAction(this); aMoveToHand = new QAction(this); aMoveToHand->setData(cmMoveToHand); @@ -22,6 +24,7 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to")) connect(aMoveToBottomLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToXfromTopOfLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actMoveCardXCardsFromTop); + connect(aMoveToTable, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToHand, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToGraveyard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); connect(aMoveToExile, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); @@ -30,6 +33,8 @@ MoveMenu::MoveMenu(Player *player) : QMenu(tr("Move to")) addAction(aMoveToXfromTopOfLibrary); addAction(aMoveToBottomLibrary); addSeparator(); + addAction(aMoveToTable); + addSeparator(); addAction(aMoveToHand); addSeparator(); addAction(aMoveToGraveyard); @@ -47,6 +52,7 @@ void MoveMenu::setShortcutsActive() aMoveToTopLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToTopLibrary")); aMoveToBottomLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToBottomLibrary")); + aMoveToTable->setShortcuts(shortcuts.getShortcut("Player/aMoveToTable")); aMoveToHand->setShortcuts(shortcuts.getShortcut("Player/aMoveToHand")); aMoveToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveToGraveyard")); aMoveToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveToExile")); @@ -57,7 +63,8 @@ void MoveMenu::retranslateUi() aMoveToTopLibrary->setText(tr("&Top of library in random order")); aMoveToXfromTopOfLibrary->setText(tr("X cards from the top of library...")); aMoveToBottomLibrary->setText(tr("&Bottom of library in random order")); + aMoveToTable->setText(tr("T&able")); aMoveToHand->setText(tr("&Hand")); aMoveToGraveyard->setText(tr("&Graveyard")); aMoveToExile->setText(tr("&Exile")); -} \ No newline at end of file +} diff --git a/cockatrice/src/game/player/menu/move_menu.h b/cockatrice/src/game/player/menu/move_menu.h index 5bf657fa4..dc39cb6a5 100644 --- a/cockatrice/src/game/player/menu/move_menu.h +++ b/cockatrice/src/game/player/menu/move_menu.h @@ -23,6 +23,7 @@ public: QAction *aMoveToBottomLibrary = nullptr; QAction *aMoveToHand = nullptr; + QAction *aMoveToTable = nullptr; QAction *aMoveToGraveyard = nullptr; QAction *aMoveToExile = nullptr; }; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 287231402..cee04ac6b 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -75,7 +75,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) cmd.set_y(0); } else { tableRow = faceDown ? 2 : info.getUiAttributes().tableRow; - QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow)); cardToMove->set_face_down(faceDown); if (!faceDown) { cardToMove->set_pt(info.getPowTough().toStdString()); @@ -114,12 +114,7 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown) const CardInfo &info = exactCard.getInfo(); int tableRow = faceDown ? 2 : info.getUiAttributes().tableRow; - // default instant/sorcery cards to the noncreatures row - if (tableRow > 2) { - tableRow = 1; - } - - QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(tableRow)); cardToMove->set_face_down(faceDown); if (!faceDown) { cardToMove->set_pt(info.getPowTough().toStdString()); @@ -866,7 +861,7 @@ void PlayerActions::actCreateToken() ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId}); if (correctedCard) { lastTokenInfo.name = correctedCard.getName(); - lastTokenTableRow = TableZone::clampValidTableRow(2 - correctedCard.getInfo().getUiAttributes().tableRow); + lastTokenTableRow = TableZone::tableRowToGridY(correctedCard.getInfo().getUiAttributes().tableRow); if (lastTokenInfo.pt.isEmpty()) { lastTokenInfo.pt = correctedCard.getInfo().getPowTough(); } @@ -917,7 +912,7 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo) .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardInfo->getName())}; - lastTokenTableRow = TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow); + lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); utilityMenu->setAndEnableCreateAnotherTokenAction(tr("C&reate another %1 token").arg(lastTokenInfo.name)); } @@ -1085,9 +1080,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, return; } - // get the target token's location - // TODO: Define this QPoint into its own function along with the one below - QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - cardInfo->getUiAttributes().tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow)); // create the token for the related card Command_CreateToken cmd; @@ -1930,6 +1923,34 @@ void PlayerActions::cardMenuAction() commandList.append(cmd); break; } + case cmMoveToTable: { + // Each card needs its own command because table row, pt, and cipt vary per card + for (const auto &card : cardList) { + auto *cmd = new Command_MoveCard; + cmd->set_start_player_id(startPlayerId); + cmd->set_start_zone(startZone.toStdString()); + cmd->set_target_player_id(player->getPlayerInfo()->getId()); + cmd->set_target_zone(ZoneNames::TABLE); + cmd->set_x(-1); + + CardToMove *ctm = cmd->mutable_cards_to_move()->add_card(); + ctm->set_card_id(card->getId()); + ctm->set_face_down(false); + + int tableRow = 0; + ExactCard exactCard = card->getCard(); + if (exactCard) { + const CardInfo &info = exactCard.getInfo(); + tableRow = info.getUiAttributes().tableRow; + ctm->set_pt(info.getPowTough().toStdString()); + ctm->set_tapped(info.getUiAttributes().cipt); + } + + cmd->set_y(TableZone::tableRowToGridY(tableRow)); + commandList.append(cmd); + } + break; + } default: break; } diff --git a/cockatrice/src/game/zones/table_zone.cpp b/cockatrice/src/game/zones/table_zone.cpp index b6ac2150b..2a382fafe 100644 --- a/cockatrice/src/game/zones/table_zone.cpp +++ b/cockatrice/src/game/zones/table_zone.cpp @@ -382,3 +382,11 @@ int TableZone::clampValidTableRow(const int row) return TABLEROWS - 1; return row; } + +int TableZone::tableRowToGridY(int tableRow) +{ + if (tableRow > 2) { + tableRow = 1; + } + return clampValidTableRow(2 - tableRow); +} diff --git a/cockatrice/src/game/zones/table_zone.h b/cockatrice/src/game/zones/table_zone.h index 61eb48d7b..7a53a9eb4 100644 --- a/cockatrice/src/game/zones/table_zone.h +++ b/cockatrice/src/game/zones/table_zone.h @@ -151,6 +151,13 @@ public: static int clampValidTableRow(const int row); + /** + * Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands) + * to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g., + * instants/sorceries) default to the noncreatures row. + */ + static int tableRowToGridY(int tableRow); + /** Resizes the TableZone in case CardItems are within or outside of the TableZone constraints. From 5ef428b9d0fec65b1b264023ce2b2196945ea7c5 Mon Sep 17 00:00:00 2001 From: scotland0208 Date: Wed, 25 Mar 2026 16:15:08 -0500 Subject: [PATCH 3/4] Add visual indicator to toggle untap button (#6737) * Add visual indicator to toggle untap button * Rename button to match tooltip * Change name of string in shortcut settings --- cockatrice/src/client/settings/shortcuts_settings.h | 2 +- cockatrice/src/game/player/menu/card_menu.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index d0849042b..1de73c165 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -501,7 +501,7 @@ private: {"Player/aUntapAll", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Untap All"), parseSequenceString("Ctrl+U"), ShortcutGroup::Playing_Area)}, - {"Player/aDoesntUntap", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Untap"), + {"Player/aDoesntUntap", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Skip Untapping"), parseSequenceString("Alt+U"), ShortcutGroup::Playing_Area)}, {"Player/aFlip", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Turn Card Over"), diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index cd77c2968..f2479e1da 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -35,6 +35,8 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive connect(aTap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); aDoesntUntap = new QAction(this); aDoesntUntap->setData(cmDoesntUntap); + aDoesntUntap->setCheckable(true); + aDoesntUntap->setChecked(card != nullptr && card->getDoesntUntap()); connect(aDoesntUntap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); aAttach = new QAction(this); connect(aAttach, &QAction::triggered, playerActions, &PlayerActions::actAttach); @@ -449,7 +451,7 @@ void CardMenu::retranslateUi() aRevealToAll->setText(tr("&All players")); //: Turn sideways or back again aTap->setText(tr("&Tap / Untap")); - aDoesntUntap->setText(tr("Toggle &normal untapping")); + aDoesntUntap->setText(tr("Skip &untapping")); //: Turn face up/face down aFlip->setText(tr("T&urn Over")); // Only the user facing names in client got renamed to "turn over" // All code and proto bits are still unchanged (flip) for compatibility reasons From dd053c76dfc357896b7a49a5ab636161b32a7aa7 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Wed, 25 Mar 2026 18:03:59 -0400 Subject: [PATCH 4/4] [Game] Improve context menus and fix face-down play from stack (#6739) Reorganize card context menus across table, stack, and graveyard/exile zones for better consistency: promote Draw Arrow and Clone actions, move related card entries to the bottom, add Play/Play Face Down to the stack menu, and flatten if/else blocks with early returns. Also fix playCard() ignoring the faceDown flag when routing instants/sorceries from the stack, which sent them to the graveyard instead of the table. --- cockatrice/src/game/player/menu/card_menu.cpp | 79 ++++++++++--------- cockatrice/src/game/player/player_actions.cpp | 2 +- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index f2479e1da..66ca5e46b 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -110,6 +110,7 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive if (revealedCard) { addAction(aHide); + addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); @@ -148,16 +149,14 @@ void CardMenu::createTableMenu(bool canModifyCard) { // Card is on the battlefield if (!canModifyCard) { - addRelatedCardView(); - addRelatedCardActions(); - - addSeparator(); addAction(aDrawArrow); addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); addAction(aSelectRow); + addRelatedCardView(); + addRelatedCardActions(); return; } @@ -167,10 +166,9 @@ void CardMenu::createTableMenu(bool canModifyCard) if (card->getFaceDown()) { addAction(aPeek); } - - addRelatedCardView(); - addRelatedCardActions(); - + addSeparator(); + addAction(aClone); + addMenu(new MoveMenu(player)); addSeparator(); addAction(aAttach); if (card->getAttachedTo()) { @@ -181,9 +179,6 @@ void CardMenu::createTableMenu(bool canModifyCard) addMenu(new PtMenu(player)); addAction(aSetAnnotation); addSeparator(); - addAction(aClone); - addMenu(new MoveMenu(player)); - addSeparator(); addAction(aSelectAll); addAction(aSelectRow); @@ -199,27 +194,34 @@ void CardMenu::createTableMenu(bool canModifyCard) } addSeparator(); addMenu(mCardCounters); + addRelatedCardView(); + addRelatedCardActions(); } void CardMenu::createStackMenu(bool canModifyCard) { // Card is on the stack - if (canModifyCard) { - addAction(aAttach); - addAction(aDrawArrow); - addSeparator(); - addAction(aClone); - addMenu(new MoveMenu(player)); - addSeparator(); - addAction(aSelectAll); - } else { + if (!canModifyCard) { addAction(aDrawArrow); addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); + addRelatedCardView(); + addRelatedCardActions(); + return; } + addAction(aPlay); + addAction(aPlayFacedown); + addSeparator(); + addAction(aClone); + addMenu(new MoveMenu(player)); + addSeparator(); + addAction(aAttach); + addAction(aDrawArrow); + addSeparator(); + addAction(aSelectAll); addRelatedCardView(); addRelatedCardActions(); } @@ -227,29 +229,29 @@ void CardMenu::createStackMenu(bool canModifyCard) void CardMenu::createGraveyardOrExileMenu(bool canModifyCard) { // Card is in the graveyard or exile - if (canModifyCard) { - addAction(aPlay); - addAction(aPlayFacedown); - - addSeparator(); - addAction(aClone); - addMenu(new MoveMenu(player)); - addSeparator(); - addAction(aSelectAll); - addAction(aSelectColumn); - - addSeparator(); - addAction(aAttach); + if (!canModifyCard) { addAction(aDrawArrow); - } else { + addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); addAction(aSelectColumn); - addSeparator(); - addAction(aDrawArrow); + addRelatedCardView(); + addRelatedCardActions(); + return; } + addAction(aPlay); + addAction(aPlayFacedown); + addSeparator(); + addAction(aClone); + addMenu(new MoveMenu(player)); + addSeparator(); + addAction(aAttach); + addAction(aDrawArrow); + addSeparator(); + addAction(aSelectAll); + addAction(aSelectColumn); addRelatedCardView(); addRelatedCardActions(); } @@ -259,12 +261,11 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) if (!canModifyCard) { addAction(aDrawArrow); addSeparator(); - addRelatedCardView(); - addRelatedCardActions(); - addSeparator(); addAction(aClone); addSeparator(); addAction(aSelectAll); + addRelatedCardView(); + addRelatedCardActions(); return; } diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index cee04ac6b..ca0967636 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -64,7 +64,7 @@ void PlayerActions::playCard(CardItem *card, bool faceDown) int tableRow = info.getUiAttributes().tableRow; bool playToStack = SettingsCache::instance().getPlayToStack(); QString currentZone = card->getZone()->getName(); - if (currentZone == ZoneNames::STACK && tableRow == 3) { + if (!faceDown && currentZone == ZoneNames::STACK && tableRow == 3) { cmd.set_target_zone(ZoneNames::GRAVE); cmd.set_x(0); cmd.set_y(0);