diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index f34961b0b..93195770d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -86,6 +86,7 @@ set(cockatrice_SOURCES src/game_graphics/deckview/tabbed_deck_view_container.cpp src/game_graphics/dialogs/dlg_annotation.cpp src/game_graphics/dialogs/dlg_create_token.cpp + src/game_graphics/dialogs/token_preview_widget.cpp src/game_graphics/dialogs/dlg_move_top_cards_until.cpp src/game_graphics/dialogs/dlg_roll_dice.cpp src/game_graphics/game_scene.cpp diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index 5ce050380..e9ed36d19 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -1,9 +1,9 @@ #include "dlg_create_token.h" #include "../../client/settings/cache_settings.h" -#include "../../interface/widgets/cards/card_info_picture_widget.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database_querier.h" +#include "token_preview_widget.h" #include #include @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -41,7 +42,7 @@ class QWidget; DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent) : QDialog(parent), predefinedTokens(_predefinedTokens) { - pic = new CardInfoPictureWidget(); + pic = new TokenPreviewWidget(); pic->setObjectName("pic"); nameLabel = new QLabel(tr("&Name:")); @@ -173,6 +174,12 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa setWindowTitle(tr("Create token")); + connect(nameEdit, &QLineEdit::textChanged, this, &DlgCreateToken::updatePreview); + connect(colorEdit, qOverload(&QComboBox::currentIndexChanged), this, &DlgCreateToken::updatePreview); + connect(ptEdit, &QLineEdit::textChanged, this, &DlgCreateToken::updatePreview); + connect(annotationEdit, &QLineEdit::textChanged, this, &DlgCreateToken::updatePreview); + updatePreview(); + resize(600, 500); restoreGeometry(SettingsCache::instance().layouts().getTokenDialogGeometry()); } @@ -185,17 +192,14 @@ void DlgCreateToken::closeEvent(QCloseEvent *event) void DlgCreateToken::faceDownCheckBoxToggled(bool checked) { + // Keep the face-up draft visible while choosing what to create. Only the + // outgoing command applies face-down color/P/T restrictions. + colorEdit->setEnabled(!checked); + ptEdit->setEnabled(!checked); if (checked) { - colorEdit->setCurrentIndex(6); - colorEdit->setEnabled(false); - ptEdit->clear(); ptEdit->clearFocus(); - ptEdit->setEnabled(false); - } else { - colorEdit->setEnabled(true); - ptEdit->setEnabled(true); - annotationEdit->setEnabled(true); } + updatePreview(); } void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QModelIndex & /*previous*/) @@ -204,11 +208,14 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo CardInfoPtr cardInfo; - if (current.row() >= 0) { + if (realIndex.isValid()) { cardInfo = cardDatabaseModel->getCard(realIndex.row()); } if (cardInfo) { + const QSignalBlocker colorBlocker(colorEdit); + const QSignalBlocker ptBlocker(ptEdit); + const QSignalBlocker annotationBlocker(annotationEdit); updateSearchFieldWithoutUpdatingFilter(cardInfo->getName()); const QChar cardColor = cardInfo->getColorChar(); colorEdit->setCurrentIndex(colorEdit->findData(cardColor, Qt::UserRole, Qt::MatchFixedString)); @@ -216,21 +223,22 @@ void DlgCreateToken::tokenSelectionChanged(const QModelIndex ¤t, const QMo if (SettingsCache::instance().userInterface().getAnnotateTokens()) { annotationEdit->setText(cardInfo->getText()); } - } else { - nameEdit->setText(""); - colorEdit->setCurrentIndex(colorEdit->findData(QString(), Qt::UserRole, Qt::MatchFixedString)); - ptEdit->setText(""); - annotationEdit->setText(""); } - pic->setCard(CardDatabaseManager::query()->getPreferredCard(cardInfo)); + // Filtering, changing the deck/all scope, or reloading the database can + // invalidate the current row. None of these actions discard the token draft. + updatePreview(); } void DlgCreateToken::updateSearchFieldWithoutUpdatingFilter(const QString &newValue) const { - nameEdit->blockSignals(true); + const QSignalBlocker blocker(nameEdit); nameEdit->setText(newValue); - nameEdit->blockSignals(false); +} + +void DlgCreateToken::updatePreview() +{ + pic->setToken(tokenDraft()); } void DlgCreateToken::updateSearch(const QString &search) @@ -264,13 +272,35 @@ void DlgCreateToken::actReject() reject(); } +TokenInfo DlgCreateToken::tokenDraft() const +{ + TokenInfo token = {.name = nameEdit->text(), + .color = colorEdit->itemData(colorEdit->currentIndex()).toString(), + .pt = ptEdit->text(), + .annotation = annotationEdit->text(), + .destroy = destroyCheckBox->isChecked(), + .faceDown = faceDownCheckBox->isChecked(), + .providerId = + SettingsCache::instance().cardOverrides().getCardPreferenceOverride(nameEdit->text())}; + + // Match the normalization applied when the player creates the token, so + // manually typed aliases and the default P/T preview the actual result. + const ExactCard correctedCard = CardDatabaseManager::query()->guessCard({token.name, token.providerId}); + if (correctedCard) { + token.name = correctedCard.getName(); + if (token.pt.isEmpty()) { + token.pt = correctedCard.getInfo().getPowTough(); + } + } + return token; +} + TokenInfo DlgCreateToken::getTokenInfo() const { - return {.name = nameEdit->text(), - .color = colorEdit->itemData(colorEdit->currentIndex()).toString(), - .pt = ptEdit->text(), - .annotation = annotationEdit->text(), - .destroy = destroyCheckBox->isChecked(), - .faceDown = faceDownCheckBox->isChecked(), - .providerId = SettingsCache::instance().cardOverrides().getCardPreferenceOverride(nameEdit->text())}; + TokenInfo token = tokenDraft(); + if (token.faceDown) { + token.color = QString(QChar()); + token.pt.clear(); + } + return token; } diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.h b/cockatrice/src/game_graphics/dialogs/dlg_create_token.h index 323e61829..c39928164 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.h +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.h @@ -22,7 +22,7 @@ class QRadioButton; class QTreeView; class CardDatabaseModel; class TokenDisplayModel; -class CardInfoPictureWidget; +class TokenPreviewWidget; class QModelIndex; class QWidget; @@ -39,6 +39,7 @@ private slots: void faceDownCheckBoxToggled(bool checked); void tokenSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); void updateSearch(const QString &search); + void updatePreview(); void actChooseTokenFromAll(bool checked); void actChooseTokenFromDeck(bool checked); void actOk(); @@ -54,9 +55,10 @@ private: QCheckBox *destroyCheckBox; QCheckBox *faceDownCheckBox; QRadioButton *chooseTokenFromAllRadioButton, *chooseTokenFromDeckRadioButton; - CardInfoPictureWidget *pic; + TokenPreviewWidget *pic; QTreeView *chooseTokenView; + [[nodiscard]] TokenInfo tokenDraft() const; void updateSearchFieldWithoutUpdatingFilter(const QString &newValue) const; }; diff --git a/cockatrice/src/game_graphics/dialogs/token_preview_widget.cpp b/cockatrice/src/game_graphics/dialogs/token_preview_widget.cpp new file mode 100644 index 000000000..3e9a266a0 --- /dev/null +++ b/cockatrice/src/game_graphics/dialogs/token_preview_widget.cpp @@ -0,0 +1,85 @@ +#include "token_preview_widget.h" + +#include "../board/abstract_card_item.h" + +#include +#include +#include +#include + +class TokenPreviewCardItem : public AbstractCardItem +{ +public: + TokenPreviewCardItem() + { + setFlag(ItemIsSelectable, false); + setAcceptedMouseButtons(Qt::NoButton); + setAcceptHoverEvents(false); + setCursor(Qt::ArrowCursor); + connect(state, &CardState::stateChanged, this, [this] { update(); }); + } + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override + { + // Show what the user is creating even when it will enter face down. + // Reuse the board's artwork, custom-token background and name rendering. + AbstractCardItem::paint(painter, option, widget); + + const QSizeF translatedSize = getTranslatedSize(painter); + const qreal scale = translatedSize.width() / boundingRect().width(); + painter->save(); + transformPainter(painter, translatedSize, 0); + painter->setPen(Qt::white); + painter->setBackground(Qt::black); + painter->setBackgroundMode(Qt::OpaqueMode); + const QRectF textRect(4 * scale, 4 * scale, translatedSize.width() - 8 * scale, + translatedSize.height() - 8 * scale); + painter->drawText(textRect, Qt::AlignCenter | Qt::TextWrapAnywhere, state->getAnnotation()); + if (state->getPT() != getCardInfo().getPowTough()) { + painter->setPen(QColor(255, 150, 0)); + } + painter->drawText(textRect, Qt::AlignRight | Qt::AlignBottom, state->getPT()); + painter->restore(); + } +}; + +TokenPreviewWidget::TokenPreviewWidget(QWidget *parent) : QGraphicsView(parent), card(new TokenPreviewCardItem) +{ + auto *previewScene = new QGraphicsScene(this); + previewScene->addItem(card); + previewScene->setSceneRect(card->boundingRect()); + setScene(previewScene); + setInteractive(false); + setFocusPolicy(Qt::NoFocus); + setFrameShape(QFrame::NoFrame); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setMinimumSize(150, 200); + setAccessibleName(tr("Token preview")); +} + +void TokenPreviewWidget::setToken(const TokenInfo &token) +{ + card->setCardRef({token.name, token.providerId}); + card->setColor(token.color); + card->setFaceDown(false); + card->getState()->setPT(token.pt); + card->getState()->setAnnotation(token.annotation); + setToolTip(token.annotation); + QString description = token.name + " " + token.pt + " " + token.annotation; + if (token.faceDown) { + description += " " + tr("Will be created face down."); + } + setAccessibleDescription(description); +} + +QSize TokenPreviewWidget::sizeHint() const +{ + return {200, 280}; +} + +void TokenPreviewWidget::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + fitInView(sceneRect(), Qt::KeepAspectRatio); +} diff --git a/cockatrice/src/game_graphics/dialogs/token_preview_widget.h b/cockatrice/src/game_graphics/dialogs/token_preview_widget.h new file mode 100644 index 000000000..c3ea94777 --- /dev/null +++ b/cockatrice/src/game_graphics/dialogs/token_preview_widget.h @@ -0,0 +1,25 @@ +#ifndef COCKATRICE_TOKEN_PREVIEW_WIDGET_H +#define COCKATRICE_TOKEN_PREVIEW_WIDGET_H + +#include +#include + +class TokenPreviewCardItem; + +/** A noninteractive preview of the token that the dialog will create. */ +class TokenPreviewWidget : public QGraphicsView +{ + Q_OBJECT +public: + explicit TokenPreviewWidget(QWidget *parent = nullptr); + void setToken(const TokenInfo &token); + QSize sizeHint() const override; + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + TokenPreviewCardItem *card; +}; + +#endif diff --git a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp b/libcockatrice_game/libcockatrice/game/player/player_actions.cpp index 90823fec6..3c8765323 100644 --- a/libcockatrice_game/libcockatrice/game/player/player_actions.cpp +++ b/libcockatrice_game/libcockatrice/game/player/player_actions.cpp @@ -951,7 +951,7 @@ void PlayerActions::actCreateToken(TokenInfo tokenToCreate) lastTokenInfo.name = correctedCard.getName(); int tableRow = lastTokenInfo.faceDown ? 2 : correctedCard.getInfo().getUiAttributes().tableRow; lastTokenTableRow = TableZoneLogic::tableRowToGridY(tableRow); - if (lastTokenInfo.pt.isEmpty()) { + if (!lastTokenInfo.faceDown && lastTokenInfo.pt.isEmpty()) { lastTokenInfo.pt = correctedCard.getInfo().getPowTough(); } } diff --git a/tests/game/player_actions_test.cpp b/tests/game/player_actions_test.cpp index 11ae7338d..e483eb4e6 100644 --- a/tests/game/player_actions_test.cpp +++ b/tests/game/player_actions_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -117,3 +118,64 @@ TEST(PlayerActionsTest, PlayCiptCreatureSendsTapped) EXPECT_TRUE(card.has_tapped()); EXPECT_TRUE(card.tapped()); } + +// These three #7737 command cases use the current shared game harness. +TEST(TokenCommandTest, FaceDownKnownTokenDoesNotRestoreDatabasePowerToughness) +{ + GameHarness harness; + harness.client()->sent.clear(); + TokenInfo token; + token.name = QStringLiteral("Kitten"); + token.color = QString(QChar()); + token.faceDown = true; + harness.player()->getPlayerActions()->actCreateToken(token); + ASSERT_EQ(harness.client()->sent.size(), 1); + ASSERT_EQ(harness.lastCommand().game_command_size(), 1); + const auto &gameCommand = harness.lastCommand().game_command(0); + ASSERT_TRUE(gameCommand.HasExtension(Command_CreateToken::ext)); + const auto &command = gameCommand.GetExtension(Command_CreateToken::ext); + EXPECT_EQ(command.card_name(), "Kitten"); + EXPECT_TRUE(command.face_down()); + EXPECT_TRUE(command.pt().empty()); + harness.player()->getPlayerActions()->actCreateAnotherToken(); + ASSERT_EQ(harness.client()->sent.size(), 2); + const auto &repeat = harness.lastCommand().game_command(0).GetExtension(Command_CreateToken::ext); + EXPECT_TRUE(repeat.face_down()); + EXPECT_TRUE(repeat.pt().empty()); +} + +TEST(TokenCommandTest, FaceUpKnownAliasKeepsCanonicalNameAndDefaultPowerToughness) +{ + GameHarness harness; + harness.client()->sent.clear(); + TokenInfo token; + token.name = QStringLiteral("kitten"); + harness.player()->getPlayerActions()->actCreateToken(token); + ASSERT_EQ(harness.client()->sent.size(), 1); + ASSERT_EQ(harness.lastCommand().game_command_size(), 1); + const auto &gameCommand = harness.lastCommand().game_command(0); + ASSERT_TRUE(gameCommand.HasExtension(Command_CreateToken::ext)); + const auto &command = gameCommand.GetExtension(Command_CreateToken::ext); + EXPECT_EQ(command.card_name(), "Kitten"); + EXPECT_FALSE(command.face_down()); + EXPECT_EQ(command.pt(), "1/1"); +} + +TEST(TokenCommandTest, FaceUpKnownTokenPreservesExplicitPowerToughness) +{ + GameHarness harness; + harness.client()->sent.clear(); + TokenInfo token; + token.name = QStringLiteral("Kitten"); + token.pt = QStringLiteral("7/9"); + token.annotation = QStringLiteral("Flying"); + harness.player()->getPlayerActions()->actCreateToken(token); + ASSERT_EQ(harness.client()->sent.size(), 1); + ASSERT_EQ(harness.lastCommand().game_command_size(), 1); + const auto &gameCommand = harness.lastCommand().game_command(0); + ASSERT_TRUE(gameCommand.HasExtension(Command_CreateToken::ext)); + const auto &command = gameCommand.GetExtension(Command_CreateToken::ext); + EXPECT_FALSE(command.face_down()); + EXPECT_EQ(command.pt(), "7/9"); + EXPECT_EQ(command.annotation(), "Flying"); +}