From 5acce8998e23aeaf0a230041f152daeae1c349ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vasco=20Guerreiro=20Vint=C3=A9m=20Morais?= Date: Tue, 2 Jun 2026 17:09:39 +0100 Subject: [PATCH] Implement in-game navigation with keyboard Implements a start to the keyboard navigation with the direction arrows and the space key for card selection. Some binds are still needed, but navigating the board is now possible. The feature includes tests for the implemented feature. Closes #5043 Co-authored-by: Manuel Ramos Monge --- cockatrice/CMakeLists.txt | 1 + .../src/game/board/abstract_card_item.cpp | 19 ++ .../src/game/board/abstract_card_item.h | 1 + cockatrice/src/game/board/arrow_item.cpp | 96 ++++--- cockatrice/src/game/board/arrow_item.h | 5 + cockatrice/src/game/board/card_item.cpp | 69 +++++ cockatrice/src/game/board/card_item.h | 1 + cockatrice/src/game/game_scene.cpp | 46 +++- cockatrice/src/game/game_scene.h | 19 ++ .../src/game/keyboard_card_navigator.cpp | 256 ++++++++++++++++++ cockatrice/src/game/keyboard_card_navigator.h | 61 +++++ cockatrice/src/game/player/player_logic.cpp | 21 ++ cockatrice/src/game/player/player_logic.h | 2 + cockatrice/src/interface/key_signals.cpp | 21 ++ cockatrice/src/interface/key_signals.h | 4 + .../src/interface/widgets/tabs/tab_game.cpp | 10 + .../src/interface/widgets/tabs/tab_game.h | 2 + tests/CMakeLists.txt | 1 + tests/keyboard_navigator_tests/CMakeLists.txt | 32 +++ .../keyboard_card_navigator_test.cpp | 229 ++++++++++++++++ .../keyboard_navigator_test_fakes.h | 67 +++++ .../keyboard_navigator_test_stubs.cpp | 106 ++++++++ 22 files changed, 1023 insertions(+), 46 deletions(-) create mode 100644 cockatrice/src/game/keyboard_card_navigator.cpp create mode 100644 cockatrice/src/game/keyboard_card_navigator.h create mode 100644 tests/keyboard_navigator_tests/CMakeLists.txt create mode 100644 tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp create mode 100644 tests/keyboard_navigator_tests/keyboard_navigator_test_fakes.h create mode 100644 tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 0b2192399..2706d9865 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -81,6 +81,7 @@ set(cockatrice_SOURCES src/game/game_scene.cpp src/game/game_state.cpp src/game/game_view.cpp + src/game/keyboard_card_navigator.cpp src/game/hand_counter.cpp src/game/log/message_log_widget.cpp src/game/phase.cpp diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game/board/abstract_card_item.cpp index 86b3e27c8..3b19c8efa 100644 --- a/cockatrice/src/game/board/abstract_card_item.cpp +++ b/cockatrice/src/game/board/abstract_card_item.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef { setCursor(Qt::OpenHandCursor); setFlag(ItemIsSelectable); + setFlag(ItemIsFocusable); setCacheMode(DeviceCoordinateCache); connect(&SettingsCache::instance(), &SettingsCache::displayCardNamesChanged, this, [this] { update(); }); @@ -347,3 +349,20 @@ QVariant AbstractCardItem::itemChange(QGraphicsItem::GraphicsItemChange change, return ArrowTarget::itemChange(change, value); } } + +void AbstractCardItem::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { + if (event->modifiers() & Qt::AltModifier) { + emit cardShiftClicked(cardRef.name); + } else if (event->modifiers() & Qt::ControlModifier) { + setSelected(!isSelected()); + } else if (!isSelected() && isHovered) { + scene()->clearSelection(); + setSelected(true); + } + event->accept(); + } else { + ArrowTarget::keyPressEvent(event); + } +} diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game/board/abstract_card_item.h index ed545e1ab..93921d874 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game/board/abstract_card_item.h @@ -127,6 +127,7 @@ protected: void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override; void cacheBgColor(); + void keyPressEvent(QKeyEvent *event) override; }; #endif diff --git a/cockatrice/src/game/board/arrow_item.cpp b/cockatrice/src/game/board/arrow_item.cpp index 430477d76..58de20459 100644 --- a/cockatrice/src/game/board/arrow_item.cpp +++ b/cockatrice/src/game/board/arrow_item.cpp @@ -228,52 +228,9 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) if (targetItem && targetItem != startItem) { CardItem *startCard = qgraphicsitem_cast(startItem); - // For now, we can safely assume that the start item is always a card. - // The target item can be a player as well. - if (!startCard) { - delArrow(); - return; + if (startCard) { + ArrowItem::sendCreateArrowCommand(player, startCard, targetItem, color, deleteInPhase); } - - CardZoneLogic *startZone = startCard->getZone(); - - Command_CreateArrow cmd; - cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(color)); - cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); - cmd.set_start_zone(startZone->getName().toStdString()); - cmd.set_start_card_id(startCard->getId()); - - if (auto *targetCard = qgraphicsitem_cast(targetItem)) { - CardZoneLogic *targetZone = targetCard->getZone(); - cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId()); - cmd.set_target_zone(targetZone->getName().toStdString()); - cmd.set_target_card_id(targetCard->getId()); - } else if (auto *targetPlayer = qgraphicsitem_cast(targetItem)) { - cmd.set_target_player_id(targetPlayer->getOwner()->getPlayerInfo()->getId()); - } else { - delArrow(); - return; - } - - // if the card is in hand then we will move the card to stack or table as part of drawing the arrow - if (startZone->getName() == ZoneNames::HAND) { - startCard->playCard(false); - CardInfoPtr ci = startCard->getCard().getCardPtr(); - bool playToStack = SettingsCache::instance().getPlayToStack(); - if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || - (playToStack && ci->getUiAttributes().tableRow != 0 && - startCard->getZone()->getName() != ZoneNames::STACK))) { - cmd.set_start_zone(ZoneNames::STACK); - } else { - cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE); - } - } - - if (deleteInPhase != 0) { - cmd.set_delete_in_phase(deleteInPhase); - } - - player->getPlayerActions()->sendGameCommand(cmd); } delArrow(); @@ -282,6 +239,55 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } } +void ArrowItem::sendCreateArrowCommand(PlayerLogic *player, + CardItem *startCard, + ArrowTarget *targetItem, + const QColor &color, + int deleteInPhase) +{ + if (!startCard || !targetItem || !player) { + return; + } + + CardZoneLogic *startZone = startCard->getZone(); + + Command_CreateArrow cmd; + cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(color)); + cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_start_zone(startZone->getName().toStdString()); + cmd.set_start_card_id(startCard->getId()); + + if (auto *targetCard = qgraphicsitem_cast(targetItem)) { + CardZoneLogic *targetZone = targetCard->getZone(); + cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId()); + cmd.set_target_zone(targetZone->getName().toStdString()); + cmd.set_target_card_id(targetCard->getId()); + } else if (auto *targetPlayer = qgraphicsitem_cast(targetItem)) { + cmd.set_target_player_id(targetPlayer->getOwner()->getPlayerInfo()->getId()); + } else { + return; + } + + // if the card is in hand then we will move the card to stack or table as part of drawing the arrow + if (startZone->getName() == ZoneNames::HAND) { + startCard->playCard(false); + CardInfoPtr ci = startCard->getCard().getCardPtr(); + bool playToStack = SettingsCache::instance().getPlayToStack(); + if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) || + (playToStack && ci->getUiAttributes().tableRow != 0 && + startCard->getZone()->getName() != ZoneNames::STACK))) { + cmd.set_start_zone(ZoneNames::STACK); + } else { + cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE); + } + } + + if (deleteInPhase != 0) { + cmd.set_delete_in_phase(deleteInPhase); + } + + player->getPlayerActions()->sendGameCommand(cmd); +} // ArrowAttachItem ArrowAttachItem::ArrowAttachItem(ArrowTarget *_startItem) : ArrowItem(_startItem->getOwner(), -1, _startItem, nullptr, Qt::green) diff --git a/cockatrice/src/game/board/arrow_item.h b/cockatrice/src/game/board/arrow_item.h index 7dc0f9477..9a7fa36ab 100644 --- a/cockatrice/src/game/board/arrow_item.h +++ b/cockatrice/src/game/board/arrow_item.h @@ -77,6 +77,11 @@ public: } void delArrow(); + static void sendCreateArrowCommand(PlayerLogic *player, + CardItem *startCard, + ArrowTarget *targetItem, + const QColor &color, + int deleteInPhase = 0); }; class ArrowDragItem : public ArrowItem diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp index a08194540..fa4e97093 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game/board/card_item.cpp @@ -5,6 +5,7 @@ #include "../../game_graphics/zones/view_zone.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../game_scene.h" +#include "../keyboard_card_navigator.h" #include "../phase.h" #include "../player/player_actions.h" #include "../player/player_logic.h" @@ -14,7 +15,10 @@ #include <../../client/settings/card_counter_settings.h> #include +#include #include +#include +#include #include #include #include @@ -500,6 +504,71 @@ void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) event->accept(); } +void CardItem::keyPressEvent(QKeyEvent *event) +{ + + auto *gameScene = static_cast(scene()); + KeyboardCardNavigator *navigator = gameScene ? gameScene->getCardNavigator() : nullptr; + + if (event->key() == Qt::Key_Escape && navigator && navigator->isArrowModeActiveVar()) { + navigator->cancelArrowMode(); + event->accept(); + qWarning() << "Arrow mode cancelled from CardItem with id:" << id; + return; + } + + if (event->key() == Qt::Key_A && (event->modifiers() & Qt::ShiftModifier) && getIsHovered()) { + qWarning() << "Starting arrow mode from CardItem with id:" << id; + if (navigator) { + qWarning() << "Navigator found, starting arrow mode."; + scene()->clearSelection(); + setSelected(true); + navigator->startArrowMode(this); + event->accept(); + return; + } + } + + if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && navigator && + navigator->isArrowModeActiveVar() && getIsHovered()) { + + qWarning() << "Finalizing arrow mode from CardItem with id:" << id; + navigator->createArrow(this); + navigator->cancelArrowMode(); + event->accept(); + return; + } + + if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && isSelected() && + SettingsCache::instance().getDoubleClickToPlay()) { + handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); + event->accept(); + } else if (event->key() == Qt::Key_Space && getIsHovered()) { + if (QWidget *popup = QApplication::activePopupWidget()) { + popup->close(); + event->accept(); + return; + } + + if (owner != nullptr) { + scene()->clearSelection(); + setSelected(true); + owner->getGame()->setActiveCard(this); + if (QMenu *cardMenu = owner->getPlayerMenu()->updateCardMenu(this)) { + QPointF scenePos = sceneBoundingRect().center(); + if (!scene()->views().isEmpty()) { + QGraphicsView *view = scene()->views().first(); + QPoint screenPos = view->mapToGlobal(view->mapFromScene(scenePos)); + cardMenu->popup(screenPos); + } + } + } + event->accept(); + } else { + AbstractCardItem::keyPressEvent(event); + } +} + bool CardItem::animationEvent() { int rotation = ROTATION_DEGREES_PER_FRAME; diff --git a/cockatrice/src/game/board/card_item.h b/cockatrice/src/game/board/card_item.h index 87f9667de..5ee603e88 100644 --- a/cockatrice/src/game/board/card_item.h +++ b/cockatrice/src/game/board/card_item.h @@ -169,6 +169,7 @@ protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; }; diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 1b4f0d461..ee831fc4d 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -5,6 +5,7 @@ #include "../game_graphics/zones/view_zone.h" #include "../game_graphics/zones/view_zone_widget.h" #include "board/card_item.h" +#include "keyboard_card_navigator.h" #include "phases_toolbar.h" #include "player/player_graphics_item.h" #include "player/player_logic.h" @@ -28,9 +29,11 @@ * Finally, calls rearrange() to layout players initially. */ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) - : QGraphicsScene(parent), phasesToolbar(_phasesToolbar), viewSize(QSize()), playerRotation(0) + : QGraphicsScene(parent), phasesToolbar(_phasesToolbar), viewSize(QSize()), playerRotation(0), + cardNavigator(nullptr) { animationTimer = new QBasicTimer; + cardNavigator = new KeyboardCardNavigator(nullptr); addItem(phasesToolbar); connect(&SettingsCache::instance(), &SettingsCache::minPlayersForMultiColumnLayoutChanged, this, &GameScene::rearrange); @@ -41,6 +44,7 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) GameScene::~GameScene() { delete animationTimer; + delete cardNavigator; // DO NOT call clearViews() here // clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults @@ -460,6 +464,46 @@ void GameScene::onCardZoneChanged(CardItem *card, bool sameZone) } } +void GameScene::handleLeftArrow() +{ + if (cardNavigator) { + QKeyEvent event(QEvent::KeyPress, Qt::Key_Left, Qt::NoModifier); + cardNavigator->switchCardInZone(&event); + } +} + +void GameScene::handleRightArrow() +{ + if (cardNavigator) { + QKeyEvent event(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier); + cardNavigator->switchCardInZone(&event); + } +} + +void GameScene::handleUpArrow() +{ + if (cardNavigator) { + QKeyEvent event(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); + cardNavigator->switchZone(&event); + } +} + +void GameScene::handleDownArrow() +{ + if (cardNavigator) { + QKeyEvent event(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); + cardNavigator->switchZone(&event); + } +} + +void GameScene::setActivePlayer(PlayerLogic *player) +{ + if (cardNavigator) { + cardNavigator->setPlayer(player); + cardNavigator->setCurrentZone(player->getHandZone()); + } +} + // ---------- Hover Handling ---------- void GameScene::updateHover(const QPointF &scenePos) diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h index 1551c8365..b33b6cce5 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game/game_scene.h @@ -23,6 +23,7 @@ class CardItem; class ServerInfo_Card; class PhasesToolbar; class QBasicTimer; +class KeyboardCardNavigator; /** * @class GameScene @@ -52,6 +53,7 @@ private: QBasicTimer *animationTimer; ///< Timer for card animations QSet cardsToAnimate; ///< Cards currently animating int playerRotation; ///< Rotation offset for player layout + KeyboardCardNavigator *cardNavigator; ///< Handles keyboard-based card navigation /** * @brief Updates which card is currently hovered based on scene coordinates. @@ -171,6 +173,12 @@ public: /** @brief Updates hovered card highlighting. */ void updateHoveredCard(CardItem *newCard); + /** @brief Gets the keyboard card navigator. */ + KeyboardCardNavigator *getCardNavigator() const + { + return cardNavigator; + } + /** @brief Registers a card for animation updates. */ void registerAnimationItem(AbstractCardItem *card); @@ -206,6 +214,17 @@ public slots: void deleteArrow(int arrowId); void clearArrowsForPlayer(int playerId); + /** @brief Handles left arrow key for card navigation. */ + void handleLeftArrow(); + /** @brief Handles right arrow key for card navigation. */ + void handleRightArrow(); + /** @brief Handles up arrow key for zone navigation. */ + void handleUpArrow(); + /** @brief Handles down arrow key for zone navigation. */ + void handleDownArrow(); + /** @brief Sets the active player for keyboard navigation. */ + void setActivePlayer(PlayerLogic *player); + /// Queues up arrow deletion but doesn't directly modify the scene void requestArrowDeletion(int arrowId); void requestClearArrowsForPlayer(int playerId); diff --git a/cockatrice/src/game/keyboard_card_navigator.cpp b/cockatrice/src/game/keyboard_card_navigator.cpp new file mode 100644 index 000000000..055b522b3 --- /dev/null +++ b/cockatrice/src/game/keyboard_card_navigator.cpp @@ -0,0 +1,256 @@ +#include "keyboard_card_navigator.h" + +#include "../../client/settings/cache_settings.h" +#include "board/arrow_item.h" +#include "board/card_item.h" +#include "player/player_logic.h" + +#include +#include +KeyboardCardNavigator::KeyboardCardNavigator(PlayerLogic *player) + : currentZone(nullptr), hoveredCardIndex(-1), isArrowModeActive(false), arrowOriginCard(nullptr), + previewArrow(nullptr), playerLogic(player) +{ +} + +int KeyboardCardNavigator::getHoveredIndex() +{ + return hoveredCardIndex; +} + +CardZoneLogic *KeyboardCardNavigator::getCurrentZone() +{ + return currentZone; +} + +void KeyboardCardNavigator::setCurrentZone(CardZoneLogic *zone) +{ + currentZone = zone; +} + +void KeyboardCardNavigator::switchCardInZone(QKeyEvent *event) +{ + if (!playerLogic) { + return; + } + if (!currentZone) { + return; + } + if (QApplication::activePopupWidget()) { + return; + } + + const CardList &zoneCards = currentZone->getCards(); + if (zoneCards.isEmpty()) { + // if the current zone is empty, try to force a zone change. + QKeyEvent event(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); + KeyboardCardNavigator::switchZone(&event); + return; + } + event->accept(); + + // Check if this is an arrow key we care about + int keyCode = event->key(); + if (keyCode != Qt::Key_Right && keyCode != Qt::Key_Left) { + return; + } + + // Calculate new index + int newIndex = hoveredCardIndex; + bool isInitial = (hoveredCardIndex < 0); + + if (isInitial) { + // If the first key pressed is the right, spawn the cursor at the first card + // Otherwise, spawn at the last card. + newIndex = keyCode == Qt::Key_Right ? 0 : zoneCards.size() - 1; + } else { + if (hoveredCardIndex >= zoneCards.size()) { + hoveredCardIndex = 0; + newIndex = 0; + } + + if (keyCode == Qt::Key_Right) { + newIndex = (hoveredCardIndex + 1) % zoneCards.size(); + } else { + newIndex = (hoveredCardIndex - 1 + zoneCards.size()) % zoneCards.size(); + } + } + + KeyboardCardNavigator::changeHoverCard(hoveredCardIndex, false); + + hoveredCardIndex = newIndex; + KeyboardCardNavigator::changeHoverCard(newIndex, true); +} + +void KeyboardCardNavigator::changeHoverCard(int cardIndex, bool hover) +{ + const CardList &zoneCards = currentZone->getCards(); + if (cardIndex >= 0 && cardIndex < zoneCards.size()) { + CardItem *card = zoneCards[cardIndex]; + if (card) { + card->setHovered(hover); + card->setFocus(); + // Force update of new card's area + if (card->scene()) { + card->scene()->update(card->sceneBoundingRect()); + } + if (isArrowModeActive && hover) { + createTempArrow(card); + } + } + } +} + +void KeyboardCardNavigator::setPlayer(PlayerLogic *player) +{ + playerLogic = player; +} + +void KeyboardCardNavigator::setHoveredCardIndex(int index) +{ + hoveredCardIndex = index; +} + +void KeyboardCardNavigator::unhoverCard() +{ + if (!playerLogic || !currentZone) { + return; + } + + changeHoverCard(hoveredCardIndex, false); +} + +void KeyboardCardNavigator::createTempArrow(CardItem *targetCard) +{ + if (!isArrowModeActive || !arrowOriginCard || !targetCard || !playerLogic) { + return; + } + + if (previewArrow) { + delete previewArrow; + previewArrow = nullptr; + } + + previewArrow = new ArrowItem(playerLogic, -1, arrowOriginCard, targetCard, Qt::red); + if (arrowOriginCard->scene()) { + arrowOriginCard->scene()->addItem(previewArrow); + } +} +void KeyboardCardNavigator::createArrow(CardItem *targetCard) +{ + if (!isArrowModeActive || !arrowOriginCard || !targetCard || !playerLogic) { + return; + } + + if (previewArrow) { + delete previewArrow; + previewArrow = nullptr; + } + isArrowModeActive = false; + + if (arrowOriginCard == targetCard) { + arrowOriginCard = nullptr; + return; + } + + ArrowItem::sendCreateArrowCommand(playerLogic, arrowOriginCard, targetCard, Qt::red); + + arrowOriginCard = nullptr; +} + +void KeyboardCardNavigator::startArrowMode(CardItem *originCard) +{ + if (!originCard || !originCard->scene() || !playerLogic) { + return; + } + + isArrowModeActive = true; + arrowOriginCard = originCard; + + createTempArrow(originCard); +} + +void KeyboardCardNavigator::cancelArrowMode() +{ + if (previewArrow) { + delete previewArrow; + previewArrow = nullptr; + } + isArrowModeActive = false; + arrowOriginCard = nullptr; +} + +CardZoneLogic * +KeyboardCardNavigator::findZoneWithCards(QList &zonesList, int currentZoneIndex, bool upperZone) +{ + CardZoneLogic *newZone; + int newZoneIndex = currentZoneIndex; + do { + // Calculate new zone index + if (upperZone) { + newZoneIndex = (newZoneIndex + 1) % zonesList.size(); + } else { + newZoneIndex = (newZoneIndex - 1 + zonesList.size()) % zonesList.size(); + } + newZone = zonesList[newZoneIndex]; + // Prevent switching zone if the others are empty + } while (newZoneIndex != currentZoneIndex && newZone->getCards().size() == 0); + return newZone; +} + +void KeyboardCardNavigator::switchZone(QKeyEvent *event) +{ + if (!playerLogic) { + return; + } + + if (QApplication::activePopupWidget()) { + return; + } + + int keyCode = event->key(); + if (keyCode != Qt::Key_Up && keyCode != Qt::Key_Down) { + return; + } + + event->accept(); + // Build list with only the zones of interest + QList zonesList; + + TableZoneLogic *tableZone = playerLogic->getTableZone(); + StackZoneLogic *stackZone = playerLogic->getStackZone(); + HandZoneLogic *handZone = playerLogic->getHandZone(); + + if (tableZone) { + zonesList.append(tableZone); + } + if (stackZone) { + zonesList.append(stackZone); + } + if (handZone) { + zonesList.append(handZone); + } + + if (zonesList.isEmpty()) { + return; + } + + int currentZoneIndex = zonesList.indexOf(currentZone); + if (currentZoneIndex < 0) { + + currentZoneIndex = 0; + } + + CardZoneLogic *newZone = + KeyboardCardNavigator::findZoneWithCards(zonesList, currentZoneIndex, keyCode == Qt::Key_Up); + + if (currentZone != newZone) { + + changeHoverCard(hoveredCardIndex, false); + setCurrentZone(newZone); + // Reset card index since we're in a new zone + hoveredCardIndex = 0; + // The new zone has to have cards, hover and select the first one + changeHoverCard(hoveredCardIndex, true); + } +} diff --git a/cockatrice/src/game/keyboard_card_navigator.h b/cockatrice/src/game/keyboard_card_navigator.h new file mode 100644 index 000000000..c420ee85e --- /dev/null +++ b/cockatrice/src/game/keyboard_card_navigator.h @@ -0,0 +1,61 @@ +/** + * @file keyboard_card_navigator.h + * @ingroup GameInput + * @brief Handles keyboard navigation for selecting cards using arrow keys. + * + * Allows players to navigate through hand cards using directional arrow keys. + * Spatially-aware: finds adjacent cards based on screen position. + */ + +#ifndef KEYBOARD_CARD_NAVIGATOR_H +#define KEYBOARD_CARD_NAVIGATOR_H + +#include "zones/card_zone_logic.h" + +class PlayerLogic; +class CardItem; +class QKeyEvent; +class ArrowItem; + +class KeyboardCardNavigator +{ +private: + CardZoneLogic *currentZone; + int hoveredCardIndex = -1; + bool isArrowModeActive; + CardItem *arrowOriginCard; + ArrowItem *previewArrow; + PlayerLogic *playerLogic; + /** + * @brief Gets hand cards sorted by visual position. + * @return List of cards sorted by visual order (left-to-right for horizontal, top-to-bottom for vertical). + */ + CardList getVisuallyOrderedHandCards() const; + +public: + KeyboardCardNavigator(PlayerLogic *player = nullptr); + int getHoveredIndex(); + CardZoneLogic *getCurrentZone(); + void setCurrentZone(CardZoneLogic *zone); + void switchCardInZone(QKeyEvent *event); + void switchZone(QKeyEvent *event); + void setPlayer(PlayerLogic *player); + /** + * @brief Validates and resets the hovered card if needed. + * Call this when hand composition changes. + */ + void setHoveredCardIndex(int index); + void unhoverCard(); + void changeHoverCard(int cardIndex, bool hover); + void createArrow(CardItem *targetCard); + void createTempArrow(CardItem *targetCard); + void startArrowMode(CardItem *originCard); + void cancelArrowMode(); + CardZoneLogic *findZoneWithCards(QList &zonesList, int currentZoneIndex, bool upperZone); + bool isArrowModeActiveVar() const + { + return isArrowModeActive; + } +}; + +#endif diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 67c6e9519..200e3936c 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -11,6 +11,7 @@ #include "../board/card_list.h" #include "../board/counter_general.h" #include "../game_scene.h" +#include "../keyboard_card_navigator.h" #include "player_actions.h" #include "player_target.h" @@ -323,6 +324,9 @@ bool PlayerLogic::clearCardsToDelete() void PlayerLogic::setActive(bool _active) { active = _active; + if (_active == true) { + hoverFirstCardInHand(); + } emit activeChanged(active); } @@ -348,3 +352,20 @@ void PlayerLogic::setGameStarted() } setConceded(false); } + +void PlayerLogic::hoverFirstCardInHand() +{ + HandZoneLogic *handZone = getHandZone(); + if (!handZone) { + return; + } + const CardList &handCards = handZone->getCards(); + if (!handCards.isEmpty()) { + CardItem *firstCard = handCards.at(0); + if (firstCard) { + firstCard->setHovered(true); + getGameScene()->getCardNavigator()->unhoverCard(); + getGameScene()->getCardNavigator()->setHoveredCardIndex(0); + } + } +} diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index 20d7597b4..f5c558859 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -229,6 +229,8 @@ public: void setZoneId(int _zoneId); + void hoverFirstCardInHand(); + private: AbstractGame *game; PlayerInfo *playerInfo; diff --git a/cockatrice/src/interface/key_signals.cpp b/cockatrice/src/interface/key_signals.cpp index a78997cc2..1b8995491 100644 --- a/cockatrice/src/interface/key_signals.cpp +++ b/cockatrice/src/interface/key_signals.cpp @@ -1,5 +1,6 @@ #include "key_signals.h" +#include #include bool KeySignals::eventFilter(QObject * /*object*/, QEvent *event) @@ -26,15 +27,35 @@ bool KeySignals::eventFilter(QObject * /*object*/, QEvent *event) case Qt::Key_Right: if (kevent->modifiers() & Qt::ShiftModifier) { emit onShiftRight(); + } else { + emit onRightArrow(); + return true; } break; case Qt::Key_Left: if (kevent->modifiers() & Qt::ShiftModifier) { emit onShiftLeft(); + } else { + emit onLeftArrow(); + return true; } break; + case Qt::Key_Up: + // This check exists so up and down arrows can still navigate all menu boxes while having + // normal browsing functionality in game + if (qApp->activePopupWidget() != nullptr || qApp->activeModalWidget() != nullptr) { + return false; + } + emit onUpArrow(); + return true; + case Qt::Key_Down: + if (qApp->activePopupWidget() != nullptr || qApp->activeModalWidget() != nullptr) { + return false; + } + emit onDownArrow(); + return true; case Qt::Key_Delete: case Qt::Key_Backspace: emit onDelete(); diff --git a/cockatrice/src/interface/key_signals.h b/cockatrice/src/interface/key_signals.h index 30dcee0ba..1e44381c1 100644 --- a/cockatrice/src/interface/key_signals.h +++ b/cockatrice/src/interface/key_signals.h @@ -28,6 +28,10 @@ signals: void onCtrlAltRBracket(); void onShiftS(); void onCtrlC(); + void onLeftArrow(); + void onRightArrow(); + void onUpArrow(); + void onDownArrow(); protected: bool eventFilter(QObject *, QEvent *event) override; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 9fc123a8c..6902319fe 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -14,6 +14,7 @@ #include "../game/player/player_logic.h" #include "../game/replay.h" #include "../interface/card_picture_loader/card_picture_loader.h" +#include "../interface/key_signals.h" #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/server/user/user_list_manager.h" @@ -863,6 +864,7 @@ PlayerLogic *TabGame::setActivePlayer(int id) } playerListWidget->setActivePlayer(id); + scene->setActivePlayer(player); QMapIterator i(game->getPlayerManager()->getPlayers()); while (i.hasNext()) { i.next(); @@ -1153,6 +1155,14 @@ void TabGame::createPlayAreaWidget(bool bReplay) connect(scene, &GameScene::arrowDeletionRequested, game->getGameEventHandler(), &GameEventHandler::handleArrowDeletion); connect(game->getGameEventHandler(), &GameEventHandler::arrowDeleted, scene, &GameScene::deleteArrow); + + keySignals = new KeySignals(); + qApp->installEventFilter(keySignals); + connect(keySignals, &KeySignals::onLeftArrow, scene, &GameScene::handleLeftArrow); + connect(keySignals, &KeySignals::onRightArrow, scene, &GameScene::handleRightArrow); + connect(keySignals, &KeySignals::onUpArrow, scene, &GameScene::handleUpArrow); + connect(keySignals, &KeySignals::onDownArrow, scene, &GameScene::handleDownArrow); + gameView = new GameView(scene); auto gamePlayAreaVBox = new QVBoxLayout; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index 7f9392034..043fe429d 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -51,6 +51,7 @@ class GameReplay; class LineEditCompleter; class QDockWidget; class QStackedWidget; +class KeySignals; class TabGame : public Tab { @@ -73,6 +74,7 @@ private: PhasesToolbar *phasesToolbar; GameScene *scene; GameView *gameView; + KeySignals *keySignals; QMap deckViewContainers; QVBoxLayout *deckViewContainerLayout; QWidget *gamePlayAreaWidget, *deckViewContainerWidget; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 00eba288e..bcaf3831e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -75,6 +75,7 @@ target_link_libraries( ) add_subdirectory(card_zone_algorithms) +add_subdirectory(keyboard_navigator_tests) add_subdirectory(carddatabase) add_subdirectory(loading_from_clipboard) add_subdirectory(movecard_tests) diff --git a/tests/keyboard_navigator_tests/CMakeLists.txt b/tests/keyboard_navigator_tests/CMakeLists.txt new file mode 100644 index 000000000..2c0a7f632 --- /dev/null +++ b/tests/keyboard_navigator_tests/CMakeLists.txt @@ -0,0 +1,32 @@ +add_executable(keyboard_navigator_test keyboard_card_navigator_test.cpp keyboard_navigator_test_stubs.cpp) + +target_compile_options(keyboard_navigator_test PRIVATE --coverage) +target_link_options(keyboard_navigator_test PRIVATE --coverage) + +target_include_directories( + keyboard_navigator_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src ${CMAKE_SOURCE_DIR}/cockatrice/src/game + ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/libcockatrice_interfaces +) + +target_link_libraries( + keyboard_navigator_test + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} + PRIVATE libcockatrice_settings + PRIVATE libcockatrice_interfaces + PRIVATE libcockatrice_protocol + PRIVATE libcockatrice_card + PRIVATE libcockatrice_deck_list + PRIVATE libcockatrice_models + PRIVATE libcockatrice_rng + PRIVATE libcockatrice_network + PRIVATE libcockatrice_utility + PRIVATE Qt6::Widgets +) + +add_test(NAME keyboard_navigator_test COMMAND keyboard_navigator_test) + +if(NOT GTEST_FOUND) + add_dependencies(keyboard_navigator_test gtest) +endif() diff --git a/tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp b/tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp new file mode 100644 index 000000000..29d0c08b2 --- /dev/null +++ b/tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp @@ -0,0 +1,229 @@ +#include "game/keyboard_card_navigator.h" + +#include +#include +#include +#include +#include +#include + +// Some tests require us to get the zones out of a player, so instead of changing code we didn't make, we +// decided to use this dirty trick. +#define private public +#include "game/player/player_logic.h" +#undef private + +#include "game/board/abstract_card_item.h" +#include "game/board/arrow_item.h" +#include "game/board/card_item.h" +#include "game/board/card_list.h" +#include "game/keyboard_card_navigator.cpp" +#include "game/zones/card_zone_logic.h" +#include "game/zones/hand_zone_logic.h" +#include "game/zones/stack_zone_logic.h" +#include "game/zones/table_zone_logic.h" +#include "keyboard_navigator_test_fakes.h" + +class KeyboardCardNavigatorTest : public ::testing::Test +{ +protected: + KeyboardCardNavigator *navigator; + FakeHandZoneLogic *handZone; + FakeTableZoneLogic *tableZone; + FakeStackZoneLogic *stackZone; + PlayerLogic *player; + + void SetUp() override + { + handZone = new FakeHandZoneLogic("hand"); + tableZone = new FakeTableZoneLogic("table"); + stackZone = new FakeStackZoneLogic("stack"); + // Player is mocked like this, so we fill the zones for the tests. + player = (PlayerLogic *)malloc(sizeof(PlayerLogic)); + new (&player->zones) QMap(); + player->zones.insert("table", tableZone); + player->zones.insert("stack", stackZone); + player->zones.insert("hand", handZone); + navigator = new KeyboardCardNavigator(player); + } + + void TearDown() override + { + delete navigator; + free(player); + delete handZone; + delete tableZone; + delete stackZone; + } +}; + +/* This test verifies the behaviour of spawning the cursor: + When pressing the left arrow, it goes to the first card. + When pressing the right arrow, it goes to the last card of the zone. */ +TEST_F(KeyboardCardNavigatorTest, LeftRightArrowTest) +{ + // Set an arbitrary amount of cards for the zone + handZone->setDummyCardCount(5); + + // Set the default index + navigator->setHoveredCardIndex(-1); + + navigator->setCurrentZone(handZone); + + // Simulate a key press and a card switch + QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier); + navigator->switchCardInZone(&eRight); + + // Verify the first card is selected + EXPECT_EQ(navigator->getHoveredIndex(), 0); + + // Reset the index + navigator->setHoveredCardIndex(-1); + + // Simulate a key press and a card switch + QKeyEvent eLeft(QEvent::KeyPress, Qt::Key_Left, Qt::NoModifier); + navigator->switchCardInZone(&eLeft); + + // Check if the last card was selected + EXPECT_EQ(navigator->getHoveredIndex(), 4); +} + +/* This test verifies the moving behaviour of the cursor. */ +TEST_F(KeyboardCardNavigatorTest, NormalSwitchCards) +{ + // Set an arbitrary amount of cards for the zone + handZone->setDummyCardCount(5); + navigator->setCurrentZone(handZone); + // Select the second card + navigator->setHoveredCardIndex(1); + + // If right is pressed, go to index + 1 + QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier); + navigator->switchCardInZone(&eRight); + EXPECT_EQ(navigator->getHoveredIndex(), 2); + + // If left is pressed, go to index - 1 + QKeyEvent eLeft(QEvent::KeyPress, Qt::Key_Left, Qt::NoModifier); + navigator->switchCardInZone(&eLeft); + EXPECT_EQ(navigator->getHoveredIndex(), 1); +} + +/* This test verifies the edge case moving behaviour of the cursor. + If we are on the first card, and left is pressed, we should go to the + last card, and vice-versa */ +TEST_F(KeyboardCardNavigatorTest, ZoneLoopsTest) +{ + // Set an arbitrary amount of cards for the zone + tableZone->setDummyCardCount(2); + + // Select the first card + navigator->setCurrentZone(tableZone); + navigator->setHoveredCardIndex(0); + + // If we press left, it wraps around to the end, and the zone doesn't change + QKeyEvent eLeft(QEvent::KeyPress, Qt::Key_Left, Qt::NoModifier); + navigator->switchCardInZone(&eLeft); + EXPECT_EQ(navigator->getHoveredIndex(), 1); + EXPECT_EQ(navigator->getCurrentZone(), tableZone); + + // If we press right, it wraps around to the start, and the zone doesn't change + QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier); + navigator->switchCardInZone(&eRight); + EXPECT_EQ(navigator->getHoveredIndex(), 0); + EXPECT_EQ(navigator->getCurrentZone(), tableZone); +} + +/* This test verifies the switching of zones if the zone we are currently on is empty. */ +TEST_F(KeyboardCardNavigatorTest, EmptyZoneLoopTest) +{ + QList zonesList; + zonesList.append(tableZone); + zonesList.append(stackZone); + zonesList.append(handZone); + + tableZone->setDummyCardCount(2); + stackZone->setDummyCardCount(0); // empty + handZone->setDummyCardCount(2); + + // Simulate key up when switching zones + CardZoneLogic *newZone = navigator->findZoneWithCards(zonesList, 0, true); + // The result should be zone 2 + EXPECT_EQ(newZone, handZone); + + // Simulate key down when switching zones + newZone = navigator->findZoneWithCards(zonesList, 2, false); + // The result should be zone 2 + EXPECT_EQ(newZone, tableZone); +} + +/* This test verifies the switching of zones with the up and down keys. */ +TEST_F(KeyboardCardNavigatorTest, SwitchZoneTest) +{ + tableZone->setDummyCardCount(2); + stackZone->setDummyCardCount(2); + handZone->setDummyCardCount(2); + + navigator->setCurrentZone(tableZone); + navigator->setHoveredCardIndex(0); + + // Simulate key up when switching zones + QKeyEvent eUp(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); + navigator->switchZone(&eUp); + // The result should be zone 1 and the card selected is the first one + EXPECT_EQ(navigator->getCurrentZone(), stackZone); + EXPECT_EQ(navigator->getHoveredIndex(), 0); + + // Simulate key down when switching zones + QKeyEvent eDown(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); + navigator->switchZone(&eDown); + // The result should be zone 0 and the card selected is the first one + EXPECT_EQ(navigator->getCurrentZone(), tableZone); + EXPECT_EQ(navigator->getHoveredIndex(), 0); +} + +/* This test verifies the case where every zone is empty. */ +TEST_F(KeyboardCardNavigatorTest, EmptyZoneSwitchZones) +{ + QList zonesList; + zonesList.append(tableZone); + zonesList.append(stackZone); + zonesList.append(handZone); + + tableZone->setDummyCardCount(0); + stackZone->setDummyCardCount(0); + handZone->setDummyCardCount(0); + + // The expected zone is the starting one + CardZoneLogic *newZone = navigator->findZoneWithCards(zonesList, 0, true); + EXPECT_EQ(newZone, tableZone); +} + +/* This test verifies the behaviour when someone moves a card with the mouse and the + keyboard is used after a zone becomes empty. */ +TEST_F(KeyboardCardNavigatorTest, ZoneEmptySwitchTest) +{ + tableZone->setDummyCardCount(1); + stackZone->setDummyCardCount(0); + handZone->setDummyCardCount(1); + + // Set a card as hovered in the table zone + navigator->setCurrentZone(tableZone); + navigator->setHoveredCardIndex(0); + + // Simulate the user deleting/moving all cards out of the table zone + tableZone->setDummyCardCount(0); + + // Now the user presses an arrow key in the empty zone + QKeyEvent eRight(QEvent::KeyPress, Qt::Key_Right, Qt::NoModifier); + navigator->switchCardInZone(&eRight); + + // The expected zone is the next one with cards + EXPECT_EQ(navigator->getCurrentZone(), handZone); + EXPECT_EQ(navigator->getHoveredIndex(), 0); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/keyboard_navigator_tests/keyboard_navigator_test_fakes.h b/tests/keyboard_navigator_tests/keyboard_navigator_test_fakes.h new file mode 100644 index 000000000..f1c60e40d --- /dev/null +++ b/tests/keyboard_navigator_tests/keyboard_navigator_test_fakes.h @@ -0,0 +1,67 @@ +#ifndef KEYBOARD_NAVIGATOR_TEST_FAKES_H +#define KEYBOARD_NAVIGATOR_TEST_FAKES_H + +#include "game/board/card_item.h" +#include "game/zones/card_zone_logic.h" +#include "game/zones/hand_zone_logic.h" +#include "game/zones/stack_zone_logic.h" +#include "game/zones/table_zone_logic.h" + +// Define safe macro replacements for the visual tests +#define setFocus() zValue() +#define getVisuallyOrderedHandCards() getCards() + +namespace QApplicationMock +{ +extern bool hasPopup; +} + +class FakeCardZoneLogic : public CardZoneLogic +{ +public: + QString name; + FakeCardZoneLogic(const QString &n) : CardZoneLogic(nullptr, n, false, false, true, nullptr), name(n) + { + } + void addCardImpl(CardItem *, int, int) override + { + } + void setDummyCardCount(int count) + { + cards.clear(); + for (int i = 0; i < count; i++) { + cards.insert(i, (CardItem *)nullptr); + } + } + const QString getName() const + { + return name; + } +}; + +#define DECLARE_FAKE_ZONE(FakeName, TargetName) \ + class FakeName : public FakeCardZoneLogic \ + { \ + public: \ + FakeName(const QString &n) : FakeCardZoneLogic(n) \ + { \ + } \ + const QMetaObject *metaObject() const override \ + { \ + return &TargetName::staticMetaObject; \ + } \ + void *qt_metacast(const char *clname) override \ + { \ + if (!clname) \ + return nullptr; \ + if (!strcmp(clname, #TargetName)) \ + return static_cast(this); \ + return FakeCardZoneLogic::qt_metacast(clname); \ + } \ + }; + +DECLARE_FAKE_ZONE(FakeTableZoneLogic, TableZoneLogic) +DECLARE_FAKE_ZONE(FakeStackZoneLogic, StackZoneLogic) +DECLARE_FAKE_ZONE(FakeHandZoneLogic, HandZoneLogic) + +#endif // KEYBOARD_NAVIGATOR_TEST_FAKES_H diff --git a/tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp b/tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp new file mode 100644 index 000000000..276cc7316 --- /dev/null +++ b/tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp @@ -0,0 +1,106 @@ +#include "game/board/abstract_card_item.h" +#include "game/board/arrow_item.h" +#include "game/board/card_item.h" +#include "game/board/card_list.h" +#include "game/player/player_logic.h" +#include "game/zones/card_zone_logic.h" +#include "game/zones/hand_zone_logic.h" +#include "game/zones/stack_zone_logic.h" +#include "game/zones/table_zone_logic.h" + +#include +#include +#include +#include + +// Stubs for AbstractCardItem +void AbstractCardItem::setHovered(bool) +{ +} + +// Stubs for ArrowItem +ArrowItem::ArrowItem(PlayerLogic *, int, ArrowTarget *, ArrowTarget *, QColor const &) + : QObject(nullptr), QGraphicsItem(nullptr) +{ +} +void ArrowItem::sendCreateArrowCommand(PlayerLogic *, CardItem *, ArrowTarget *, QColor const &, int) +{ +} +void ArrowItem::paint(QPainter *, QStyleOptionGraphicsItem const *, QWidget *) +{ +} +void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *) +{ +} +const QMetaObject *ArrowItem::metaObject() const +{ + return nullptr; +} +void *ArrowItem::qt_metacast(const char *) +{ + return nullptr; +} +int ArrowItem::qt_metacall(QMetaObject::Call, int, void **) +{ + return 0; +} + +// Stubs for QMetaObject +const QMetaObject TableZoneLogic::staticMetaObject = { + {&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}}; +const QMetaObject StackZoneLogic::staticMetaObject = { + {&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}}; +const QMetaObject HandZoneLogic::staticMetaObject = { + {&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}}; +const QMetaObject PlayerLogic::staticMetaObject = { + {&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}}; +const QMetaObject CardZoneLogic::staticMetaObject = { + {&QObject::staticMetaObject, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}}; + +// Stubs for CardList +CardList::CardList(bool) : QList() +{ +} + +// Stubs for CardZoneLogic +CardZoneLogic::CardZoneLogic(PlayerLogic *, const QString &, bool, bool, bool _contentsKnown, QObject *) + : QObject(nullptr), cards(_contentsKnown) +{ +} +CardItem *CardZoneLogic::getCard(int) +{ + return nullptr; +} +CardItem *CardZoneLogic::takeCard(int, int, bool) +{ + return nullptr; +} +void CardZoneLogic::addCardImpl(CardItem *, int, int) +{ +} +QString CardZoneLogic::getTranslatedName(bool, GrammaticalCase) const +{ + return QString(); +} +const QMetaObject *CardZoneLogic::metaObject() const +{ + return nullptr; +} +void *CardZoneLogic::qt_metacast(const char *) +{ + return nullptr; +} +int CardZoneLogic::qt_metacall(QMetaObject::Call, int, void **) +{ + return 0; +} + +// QApplication Mock implementation +namespace QApplicationMock +{ +bool hasPopup = false; +} +QWidget *QApplication::activePopupWidget() +{ + return QApplicationMock::hasPopup ? (QWidget *)1 : nullptr; +}