mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-16 20:17:45 -07:00
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 <manuel.monge@tecnico.ulisboa.pt>
This commit is contained in:
parent
3fa377a11c
commit
5acce8998e
22 changed files with 1023 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include <QCursor>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QPainter>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -228,52 +228,9 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|||
|
||||
if (targetItem && targetItem != startItem) {
|
||||
CardItem *startCard = qgraphicsitem_cast<CardItem *>(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<CardItem *>(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<PlayerTarget *>(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<CardItem *>(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<PlayerTarget *>(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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <QApplication>
|
||||
#include <QCursor>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
|
@ -500,6 +504,71 @@ void CardItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
|||
event->accept();
|
||||
}
|
||||
|
||||
void CardItem::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
|
||||
auto *gameScene = static_cast<GameScene *>(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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<CardItem *> 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);
|
||||
|
|
|
|||
256
cockatrice/src/game/keyboard_card_navigator.cpp
Normal file
256
cockatrice/src/game/keyboard_card_navigator.cpp
Normal file
|
|
@ -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 <QApplication>
|
||||
#include <QKeyEvent>
|
||||
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<CardZoneLogic *> &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<CardZoneLogic *> 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);
|
||||
}
|
||||
}
|
||||
61
cockatrice/src/game/keyboard_card_navigator.h
Normal file
61
cockatrice/src/game/keyboard_card_navigator.h
Normal file
|
|
@ -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<CardZoneLogic *> &zonesList, int currentZoneIndex, bool upperZone);
|
||||
bool isArrowModeActiveVar() const
|
||||
{
|
||||
return isArrowModeActive;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ public:
|
|||
|
||||
void setZoneId(int _zoneId);
|
||||
|
||||
void hoverFirstCardInHand();
|
||||
|
||||
private:
|
||||
AbstractGame *game;
|
||||
PlayerInfo *playerInfo;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "key_signals.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<int, PlayerLogic *> 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;
|
||||
|
|
|
|||
|
|
@ -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<int, TabbedDeckViewContainer *> deckViewContainers;
|
||||
QVBoxLayout *deckViewContainerLayout;
|
||||
QWidget *gamePlayAreaWidget, *deckViewContainerWidget;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
32
tests/keyboard_navigator_tests/CMakeLists.txt
Normal file
32
tests/keyboard_navigator_tests/CMakeLists.txt
Normal file
|
|
@ -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()
|
||||
229
tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp
Normal file
229
tests/keyboard_navigator_tests/keyboard_card_navigator_test.cpp
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
#include "game/keyboard_card_navigator.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QMetaObject>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// 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<QString, CardZoneLogic *>();
|
||||
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<CardZoneLogic *> 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<CardZoneLogic *> 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();
|
||||
}
|
||||
|
|
@ -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<void *>(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
|
||||
106
tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp
Normal file
106
tests/keyboard_navigator_tests/keyboard_navigator_test_stubs.cpp
Normal file
|
|
@ -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 <QApplication>
|
||||
#include <QColor>
|
||||
#include <QMetaObject>
|
||||
#include <QString>
|
||||
|
||||
// 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<CardItem *>()
|
||||
{
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue