From c5372a9e92ca82fc9a32cc4a961c50af80944346 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 31 May 2026 03:14:21 -0700 Subject: [PATCH 01/50] [DeckEditor] Refactor: clean up addCardHelper (#6939) * [DeckEditor] Refactor: clean up addCardHelper * remove setSaveStatus --- .../widgets/tabs/abstract_tab_deck_editor.cpp | 23 ++++++++----------- .../widgets/tabs/abstract_tab_deck_editor.h | 21 ++++++++++++++--- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 66609456e..9a4588a25 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -129,16 +129,16 @@ void AbstractTabDeckEditor::onDeckModified() emit tabTextChanged(this, getTabText()); } -/** - * @brief Helper for adding a card to a deck zone. - * @param card Card to add. - * @param zoneName Zone to add the card to. - */ -void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName) +void AbstractTabDeckEditor::addCard(const ExactCard &card, const QString &zoneName) { deckStateManager->addCard(card, zoneName); } +void AbstractTabDeckEditor::decrementCard(const ExactCard &card, const QString &zoneName) +{ + deckStateManager->decrementCard(card, zoneName); +} + /** * @brief Adds a card to the main deck or sideboard depending on Ctrl key. */ @@ -147,29 +147,26 @@ void AbstractTabDeckEditor::actAddCard(const ExactCard &card) if (QApplication::keyboardModifiers() & Qt::ControlModifier) { actAddCardToSideboard(card); } else { - addCardHelper(card, DECK_ZONE_MAIN); + addCard(card, DECK_ZONE_MAIN); } - - deckMenu->setSaveStatus(true); } /** @brief Adds a card to the sideboard explicitly. */ void AbstractTabDeckEditor::actAddCardToSideboard(const ExactCard &card) { - addCardHelper(card, DECK_ZONE_SIDE); - deckMenu->setSaveStatus(true); + addCard(card, DECK_ZONE_SIDE); } /** @brief Decrements a card from the main deck. */ void AbstractTabDeckEditor::actDecrementCard(const ExactCard &card) { - deckStateManager->decrementCard(card, DECK_ZONE_MAIN); + decrementCard(card, DECK_ZONE_MAIN); } /** @brief Decrements a card from the sideboard. */ void AbstractTabDeckEditor::actDecrementCardFromSideboard(const ExactCard &card) { - deckStateManager->decrementCard(card, DECK_ZONE_SIDE); + decrementCard(card, DECK_ZONE_SIDE); } /** diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index 477c3f973..61b70ed8e 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -145,6 +145,24 @@ public slots: */ void updateCard(const ExactCard &card); + /** + * @brief Adds a card to the given zone + * @param card Card to add. + * @param zoneName Zone to add the card to. + */ + void addCard(const ExactCard &card, const QString &zoneName); + + /** + * @brief Decrements a card from the given zone + * + * Use an ExactCard with empty PrintingInfo if you want to remove a card by name regardless of printing. + * Otherwise, it won't remove anything unless there's an exact printing match. + * + * @param card Card to decrement. + * @param zoneName Zone to decrement from. + */ + void decrementCard(const ExactCard &card, const QString &zoneName); + /** @brief Adds a card to the main deck or sideboard based on Ctrl key. */ void actAddCard(const ExactCard &card); @@ -293,9 +311,6 @@ protected: */ QMessageBox *createSaveConfirmationWindow(); - /** @brief Helper function to add a card to a specific deck zone. */ - void addCardHelper(const ExactCard &card, const QString &zoneName); - /** @brief Opens a deck from a file. */ virtual void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation); From 3fa377a11cd511c88a52bf7ca902e83266eb0273 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 31 May 2026 03:44:40 -0700 Subject: [PATCH 02/50] [TabDeckEditor] Refactor check ctrl to be on click (#6956) --- .../deck_editor_database_display_widget.cpp | 11 ++++++++++- .../deck_editor/deck_editor_database_display_widget.h | 1 + .../widgets/tabs/abstract_tab_deck_editor.cpp | 6 +----- .../visual_deck_editor/tab_deck_editor_visual.cpp | 6 +++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index 580db67f4..e09a4311f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -79,7 +79,7 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent &DeckEditorDatabaseDisplayWidget::databaseCustomMenu); connect(databaseView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DeckEditorDatabaseDisplayWidget::updateCard); - connect(databaseView, &QTreeView::doubleClicked, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); + connect(databaseView, &QTreeView::doubleClicked, this, &DeckEditorDatabaseDisplayWidget::actAddCard); QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState(); if (dbHeaderState.isNull()) { @@ -146,6 +146,15 @@ void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex ¤t, con } } +void DeckEditorDatabaseDisplayWidget::actAddCard() +{ + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + actAddCardToSideboard(); + } else { + actAddCardToMainDeck(); + } +} + void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck() { highlightAllSearchEdit(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h index 0f62998ef..c5b1d2f2f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h @@ -39,6 +39,7 @@ public slots: void clearAllDatabaseFilters(); void updateSearch(const QString &search); void updateCard(const QModelIndex ¤t, const QModelIndex &); + void actAddCard(); void actAddCardToMainDeck(); void actAddCardToSideboard(); void actDecrementCardFromMainDeck(); diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 9a4588a25..e9fc08c76 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -144,11 +144,7 @@ void AbstractTabDeckEditor::decrementCard(const ExactCard &card, const QString & */ void AbstractTabDeckEditor::actAddCard(const ExactCard &card) { - if (QApplication::keyboardModifiers() & Qt::ControlModifier) { - actAddCardToSideboard(card); - } else { - addCard(card, DECK_ZONE_MAIN); - } + addCard(card, DECK_ZONE_MAIN); } /** @brief Adds a card to the sideboard explicitly. */ diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 3cdad91fc..30bf6bfaa 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -223,7 +223,11 @@ void TabDeckEditorVisual::processCardClickDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) { if (event->button() == Qt::LeftButton) { - actAddCard(instance->getCard()); + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + actAddCardToSideboard(instance->getCard()); + } else { + actAddCard(instance->getCard()); + } } else if (event->button() == Qt::RightButton) { actDecrementCard(instance->getCard()); } else if (event->button() == Qt::MiddleButton) { From f52dc6dda8d0a7bd405aa946cd015d4a8c60996d Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:13:39 -0700 Subject: [PATCH 03/50] [TabDeckEditor] Refactor: consolidate add/decrement card signals (#6961) --- .../cards/card_info_picture_widget.cpp | 4 +-- .../deck_editor_card_database_dock_widget.cpp | 12 +++------ .../deck_editor_database_display_widget.cpp | 8 +++--- .../deck_editor_database_display_widget.h | 6 ++--- .../widgets/tabs/abstract_tab_deck_editor.cpp | 26 ------------------- .../widgets/tabs/abstract_tab_deck_editor.h | 16 ++---------- .../tab_deck_editor_visual.cpp | 18 ++++--------- .../tab_deck_editor_visual_tab_widget.cpp | 16 ++++++++++-- .../tab_deck_editor_visual_tab_widget.h | 6 +++++ .../visual_database_display_widget.cpp | 4 +-- 10 files changed, 40 insertions(+), 76 deletions(-) diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 555d69381..1dd65684f 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -431,13 +431,13 @@ QMenu *CardInfoPictureWidget::createAddToOpenDeckMenu() QAction *addCard = addCardMenu->addAction(tr("Mainboard")); connect(addCard, &QAction::triggered, this, [this, deckEditorTab] { deckEditorTab->updateCard(exactCard); - deckEditorTab->actAddCard(exactCard); + deckEditorTab->addCard(exactCard, DECK_ZONE_MAIN); }); QAction *addCardSideboard = addCardMenu->addAction(tr("Sideboard")); connect(addCardSideboard, &QAction::triggered, this, [this, deckEditorTab] { deckEditorTab->updateCard(exactCard); - deckEditorTab->actAddCardToSideboard(exactCard); + deckEditorTab->addCard(exactCard, DECK_ZONE_SIDE); }); } diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index f2a2ab4ea..546161506 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -29,14 +29,10 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck // connect signals connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, deckEditor, &AbstractTabDeckEditor::updateCard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, deckEditor, - &AbstractTabDeckEditor::actAddCard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, deckEditor, - &AbstractTabDeckEditor::actAddCardToSideboard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromMainDeck, deckEditor, - &AbstractTabDeckEditor::actDecrementCard); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromSideboard, deckEditor, - &AbstractTabDeckEditor::actDecrementCardFromSideboard); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardAdded, deckEditor, + &AbstractTabDeckEditor::addCard); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardDecremented, deckEditor, + &AbstractTabDeckEditor::decrementCard); } CardDatabase *DeckEditorCardDatabaseDockWidget::getDatabase() const diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index e09a4311f..3f397d2a0 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -158,23 +158,23 @@ void DeckEditorDatabaseDisplayWidget::actAddCard() void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck() { highlightAllSearchEdit(); - emit addCardToMainDeck(currentCard()); + emit cardAdded(currentCard(), DECK_ZONE_MAIN); } void DeckEditorDatabaseDisplayWidget::actAddCardToSideboard() { highlightAllSearchEdit(); - emit addCardToSideboard(currentCard()); + emit cardAdded(currentCard(), DECK_ZONE_SIDE); } void DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck() { - emit decrementCardFromMainDeck(currentCard()); + emit cardDecremented(currentCard(), DECK_ZONE_MAIN); } void DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard() { - emit decrementCardFromSideboard(currentCard()); + emit cardDecremented(currentCard(), DECK_ZONE_SIDE); } ExactCard DeckEditorDatabaseDisplayWidget::currentCard() const diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h index c5b1d2f2f..a0062a9be 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h @@ -48,10 +48,8 @@ public slots: void copyDatabaseCellContents(); signals: - void addCardToMainDeck(const ExactCard &card); - void addCardToSideboard(const ExactCard &card); - void decrementCardFromMainDeck(const ExactCard &card); - void decrementCardFromSideboard(const ExactCard &card); + void cardAdded(const ExactCard &card, const QString &zoneName); + void cardDecremented(const ExactCard &card, const QString &zoneName); void cardChanged(const ExactCard &_card); private: diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index e9fc08c76..afa2d4f41 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -139,32 +139,6 @@ void AbstractTabDeckEditor::decrementCard(const ExactCard &card, const QString & deckStateManager->decrementCard(card, zoneName); } -/** - * @brief Adds a card to the main deck or sideboard depending on Ctrl key. - */ -void AbstractTabDeckEditor::actAddCard(const ExactCard &card) -{ - addCard(card, DECK_ZONE_MAIN); -} - -/** @brief Adds a card to the sideboard explicitly. */ -void AbstractTabDeckEditor::actAddCardToSideboard(const ExactCard &card) -{ - addCard(card, DECK_ZONE_SIDE); -} - -/** @brief Decrements a card from the main deck. */ -void AbstractTabDeckEditor::actDecrementCard(const ExactCard &card) -{ - decrementCard(card, DECK_ZONE_MAIN); -} - -/** @brief Decrements a card from the sideboard. */ -void AbstractTabDeckEditor::actDecrementCardFromSideboard(const ExactCard &card) -{ - decrementCard(card, DECK_ZONE_SIDE); -} - /** * @brief Opens a deck in this tab. * @param deck The deck diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index 61b70ed8e..398d4b297 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -77,8 +77,8 @@ class QAction; * * **Key Methods:** * - * - actAddCard(const ExactCard &card) — Adds a card to the deck. - * - actDecrementCard(const ExactCard &card) — Removes a single instance of a card from the deck. + * - addCard(const ExactCard &card, const QString &zoneName) — Adds a card to the deck. + * - decrementCard(const ExactCard &card, const QString &zoneName) — Removes a single instance of a card from the deck. * - actRemoveCard() — Removes the currently selected card from the deck. * - actSaveDeckAs() — Performs a "Save As" action for the deck. * - updateCard(const ExactCard &card) — Updates the currently displayed card info in the dock. @@ -163,18 +163,6 @@ public slots: */ void decrementCard(const ExactCard &card, const QString &zoneName); - /** @brief Adds a card to the main deck or sideboard based on Ctrl key. */ - void actAddCard(const ExactCard &card); - - /** @brief Adds a card to the sideboard explicitly. */ - void actAddCardToSideboard(const ExactCard &card); - - /** @brief Decrements a card from the main deck. */ - void actDecrementCard(const ExactCard &card); - - /** @brief Decrements a card from the sideboard. */ - void actDecrementCardFromSideboard(const ExactCard &card); - /** @brief Opens a recently opened deck file. */ void actOpenRecent(const QString &fileName); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 30bf6bfaa..60bf75fac 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -168,22 +168,14 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, // Alt + Right-click = decrement if (event->button() == Qt::RightButton && event->modifiers().testFlag(Qt::AltModifier)) { - if (zoneName == DECK_ZONE_MAIN) { - actDecrementCard(card); - } else { - actDecrementCardFromSideboard(card); - } + decrementCard(card, zoneName); // Keep selection intact. return; } // Alt + Left click = increment if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::AltModifier)) { - if (zoneName == DECK_ZONE_MAIN) { - actAddCard(card); - } else { - actAddCardToSideboard(card); - } + addCard(card, zoneName); // Keep selection intact. return; } @@ -224,12 +216,12 @@ void TabDeckEditorVisual::processCardClickDatabaseDisplay(QMouseEvent *event, { if (event->button() == Qt::LeftButton) { if (QApplication::keyboardModifiers() & Qt::ControlModifier) { - actAddCardToSideboard(instance->getCard()); + addCard(instance->getCard(), DECK_ZONE_SIDE); } else { - actAddCard(instance->getCard()); + addCard(instance->getCard(), DECK_ZONE_MAIN); } } else if (event->button() == Qt::RightButton) { - actDecrementCard(instance->getCard()); + decrementCard(instance->getCard(), DECK_ZONE_MAIN); } else if (event->button() == Qt::MiddleButton) { deckDockWidget->actRemoveCard(); } diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index 8a4d5903d..f3ef46e22 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -34,8 +34,8 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, &TabDeckEditorVisualTabWidget::onCardChanged); connect(visualDeckView, &VisualDeckEditorWidget::cardClicked, this, &TabDeckEditorVisualTabWidget::onCardClickedDeckEditor); - connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, deckEditor, - &AbstractTabDeckEditor::actAddCard); + connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, this, + &TabDeckEditorVisualTabWidget::actAddCard); visualDatabaseDisplay = new VisualDatabaseDisplayWidget(this, deckEditor, _cardDatabaseModel, _cardDatabaseDisplayModel); @@ -166,3 +166,15 @@ void TabDeckEditorVisualTabWidget::handleTabClose(int index) this->removeTab(index); delete tab; } + +void TabDeckEditorVisualTabWidget::actAddCard(const ExactCard &card) +{ + QString zoneName; + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + zoneName = DECK_ZONE_SIDE; + } else { + zoneName = DECK_ZONE_MAIN; + } + + deckEditor->addCard(card, zoneName); +} diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h index 48dd8ea9d..a825068df 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -132,6 +132,12 @@ private slots: * @param index Index of the tab to close. */ void handleTabClose(int index); + + /** + * @brief Adds card to maindeck or side depending on whether ctrl is held + * @param card + */ + void actAddCard(const ExactCard &card); }; #endif // TAB_DECK_EDITOR_VISUAL_TAB_WIDGET_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 70218d478..83c66ae53 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -97,9 +97,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this, - &VisualDatabaseDisplayWidget::highlightAllSearchEdit); - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this, + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardAdded, this, &VisualDatabaseDisplayWidget::highlightAllSearchEdit); databaseView = databaseDisplayWidget->getDatabaseView(); From e0cbb7f06c11fef3a49a7a1cdd7ec1818ee68d98 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:22:06 -0700 Subject: [PATCH 04/50] [TabDeckEditor] Refactor: pass ExactCard in signal instead of widget (#6962) * [TabDeckEditor] Refactor: pass ExactCard in signal instead of widget * address comments --- .../card_group_display_widget.cpp | 14 ++------------ .../card_group_display_widget.h | 4 +--- .../widgets/cards/card_info_picture_widget.cpp | 2 +- .../widgets/cards/card_info_picture_widget.h | 2 +- ...rd_info_picture_with_text_overlay_widget.cpp | 2 +- ...card_info_picture_with_text_overlay_widget.h | 2 -- .../cards/deck_card_zone_display_widget.cpp | 2 +- .../cards/deck_card_zone_display_widget.h | 4 ++-- .../tab_deck_editor_visual.cpp | 17 +++++++---------- .../visual_deck_editor/tab_deck_editor_visual.h | 12 +++++------- .../tab_deck_editor_visual_tab_widget.cpp | 15 +++++++-------- .../tab_deck_editor_visual_tab_widget.h | 12 ++++++------ .../visual_database_display_widget.cpp | 6 +++--- .../visual_database_display_widget.h | 4 ++-- .../visual_deck_editor_widget.cpp | 9 +-------- .../visual_deck_editor_widget.h | 3 +-- 16 files changed, 41 insertions(+), 69 deletions(-) diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp index 5fb0cb343..3f36e559c 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp @@ -58,16 +58,6 @@ void CardGroupDisplayWidget::mousePressEvent(QMouseEvent *event) } } -void CardGroupDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) -{ - emit cardClicked(event, card); -} - -void CardGroupDisplayWidget::onHover(const ExactCard &card) -{ - emit cardHovered(card); -} - void CardGroupDisplayWidget::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { auto proxyModel = qobject_cast(selectionModel->model()); @@ -154,8 +144,8 @@ QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex i widget->setScaleFactor(cardSizeWidget->getSlider()->value()); widget->setCard(CardDatabaseManager::query()->getCard({cardName, cardProviderId})); - connect(widget, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &CardGroupDisplayWidget::onClick); - connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::onHover); + connect(widget, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &CardGroupDisplayWidget::cardClicked); + connect(widget, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &CardGroupDisplayWidget::cardHovered); connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, widget, &CardInfoPictureWidget::setScaleFactor); indexToWidgetMap[index].append(widget); diff --git a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h index 848bebb7e..2308ccf8d 100644 --- a/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.h @@ -48,8 +48,6 @@ public: public slots: void mousePressEvent(QMouseEvent *event) override; - void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); - void onHover(const ExactCard &card); virtual QWidget *constructWidgetForIndex(QPersistentModelIndex index); virtual void updateCardDisplays(); virtual void onCardAddition(const QModelIndex &parent, int first, int last); @@ -59,7 +57,7 @@ public slots: void resizeEvent(QResizeEvent *event) override; signals: - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void cardClicked(QMouseEvent *event, const ExactCard &card); void cardHovered(const ExactCard &card); void cleanupRequested(CardGroupDisplayWidget *cardGroupDisplayWidget); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 1dd65684f..66ec1c197 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -345,7 +345,7 @@ void CardInfoPictureWidget::mousePressEvent(QMouseEvent *event) createRightClickMenu()->popup(QCursor::pos()); } - emit cardClicked(event); + emit cardClicked(event, exactCard); } void CardInfoPictureWidget::hideEvent(QHideEvent *event) diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h index bfa6584b1..1f065eed9 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h @@ -43,7 +43,7 @@ signals: void hoveredOnCard(const ExactCard &hoveredCard); void cardScaleFactorChanged(int _scale); void cardChanged(const ExactCard &card); - void cardClicked(QMouseEvent *event); + void cardClicked(QMouseEvent *event, const ExactCard &card); protected: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 2f0aeccfd..c5cb59b3b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -93,7 +93,7 @@ void CardInfoPictureWithTextOverlayWidget::setHighlighted(bool _highlighted) void CardInfoPictureWithTextOverlayWidget::mousePressEvent(QMouseEvent *event) { - emit imageClicked(event, this); + emit cardClicked(event, getCard()); } /** diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h index 0cc7e501c..ba978498d 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h @@ -35,8 +35,6 @@ public: void setHighlighted(bool _highlighted); [[nodiscard]] QSize sizeHint() const override; -signals: - void imageClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); protected: void paintEvent(QPaintEvent *event) override; diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp index a8a97a4ca..eaf3a67b0 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.cpp @@ -51,7 +51,7 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent, // User Interaction // ===================================================================================================================== -void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card) +void DeckCardZoneDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) { emit cardClicked(event, card, zoneName); } diff --git a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h index 074a77e53..b426fca30 100644 --- a/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h +++ b/cockatrice/src/interface/widgets/cards/deck_card_zone_display_widget.h @@ -42,7 +42,7 @@ public: void addCardsToOverlapWidget(); public slots: - void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); + void onClick(QMouseEvent *event, const ExactCard &card); void onHover(const ExactCard &card); void cleanupInvalidCardGroup(CardGroupDisplayWidget *displayWidget); void constructAppropriateWidget(QPersistentModelIndex index); @@ -55,7 +55,7 @@ public slots: void onCategoryRemoval(const QModelIndex &parent, int first, int last); signals: - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card, QString zoneName); + void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName); void cardHovered(const ExactCard &card); void activeSortCriteriaChanged(QStringList activeSortCriteria); void requestCleanup(DeckCardZoneDisplayWidget *displayWidget); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 60bf75fac..58246180d 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -74,7 +74,7 @@ void TabDeckEditorVisual::createCentralFrame() connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClicked, this, &TabDeckEditorVisual::processMainboardCardClick); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClickedDatabaseDisplay, this, - &TabDeckEditorVisual::processCardClickDatabaseDisplay); + &TabDeckEditorVisual::processDatabaseCardClick); centralFrame->addWidget(tabContainer); setCentralWidget(centralWidget); @@ -143,12 +143,10 @@ void TabDeckEditorVisual::changeModelIndexToCard(const ExactCard &activeCard) } } -void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, +void TabDeckEditorVisual::processMainboardCardClick(const QMouseEvent *event, + const ExactCard &card, const QString &zoneName) { - auto card = instance->getCard(); - // Get the model index for the card QModelIndex idx = deckStateManager->getModel()->findCard(card.getName(), zoneName); if (!idx.isValid()) { @@ -211,17 +209,16 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, } /** @brief Handles clicks on cards in the database display. */ -void TabDeckEditorVisual::processCardClickDatabaseDisplay(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance) +void TabDeckEditorVisual::processDatabaseCardClick(const QMouseEvent *event, const ExactCard &card) { if (event->button() == Qt::LeftButton) { if (QApplication::keyboardModifiers() & Qt::ControlModifier) { - addCard(instance->getCard(), DECK_ZONE_SIDE); + addCard(card, DECK_ZONE_SIDE); } else { - addCard(instance->getCard(), DECK_ZONE_MAIN); + addCard(card, DECK_ZONE_MAIN); } } else if (event->button() == Qt::RightButton) { - decrementCard(instance->getCard(), DECK_ZONE_MAIN); + decrementCard(card, DECK_ZONE_MAIN); } else if (event->button() == Qt::MiddleButton) { deckDockWidget->actRemoveCard(); } diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h index 8a0677c9d..5c09ad5db 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h @@ -41,7 +41,7 @@ * - changeModelIndexAndCardInfo(const ExactCard &card) — Updates deck model selection and card info. * - changeModelIndexToCard(const ExactCard &card) — Selects the card in the deck view. * - processMainboardCardClick(QMouseEvent *event, ...) — Handles clicks on mainboard cards. - * - processCardClickDatabaseDisplay(QMouseEvent *event, ...) — Handles clicks on database cards. + * - processDatabaseCardClick(QMouseEvent *event, ...) — Handles clicks on database cards. * - actSaveDeckAs() — Overrides save action with temporary UI adjustments. * - showPrintingSelector() — Opens the printing selector dock for the current card. * - freeDocksSize() — Frees constraints on dock widget sizes. @@ -152,19 +152,17 @@ public slots: /** * @brief Handle card clicks in the mainboard visual deck. * @param event Mouse event triggering the action. - * @param instance Widget representing the clicked card. + * @param card The clicked card. * @param zoneName Deck zone of the card. */ - void processMainboardCardClick(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, - const QString &zoneName); + void processMainboardCardClick(const QMouseEvent *event, const ExactCard &card, const QString &zoneName); /** * @brief Handle card clicks in the database visual display. * @param event Mouse event triggering the action. - * @param instance Widget representing the clicked card. + * @param card The clicked card. */ - void processCardClickDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void processDatabaseCardClick(const QMouseEvent *event, const ExactCard &card); /** * @brief Save the deck under a new name. diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index f3ef46e22..c5cc2a85f 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -82,25 +82,24 @@ void TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay(const ExactCard /** * @brief Emits the cardClicked signal when a card is clicked in the visual deck view. * @param event The mouse event. - * @param instance The widget instance of the clicked card. + * @param card The clicked card. * @param zoneName The zone of the deck where the card is located. */ void TabDeckEditorVisualTabWidget::onCardClickedDeckEditor(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, - QString zoneName) + const ExactCard &card, + const QString &zoneName) { - emit cardClicked(event, instance, zoneName); + emit cardClicked(event, card, zoneName); } /** * @brief Emits the cardClickedDatabaseDisplay signal when a card is clicked in the database display. * @param event The mouse event. - * @param instance The widget instance of the clicked card. + * @param card The clicked card. */ -void TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance) +void TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card) { - emit cardClickedDatabaseDisplay(event, instance); + emit cardClickedDatabaseDisplay(event, card); } /** diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h index a825068df..7314f23ee 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -101,23 +101,23 @@ public slots: /** * @brief Emitted when a card is clicked in the deck view. * @param event Mouse event. - * @param instance Widget representing the clicked card. + * @param card The clicked card. * @param zoneName Deck zone of the card. */ - void onCardClickedDeckEditor(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void onCardClickedDeckEditor(QMouseEvent *event, const ExactCard &card, const QString &zoneName); /** * @brief Emitted when a card is clicked in the database display. * @param event Mouse event. - * @param instance Widget representing the clicked card. + * @param card The clicked card. */ - void onCardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onCardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); signals: void cardChanged(const ExactCard &activeCard); void cardChangedDatabaseDisplay(const ExactCard &activeCard); - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); - void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName); + void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); private: QVBoxLayout *layout; ///< Layout for tabs and controls. diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 83c66ae53..989ca7330 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -214,9 +214,9 @@ void VisualDatabaseDisplayWidget::onDisplayModeChanged(bool checked) } } -void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, const ExactCard &card) { - emit cardClickedDatabaseDisplay(event, instance); + emit cardClickedDatabaseDisplay(event, card); } void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard) @@ -231,7 +231,7 @@ void VisualDatabaseDisplayWidget::addCard(const ExactCard &cardToAdd) display->setScaleFactor(cardSizeWidget->getSlider()->value()); display->setCard(cardToAdd); flowWidget->addWidget(display); - connect(display, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &VisualDatabaseDisplayWidget::onClick); + connect(display, &CardInfoPictureWithTextOverlayWidget::cardClicked, this, &VisualDatabaseDisplayWidget::onClick); connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover); connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor); } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index 48a026d11..baa9c7c49 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -76,12 +76,12 @@ public slots: void onSearchModelChanged(); signals: - void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); void cardHoveredDatabaseDisplay(const ExactCard &hoveredCard); protected slots: void initialize(); - void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onClick(QMouseEvent *event, const ExactCard &card); void onHover(const ExactCard &hoveredCard); void addCard(const ExactCard &cardToAdd); void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 4a67edcd1..815892f4c 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -281,7 +281,7 @@ void VisualDeckEditorWidget::constructZoneWidgetForIndex(QPersistentModelIndex p displayOptionsWidget->getActiveGroupCriteria(), displayOptionsWidget->getActiveSortCriteria(), displayOptionsWidget->getDisplayType(), 20, 10, cardSizeWidget); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover); - connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick); + connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::cardClicked); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::requestCleanup, this, &VisualDeckEditorWidget::cleanupInvalidZones); connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget, @@ -401,13 +401,6 @@ void VisualDeckEditorWidget::decklistDataChanged(QModelIndex topLeft, QModelInde // User Interaction // ===================================================================================================================== -void VisualDeckEditorWidget::onCardClick(QMouseEvent *event, - CardInfoPictureWithTextOverlayWidget *instance, - QString zoneName) -{ - emit cardClicked(event, instance, zoneName); -} - void VisualDeckEditorWidget::onHover(const ExactCard &hoveredCard) { // If user has any card selected, ignore hover diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h index 1af565b29..da02b5c1f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -69,7 +69,7 @@ signals: void activeCardChanged(const ExactCard &activeCard); void activeGroupCriteriaChanged(QString activeGroupCriteria); void activeSortCriteriaChanged(QStringList activeSortCriteria); - void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName); void cardAdditionRequested(const ExactCard &card); void displayTypeChanged(DisplayType displayType); @@ -82,7 +82,6 @@ protected: protected slots: void onHover(const ExactCard &hoveredCard); - void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); void decklistModelReset(); void resizeEvent(QResizeEvent *event) override; From 46d3b820db602a07abdc05f4a0f6a2b02aa228b3 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:35:48 -0700 Subject: [PATCH 05/50] [TabDeckEditor] Refactor: pull up showPrintingSelector (#6964) * [TabDeckEditor] Refactor: pull up showPrintingSelector * trailing newline --- .../widgets/tabs/abstract_tab_deck_editor.cpp | 7 +++++++ .../interface/widgets/tabs/abstract_tab_deck_editor.h | 4 ++-- .../src/interface/widgets/tabs/tab_deck_editor.cpp | 10 ---------- .../src/interface/widgets/tabs/tab_deck_editor.h | 4 ---- .../tabs/visual_deck_editor/tab_deck_editor_visual.cpp | 8 -------- .../tabs/visual_deck_editor/tab_deck_editor_visual.h | 5 ----- 6 files changed, 9 insertions(+), 29 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index afa2d4f41..2354fe6d9 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -588,3 +588,10 @@ bool AbstractTabDeckEditor::closeRequest() } return close(); } + +void AbstractTabDeckEditor::showPrintingSelector() +{ + printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); + printingSelectorDockWidget->printingSelector->updateDisplay(); + printingSelectorDockWidget->setVisible(true); +} diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index 398d4b297..7bdfd3dcb 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -172,8 +172,8 @@ public slots: /** @brief Requests closing the tab. */ bool closeRequest() override; - /** @brief Shows the printing selector dock. Pure virtual. */ - virtual void showPrintingSelector() = 0; + /** @brief Shows the printing selector dock and updates it with the current card. */ + void showPrintingSelector(); signals: /** @brief Emitted when a deck should be opened in a new editor tab. */ diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 77dfddb4a..4e7cbfecf 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -120,16 +120,6 @@ void TabDeckEditor::refreshShortcuts() aResetLayout->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aResetLayout")); } -/** - * @brief Displays the printing selector dock with the current card. - */ -void TabDeckEditor::showPrintingSelector() -{ - printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); - printingSelectorDockWidget->printingSelector->updateDisplay(); - printingSelectorDockWidget->setVisible(true); -} - /** * @brief Loads deck editor layout from settings or resets to default. */ diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h index ab7a0bfc5..14be59cd7 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.h @@ -83,10 +83,6 @@ public: /** @brief Creates menus for deck editing and view options. */ void createMenus() override; - -public slots: - /** @brief Shows the printing selector dock and updates it with current card. */ - void showPrintingSelector() override; }; #endif diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 58246180d..662f1b76b 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -233,14 +233,6 @@ bool TabDeckEditorVisual::actSaveDeckAs() return result; } -/** @brief Shows the printing selector dock and updates it with the current card. */ -void TabDeckEditorVisual::showPrintingSelector() -{ - printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); - printingSelectorDockWidget->printingSelector->updateDisplay(); - printingSelectorDockWidget->setVisible(true); -} - /** @brief Refreshes keyboard shortcuts for this tab from settings. */ void TabDeckEditorVisual::refreshShortcuts() { diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h index 5c09ad5db..7d7a3f3a2 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.h @@ -144,11 +144,6 @@ public slots: */ void onDeckChanged() override; - /** - * @brief Show the printing selector dock for the currently active card. - */ - void showPrintingSelector() override; - /** * @brief Handle card clicks in the mainboard visual deck. * @param event Mouse event triggering the action. From f37c41886574c9f65f6dc0adeb06ff2ac8c23ab0 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:08:57 -0700 Subject: [PATCH 06/50] [TabDeckEditor] Refactor: Remove cardDatabase field from analysis interfaces (#6963) * [TabDeckEditor] Refactor: Remove cardDatabase field from analysis interfaces * update includes --- .../client/network/interfaces/deck_stats_interface.cpp | 8 ++++---- .../src/client/network/interfaces/deck_stats_interface.h | 5 +---- .../client/network/interfaces/tapped_out_interface.cpp | 8 ++++---- .../src/client/network/interfaces/tapped_out_interface.h | 7 +++---- .../interface/widgets/tabs/abstract_tab_deck_editor.cpp | 4 ++-- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp index 0298daa6b..8689a19e9 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp @@ -6,12 +6,12 @@ #include #include #include +#include #include #include #include -DeckStatsInterface::DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent) - : QObject(parent), cardDatabase(_cardDatabase) +DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent) { manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished); @@ -70,8 +70,8 @@ void DeckStatsInterface::analyzeDeck(const DeckList &deck) void DeckStatsInterface::copyDeckWithoutTokens(const DeckList &source, DeckList &destination) { - auto copyIfNotAToken = [this, &destination](const auto node, const auto card) { - CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName()); + auto copyIfNotAToken = [&destination](const auto node, const auto card) { + CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); if (dbCard && !dbCard->getIsToken()) { DecklistCardNode *addedCard = destination.addCard(card->getName(), node->getName(), -1); addedCard->setNumber(card->getNumber()); diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.h b/cockatrice/src/client/network/interfaces/deck_stats_interface.h index 2ec67a5a7..09bf998de 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.h +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.h @@ -7,7 +7,6 @@ #ifndef DECKSTATS_INTERFACE_H #define DECKSTATS_INTERFACE_H -#include #include class QByteArray; @@ -21,8 +20,6 @@ class DeckStatsInterface : public QObject private: QNetworkAccessManager *manager; - CardDatabase &cardDatabase; - /** * Deckstats doesn't recognize token cards, and instead tries to find the * closest non-token card instead. So we construct a new deck which has no @@ -35,7 +32,7 @@ private slots: void getAnalyzeRequestData(const DeckList &deck, QByteArray &data); public: - explicit DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr); + explicit DeckStatsInterface(QObject *parent = nullptr); void analyzeDeck(const DeckList &deck); }; diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index cd39ea251..5dc77fa2c 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -6,12 +6,12 @@ #include #include #include +#include #include #include #include -TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent) - : QObject(parent), cardDatabase(_cardDatabase) +TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent) { manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished); @@ -97,8 +97,8 @@ void TappedOutInterface::analyzeDeck(const DeckList &deck) void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard) { - auto copyMainOrSide = [this, &mainboard, &sideboard](const auto node, const auto card) { - CardInfoPtr dbCard = cardDatabase.query()->getCardInfo(card->getName()); + auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) { + CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); if (!dbCard || dbCard->getIsToken()) { return; } diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.h b/cockatrice/src/client/network/interfaces/tapped_out_interface.h index f1cc1cbeb..32f9369d5 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.h +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.h @@ -7,8 +7,8 @@ #ifndef TAPPEDOUT_INTERFACE_H #define TAPPEDOUT_INTERFACE_H -#include -#include +#include +#include inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface"); @@ -29,14 +29,13 @@ class TappedOutInterface : public QObject private: QNetworkAccessManager *manager; - CardDatabase &cardDatabase; void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard); private slots: void queryFinished(QNetworkReply *reply); void getAnalyzeRequestData(const DeckList &deck, QByteArray &data); public: - explicit TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr); + explicit TappedOutInterface(QObject *parent = nullptr); void analyzeDeck(const DeckList &deck); }; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 2354fe6d9..eacc8bf88 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -538,14 +538,14 @@ void AbstractTabDeckEditor::actExportDeckDecklistXyz() /** @brief Analyzes the deck using DeckStats. */ void AbstractTabDeckEditor::actAnalyzeDeckDeckstats() { - auto *interface = new DeckStatsInterface(*cardDatabaseDockWidget->getDatabase(), this); + auto *interface = new DeckStatsInterface(this); interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Analyzes the deck using TappedOut. */ void AbstractTabDeckEditor::actAnalyzeDeckTappedout() { - auto *interface = new TappedOutInterface(*cardDatabaseDockWidget->getDatabase(), this); + auto *interface = new TappedOutInterface(this); interface->analyzeDeck(deckStateManager->getDeckList()); } From 86256602ffdbd6281fc703180bed3ce6880625ac Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:41:55 -0700 Subject: [PATCH 07/50] [TabDeckEditor] Refactor to use signal instead of calling tab (#6965) * [TabDeckEditor] Refactor to use signal instead of calling tab * update docs * fix cardInfoRequest --- .../deck_editor_card_database_dock_widget.cpp | 6 ++++++ .../deck_editor_database_display_widget.cpp | 13 +++++++------ .../deck_editor_database_display_widget.h | 4 ++++ .../widgets/tabs/abstract_tab_deck_editor.cpp | 14 ++++++++++---- .../widgets/tabs/abstract_tab_deck_editor.h | 18 ++++++++++++++++-- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index 546161506..ca9bf24fa 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -33,6 +33,12 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck &AbstractTabDeckEditor::addCard); connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardDecremented, deckEditor, &AbstractTabDeckEditor::decrementCard); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::edhrecRequested, deckEditor, + &AbstractTabDeckEditor::openEdhrecTab); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::printingSelectorRequested, deckEditor, + &AbstractTabDeckEditor::showPrintingSelector); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardInfoRequested, deckEditor, + &AbstractTabDeckEditor::updateCardInfo); } CardDatabase *DeckEditorCardDatabaseDockWidget::getDatabase() const diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index 3f397d2a0..50ff8851f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -200,18 +200,18 @@ void DeckEditorDatabaseDisplayWidget::databaseCustomMenu(QPoint point) addToDeck = menu.addAction(tr("Add to Deck")); addToSideboard = menu.addAction(tr("Add to Sideboard")); selectPrinting = menu.addAction(tr("Select Printing")); - connect(selectPrinting, &QAction::triggered, this, [this, card] { deckEditor->showPrintingSelector(); }); + connect(selectPrinting, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::printingSelectorRequested); if (canBeCommander(card.getInfo())) { edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)")); connect(edhRecCommander, &QAction::triggered, this, - [this, card] { deckEditor->getTabSupervisor()->addEdhrecTab(card.getCardPtr(), true); }); + [this, card] { emit edhrecRequested(card.getCardPtr(), true); }); } edhRecCard = menu.addAction(tr("Show on EDHRec (Card)")); connect(addToDeck, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); connect(addToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); connect(edhRecCard, &QAction::triggered, this, - [this, card] { deckEditor->getTabSupervisor()->addEdhrecTab(card.getCardPtr()); }); + [this, card] { emit edhrecRequested(card.getCardPtr(), false); }); // filling out the related cards submenu auto *relatedMenu = new QMenu(tr("Show Related cards")); @@ -223,9 +223,10 @@ void DeckEditorDatabaseDisplayWidget::databaseCustomMenu(QPoint point) for (const CardRelation *rel : relatedCards) { const QString &relatedCardName = rel->getName(); QAction *relatedCard = relatedMenu->addAction(relatedCardName); - connect( - relatedCard, &QAction::triggered, deckEditor->cardInfoDockWidget->cardInfo, - [this, relatedCardName] { deckEditor->cardInfoDockWidget->cardInfo->setCard(relatedCardName); }); + connect(relatedCard, &QAction::triggered, this, [this, relatedCardName] { + ExactCard card = CardDatabaseManager::query()->guessCard({relatedCardName}); + emit cardInfoRequested(card); + }); } } menu.exec(databaseView->mapToGlobal(point)); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h index a0062a9be..e36037bbb 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h @@ -52,6 +52,10 @@ signals: void cardDecremented(const ExactCard &card, const QString &zoneName); void cardChanged(const ExactCard &_card); + void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander); + void printingSelectorRequested(); + void cardInfoRequested(const ExactCard &card); + private: KeySignals searchKeySignals; QTreeView *databaseView; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index eacc8bf88..32e29379f 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -105,16 +105,17 @@ void AbstractTabDeckEditor::registerDockWidget(QMenu *_viewMenu, QDockWidget *wi dockToActions.insert(widget, {menu, aVisible, aFloating, defaultSize}); } -/** - * @brief Updates the card info dock and printing selector. - * @param card The card to display. - */ void AbstractTabDeckEditor::updateCard(const ExactCard &card) { cardInfoDockWidget->updateCard(card); printingSelectorDockWidget->printingSelector->setCard(card.getCardPtr()); } +void AbstractTabDeckEditor::updateCardInfo(const ExactCard &card) +{ + cardInfoDockWidget->updateCard(card); +} + /** @brief Placeholder: called when the deck changes. */ void AbstractTabDeckEditor::onDeckChanged() { @@ -595,3 +596,8 @@ void AbstractTabDeckEditor::showPrintingSelector() printingSelectorDockWidget->printingSelector->updateDisplay(); printingSelectorDockWidget->setVisible(true); } + +void AbstractTabDeckEditor::openEdhrecTab(const CardInfoPtr &info, bool isCommander) +{ + getTabSupervisor()->addEdhrecTab(info, isCommander); +} diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index 7bdfd3dcb..467722793 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -140,11 +140,18 @@ public slots: /** @brief Called when the deck is modified. */ virtual void onDeckModified(); - /** @brief Updates the card info panel. - * @param card The card to display. + /** + * @brief Updates the card info dock and printing selector. + * @param card The card to display. */ void updateCard(const ExactCard &card); + /** + * @brief Updates just the card info dock + * @param card The card to display + */ + void updateCardInfo(const ExactCard &card); + /** * @brief Adds a card to the given zone * @param card Card to add. @@ -175,6 +182,13 @@ public slots: /** @brief Shows the printing selector dock and updates it with the current card. */ void showPrintingSelector(); + /** + * @brief Opens an EDHRec tab for the given card + * @param info The card + * @param isCommander The type of search + */ + void openEdhrecTab(const CardInfoPtr &info, bool isCommander); + signals: /** @brief Emitted when a deck should be opened in a new editor tab. */ void openDeckEditor(const LoadedDeck &deck); From 29cc622ce3e7956eeb8710ace07ef92cf12615d2 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:21:13 -0700 Subject: [PATCH 08/50] [TabDeckEditor] Refactor: Create shared CardDatabaseModel for tab (#6968) --- .../deck_editor/deck_editor_card_database_dock_widget.cpp | 2 +- .../deck_editor/deck_editor_database_display_widget.cpp | 6 ++---- .../deck_editor/deck_editor_database_display_widget.h | 3 +-- .../src/interface/widgets/tabs/abstract_tab_deck_editor.cpp | 3 +++ .../src/interface/widgets/tabs/abstract_tab_deck_editor.h | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index ca9bf24fa..3f2bfb31e 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -13,7 +13,7 @@ DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDe void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor) { - databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor); + databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel); auto *frame = new QVBoxLayout; frame->setObjectName("databaseDisplayFrame"); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index 50ff8851f..5400fbf82 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -21,8 +21,8 @@ static bool canBeCommander(const CardInfo &cardInfo) cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive); } -DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor) - : QWidget(parent), deckEditor(deckEditor) +DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel) + : QWidget(parent), databaseModel(databaseModel) { setObjectName("databaseDisplayWidget"); @@ -58,8 +58,6 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent connect(&searchKeySignals, &KeySignals::onCtrlC, this, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); - databaseModel->setObjectName("databaseModel"); databaseDisplayModel = new CardDatabaseDisplayModel(this); databaseDisplayModel->setObjectName("databaseDisplayModel"); databaseDisplayModel->setSourceModel(databaseModel); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h index e36037bbb..a1802081c 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h @@ -23,8 +23,7 @@ class DeckEditorDatabaseDisplayWidget : public QWidget Q_OBJECT public: - explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor); - AbstractTabDeckEditor *deckEditor; + explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel); CardDatabaseModel *databaseModel; CardDatabaseDisplayModel *databaseDisplayModel; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 32e29379f..a8cc4cee6 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -56,6 +56,9 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta deckStateManager = new DeckStateManager(this); + databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + cardDatabaseDockWidget = new DeckEditorCardDatabaseDockWidget(this); deckDockWidget = new DeckEditorDeckDockWidget(this); cardInfoDockWidget = new DeckEditorCardInfoDockWidget(this); diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index 467722793..34c585597 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -126,6 +126,7 @@ public: // UI Elements DeckStateManager *deckStateManager; + CardDatabaseModel *databaseModel; ///< Card database DeckEditorMenu *deckMenu; ///< Menu for deck operations DeckEditorCardDatabaseDockWidget *cardDatabaseDockWidget; ///< Database dock DeckEditorCardInfoDockWidget *cardInfoDockWidget; ///< Card info dock From 0da2ac408762ffb58d956fcd864ce021f2c96a3f Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:21:28 -0700 Subject: [PATCH 09/50] [TabDeckEditor] Refactor: pass nullable deck model into filter widget (#6969) --- .../tab_deck_editor_visual_tab_widget.cpp | 2 +- .../visual_database_display_filter_toolbar_widget.cpp | 8 ++++---- .../visual_database_display_filter_toolbar_widget.h | 4 +++- .../visual_database_display_name_filter_widget.cpp | 8 +++----- .../visual_database_display_name_filter_widget.h | 6 +++--- .../visual_database_display_widget.cpp | 5 +++-- .../visual_database_display_widget.h | 8 ++------ 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index c5cc2a85f..d887066de 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -38,7 +38,7 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, &TabDeckEditorVisualTabWidget::actAddCard); visualDatabaseDisplay = - new VisualDatabaseDisplayWidget(this, deckEditor, _cardDatabaseModel, _cardDatabaseDisplayModel); + new VisualDatabaseDisplayWidget(this, deckEditor, _cardDatabaseModel, _cardDatabaseDisplayModel, deckModel); visualDatabaseDisplay->setObjectName("visualDatabaseView"); connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index 0c1280009..54705e940 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -4,9 +4,10 @@ #include -VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent) +VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent, + DeckListModel *deckListModel) : FlowWidget(_parent, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff), - visualDatabaseDisplay(_parent) + visualDatabaseDisplay(_parent), deckListModel(deckListModel) { connect(this, &VisualDatabaseDisplayFilterToolbarWidget::searchModelChanged, visualDatabaseDisplay, &VisualDatabaseDisplayWidget::onSearchModelChanged); @@ -97,8 +98,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize() auto filterModel = visualDatabaseDisplay->getFilterModel(); saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel); - nameFilterWidget = - new VisualDatabaseDisplayNameFilterWidget(this, visualDatabaseDisplay->getDeckEditor(), filterModel); + nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, filterModel, deckListModel); mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel); formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel); subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h index 5b55f4ba6..8a3555455 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h @@ -18,12 +18,14 @@ signals: void searchModelChanged(); public: - explicit VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *parent); + explicit VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *parent, + DeckListModel *deckListModel = nullptr); void initialize(); void retranslateUi(); private: VisualDatabaseDisplayWidget *visualDatabaseDisplay; + DeckListModel *deckListModel; QGroupBox *sortGroupBox; QLabel *sortByLabel; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp index fd03e17e6..3fa1a782a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp @@ -8,9 +8,9 @@ #include VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWidget *parent, - AbstractTabDeckEditor *_deckEditor, - FilterTreeModel *_filterModel) - : QWidget(parent), deckEditor(_deckEditor), filterModel(_filterModel) + FilterTreeModel *_filterModel, + DeckListModel *deckListModel) + : QWidget(parent), filterModel(_filterModel), deckListModel(deckListModel) { setMinimumWidth(300); setMaximumHeight(300); @@ -62,8 +62,6 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi() void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck() { - DeckListModel *deckListModel = deckEditor->deckStateManager->getModel(); - if (!deckListModel) { return; } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h index 5a8438a05..0c10408ae 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.h @@ -21,8 +21,8 @@ class VisualDatabaseDisplayNameFilterWidget : public QWidget Q_OBJECT public: explicit VisualDatabaseDisplayNameFilterWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - FilterTreeModel *filterModel); + FilterTreeModel *filterModel, + DeckListModel *deckListModel = nullptr); void createNameFilter(const QString &name); void removeNameFilter(const QString &name); @@ -34,8 +34,8 @@ public slots: void retranslateUi(); private: - AbstractTabDeckEditor *deckEditor; FilterTreeModel *filterModel; + DeckListModel *deckListModel; QVBoxLayout *layout; QLineEdit *searchBox; FlowWidget *flowWidget; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 989ca7330..399b319f7 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -25,7 +25,8 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, CardDatabaseModel *database_model, - CardDatabaseDisplayModel *database_display_model) + CardDatabaseDisplayModel *database_display_model, + DeckListModel *deckListModel) : QWidget(parent), deckEditor(_deckEditor), databaseModel(database_model), databaseDisplayModel(database_display_model) { @@ -109,7 +110,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel); - filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this); + filterContainer = new VisualDatabaseDisplayFilterToolbarWidget(this, deckListModel); clearFilterWidget = new QToolButton(); clearFilterWidget->setFixedSize(32, 32); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index baa9c7c49..61dcea487 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -36,7 +36,8 @@ public: explicit VisualDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor, CardDatabaseModel *database_model, - CardDatabaseDisplayModel *database_display_model); + CardDatabaseDisplayModel *database_display_model, + DeckListModel *deckListModel = nullptr); void retranslateUi(); void adjustCardsPerPage(); @@ -47,11 +48,6 @@ public: void sortCardList(const QStringList &properties, Qt::SortOrder order) const; void setDeckList(const DeckList &new_deck_list_model); - AbstractTabDeckEditor *getDeckEditor() - { - return deckEditor; - } - CardDatabaseDisplayModel *getDatabaseDisplayModel() { return databaseDisplayModel; From c14a00808074822fde7a698533d3d1953e029e02 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:20:46 -0700 Subject: [PATCH 10/50] [TabDeckEditor] Refactor card database view into own class (#6967) * rename method * [TabDeckEditor] Refactor card database view into own class * fix include guard * directly get key signals for eventFilter * fix includes --- cockatrice/CMakeLists.txt | 1 + .../deck_editor/card_database_view.cpp | 167 ++++++++++++++++ .../widgets/deck_editor/card_database_view.h | 59 ++++++ .../deck_editor_card_database_dock_widget.cpp | 5 - .../deck_editor_card_database_dock_widget.h | 1 - .../deck_editor_database_display_widget.cpp | 189 ++++-------------- .../deck_editor_database_display_widget.h | 27 ++- .../tabs/tab_visual_database_display.cpp | 20 +- .../tabs/tab_visual_database_display.h | 4 +- .../tab_deck_editor_visual.cpp | 16 +- .../tab_deck_editor_visual_tab_widget.cpp | 20 +- .../tab_deck_editor_visual_tab_widget.h | 19 +- ...database_display_filter_toolbar_widget.cpp | 1 + .../visual_database_display_widget.cpp | 92 +++++---- .../visual_database_display_widget.h | 23 ++- 15 files changed, 392 insertions(+), 252 deletions(-) create mode 100644 cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp create mode 100644 cockatrice/src/interface/widgets/deck_editor/card_database_view.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 0b2192399..ee0102ee9 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -181,6 +181,7 @@ set(cockatrice_SOURCES src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp + src/interface/widgets/deck_editor/card_database_view.cpp src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp new file mode 100644 index 000000000..a1c29e241 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp @@ -0,0 +1,167 @@ +#include "card_database_view.h" + +#include "../../../client/settings/cache_settings.h" +#include "card_database_display_model.h" +#include "card_database_model.h" + +#include +#include +#include +#include +#include +#include +#include + +static bool canBeCommander(const CardInfo &cardInfo) +{ + return (cardInfo.getCardType().contains("Legendary", Qt::CaseInsensitive) && + cardInfo.getCardType().contains("Creature", Qt::CaseInsensitive)) || + cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive); +} + +CardDatabaseView::CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model) + : QTreeView(parent), databaseDisplayModel(model) +{ + // set up object + setUniformRowHeights(true); + setRootIsDecorated(false); + setAlternatingRowColors(true); + setSortingEnabled(true); + sortByColumn(0, Qt::AscendingOrder); + QTreeView::setModel(databaseDisplayModel); + setContextMenuPolicy(Qt::CustomContextMenu); + + connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, + &CardDatabaseView::resetSelectionIfEmpty); + + connect(this, &QTreeView::customContextMenuRequested, this, &CardDatabaseView::openCustomMenu); + connect(selectionModel(), &QItemSelectionModel::currentRowChanged, this, &CardDatabaseView::updateCard); + connect(this, &QTreeView::doubleClicked, this, &CardDatabaseView::actDoubleClick); + + // layout settings + QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState(); + if (dbHeaderState.isNull()) { + // first run + setColumnWidth(0, 200); + } else { + header()->restoreState(dbHeaderState); + } + connect(header(), &QHeaderView::geometriesChanged, this, &CardDatabaseView::saveDbHeaderState); + + // create key filters + searchKeySignals.setObjectName("searchKeySignals"); + connect(&searchKeySignals, &KeySignals::onEnter, this, [this] { addCard(DECK_ZONE_MAIN); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, this, [this] { addCard(DECK_ZONE_MAIN); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, this, [this] { addCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, this, [this] { decrementCard(DECK_ZONE_MAIN); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, this, [this] { decrementCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, this, [this] { addCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlEnter, this, [this] { addCard(DECK_ZONE_SIDE); }); + connect(&searchKeySignals, &KeySignals::onCtrlC, this, &CardDatabaseView::copyDatabaseCellContents); +} + +QString CardDatabaseView::currentCardName() const +{ + const QModelIndex currentIndex = selectionModel()->currentIndex(); + if (!currentIndex.isValid()) { + return {}; + } + + return currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); +} + +void CardDatabaseView::actDoubleClick() +{ + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + addCard(DECK_ZONE_SIDE); + } else { + addCard(DECK_ZONE_MAIN); + } +} + +void CardDatabaseView::addCard(const QString &zoneName) +{ + emit cardAdded(currentCardName(), zoneName); +} + +void CardDatabaseView::decrementCard(const QString &zoneName) +{ + emit cardDecremented(currentCardName(), zoneName); +} + +void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + if (!current.isValid()) { + return; + } + + const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); + + if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) { + emit cardChanged(cardName); + } +} + +void CardDatabaseView::resetSelectionIfEmpty() +{ + QModelIndexList sel = selectionModel()->selectedRows(); + if (sel.isEmpty() && databaseDisplayModel->rowCount() > 0) { + selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + } +} + +void CardDatabaseView::copyDatabaseCellContents() const +{ + auto _data = selectionModel()->currentIndex().data(); + QApplication::clipboard()->setText(_data.toString()); +} + +void CardDatabaseView::saveDbHeaderState() +{ + SettingsCache::instance().layouts().setDeckEditorDbHeaderState(header()->saveState()); +} + +void CardDatabaseView::openCustomMenu(QPoint point) +{ + CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(currentCardName()); + + if (!card) { + return; + } + + QMenu menu; + // add to deck and sideboard options + QAction *addToDeck = menu.addAction(tr("Add to Deck")); + QAction *addToSideboard = menu.addAction(tr("Add to Sideboard")); + QAction *selectPrinting = menu.addAction(tr("Select Printing")); + + connect(addToDeck, &QAction::triggered, this, [this, card] { emit cardAdded(card->getName(), DECK_ZONE_MAIN); }); + connect(addToSideboard, &QAction::triggered, this, + [this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); }); + connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked); + + if (canBeCommander(*card)) { + QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)")); + connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); }); + } + QAction *edhRecCard = menu.addAction(tr("Show on EDHRec (Card)")); + connect(edhRecCard, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, false); }); + + // filling out the related cards submenu + auto *relatedMenu = new QMenu(tr("Show Related cards")); + menu.addMenu(relatedMenu); + auto relatedCards = card->getAllRelatedCards(); + if (relatedCards.isEmpty()) { + relatedMenu->setDisabled(true); + } else { + for (const CardRelation *rel : relatedCards) { + const QString &relatedCardName = rel->getName(); + QAction *relatedCard = relatedMenu->addAction(relatedCardName); + connect(relatedCard, &QAction::triggered, this, + [this, relatedCardName] { emit relatedCardClicked(relatedCardName); }); + } + } + + menu.exec(mapToGlobal(point)); +} diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.h b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h new file mode 100644 index 000000000..175ec12b9 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.h @@ -0,0 +1,59 @@ +#ifndef COCKATRICE_CARD_DATABASE_VIEW_H +#define COCKATRICE_CARD_DATABASE_VIEW_H + +#include "../../key_signals.h" + +#include +#include + +class CardDatabaseModel; +class CardDatabaseDisplayModel; + +/** + * @brief The card database table. + */ +class CardDatabaseView : public QTreeView +{ + Q_OBJECT + + KeySignals searchKeySignals; + CardDatabaseDisplayModel *databaseDisplayModel; + +public: + explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model); + + QString currentCardName() const; + + /** + * @brief Get the KeySignals that are connected to this view. + * You can install the KeySignals as an eventFilter to capture keyboard shortcuts for adding and decrementing cards. + */ + KeySignals *getKeySignals() + { + return &searchKeySignals; + } + +signals: + void cardChanged(const QString &cardName); + + void cardAdded(const QString &cardName, const QString &zoneName); + void cardDecremented(const QString &cardName, const QString &zoneName); + + void edhrecClicked(const CardInfoPtr &cardInfo, bool isCommander); + void selectPrintingClicked(); + void relatedCardClicked(const QString &relatedCard); + +private slots: + void actDoubleClick(); + + void addCard(const QString &zoneName); + void decrementCard(const QString &zoneName); + void updateCard(const QModelIndex ¤t, const QModelIndex &); + + void resetSelectionIfEmpty(); + void copyDatabaseCellContents() const; + void saveDbHeaderState(); + void openCustomMenu(QPoint point); +}; + +#endif // COCKATRICE_CARD_DATABASE_VIEW_H diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index 3f2bfb31e..2a491de4f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -41,11 +41,6 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck &AbstractTabDeckEditor::updateCardInfo); } -CardDatabase *DeckEditorCardDatabaseDockWidget::getDatabase() const -{ - return databaseDisplayWidget->databaseModel->getDatabase(); -} - void DeckEditorCardDatabaseDockWidget::retranslateUi() { setWindowTitle(tr("Card Database")); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h index bff9ee36f..6af2e4432 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h @@ -17,7 +17,6 @@ public: DeckEditorDatabaseDisplayWidget *databaseDisplayWidget; - CardDatabase *getDatabase() const; void setFilterTree(FilterTree *filterTree); public slots: diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index 5400fbf82..9da821813 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -5,24 +5,17 @@ #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../pixel_map_generator.h" +#include "card_database_view.h" #include #include -#include #include #include #include #include -static bool canBeCommander(const CardInfo &cardInfo) -{ - return (cardInfo.getCardType().contains("Legendary", Qt::CaseInsensitive) && - cardInfo.getCardType().contains("Creature", Qt::CaseInsensitive)) || - cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive); -} - DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel) - : QWidget(parent), databaseModel(databaseModel) + : QWidget(parent) { setObjectName("databaseDisplayWidget"); @@ -36,26 +29,10 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent searchEdit->setClearButtonEnabled(true); searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); - searchEdit->installEventFilter(&searchKeySignals); setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); - searchKeySignals.setObjectName("searchKeySignals"); - connect(searchEdit, &SearchLineEdit::textChanged, this, &DeckEditorDatabaseDisplayWidget::updateSearch); - connect(&searchKeySignals, &KeySignals::onEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, this, - &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, this, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, this, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, this, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, this, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlEnter, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlC, this, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); databaseDisplayModel = new CardDatabaseDisplayModel(this); @@ -63,33 +40,23 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent databaseDisplayModel->setSourceModel(databaseModel); databaseDisplayModel->setFilterKeyColumn(0); - databaseView = new QTreeView(this); + databaseView = new CardDatabaseView(this, databaseDisplayModel); databaseView->setObjectName("databaseView"); databaseView->setFocusProxy(searchEdit); - databaseView->setUniformRowHeights(true); - databaseView->setRootIsDecorated(false); - databaseView->setAlternatingRowColors(true); - databaseView->setSortingEnabled(true); - databaseView->sortByColumn(0, Qt::AscendingOrder); - databaseView->setModel(databaseDisplayModel); - databaseView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(databaseView, &QTreeView::customContextMenuRequested, this, - &DeckEditorDatabaseDisplayWidget::databaseCustomMenu); - connect(databaseView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, - &DeckEditorDatabaseDisplayWidget::updateCard); - connect(databaseView, &QTreeView::doubleClicked, this, &DeckEditorDatabaseDisplayWidget::actAddCard); - - QByteArray dbHeaderState = SettingsCache::instance().layouts().getDeckEditorDbHeaderState(); - if (dbHeaderState.isNull()) { - // first run - databaseView->setColumnWidth(0, 200); - } else { - databaseView->header()->restoreState(dbHeaderState); - } - connect(databaseView->header(), &QHeaderView::geometriesChanged, this, - &DeckEditorDatabaseDisplayWidget::saveDbHeaderState); searchEdit->setTreeView(databaseView); + searchEdit->installEventFilter(databaseView->getKeySignals()); + + connect(searchEdit, &SearchLineEdit::textChanged, databaseDisplayModel, &CardDatabaseDisplayModel::setStringFilter); + connect(databaseView, &CardDatabaseView::cardAdded, this, &DeckEditorDatabaseDisplayWidget::addCard); + connect(databaseView, &CardDatabaseView::cardDecremented, this, &DeckEditorDatabaseDisplayWidget::decrementCard); + connect(databaseView, &CardDatabaseView::cardChanged, this, &DeckEditorDatabaseDisplayWidget::updateCard); + + connect(databaseView, &CardDatabaseView::edhrecClicked, this, &DeckEditorDatabaseDisplayWidget::edhrecRequested); + connect(databaseView, &CardDatabaseView::selectPrintingClicked, this, + &DeckEditorDatabaseDisplayWidget::printingSelectorRequested); + connect(databaseView, &CardDatabaseView::relatedCardClicked, this, + &DeckEditorDatabaseDisplayWidget::onRelatedCardClicked); aAddCard = new QAction(QString(), this); aAddCard->setIcon(QPixmap("theme:icons/arrow_right_green")); @@ -115,131 +82,39 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent retranslateUi(); } -void DeckEditorDatabaseDisplayWidget::updateSearch(const QString &search) -{ - databaseDisplayModel->setStringFilter(search); - QModelIndexList sel = databaseView->selectionModel()->selectedRows(); - if (sel.isEmpty() && databaseDisplayModel->rowCount()) { - databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), - QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); - } -} - void DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters() { databaseDisplayModel->clearFilterAll(); searchEdit->setText(""); } -void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/) -{ - if (!current.isValid()) { - return; - } - - const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); - - if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) { - emit cardChanged(CardDatabaseManager::query()->getPreferredCard(cardName)); - } -} - -void DeckEditorDatabaseDisplayWidget::actAddCard() -{ - if (QApplication::keyboardModifiers() & Qt::ControlModifier) { - actAddCardToSideboard(); - } else { - actAddCardToMainDeck(); - } -} - void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck() { - highlightAllSearchEdit(); - emit cardAdded(currentCard(), DECK_ZONE_MAIN); + addCard(databaseView->currentCardName(), DECK_ZONE_MAIN); } void DeckEditorDatabaseDisplayWidget::actAddCardToSideboard() +{ + addCard(databaseView->currentCardName(), DECK_ZONE_SIDE); +} + +void DeckEditorDatabaseDisplayWidget::addCard(const QString &cardName, const QString &zoneName) { highlightAllSearchEdit(); - emit cardAdded(currentCard(), DECK_ZONE_SIDE); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardAdded(exactCard, zoneName); } -void DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck() +void DeckEditorDatabaseDisplayWidget::decrementCard(const QString &cardName, const QString &zoneName) { - emit cardDecremented(currentCard(), DECK_ZONE_MAIN); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardDecremented(exactCard, zoneName); } -void DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard() +void DeckEditorDatabaseDisplayWidget::updateCard(const QString &cardName) { - emit cardDecremented(currentCard(), DECK_ZONE_SIDE); -} - -ExactCard DeckEditorDatabaseDisplayWidget::currentCard() const -{ - const QModelIndex currentIndex = databaseView->selectionModel()->currentIndex(); - if (!currentIndex.isValid()) { - return {}; - } - - const QString cardName = currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); - - return CardDatabaseManager::query()->getPreferredCard(cardName); -} - -void DeckEditorDatabaseDisplayWidget::databaseCustomMenu(QPoint point) -{ - QMenu menu; - ExactCard card = currentCard(); - - if (card) { - // add to deck and sideboard options - QAction *addToDeck, *addToSideboard, *selectPrinting, *edhRecCommander, *edhRecCard; - addToDeck = menu.addAction(tr("Add to Deck")); - addToSideboard = menu.addAction(tr("Add to Sideboard")); - selectPrinting = menu.addAction(tr("Select Printing")); - connect(selectPrinting, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::printingSelectorRequested); - if (canBeCommander(card.getInfo())) { - edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)")); - connect(edhRecCommander, &QAction::triggered, this, - [this, card] { emit edhrecRequested(card.getCardPtr(), true); }); - } - edhRecCard = menu.addAction(tr("Show on EDHRec (Card)")); - - connect(addToDeck, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(addToSideboard, &QAction::triggered, this, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(edhRecCard, &QAction::triggered, this, - [this, card] { emit edhrecRequested(card.getCardPtr(), false); }); - - // filling out the related cards submenu - auto *relatedMenu = new QMenu(tr("Show Related cards")); - menu.addMenu(relatedMenu); - auto relatedCards = card.getInfo().getAllRelatedCards(); - if (relatedCards.isEmpty()) { - relatedMenu->setDisabled(true); - } else { - for (const CardRelation *rel : relatedCards) { - const QString &relatedCardName = rel->getName(); - QAction *relatedCard = relatedMenu->addAction(relatedCardName); - connect(relatedCard, &QAction::triggered, this, [this, relatedCardName] { - ExactCard card = CardDatabaseManager::query()->guessCard({relatedCardName}); - emit cardInfoRequested(card); - }); - } - } - menu.exec(databaseView->mapToGlobal(point)); - } -} - -void DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents() -{ - auto _data = databaseView->selectionModel()->currentIndex().data(); - QApplication::clipboard()->setText(_data.toString()); -} - -void DeckEditorDatabaseDisplayWidget::saveDbHeaderState() -{ - SettingsCache::instance().layouts().setDeckEditorDbHeaderState(databaseView->header()->saveState()); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardChanged(exactCard); } void DeckEditorDatabaseDisplayWidget::setFilterTree(FilterTree *filterTree) @@ -256,4 +131,10 @@ void DeckEditorDatabaseDisplayWidget::retranslateUi() void DeckEditorDatabaseDisplayWidget::highlightAllSearchEdit() { searchEdit->setSelection(0, searchEdit->text().length()); +} + +void DeckEditorDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCard) +{ + ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard}); + emit cardInfoRequested(exactCard); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h index a1802081c..5de4d211d 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h @@ -9,7 +9,6 @@ #define DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" -#include "../../key_signals.h" #include "../utility/custom_line_edit.h" #include @@ -17,34 +16,31 @@ #include #include +class CardDatabaseView; class AbstractTabDeckEditor; + class DeckEditorDatabaseDisplayWidget : public QWidget { Q_OBJECT public: explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel); - CardDatabaseModel *databaseModel; - CardDatabaseDisplayModel *databaseDisplayModel; - QTreeView *getDatabaseView() + CardDatabaseView *getDatabaseView() const { return databaseView; } public slots: - ExactCard currentCard() const; void setFilterTree(FilterTree *filterTree); void clearAllDatabaseFilters(); - void updateSearch(const QString &search); - void updateCard(const QModelIndex ¤t, const QModelIndex &); - void actAddCard(); + void actAddCardToMainDeck(); void actAddCardToSideboard(); - void actDecrementCardFromMainDeck(); - void actDecrementCardFromSideboard(); - void databaseCustomMenu(QPoint point); - void copyDatabaseCellContents(); + + void addCard(const QString &cardName, const QString &zoneName); + void decrementCard(const QString &cardName, const QString &zoneName); + void updateCard(const QString &cardName); signals: void cardAdded(const ExactCard &card, const QString &zoneName); @@ -56,8 +52,8 @@ signals: void cardInfoRequested(const ExactCard &card); private: - KeySignals searchKeySignals; - QTreeView *databaseView; + CardDatabaseDisplayModel *databaseDisplayModel; + CardDatabaseView *databaseView; QHBoxLayout *searchLayout; SearchLineEdit *searchEdit; QAction *aAddCard, *aAddCardToSideboard; @@ -68,7 +64,8 @@ private: private slots: void retranslateUi(); - void saveDbHeaderState(); + + void onRelatedCardClicked(const QString &relatedCard); }; #endif // DECK_EDITOR_DATABASE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp index 5e8fb8670..3112e7ada 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp @@ -1,14 +1,19 @@ #include "tab_visual_database_display.h" #include "tab_deck_editor.h" +#include "tab_supervisor.h" + +#include TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { - deckEditor = new TabDeckEditor(_tabSupervisor); - deckEditor->setHidden(true); - visualDatabaseDisplayWidget = new VisualDatabaseDisplayWidget( - this, deckEditor, deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseModel, - deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel); + auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + + visualDatabaseDisplayWidget = new VisualDatabaseDisplayWidget(this, databaseModel); + + connect(visualDatabaseDisplayWidget, &VisualDatabaseDisplayWidget::edhrecRequested, this, + &TabVisualDatabaseDisplay::openEdhrecTab); setCentralWidget(visualDatabaseDisplayWidget); @@ -18,3 +23,8 @@ TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor void TabVisualDatabaseDisplay::retranslateUi() { } + +void TabVisualDatabaseDisplay::openEdhrecTab(const CardInfoPtr &info, bool isCommander) const +{ + getTabSupervisor()->addEdhrecTab(info, isCommander); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h index f5aef6d9b..3a4bcd6ea 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h +++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h @@ -15,9 +15,11 @@ class TabVisualDatabaseDisplay : public Tab Q_OBJECT private: - TabDeckEditor *deckEditor; VisualDatabaseDisplayWidget *visualDatabaseDisplayWidget; +private slots: + void openEdhrecTab(const CardInfoPtr &info, bool isCommander) const; + public: TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor); void retranslateUi() override; diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 662f1b76b..fd465ec21 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -1,6 +1,7 @@ #include "tab_deck_editor_visual.h" #include "../../../../client/settings/cache_settings.h" +#include "../../cards/card_info_display_widget.h" #include "../../deck_editor/deck_state_manager.h" #include "../../filters/filter_builder.h" #include "../../interface/pixel_map_generator.h" @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -63,9 +65,10 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame = new QVBoxLayout; centralWidget->setLayout(centralFrame); - tabContainer = new TabDeckEditorVisualTabWidget( - centralWidget, this, deckStateManager->getModel(), cardDatabaseDockWidget->databaseDisplayWidget->databaseModel, - cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel); + auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); + databaseModel->setObjectName("databaseModel"); + + tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseModel); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this, &TabDeckEditorVisual::changeModelIndexAndCardInfo); @@ -76,6 +79,13 @@ void TabDeckEditorVisual::createCentralFrame() connect(tabContainer, &TabDeckEditorVisualTabWidget::cardClickedDatabaseDisplay, this, &TabDeckEditorVisual::processDatabaseCardClick); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardAdded, this, &TabDeckEditorVisual::addCard); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardDecremented, this, &TabDeckEditorVisual::decrementCard); + connect(tabContainer, &TabDeckEditorVisualTabWidget::edhrecRequested, this, &TabDeckEditorVisual::openEdhrecTab); + connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this, + &TabDeckEditorVisual::showPrintingSelector); + connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo); + centralFrame->addWidget(tabContainer); setCentralWidget(centralWidget); setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index d887066de..2ee560859 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -9,7 +9,6 @@ * @param _deckEditor Pointer to the associated deck editor. * @param _deckModel Pointer to the deck list model. * @param _cardDatabaseModel Pointer to the card database model. - * @param _cardDatabaseDisplayModel Pointer to the card database display model. * * Initializes all sub-widgets (visual deck view, database display, deck analytics, * sample hand) and sets up the tab layout and signal connections. @@ -17,10 +16,8 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, DeckListModel *_deckModel, - CardDatabaseModel *_cardDatabaseModel, - CardDatabaseDisplayModel *_cardDatabaseDisplayModel) - : QTabWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), cardDatabaseModel(_cardDatabaseModel), - cardDatabaseDisplayModel(_cardDatabaseDisplayModel) + CardDatabaseModel *_cardDatabaseModel) + : QTabWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), cardDatabaseModel(_cardDatabaseModel) { this->setTabsClosable(true); // Enable tab closing connect(this, &QTabWidget::tabCloseRequested, this, &TabDeckEditorVisualTabWidget::handleTabClose); @@ -37,13 +34,22 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, this, &TabDeckEditorVisualTabWidget::actAddCard); - visualDatabaseDisplay = - new VisualDatabaseDisplayWidget(this, deckEditor, _cardDatabaseModel, _cardDatabaseDisplayModel, deckModel); + visualDatabaseDisplay = new VisualDatabaseDisplayWidget(this, _cardDatabaseModel, deckModel); visualDatabaseDisplay->setObjectName("visualDatabaseView"); connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardHoveredDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardChangedDatabaseDisplay); connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardAdded, this, + &TabDeckEditorVisualTabWidget::cardAdded); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardDecremented, this, + &TabDeckEditorVisualTabWidget::cardDecremented); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::edhrecRequested, this, + &TabDeckEditorVisualTabWidget::edhrecRequested); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::printingSelectorRequested, this, + &TabDeckEditorVisualTabWidget::printingSelectorRequested); + connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardInfoRequested, this, + &TabDeckEditorVisualTabWidget::cardInfoRequested); statsAnalyzer = new DeckListStatisticsAnalyzer(this, deckModel); statsAnalyzer->analyze(); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h index 7314f23ee..2aabbb26a 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h @@ -55,13 +55,11 @@ public: * @param _deckEditor Pointer to the deck editor instance. * @param _deckModel Deck list model. * @param _cardDatabaseModel Card database model. - * @param _cardDatabaseDisplayModel Database display model. */ explicit TabDeckEditorVisualTabWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, DeckListModel *_deckModel, - CardDatabaseModel *_cardDatabaseModel, - CardDatabaseDisplayModel *_cardDatabaseDisplayModel); + CardDatabaseModel *_cardDatabaseModel); /** @brief Add a new tab with a widget and title. */ void addNewTab(QWidget *widget, const QString &title); @@ -119,12 +117,17 @@ signals: void cardClicked(QMouseEvent *event, const ExactCard &card, const QString &zoneName); void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); + void cardAdded(const ExactCard &card, const QString &zoneName); + void cardDecremented(const ExactCard &card, const QString &zoneName); + void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander); + void printingSelectorRequested(); + void cardInfoRequested(const ExactCard &cardName); + private: - QVBoxLayout *layout; ///< Layout for tabs and controls. - AbstractTabDeckEditor *deckEditor; ///< Reference to the deck editor. - DeckListModel *deckModel; ///< Deck list model. - CardDatabaseModel *cardDatabaseModel; ///< Card database model. - CardDatabaseDisplayModel *cardDatabaseDisplayModel; ///< Card database display model. + QVBoxLayout *layout; ///< Layout for tabs and controls. + AbstractTabDeckEditor *deckEditor; ///< Reference to the deck editor. + DeckListModel *deckModel; ///< Deck list model. + CardDatabaseModel *cardDatabaseModel; ///< Card database model. private slots: /** diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index 54705e940..62e1bf5ba 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -1,5 +1,6 @@ #include "visual_database_display_filter_toolbar_widget.h" +#include "../deck_editor/card_database_view.h" #include "visual_database_display_widget.h" #include diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 399b319f7..cc4cce496 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -5,7 +5,7 @@ #include "../../../filters/syntax_help.h" #include "../../pixel_map_generator.h" #include "../cards/card_info_picture_with_text_overlay_widget.h" -#include "../quick_settings/settings_button_widget.h" +#include "../deck_editor/card_database_view.h" #include "../utility/custom_line_edit.h" #include "visual_database_display_color_filter_widget.h" #include "visual_database_display_filter_save_load_widget.h" @@ -23,18 +23,21 @@ #include VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, - AbstractTabDeckEditor *_deckEditor, CardDatabaseModel *database_model, - CardDatabaseDisplayModel *database_display_model, DeckListModel *deckListModel) - : QWidget(parent), deckEditor(_deckEditor), databaseModel(database_model), - databaseDisplayModel(database_display_model) + : QWidget(parent) { debounceTimer = new QTimer(this); debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::onSearchModelChanged); + // Create display model + databaseDisplayModel = new CardDatabaseDisplayModel(this); + databaseDisplayModel->setObjectName("databaseDisplayModel"); + databaseDisplayModel->setSourceModel(database_model); + databaseDisplayModel->setFilterKeyColumn(0); + cards = new QList; connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, &VisualDatabaseDisplayWidget::modelDirty); @@ -61,7 +64,6 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - searchEdit->installEventFilter(&searchKeySignals); setFocusProxy(searchEdit); setFocusPolicy(Qt::ClickFocus); @@ -76,37 +78,25 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, filterModel = new FilterTreeModel(); filterModel->setObjectName("filterModel"); - searchKeySignals.setObjectName("searchKeySignals"); - connect(searchEdit, &SearchLineEdit::textChanged, this, &VisualDatabaseDisplayWidget::updateSearch); + connect(searchEdit, &SearchLineEdit::textChanged, databaseDisplayModel, &CardDatabaseDisplayModel::setStringFilter); - DeckEditorDatabaseDisplayWidget *databaseDisplayWidget = deckEditor->cardDatabaseDockWidget->databaseDisplayWidget; - connect(&searchKeySignals, &KeySignals::onEnter, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlEnter, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlC, databaseDisplayWidget, - &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); - connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - - connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::cardAdded, this, - &VisualDatabaseDisplayWidget::highlightAllSearchEdit); - - databaseView = databaseDisplayWidget->getDatabaseView(); + databaseView = new CardDatabaseView(this, databaseDisplayModel); + databaseView->setObjectName("databaseView"); databaseView->setFocusProxy(searchEdit); databaseView->setItemDelegate(nullptr); databaseView->setVisible(false); searchEdit->setTreeView(databaseView); + searchEdit->installEventFilter(databaseView->getKeySignals()); + + connect(databaseView, &CardDatabaseView::cardChanged, this, &VisualDatabaseDisplayWidget::onSelectedCardChanged); + connect(databaseView, &CardDatabaseView::cardAdded, this, &VisualDatabaseDisplayWidget::actAddCard); + connect(databaseView, &CardDatabaseView::cardDecremented, this, &VisualDatabaseDisplayWidget::actDecrementCard); + connect(databaseView, &CardDatabaseView::edhrecClicked, this, &VisualDatabaseDisplayWidget::edhrecRequested); + connect(databaseView, &CardDatabaseView::selectPrintingClicked, this, + &VisualDatabaseDisplayWidget::printingSelectorRequested); + connect(databaseView, &CardDatabaseView::relatedCardClicked, this, + &VisualDatabaseDisplayWidget::onRelatedCardClicked); colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel); @@ -225,7 +215,7 @@ void VisualDatabaseDisplayWidget::onHover(const ExactCard &hoveredCard) emit cardHoveredDatabaseDisplay(hoveredCard); } -void VisualDatabaseDisplayWidget::addCard(const ExactCard &cardToAdd) +void VisualDatabaseDisplayWidget::addCardToDisplay(const ExactCard &cardToAdd) { cards->append(cardToAdd); auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false); @@ -237,16 +227,6 @@ void VisualDatabaseDisplayWidget::addCard(const ExactCard &cardToAdd) connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor); } -void VisualDatabaseDisplayWidget::updateSearch(const QString &search) const -{ - databaseDisplayModel->setStringFilter(search); - QModelIndexList sel = databaseView->selectionModel()->selectedRows(); - if (sel.isEmpty() && databaseDisplayModel->rowCount()) { - databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), - QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); - } -} - bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const { return !displayModeButton->isChecked(); @@ -268,6 +248,30 @@ void VisualDatabaseDisplayWidget::onSearchModelChanged() } } +void VisualDatabaseDisplayWidget::onSelectedCardChanged(const QString &cardName) +{ + emit cardHoveredDatabaseDisplay(CardDatabaseManager::query()->getPreferredCard(cardName)); +} + +void VisualDatabaseDisplayWidget::actAddCard(const QString &cardName, const QString &zoneName) +{ + highlightAllSearchEdit(); + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardAdded(exactCard, zoneName); +} + +void VisualDatabaseDisplayWidget::actDecrementCard(const QString &cardName, const QString &zoneName) +{ + ExactCard exactCard = CardDatabaseManager::query()->getPreferredCard(cardName); + emit cardDecremented(exactCard, zoneName); +} + +void VisualDatabaseDisplayWidget::onRelatedCardClicked(const QString &relatedCard) +{ + ExactCard exactCard = CardDatabaseManager::query()->guessCard({relatedCard}); + emit cardInfoRequested(exactCard); +} + bool VisualDatabaseDisplayWidget::nearEndOfPage() const { if (!flowWidget->isVisible()) { @@ -334,12 +338,12 @@ void VisualDatabaseDisplayWidget::loadPage(int start, int end) for (const CardFilter *setFilter : setFilters) { if (setMap.contains(setFilter->term())) { for (PrintingInfo printing : setMap[setFilter->term()]) { - addCard(ExactCard(info, printing)); + addCardToDisplay(ExactCard(info, printing)); } } } } else { - addCard(CardDatabaseManager::query()->getPreferredCard(info)); + addCardToDisplay(CardDatabaseManager::query()->getPreferredCard(info)); } } else { qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!"; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index 61dcea487..a383e8ead 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -34,9 +34,7 @@ class VisualDatabaseDisplayWidget : public QWidget public: explicit VisualDatabaseDisplayWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, CardDatabaseModel *database_model, - CardDatabaseDisplayModel *database_display_model, DeckListModel *deckListModel = nullptr); void retranslateUi(); @@ -53,7 +51,7 @@ public: return databaseDisplayModel; } - QTreeView *getDatabaseView() + CardDatabaseView *getDatabaseView() { return databaseView; } @@ -75,16 +73,26 @@ signals: void cardClickedDatabaseDisplay(QMouseEvent *event, const ExactCard &card); void cardHoveredDatabaseDisplay(const ExactCard &hoveredCard); + void cardAdded(const ExactCard &card, const QString &zoneName); + void cardDecremented(const ExactCard &card, const QString &zoneName); + void edhrecRequested(const CardInfoPtr &cardInfo, bool isCommander); + void printingSelectorRequested(); + void cardInfoRequested(const ExactCard &cardName); + protected slots: void initialize(); void onClick(QMouseEvent *event, const ExactCard &card); void onHover(const ExactCard &hoveredCard); - void addCard(const ExactCard &cardToAdd); + void addCardToDisplay(const ExactCard &cardToAdd); void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void modelDirty() const; - void updateSearch(const QString &search) const; void onDisplayModeChanged(bool checked); + void onSelectedCardChanged(const QString &cardName); + void actAddCard(const QString &cardName, const QString &zoneName); + void actDecrementCard(const QString &cardName, const QString &zoneName); + void onRelatedCardClicked(const QString &relatedCard); + private: FlowWidget *searchContainer; SearchLineEdit *searchEdit; @@ -96,11 +104,8 @@ private: QToolButton *clearFilterWidget; VisualDatabaseDisplayFilterToolbarWidget *filterContainer; - KeySignals searchKeySignals; - AbstractTabDeckEditor *deckEditor; - CardDatabaseModel *databaseModel; CardDatabaseDisplayModel *databaseDisplayModel; - QTreeView *databaseView; + CardDatabaseView *databaseView; QList *cards; QVBoxLayout *mainLayout; QScrollArea *scrollArea; From 23da49ee5b29ff09e65a4fcd3e8388a4b23d1175 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:11:02 +0200 Subject: [PATCH 11/50] [Game] [Arrows] Use arrowData/registry and generate unique server-side ids (#6973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Game] [Arrows] Track creatorId, use arrowData in arrowItem, use registry, generate unique arrow id's on server side and delete-on-exist inserts. Took 2 minutes Took 1 minute * Fix emitting slot instead of signal. Took 15 minutes * Clear arrows locally in special circumstances i.e. teardown. Took 28 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 1 + cockatrice/src/game/arrow_registry.cpp | 48 +++++++++++ cockatrice/src/game/arrow_registry.h | 43 ++++++++++ cockatrice/src/game/board/arrow_data.cpp | 4 +- cockatrice/src/game/board/arrow_data.h | 20 ++--- cockatrice/src/game/board/arrow_item.cpp | 35 +++++--- cockatrice/src/game/board/arrow_item.h | 37 ++++----- cockatrice/src/game/game_event_handler.cpp | 11 +-- cockatrice/src/game/game_event_handler.h | 6 +- cockatrice/src/game/game_scene.cpp | 80 ++++++++----------- cockatrice/src/game/game_scene.h | 13 +-- .../src/game/player/player_event_handler.cpp | 29 +++---- cockatrice/src/game/player/player_logic.cpp | 7 +- cockatrice/src/game/player/player_logic.h | 8 +- .../src/interface/widgets/tabs/tab_game.cpp | 7 +- .../remote/game/server_abstract_player.cpp | 16 +--- .../remote/game/server_abstract_player.h | 1 - .../server/remote/game/server_game.cpp | 5 ++ .../network/server/remote/game/server_game.h | 2 + 19 files changed, 225 insertions(+), 148 deletions(-) create mode 100644 cockatrice/src/game/arrow_registry.cpp create mode 100644 cockatrice/src/game/arrow_registry.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index ee0102ee9..028161ee0 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -56,6 +56,7 @@ set(cockatrice_SOURCES src/filters/filter_tree_model.cpp src/filters/syntax_help.cpp src/game/abstract_game.cpp + src/game/arrow_registry.cpp src/game/board/abstract_card_drag_item.cpp src/game/board/abstract_card_item.cpp src/game/board/abstract_counter.cpp diff --git a/cockatrice/src/game/arrow_registry.cpp b/cockatrice/src/game/arrow_registry.cpp new file mode 100644 index 000000000..e679d2972 --- /dev/null +++ b/cockatrice/src/game/arrow_registry.cpp @@ -0,0 +1,48 @@ +#include "arrow_registry.h" + +#include "board/arrow_item.h" + +void ArrowRegistry::insert(QSharedPointer data, ArrowItem *arrow) +{ + const ArrowKey key{data->creatorId, data->id}; + + if (auto *existing = take(data->creatorId, data->id)) { + existing->delArrow(); + } + + dataStore.insert(key, data); + items.insert(key, arrow); + byPlayer[data->creatorId].insert(data->id); +} + +ArrowItem *ArrowRegistry::take(int creatorId, int arrowId) +{ + const ArrowKey key{creatorId, arrowId}; + dataStore.remove(key); + auto &playerSet = byPlayer[creatorId]; + playerSet.remove(arrowId); + if (playerSet.isEmpty()) { + byPlayer.remove(creatorId); + } + return items.take(key); +} + +ArrowItem *ArrowRegistry::get(int creatorId, int arrowId) const +{ + return items.value(ArrowKey{creatorId, arrowId}, nullptr); +} + +bool ArrowRegistry::contains(int creatorId, int arrowId) const +{ + return items.contains(ArrowKey{creatorId, arrowId}); +} + +QSet ArrowRegistry::idsForPlayer(int playerId) const +{ + return byPlayer.value(playerId); +} + +QList ArrowRegistry::all() const +{ + return items.values(); +} \ No newline at end of file diff --git a/cockatrice/src/game/arrow_registry.h b/cockatrice/src/game/arrow_registry.h new file mode 100644 index 000000000..ef98229a2 --- /dev/null +++ b/cockatrice/src/game/arrow_registry.h @@ -0,0 +1,43 @@ +#ifndef COCKATRICE_ARROW_REGISTRY_H +#define COCKATRICE_ARROW_REGISTRY_H + +#include "board/arrow_data.h" + +#include +#include +#include + +class ArrowItem; + +struct ArrowKey +{ + int creatorId; + int arrowId; + + bool operator<(const ArrowKey &other) const + { + if (creatorId != other.creatorId) { + return creatorId < other.creatorId; + } + return arrowId < other.arrowId; + } +}; + +class ArrowRegistry +{ +public: + void insert(QSharedPointer data, ArrowItem *arrow); + ArrowItem *take(int creatorId, int arrowId); + + [[nodiscard]] ArrowItem *get(int creatorId, int arrowId) const; + [[nodiscard]] bool contains(int creatorId, int arrowId) const; + [[nodiscard]] QSet idsForPlayer(int playerId) const; + [[nodiscard]] QList all() const; + +private: + QMap> dataStore; + QMap items; + QMap> byPlayer; +}; + +#endif \ No newline at end of file diff --git a/cockatrice/src/game/board/arrow_data.cpp b/cockatrice/src/game/board/arrow_data.cpp index bbb70f474..9e89deed0 100644 --- a/cockatrice/src/game/board/arrow_data.cpp +++ b/cockatrice/src/game/board/arrow_data.cpp @@ -1,8 +1,10 @@ #include "arrow_data.h" -ArrowData ArrowData::fromProto(const ServerInfo_Arrow &arrow) +ArrowData ArrowData::fromProto(const ServerInfo_Arrow &arrow, int creatorId, bool isLocalCreator) { ArrowData data; + data.creatorId = creatorId; + data.isLocalCreator = isLocalCreator; data.id = arrow.id(); data.startPlayerId = arrow.start_player_id(); data.startZone = QString::fromStdString(arrow.start_zone()); diff --git a/cockatrice/src/game/board/arrow_data.h b/cockatrice/src/game/board/arrow_data.h index a8b35dad6..2752f97e3 100644 --- a/cockatrice/src/game/board/arrow_data.h +++ b/cockatrice/src/game/board/arrow_data.h @@ -8,16 +8,18 @@ struct ArrowData { - int id; - int startPlayerId; - QString startZone; - int startCardId; - int targetPlayerId; - QString targetZone; // empty = targeting a player - int targetCardId = -1; // -1 = targeting a player - QColor color; + int creatorId = -1; + bool isLocalCreator = false; + int id = -1; + int startPlayerId = -1; + QString startZone = ""; + int startCardId = -1; + int targetPlayerId = -1; + QString targetZone = ""; + int targetCardId = -1; + QColor color = ""; - static ArrowData fromProto(const ServerInfo_Arrow &arrow); + static ArrowData fromProto(const ServerInfo_Arrow &arrow, int creatorId, bool isLocalCreator); bool isPlayerTargeted() const { diff --git a/cockatrice/src/game/board/arrow_item.cpp b/cockatrice/src/game/board/arrow_item.cpp index 430477d76..0b740bc70 100644 --- a/cockatrice/src/game/board/arrow_item.cpp +++ b/cockatrice/src/game/board/arrow_item.cpp @@ -21,12 +21,8 @@ #include #include -ArrowItem::ArrowItem(PlayerLogic *_player, - int _id, - ArrowTarget *_startItem, - ArrowTarget *_targetItem, - const QColor &_color) - : player(_player), id(_id), startItem(_startItem), targetItem(_targetItem), color(_color) +ArrowItem::ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem) + : data(std::move(_data)), startItem(_startItem), targetItem(_targetItem) { setZValue(ZValues::ARROWS); @@ -52,7 +48,7 @@ ArrowItem::ArrowItem(PlayerLogic *_player, void ArrowItem::onTargetDestroyed() { - emit requestDeletion(id); + emit requestDeletion(data->creatorId, data->id); } void ArrowItem::delArrow() @@ -130,7 +126,7 @@ void ArrowItem::updatePath(const QPointF &endPoint) void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { - QColor paintColor(color); + QColor paintColor(data->color); if (fullColor) { paintColor.setAlpha(200); } else { @@ -142,7 +138,7 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if (!player->getPlayerInfo()->getLocal()) { + if (!data->isLocalCreator) { event->ignore(); return; } @@ -156,14 +152,20 @@ void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event) event->accept(); if (event->button() == Qt::RightButton) { - emit requestDeletion(id); + emit requestDeletion(data->creatorId, data->id); } } // ArrowDragItem ArrowDragItem::ArrowDragItem(PlayerLogic *_owner, ArrowTarget *_startItem, const QColor &_color, int _deleteInPhase) - : ArrowItem(_owner, -1, _startItem, nullptr, _color), deleteInPhase(_deleteInPhase) + : ArrowItem(QSharedPointer::create(ArrowData{.creatorId = _owner->getPlayerInfo()->getId(), + .isLocalCreator = true, + .id = -1, + .color = _color}), + _startItem, + nullptr), + player(_owner), deleteInPhase(_deleteInPhase) { } @@ -238,7 +240,7 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) CardZoneLogic *startZone = startCard->getZone(); Command_CreateArrow cmd; - cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(color)); + cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(data->color)); cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId()); cmd.set_start_zone(startZone->getName().toStdString()); cmd.set_start_card_id(startCard->getId()); @@ -284,7 +286,14 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) // ArrowAttachItem ArrowAttachItem::ArrowAttachItem(ArrowTarget *_startItem) - : ArrowItem(_startItem->getOwner(), -1, _startItem, nullptr, Qt::green) + : ArrowItem( + QSharedPointer::create(ArrowData{.creatorId = _startItem->getOwner()->getPlayerInfo()->getId(), + .isLocalCreator = true, + .id = -1, + .color = Qt::green}), + _startItem, + nullptr), + player(_startItem->getOwner()) { } diff --git a/cockatrice/src/game/board/arrow_item.h b/cockatrice/src/game/board/arrow_item.h index 7dc0f9477..0c04c27f8 100644 --- a/cockatrice/src/game/board/arrow_item.h +++ b/cockatrice/src/game/board/arrow_item.h @@ -1,20 +1,15 @@ -/** - * @file arrow_item.h - * @ingroup GameGraphics - */ -//! \todo Document this file. - #ifndef ARROWITEM_H #define ARROWITEM_H +#include "arrow_data.h" #include "arrow_target.h" #include #include +#include class CardItem; class QGraphicsSceneMouseEvent; -class QMenu; class PlayerLogic; class ArrowItem : public QObject, public QGraphicsItem @@ -22,25 +17,27 @@ class ArrowItem : public QObject, public QGraphicsItem Q_OBJECT Q_INTERFACES(QGraphicsItem) signals: - void requestDeletion(int id); + void requestDeletion(int creatorId, int id); private: QPainterPath path; protected: - PlayerLogic *player; - int id; + QSharedPointer data; QPointer startItem; QPointer targetItem; bool targetLocked = false; - QColor color; bool fullColor = true; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; public: - ArrowItem(PlayerLogic *_player, int _id, ArrowTarget *_startItem, ArrowTarget *_targetItem, const QColor &_color); + ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem); + void onTargetDestroyed(); + void delArrow(); + void updatePath(); + void updatePath(const QPointF &endPoint); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; [[nodiscard]] QRectF boundingRect() const override @@ -51,17 +48,13 @@ public: { return path; } - - void updatePath(); - void updatePath(const QPointF &endPoint); - [[nodiscard]] int getId() const { - return id; + return data->id; } - [[nodiscard]] PlayerLogic *getPlayer() const + [[nodiscard]] int getCreatorId() const { - return player; + return data->creatorId; } [[nodiscard]] ArrowTarget *getStartItem() const { @@ -75,14 +68,13 @@ public: { targetLocked = _targetLocked; } - - void delArrow(); }; class ArrowDragItem : public ArrowItem { Q_OBJECT private: + PlayerLogic *player; int deleteInPhase; QList childArrows; QMetaObject::Connection positionConnection; @@ -100,6 +92,7 @@ class ArrowAttachItem : public ArrowItem { Q_OBJECT private: + PlayerLogic *player; QList childArrows; QMetaObject::Connection positionConnection; void attachCards(CardItem *startCard, const CardItem *targetCard); @@ -113,4 +106,4 @@ protected: void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; }; -#endif // ARROWITEM_H +#endif \ No newline at end of file diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index cff80a1ec..629e2f6a1 100644 --- a/cockatrice/src/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -213,23 +213,24 @@ void GameEventHandler::handleChatMessageSent(const QString &chatMessage) sendGameCommand(cmd); } -void GameEventHandler::handleArrowDeletion(int arrowId) +void GameEventHandler::handleArrowDeletion(int creatorId, int arrowId) { Command_DeleteArrow cmd; cmd.set_arrow_id(arrowId); auto preparedCommand = prepareGameCommand(cmd); - connect(preparedCommand, &PendingCommand::finished, this, - [arrowId, this](const Response &response) { handleArrowDeletionFinished(response, arrowId); }); + connect(preparedCommand, &PendingCommand::finished, this, [creatorId, arrowId, this](const Response &response) { + handleArrowDeletionFinished(response, creatorId, arrowId); + }); sendGameCommand(preparedCommand); } -void GameEventHandler::handleArrowDeletionFinished(const Response &response, int arrowId) +void GameEventHandler::handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId) { if (response.response_code() == Response::RespNameNotFound) { - emit arrowDeleted(arrowId); + emit arrowDeleted(creatorId, arrowId); } } diff --git a/cockatrice/src/game/game_event_handler.h b/cockatrice/src/game/game_event_handler.h index bc4812aa4..f47116949 100644 --- a/cockatrice/src/game/game_event_handler.h +++ b/cockatrice/src/game/game_event_handler.h @@ -60,8 +60,8 @@ public: void handleActivePhaseChanged(int phase); void handleGameLeft(); void handleChatMessageSent(const QString &chatMessage); - void handleArrowDeletion(int arrowId); - void handleArrowDeletionFinished(const Response &response, int arrowId); + void handleArrowDeletion(int creatorId, int arrowId); + void handleArrowDeletionFinished(const Response &response, int creatorId, int arrowId); void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context); void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context); @@ -113,7 +113,7 @@ signals: void containerProcessingStarted(GameEventContext context); void setContextJudgeName(QString judgeName); void containerProcessingDone(); - void arrowDeleted(int arrowId); + void arrowDeleted(int creatorId, int arrowId); void logSpectatorSay(ServerInfo_User userInfo, QString message); void logSpectatorLeave(QString name, QString reason); void logGameStart(); diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 1b4f0d461..867869a3f 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -96,8 +96,8 @@ void GameScene::addPlayer(PlayerLogic *player) connect(player, &PlayerLogic::arrowDeleted, this, &GameScene::deleteArrow); connect(player, &PlayerLogic::arrowCreateRequested, this, &GameScene::addArrow); connect(player, &PlayerLogic::arrowDeleteRequested, this, &GameScene::requestArrowDeletion); - connect(player, &PlayerLogic::arrowsCleared, this, - [this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayer(id); }); + connect(player, &PlayerLogic::arrowsClearedLocally, this, + [this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayerLocally(id); }); connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardZoneChanged, this, &GameScene::onCardZoneChanged); @@ -367,86 +367,60 @@ void GameScene::resizeColumnsAndPlayers(const QList &minWidthByColumn, qr } } -void GameScene::addArrow(const ArrowData &data) +void GameScene::addArrow(QSharedPointer data) { - auto *startView = playerViews.value(data.startPlayerId); - auto *targetView = playerViews.value(data.targetPlayerId); + auto *startView = playerViews.value(data->startPlayerId); + auto *targetView = playerViews.value(data->targetPlayerId); if (!startView || !targetView) { return; } - PlayerLogic *startLogic = startView->getPlayer(); - auto *startZone = startLogic->getZones().value(data.startZone); + auto *startZone = startView->getPlayer()->getZones().value(data->startZone); if (!startZone) { return; } - CardItem *startCard = startZone->getCard(data.startCardId); + CardItem *startCard = startZone->getCard(data->startCardId); if (!startCard) { return; } ArrowTarget *targetItem = nullptr; - if (data.isPlayerTargeted()) { + if (data->isPlayerTargeted()) { targetItem = targetView->getPlayerTarget(); } else { - auto *zone = targetView->getPlayer()->getZones().value(data.targetZone); - if (zone) { - targetItem = zone->getCard(data.targetCardId); + if (auto *zone = targetView->getPlayer()->getZones().value(data->targetZone)) { + targetItem = zone->getCard(data->targetCardId); } } if (!targetItem) { return; } - auto *arrow = new ArrowItem(startView->getPlayer(), data.id, startCard, targetItem, data.color); + auto *arrow = new ArrowItem(data, startCard, targetItem); addItem(arrow); - arrowRegistry.insert(data.id, arrow); + arrowRegistry.insert(data, arrow); connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion); } -void GameScene::deleteArrow(int arrowId) +void GameScene::deleteArrow(int playerId, int arrowId) { - if (arrowRegistry.contains(arrowId)) { - arrowRegistry.take(arrowId)->delArrow(); + if (auto *arrow = arrowRegistry.take(playerId, arrowId)) { + arrow->delArrow(); } } -void GameScene::clearArrowsForPlayer(int playerId) +void GameScene::requestArrowDeletion(int playerId, int arrowId) { - QList toDelete; - for (auto i = arrowRegistry.cbegin(); i != arrowRegistry.cend(); ++i) { - auto *arrow = i.value(); - if (arrow->getPlayer()->getPlayerInfo()->getId() == playerId) { - toDelete.append(i.key()); - } - } - - for (int arrowId : toDelete) { - arrowRegistry.take(arrowId)->delArrow(); - } -} - -void GameScene::requestArrowDeletion(int arrowId) -{ - if (arrowRegistry.contains(arrowId)) { - emit arrowDeletionRequested(arrowId); - } -} - -void GameScene::requestClearArrowsForPlayer(int playerId) -{ - for (auto *arrow : arrowRegistry.values()) { - if (arrow->getPlayer()->getPlayerInfo()->getId() == playerId) { - emit requestArrowDeletion(arrow->getId()); - } + if (arrowRegistry.contains(playerId, arrowId)) { + emit arrowDeletionRequested(playerId, arrowId); } } void GameScene::onCardZoneChanged(CardItem *card, bool sameZone) { QList toDelete; - for (auto *arrow : arrowRegistry.values()) { + for (auto *arrow : arrowRegistry.all()) { if (arrow->getStartItem() == card || arrow->getTargetItem() == card) { if (sameZone) { arrow->updatePath(); @@ -456,7 +430,21 @@ void GameScene::onCardZoneChanged(CardItem *card, bool sameZone) } } for (auto *arrow : toDelete) { - deleteArrow(arrow->getId()); + deleteArrow(arrow->getCreatorId(), arrow->getId()); + } +} + +void GameScene::clearArrowsForPlayer(int playerId) +{ + for (int arrowId : arrowRegistry.idsForPlayer(playerId)) { + emit requestArrowDeletion(playerId, arrowId); + } +} + +void GameScene::clearArrowsForPlayerLocally(int playerId) +{ + for (int arrowId : arrowRegistry.idsForPlayer(playerId)) { + arrowRegistry.take(playerId, arrowId)->delArrow(); } } diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h index 1551c8365..567089fc0 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game/game_scene.h @@ -1,6 +1,7 @@ #ifndef GAMESCENE_H #define GAMESCENE_H +#include "arrow_registry.h" #include "board/arrow_data.h" #include "board/arrow_item.h" #include "zones/card_zone_logic.h" @@ -45,7 +46,7 @@ private: PhasesToolbar *phasesToolbar; ///< Toolbar showing game phases QMap playerViews; ///< ID lookup for player graphics items QList> playersByColumn; ///< Players organized by column - QMap arrowRegistry; ///< ID registry for arrow graphics items + ArrowRegistry arrowRegistry; ///< ID registry for arrow graphics items QList zoneViews; ///< Active zone view widgets QSize viewSize; ///< Current view size QPointer hoveredCard; ///< Currently hovered card @@ -202,13 +203,13 @@ public slots: QTransform getViewportTransform() const; /// Directly modifies the scene - void addArrow(const ArrowData &data); - void deleteArrow(int arrowId); + void addArrow(QSharedPointer data); + void deleteArrow(int playerId, int arrowId); void clearArrowsForPlayer(int playerId); + void clearArrowsForPlayerLocally(int playerId); /// Queues up arrow deletion but doesn't directly modify the scene - void requestArrowDeletion(int arrowId); - void requestClearArrowsForPlayer(int playerId); + void requestArrowDeletion(int playerId, int arrowId); void onCardZoneChanged(CardItem *card, bool sameZone); @@ -223,7 +224,7 @@ signals: void sigStartRubberBand(const QPointF &selectionOrigin); void sigResizeRubberBand(const QPointF &cursorPoint, int selectedCount); void sigStopRubberBand(); - void arrowDeletionRequested(int arrowId); + void arrowDeletionRequested(int creatorId, int arrowId); }; #endif diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index 3a7d0345b..debc6c8f7 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -92,26 +92,24 @@ void PlayerEventHandler::eventRollDie(const Event_RollDie &event) void PlayerEventHandler::eventCreateArrow(const Event_CreateArrow &event) { - const ArrowData data = ArrowData::fromProto(event.arrow_info()); + auto data = QSharedPointer::create(ArrowData::fromProto( + event.arrow_info(), player->getPlayerInfo()->getId(), player->getPlayerInfo()->getLocal())); - // Resolve names for logging const auto &playerList = player->getGame()->getPlayerManager()->getPlayers(); - PlayerLogic *startPlayer = playerList.value(data.startPlayerId); - PlayerLogic *targetPlayer = playerList.value(data.targetPlayerId); + PlayerLogic *startPlayer = playerList.value(data->startPlayerId); + PlayerLogic *targetPlayer = playerList.value(data->targetPlayerId); QString startCardName, targetCardName; if (startPlayer) { - auto *zone = startPlayer->getZones().value(data.startZone); - if (zone) { - if (auto *card = zone->getCard(data.startCardId)) { + if (auto *zone = startPlayer->getZones().value(data->startZone)) { + if (auto *card = zone->getCard(data->startCardId)) { startCardName = card->getName(); } } } - if (!data.isPlayerTargeted() && targetPlayer) { - auto *zone = targetPlayer->getZones().value(data.targetZone); - if (zone) { - if (auto *card = zone->getCard(data.targetCardId)) { + if (!data->isPlayerTargeted() && targetPlayer) { + if (auto *zone = targetPlayer->getZones().value(data->targetZone)) { + if (auto *card = zone->getCard(data->targetCardId)) { targetCardName = card->getName(); } } @@ -119,16 +117,15 @@ void PlayerEventHandler::eventCreateArrow(const Event_CreateArrow &event) emit player->arrowCreateRequested(data); - const bool validForLogging = !startCardName.isEmpty() && (data.isPlayerTargeted() || !targetCardName.isEmpty()); - - if (startPlayer && targetPlayer && validForLogging) { - emit logCreateArrow(player, startPlayer, startCardName, targetPlayer, targetCardName, data.isPlayerTargeted()); + if (startPlayer && targetPlayer && !startCardName.isEmpty() && + (data->isPlayerTargeted() || !targetCardName.isEmpty())) { + emit logCreateArrow(player, startPlayer, startCardName, targetPlayer, targetCardName, data->isPlayerTargeted()); } } void PlayerEventHandler::eventDeleteArrow(const Event_DeleteArrow &event) { - emit player->arrowDeleted(event.arrow_id()); + emit player->arrowDeleted(player->getPlayerInfo()->getId(), event.arrow_id()); } void PlayerEventHandler::eventCreateToken(const Event_CreateToken &event) diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 67c6e9519..0210aa0c6 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -74,7 +74,7 @@ PlayerLogic::~PlayerLogic() void PlayerLogic::clear() { - emit arrowsCleared(); + emit arrowsClearedLocally(); QMapIterator i(zones); while (i.hasNext()) { @@ -115,7 +115,7 @@ void PlayerLogic::processPlayerInfo(const ServerInfo_Player &info) /* HandZone */ ZoneNames::HAND}; clearCounters(); - emit arrowsCleared(); + emit arrowsClearedLocally(); QMutableMapIterator zoneIt(zones); while (zoneIt.hasNext()) { @@ -231,7 +231,8 @@ void PlayerLogic::processCardAttachment(const ServerInfo_Player &info) const int arrowListSize = info.arrow_list_size(); for (int i = 0; i < arrowListSize; ++i) { - emit arrowCreateRequested(ArrowData::fromProto(info.arrow_list(i))); + emit arrowCreateRequested(QSharedPointer::create( + ArrowData::fromProto(info.arrow_list(i), getPlayerInfo()->getId(), getPlayerInfo()->getLocal()))); } } diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index 20d7597b4..c3508d069 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -78,10 +78,10 @@ signals: void clearCustomZonesMenu(); void addViewCustomZoneActionToCustomZoneMenu(QString zoneName); void resetTopCardMenuActions(); - void arrowCreateRequested(ArrowData data); - void arrowDeleteRequested(int arrowId); - void arrowDeleted(int arrowId); - void arrowsCleared(); // fires on clear() and processPlayerInfo + void arrowCreateRequested(QSharedPointer data); + void arrowDeleteRequested(int creatorId, int arrowId); + void arrowDeleted(int creatorId, int arrowId); + void arrowsClearedLocally(); // fires on clear() and processPlayerInfo public slots: void setActive(bool _active); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 9fc123a8c..fb9c81f75 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -608,7 +608,7 @@ void TabGame::actRemoveLocalArrows() { auto *local = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); if (local) { - scene->requestClearArrowsForPlayer(local->getPlayerInfo()->getId()); + scene->clearArrowsForPlayer(local->getPlayerInfo()->getId()); } } @@ -895,11 +895,6 @@ void TabGame::newCardAdded(AbstractCardItem *card) connect(card, &AbstractCardItem::showCardInfoPopup, this, &TabGame::showCardInfoPopup); connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString))); connect(card, &AbstractCardItem::cardShiftClicked, this, &TabGame::linkCardToChat); - CardItem *cardItem = qobject_cast(card); - if (cardItem) { - connect(cardItem->getState(), &CardState::zoneChanged, scene, - [this, cardItem]() { scene->onCardZoneChanged(cardItem, false); }); - } } QString TabGame::getTabText() const diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index 3e489233c..157fa6441 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -81,17 +81,6 @@ int Server_AbstractPlayer::newCardId() return nextCardId++; } -int Server_AbstractPlayer::newArrowId() const -{ - int id = 0; - for (Server_Arrow *a : arrows) { - if (a->getId() > id) { - id = a->getId(); - } - } - return id + 1; -} - void Server_AbstractPlayer::setupZones() { nextCardId = 0; @@ -1144,7 +1133,7 @@ Server_AbstractPlayer::cmdCreateToken(const Command_CreateToken &cmd, ResponseCo Event_CreateArrow createEvent; ServerInfo_Arrow *arrowInfo = createEvent.mutable_arrow_info(); - const int newId = player->newArrowId(); + const int newId = game->generateArrowId(); arrow->setId(newId); arrowInfo->set_id(newId); arrowInfo->set_start_player_id(player->getPlayerId()); @@ -1267,7 +1256,8 @@ Server_AbstractPlayer::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseCo int currentPhase = game->getActivePhase(); int deletionPhase = cmd.has_delete_in_phase() ? cmd.delete_in_phase() : currentPhase; - auto arrow = new Server_Arrow(newArrowId(), startCard, targetItem, cmd.arrow_color(), currentPhase, deletionPhase); + auto arrow = new Server_Arrow(game->generateArrowId(), startCard, targetItem, cmd.arrow_color(), currentPhase, + deletionPhase); addArrow(arrow); Event_CreateArrow event; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h index 9d9809298..85fbc0557 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -74,7 +74,6 @@ public: } int newCardId(); - int newArrowId() const; void addZone(Server_CardZone *zone); void addArrow(Server_Arrow *arrow); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 50fff4812..4761199e5 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -697,6 +697,11 @@ void Server_Game::setActivePhase(int newPhase) sendGameEventContainer(prepareGameEvent(event, -1)); } +qint64 Server_Game::generateArrowId() +{ + return nextArrowId++; +} + void Server_Game::removeArrows(int newPhase, bool force) { QMutexLocker locker(&gameMutex); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 1c658f2ba..e0e7896b7 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -49,6 +49,7 @@ class Server_Game : public QObject private: Server_Room *room; int nextPlayerId; + std::atomic nextArrowId = 1; int hostId; ServerInfo_User *creatorInfo; QMap participants; @@ -196,6 +197,7 @@ public: } void setActivePlayer(int newPlayer); void setActivePhase(int newPhase); + qint64 generateArrowId(); void removeArrows(int newPhase, bool force = false); void nextTurn(); int getSecondsElapsed() const From 20cdcdb382108b2791c70acd0d50c0d5aec6f4f0 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:39:31 -0700 Subject: [PATCH 12/50] [ReplayManager] Refactor to send replayed events through signal (#6979) * [ReplayManager] Refactor to send replayed events through signal * remove blank * pass by const auto ref --- cockatrice/src/interface/widgets/replay/replay_manager.cpp | 3 +-- cockatrice/src/interface/widgets/replay/replay_manager.h | 1 + cockatrice/src/interface/widgets/tabs/tab_game.cpp | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index 525b703db..a1330a82d 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -98,8 +98,7 @@ ReplayManager::ReplayManager(TabGame *parent, GameReplay *_replay) void ReplayManager::replayNextEvent(EventProcessingOptions options) { - game->getGame()->getGameEventHandler()->processGameEventContainer( - replay->event_list(timelineWidget->getCurrentEvent()), nullptr, options); + emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options); } void ReplayManager::replayFinished() diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index d67ae5a90..a3e0126c7 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -27,6 +27,7 @@ public: signals: void requestChatAndPhaseReset(); + void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); private: // Replay related members diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index fb9c81f75..c52f73319 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -1169,6 +1169,11 @@ void TabGame::createReplayDock(GameReplay *replay) QDockWidget::DockWidgetMovable); replayDock->setWidget(replayManager); replayDock->setFloating(false); + + connect(replayManager, &ReplayManager::eventReplayed, game->getGameEventHandler(), + [this](const auto &event, auto options) { + game->getGameEventHandler()->processGameEventContainer(event, nullptr, options); + }); } void TabGame::createDeckViewContainerWidget(bool bReplay) From dc152e89f7847a91b9ea42d5b857701427642e4a Mon Sep 17 00:00:00 2001 From: tooomm Date: Mon, 8 Jun 2026 19:19:28 +0200 Subject: [PATCH 13/50] CI: Print colored diff for lint check (#6975) * print colored diff in gha * use spaces --- .ci/lint_cpp.sh | 24 ++---------------------- format.sh | 8 ++++---- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/.ci/lint_cpp.sh b/.ci/lint_cpp.sh index cfb1e1f07..9786a83fc 100755 --- a/.ci/lint_cpp.sh +++ b/.ci/lint_cpp.sh @@ -13,17 +13,9 @@ fi # Check formatting using format.sh echo "Checking your code using format.sh..." -diff="$(./format.sh --diff --cmake --shell --print-version --branch origin/master)" +./format.sh --color-diff --cmake --shell --print-version --branch origin/master err=$? -sep=" ----------- -" -used_version="${diff%%"$sep"*}" -diff="${diff#*"$sep"}" -changes_to_make="${diff%%"$sep"*}" -files_to_edit="${diff#*"$sep"}" - case $err in 1) cat < Date: Mon, 8 Jun 2026 19:37:50 +0200 Subject: [PATCH 14/50] CI: Cleanup (#6959) * Label & variables * fix bracket * other workflows * fix trailing whitespace * fixes --- .github/workflows/desktop-build.yml | 350 +++++++++++----------- .github/workflows/desktop-lint.yml | 13 +- .github/workflows/docker-release.yml | 46 +-- .github/workflows/documentation-build.yml | 22 +- .github/workflows/translations-pull.yml | 35 ++- .github/workflows/translations-push.yml | 37 ++- 6 files changed, 255 insertions(+), 248 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 62108b34a..19c9a15e3 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -1,10 +1,10 @@ name: Build Desktop permissions: + actions: write # needed to delete entries in GHA cache (update ccache) + attestations: write # needed to persist the attestation. contents: write - id-token: write - attestations: write - actions: write # needed for ccache action to be able to delete gha caches + id-token: write # needed for signing certificate in attestation on: push: @@ -19,7 +19,7 @@ on: - '.github/workflows/desktop-build.yml' - 'CMakeLists.txt' - 'vcpkg.json' - - 'vcpkg' + - 'vcpkg' # needed to match submodule bumps (gitlink) tags: - '*' pull_request: @@ -32,7 +32,7 @@ on: - '.github/workflows/desktop-build.yml' - 'CMakeLists.txt' - 'vcpkg.json' - - 'vcpkg' + - 'vcpkg' # needed to match submodule bumps (gitlink) # Cancel earlier, unfinished runs of this workflow on the same branch (unless on release) concurrency: @@ -44,11 +44,11 @@ jobs: name: Configure runs-on: ubuntu-slim outputs: - tag: ${{steps.configure.outputs.tag}} - sha: ${{steps.configure.outputs.sha}} + tag: ${{ steps.configure.outputs.tag }} + sha: ${{ steps.configure.outputs.sha }} steps: - - name: Configure + - name: "Configure" id: configure shell: bash run: | @@ -64,146 +64,150 @@ jobs: fi echo "sha=$sha" >>"$GITHUB_OUTPUT" - - name: Checkout + - name: "Checkout" if: steps.configure.outputs.tag != null uses: actions/checkout@v6 with: - fetch-depth: 0 + fetch-depth: 0 # fetch all history for all branches and tags - - name: Prepare release parameters + - name: "Prepare release parameters" id: prepare if: steps.configure.outputs.tag != null shell: bash env: - TAG: ${{steps.configure.outputs.tag}} + TAG: ${{ steps.configure.outputs.tag }} run: .ci/prep_release.sh - - name: Create release + - name: "Create release" if: steps.configure.outputs.tag != null id: create_release shell: bash env: - GH_TOKEN: ${{github.token}} - tag_name: ${{steps.configure.outputs.tag}} - target: ${{steps.configure.outputs.sha}} - release_name: ${{steps.prepare.outputs.title}} - body_path: ${{steps.prepare.outputs.body_path}} - prerelease: ${{steps.prepare.outputs.is_beta}} + GH_TOKEN: ${{ github.token }} + tag_name: ${{ steps.configure.outputs.tag }} + target: ${{ steps.configure.outputs.sha }} + release_name: ${{ steps.prepare.outputs.title }} + body_path: ${{ steps.prepare.outputs.body_path }} + prerelease: ${{ steps.prepare.outputs.is_beta }} run: | - if [[ $prerelease == yes ]]; then - args="--prerelease" - fi - gh release create "$tag_name" --draft --verify-tag $args \ - --target "$target" --title "$release_name" \ - --notes-file "$body_path" + args=() + [[ $prerelease == yes ]] && args+=(--prerelease) + + gh release create "$tag_name" --verify-tag --draft "${args[@]}" \ + --target "$target" \ + --title "$release_name" \ + --notes-file "$body_path" build-linux: strategy: fail-fast: false matrix: - # These names correspond to the files in ".ci/$distro$version" + # The files in ".ci/$distro$version" correspond to the values given here include: - distro: Arch - package: skip # We are packaged in Arch already + allow-failure: yes + package: skip # We are packaged in Arch already - distro: Servatrice_Debian version: 12 + package: DEB - test: skip server_only: yes + test: skip - distro: Debian version: 12 + package: DEB test: skip # Running tests on all distros is superfluous - distro: Debian version: 13 + package: DEB - distro: Fedora version: 43 + package: RPM test: skip # Running tests on all distros is superfluous - distro: Fedora version: 44 + package: RPM - distro: Ubuntu version: 24.04 + package: DEB test: skip # Running tests on all distros is superfluous - distro: Ubuntu version: 26.04 + package: DEB - name: ${{matrix.distro}} ${{matrix.version}} + name: ${{ matrix.distro }} ${{ matrix.version }} needs: configure runs-on: ubuntu-latest - continue-on-error: ${{matrix.allow-failure == 'yes'}} + continue-on-error: ${{ matrix.allow-failure == 'yes' }} timeout-minutes: 70 env: - NAME: ${{matrix.distro}}${{matrix.version}} - CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache - # Cache size over the entire repo is 10Gi: - # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - CCACHE_SIZE: 550M + CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache CCACHE_EVICTION_AGE: 7d + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy CMAKE_GENERATOR: 'Ninja' + NAME: ${{ matrix.distro }}${{ matrix.version }} steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v6 - - name: Restore compiler cache (ccache) + - name: "Restore compiler cache (ccache)" id: ccache_restore uses: actions/cache/restore@v5 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - path: ${{env.CACHE}} - key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}} - restore-keys: ccache-${{matrix.distro}}${{matrix.version}}- + key: ccache-${{ matrix.distro }}${{ matrix.version }}-${{ env.BRANCH_NAME }} + path: ${{ env.CACHE }} + restore-keys: ccache-${{ matrix.distro }}${{ matrix.version }}- - - name: Build ${{matrix.distro}} ${{matrix.version}} Docker image + - name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image" shell: bash run: source .ci/docker.sh --build - - name: Build debug and test + - name: "Build debug and test" if: matrix.test != 'skip' shell: bash run: | source .ci/docker.sh RUN --server --debug --test --ccache "$CCACHE_SIZE" \ - --cmake-generator "$CMAKE_GENERATOR" + --cmake-generator "$CMAKE_GENERATOR" - - name: Build release package + - name: "Build release package" id: build if: matrix.package != 'skip' shell: bash env: - SUFFIX: '-${{matrix.distro}}${{matrix.version}}' - package: '${{matrix.package}}' - server_only: '${{matrix.server_only}}' + SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}' + package: '${{ matrix.package }}' + server_only: '${{ matrix.server_only }}' run: | source .ci/docker.sh args=() - if [[ $server_only == yes ]]; then - args+=(--no-client) - fi - if [[ $GITHUB_REF == "refs/heads/master" ]]; then - args+=(--evict-ccache "$CCACHE_EVICTION_AGE") - fi + [[ $server_only == yes ]] && args+=(--no-client) + [[ $GITHUB_REF == "refs/heads/master" ]] && args+=(--evict-ccache "$CCACHE_EVICTION_AGE") args+=(--ccache "$CCACHE_SIZE") args+=(--cmake-generator "$CMAKE_GENERATOR") args+=(--suffix "$SUFFIX") + RUN --server --release --package "$package" "${args[@]}" # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - - name: Delete remote compiler cache (ccache) + - name: "Delete remote compiler cache (ccache)" if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit continue-on-error: true env: @@ -213,47 +217,47 @@ jobs: echo "Cache deleted successfully" fi - - name: Save updated compiler cache (ccache) + - name: "Save updated compiler cache (ccache)" if: github.ref == 'refs/heads/master' uses: actions/cache/save@v5 with: - path: ${{env.CACHE}} key: ${{ steps.ccache_restore.outputs.cache-primary-key }} + path: ${{ env.CACHE }} - - name: Upload artifact + - name: "Upload artifact" id: upload_artifact if: matrix.package != 'skip' uses: actions/upload-artifact@v7 with: - path: ${{steps.build.outputs.path}} archive: false if-no-files-found: error + path: ${{ steps.build.outputs.path }} - - name: Upload to release + - name: "Upload to release" id: upload_release if: matrix.package != 'skip' && needs.configure.outputs.tag != null shell: bash env: - GH_TOKEN: ${{github.token}} - tag_name: ${{needs.configure.outputs.tag}} - asset_name: ${{steps.build.outputs.fullname}} - asset_path: ${{steps.build.outputs.path}} + asset_name: ${{ steps.build.outputs.fullname }} + asset_path: ${{ steps.build.outputs.path }} + GH_TOKEN: ${{ github.token }} + tag_name: ${{ needs.configure.outputs.tag }} run: gh release upload "$tag_name" "$asset_path#$asset_name" - - name: Attest binary provenance + - name: "Attest binary provenance" id: attestation if: steps.upload_release.outcome == 'success' uses: actions/attest@v4 with: - subject-path: ${{steps.build.outputs.path}} show-summary: false + subject-path: ${{ steps.build.outputs.path }} - - name: Verify binary attestation + - name: "Verify binary attestation" if: steps.attestation.outcome == 'success' shell: bash env: - GH_TOKEN: ${{github.token}} - run: gh attestation verify "${{steps.build.outputs.path}}" --repo Cockatrice/Cockatrice + GH_TOKEN: ${{ github.token }} + run: gh attestation verify "${{ steps.build.outputs.path }}" --repo Cockatrice/Cockatrice build-vcpkg: strategy: @@ -263,200 +267,202 @@ jobs: - os: macOS target: 13 runner: macos-15-intel - soc: Intel - xcode: "16.4" - type: Release - override_target: 13 + + ccache_eviction_age: 7d + cmake_generator: Ninja make_package: 1 + override_target: 13 package_suffix: "-macOS13_Intel" qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Intel + type: Release use_ccache: 1 - ccache_eviction_age: 7d + xcode: "16.4" - os: macOS target: 14 runner: macos-14 - soc: Apple - xcode: "15.4" - type: Release + + ccache_eviction_age: 7d + cmake_generator: Ninja make_package: 1 package_suffix: "-macOS14" qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Apple + type: Release use_ccache: 1 - ccache_eviction_age: 7d + xcode: "15.4" - os: macOS target: 15 runner: macos-15 - soc: Apple - xcode: "16.4" - type: Release + + ccache_eviction_age: 7d + cmake_generator: Ninja make_package: 1 package_suffix: "-macOS15" qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Apple + type: Release use_ccache: 1 - ccache_eviction_age: 7d + xcode: "16.4" - os: macOS target: 15 runner: macos-15 - soc: Apple - xcode: "16.4" - type: Debug + + ccache_eviction_age: 7d + cmake_generator: Ninja qt_version: 6.11.0 qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: Ninja + soc: Apple + type: Debug use_ccache: 1 - ccache_eviction_age: 7d + xcode: "16.4" - os: Windows target: 10 runner: windows-2025 - type: Release + + cmake_generator: "Visual Studio 17 2022" + cmake_generator_platform: x64 make_package: 1 package_suffix: "-Win10" qt_version: 6.11.0 qt_arch: win64_msvc2022_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cmake_generator: "Visual Studio 17 2022" - cmake_generator_platform: x64 + type: Release - name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} + name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} needs: configure - runs-on: ${{matrix.runner}} + runs-on: ${{ matrix.runner }} timeout-minutes: 100 env: - CCACHE_DIR: ${{github.workspace}}/.cache/ - # Cache size over the entire repo is 10Gi: - # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - CCACHE_SIZE: 550M + CCACHE_DIR: ${{ github.workspace }}/.cache/ + CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v6 with: submodules: recursive - - name: Add msbuild to PATH + - name: "[Windows] Add msbuild to PATH" if: matrix.os == 'Windows' id: add-msbuild uses: microsoft/setup-msbuild@v3 with: msbuild-architecture: x64 - - name: Setup ccache - if: matrix.use_ccache == 1 && matrix.os == 'macOS' + - name: "[macOS] Setup ccache" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 run: brew install ccache - - name: Restore compiler cache (ccache) - if: matrix.use_ccache == 1 + - name: "[macOS] Restore compiler cache (ccache)" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 id: ccache_restore uses: actions/cache/restore@v5 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - path: ${{env.CCACHE_DIR}} - key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}} - restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}- + key: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-${{ env.BRANCH_NAME }} + path: ${{ env.CCACHE_DIR }} + restore-keys: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}- - - name: Install aqtinstall + - name: "Install aqtinstall" run: pipx install aqtinstall # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases - - name: Resolve latest Qt patch version + - name: "Resolve latest Qt patch version" id: resolve_qt_version shell: bash - run: .ci/resolve_latest_aqt_qt_version.sh "${{matrix.qt_version}}" + run: .ci/resolve_latest_aqt_qt_version.sh "${{ matrix.qt_version }}" - - name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS) + - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" if: matrix.os == 'macOS' id: restore_qt uses: actions/cache/restore@v5 with: - path: ${{ github.workspace }}/Qt key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + path: ${{ github.workspace }}/Qt # Using jurplel/install-qt-action to install Qt without using brew - # qt build using vcpkg either just fails or takes too long to build - - name: Install fat Qt ${{ steps.resolve_qt_version.outputs.version }} (${{ matrix.soc }} macOS) + # Qt build using vcpkg either just fails or takes too long to build + - name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: jurplel/install-qt-action@v4 with: - version: ${{ steps.resolve_qt_version.outputs.version }} - arch: ${{matrix.qt_arch}} - modules: ${{matrix.qt_modules}} + arch: ${{ matrix.qt_arch }} cache: false - dir: ${{github.workspace}} + dir: ${{ github.workspace }} + modules: ${{ matrix.qt_modules }} + version: ${{ steps.resolve_qt_version.outputs.version }} - - name: Thin Qt libraries (${{ matrix.soc }} macOS) + - name: "[macOS] Create thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' run: .ci/thin_macos_qtlib.sh - - name: Cache thin Qt libraries (${{ matrix.soc }} macOS) + - name: "[macOS] Cache thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: actions/cache/save@v5 with: - path: ${{ github.workspace }}/Qt key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + path: ${{ github.workspace }}/Qt - - name: Install Qt ${{matrix.qt_version}} (Windows) + - name: "[Windows] Install Qt ${{ matrix.qt_version }}" if: matrix.os == 'Windows' uses: jurplel/install-qt-action@v4 with: - # qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released + # Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released aqtsource: git+https://github.com/miurahr/aqtinstall.git - version: ${{ steps.resolve_qt_version.outputs.version }} - arch: ${{matrix.qt_arch}} - modules: ${{matrix.qt_modules}} + arch: ${{ matrix.qt_arch }} cache: true + modules: ${{ matrix.qt_modules }} + version: ${{ steps.resolve_qt_version.outputs.version }} - - name: Install NSIS + - name: "[Windows] Install NSIS" if: matrix.os == 'Windows' shell: bash run: choco install nsis - - name: Setup vcpkg cache + - name: "Setup vcpkg cache" id: vcpkg-cache uses: TAServers/vcpkg-cache@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - # uses environment variables, see compile.sh for more details - - name: Build Cockatrice + # Uses environment variables, see compile.sh for more details + - name: "Build Cockatrice" id: build shell: bash env: - BUILDTYPE: '${{matrix.type}}' - MAKE_PACKAGE: '${{matrix.make_package}}' - PACKAGE_SUFFIX: '${{matrix.package_suffix}}' - CMAKE_GENERATOR: ${{matrix.cmake_generator}} - CMAKE_GENERATOR_PLATFORM: ${{matrix.cmake_generator_platform}} - USE_CCACHE: ${{matrix.use_ccache}} - VCPKG_DISABLE_METRICS: 1 - VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' - # macOS-specific environment variables, will be ignored on Windows - MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} - MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} - MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} - DEVELOPER_DIR: '/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer' - TARGET_MACOS_VERSION: ${{ matrix.override_target }} + BUILDTYPE: '${{ matrix.type }}' CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }} + CMAKE_GENERATOR: ${{ matrix.cmake_generator }} + CMAKE_GENERATOR_PLATFORM: ${{ matrix.cmake_generator_platform }} + DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer' + MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} + MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} + MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} + MAKE_PACKAGE: '${{ matrix.make_package }}' + PACKAGE_SUFFIX: '${{ matrix.package_suffix }}' + TARGET_MACOS_VERSION: ${{ matrix.override_target }} + USE_CCACHE: ${{ matrix.use_ccache }} + VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' + VCPKG_DISABLE_METRICS: 1 run: .ci/compile.sh --server --test --vcpkg # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 - - name: Delete remote compiler cache (ccache) - if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 && steps.ccache_restore.outputs.cache-hit + - name: "[macOS] Delete remote compiler cache (ccache)" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit continue-on-error: true env: GH_TOKEN: ${{ github.token }} @@ -465,14 +471,14 @@ jobs: echo "Cache deleted successfully" fi - - name: Save updated compiler cache (ccache) - if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 + - name: "[macOS] Save updated compiler cache (ccache)" + if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' uses: actions/cache/save@v5 with: - path: ${{env.CCACHE_DIR}} key: ${{ steps.ccache_restore.outputs.cache-primary-key }} + path: ${{ env.CCACHE_DIR }} - - name: Sign app bundle + - name: "[macOS] Sign app bundle" if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null id: sign_macos env: @@ -482,15 +488,15 @@ jobs: if [[ -n "$MACOS_CERTIFICATE_NAME" ]] then security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "${{steps.build.outputs.path}}" + /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "${{ steps.build.outputs.path }}" fi - - name: Notarize app bundle - if: steps.sign_macos.outcome == 'success' + - name: "[macOS] Notarize app bundle" + if: matrix.os == 'macOS' && steps.sign_macos.outcome == 'success' env: MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} run: | if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]] then @@ -502,7 +508,7 @@ jobs: # Therefore, we create a zip file containing our app bundle, so that we can send it to the # notarization service echo "Creating temp notarization archive" - ditto -c -k --keepParent "${{steps.build.outputs.path}}" "notarization.zip" + ditto -c -k --keepParent "${{ steps.build.outputs.path }}" "notarization.zip" # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App @@ -514,51 +520,51 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" - xcrun stapler staple "${{steps.build.outputs.path}}" + xcrun stapler staple "${{ steps.build.outputs.path }}" fi - - name: Upload artifact + - name: "Upload artifact" if: matrix.make_package id: upload_artifact uses: actions/upload-artifact@v7 with: - path: ${{steps.build.outputs.path}} archive: false if-no-files-found: error + path: ${{ steps.build.outputs.path }} - - name: Upload PDBs (Program Databases) + - name: "[Windows] Upload PDBs (Program Databases)" if: matrix.os == 'Windows' && github.ref_type != 'tag' uses: actions/upload-artifact@v7 with: - name: ${{steps.build.outputs.name}}-PDBs + if-no-files-found: error + name: ${{ steps.build.outputs.name }}-PDBs path: | build/cockatrice/Release/*.pdb build/oracle/Release/*.pdb build/servatrice/Release/*.pdb - if-no-files-found: error - - name: Upload to release + - name: "Upload to release" if: needs.configure.outputs.tag != null && matrix.make_package == '1' id: upload_release shell: bash env: - GH_TOKEN: ${{github.token}} - tag_name: ${{needs.configure.outputs.tag}} - asset_name: ${{steps.build.outputs.fullname}} - asset_path: ${{steps.build.outputs.path}} + asset_name: ${{ steps.build.outputs.fullname }} + asset_path: ${{ steps.build.outputs.path }} + GH_TOKEN: ${{ github.token }} + tag_name: ${{ needs.configure.outputs.tag }} run: gh release upload "$tag_name" "$asset_path#$asset_name" - - name: Attest binary provenance + - name: "Attest binary provenance" if: steps.upload_release.outcome == 'success' id: attestation uses: actions/attest@v4 with: - subject-path: ${{steps.build.outputs.path}} show-summary: false + subject-path: ${{ steps.build.outputs.path }} - - name: Verify binary attestation + - name: "Verify binary attestation" if: steps.attestation.outcome == 'success' shell: bash env: - GH_TOKEN: ${{github.token}} - run: gh attestation verify "${{steps.build.outputs.path}}" --repo Cockatrice/Cockatrice + GH_TOKEN: ${{ github.token }} + run: gh attestation verify "${{ steps.build.outputs.path }}" --repo Cockatrice/Cockatrice diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml index df8b9f89e..54931933c 100644 --- a/.github/workflows/desktop-lint.yml +++ b/.github/workflows/desktop-lint.yml @@ -1,7 +1,7 @@ name: Code Style (C++) on: - # push trigger not needed for linting, we do not allow direct pushes to master + # Push trigger not needed for linting, we do not allow direct pushes to master pull_request: paths: - '*/**' # matches all files not in root @@ -21,17 +21,20 @@ jobs: runs-on: ubuntu-slim steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v6 with: fetch-depth: 20 # should be enough to find merge base - - name: Install dependencies + - name: "Install dependencies" shell: bash run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends clang-format cmake-format shellcheck + sudo apt-get install -y --no-install-recommends \ + clang-format \ + cmake-format \ + shellcheck - - name: Check code formatting + - name: "Check code formatting" shell: bash run: ./.ci/lint_cpp.sh diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index b869d1fa9..d9ff06282 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,9 +1,10 @@ name: Build Docker Image +permissions: + contents: read + packages: write + on: - release: - types: - - released # publishing of stable releases push: branches: - master @@ -13,6 +14,9 @@ on: paths: - '.github/workflows/docker-release.yml' - 'Dockerfile' + release: + types: + - released # publishing of stable releases # Cancel earlier, unfinished runs of this workflow on the same branch (unless on release) concurrency: @@ -24,54 +28,50 @@ jobs: name: amd64 & arm64 if: ${{ github.repository_owner == 'Cockatrice' }} runs-on: ubuntu-latest - - permissions: - contents: read - packages: write steps: - - name: Checkout + - name: "Checkout" uses: actions/checkout@v6 - - name: Docker metadata + - name: "Docker metadata" id: metadata uses: docker/metadata-action@v6 env: DOCKER_METADATA_ANNOTATIONS_LEVELS: index # needed for GHCR with: + annotations: | + org.opencontainers.image.title=Servatrice + org.opencontainers.image.url=https://cockatrice.github.io/ + org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games images: | ghcr.io/cockatrice/servatrice labels: | org.opencontainers.image.title=Servatrice org.opencontainers.image.url=https://cockatrice.github.io/ org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games - annotations: | - org.opencontainers.image.title=Servatrice - org.opencontainers.image.url=https://cockatrice.github.io/ - org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games - - name: Set up QEMU + - name: "Set up QEMU" uses: docker/setup-qemu-action@v4 - - name: Set up Docker buildx + - name: "Set up Docker buildx" uses: docker/setup-buildx-action@v4 - - name: Login to GitHub Container Registry + - name: "Login to GitHub Container Registry" if: contains(github.event.release.tag_name, 'Release') && github.event.release.target_commitish == 'master' uses: docker/login-action@v4 with: + password: ${{ github.token }} registry: ghcr.io username: ${{ github.actor }} - password: ${{ github.token }} - - name: Build and push Docker image + - name: "Build and push Docker image" uses: docker/build-push-action@v7 with: - context: . - platforms: linux/amd64,linux/arm64 - push: ${{ github.ref_type == 'tag' }} - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} annotations: ${{ steps.metadata.outputs.annotations }} cache-from: type=gha,scope=servatrice cache-to: type=gha,mode=max,scope=servatrice + context: . + labels: ${{ steps.metadata.outputs.labels }} + platforms: linux/amd64,linux/arm64 + push: ${{ github.ref_type == 'tag' }} + tags: ${{ steps.metadata.outputs.tags }} diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index b0093d6b1..717999d5a 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -1,18 +1,18 @@ name: Generate Docs on: - release: - types: - - published # publishing of stable releases and pre-releases pull_request: paths: - 'doc/doxygen/**' - '.github/workflows/documentation-build.yml' - 'Doxyfile' + release: + types: + - published # publishing of stable releases and pre-releases workflow_dispatch: env: - COCKATRICE_REF: ${{ github.ref_name }} # Tag name if the commit is tagged, otherwise branch name + COCKATRICE_REF: ${{ github.ref_name }} # tag name if the commit is tagged, otherwise branch name jobs: docs: @@ -20,22 +20,22 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code + - name: "Checkout code" uses: actions/checkout@v6 with: submodules: recursive - - name: Install Graphviz + - name: "Install Graphviz" run: | sudo apt-get install -y graphviz dot -V - - name: Install Doxygen + - name: "Install Doxygen" uses: ssciwr/doxygen-install@v2 with: version: "1.16.1" - - name: Update Doxygen Configuration + - name: "Update Doxygen Configuration" run: | git diff Doxyfile doxygen -u Doxyfile @@ -48,16 +48,16 @@ jobs: exit 1 fi - - name: Generate Documentation + - name: "Generate Documentation" if: always() run: doxygen Doxyfile - - name: Deploy to cockatrice.github.io + - name: "Deploy to cockatrice.github.io" if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' uses: peaceiris/actions-gh-pages@v4 with: deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} + destination_dir: docs # docs will be available at https://cockatrice.github.io/docs/ external_repository: Cockatrice/cockatrice.github.io publish_branch: master publish_dir: ./docs/html - destination_dir: docs # Docs will live under https://cockatrice.github.io/docs/ diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index 8f673fce9..057381f8a 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -1,14 +1,14 @@ name: Update Translations on: - workflow_dispatch: - schedule: - # runs in the middle of each month starting a quarter (UTC) = two weeks after new strings are built - - cron: '0 0 15 1,4,7,10 *' pull_request: paths: - '.tx/**' - '.github/workflows/translations-pull.yml' + schedule: + # Runs in the middle of each month starting a quarter (UTC) = two weeks after new strings are built + - cron: '0 0 15 1,4,7,10 *' + workflow_dispatch: jobs: translations: @@ -19,18 +19,18 @@ jobs: runs-on: ubuntu-slim steps: - - name: Checkout repo + - name: "Checkout repo" uses: actions/checkout@v6 - - name: Pull translated strings from Transifex + - name: "Pull translated strings from Transifex" uses: transifex/cli-action@v2 with: - # used config file: https://github.com/Cockatrice/Cockatrice/blob/master/.tx/config - # https://github.com/transifex/cli#pulling-files-from-transifex - token: ${{ secrets.TX_TOKEN }} + # Used config file: https://github.com/Cockatrice/Cockatrice/blob/master/.tx/config + # Docs: https://github.com/transifex/cli#pulling-files-from-transifex args: pull --force --all + token: ${{ secrets.TX_TOKEN }} - - name: Create pull request + - name: "Create pull request" if: github.event_name != 'pull_request' id: create_pr uses: peter-evans/create-pull-request@v8 @@ -38,12 +38,7 @@ jobs: add-paths: | cockatrice/translations/*.ts oracle/translations/*.ts - commit-message: Update translation files - # author is the owner of the commit - author: github-actions - branch: ci-update_translations - delete-branch: true - title: 'Update translations' + author: github-actions # owner of the commit body: | Pulled all translated strings from [Transifex][1]. @@ -53,12 +48,16 @@ jobs: [1]: https://explore.transifex.com/cockatrice/cockatrice/ [2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster + branch: ci-update_translations + commit-message: Update translation files + delete-branch: true + draft: false labels: | CI Translation - draft: false + title: 'Update translations' - - name: PR Status + - name: "PR Status" if: github.event_name != 'pull_request' shell: bash env: diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml index e926a58ed..4adcaf4a4 100644 --- a/.github/workflows/translations-push.yml +++ b/.github/workflows/translations-push.yml @@ -1,14 +1,14 @@ name: Update Translation Source on: - workflow_dispatch: - schedule: - # runs at the start of each quarter (UTC) - - cron: '0 0 1 1,4,7,10 *' pull_request: paths: - '.ci/update_translation_source_strings.sh' - '.github/workflows/translations-push.yml' + schedule: + # Runs at the start of each quarter (UTC) + - cron: '0 0 1 1,4,7,10 *' + workflow_dispatch: jobs: translations: @@ -19,16 +19,16 @@ jobs: runs-on: ubuntu-slim steps: - - name: Checkout repo + - name: "Checkout repo" uses: actions/checkout@v6 - - name: Install lupdate + - name: "Install lupdate" shell: bash run: | sudo apt-get update sudo apt-get install -y --no-install-recommends qttools5-dev-tools - - name: Update Cockatrice translation source + - name: "Update Cockatrice translation source" id: cockatrice shell: bash run: | @@ -36,15 +36,15 @@ jobs: export DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')" FILE="$FILE" DIRS="$DIRS" .ci/update_translation_source_strings.sh - - name: Update Oracle translation source + - name: "Update Oracle translation source" id: oracle shell: bash env: - FILE: 'oracle/oracle_en@source.ts' DIRS: 'oracle/src' + FILE: 'oracle/oracle_en@source.ts' run: .ci/update_translation_source_strings.sh - - name: Render template + - name: "Render template" id: template uses: chuhlomin/render-template/binary@v1 with: @@ -54,7 +54,7 @@ jobs: oracle_output: ${{ steps.oracle.outputs.output }} commit: ${{ github.sha }} - - name: Create pull request + - name: "Create pull request" if: github.event_name != 'pull_request' id: create_pr uses: peter-evans/create-pull-request@v8 @@ -62,19 +62,18 @@ jobs: add-paths: | cockatrice/cockatrice_en@source.ts oracle/oracle_en@source.ts - commit-message: Update translation source strings - # author is the owner of the commit - author: github-actions - branch: ci-update_translation_source - delete-branch: true - title: 'Update source strings' + author: github-actions # owner of the commit body: ${{ steps.template.outputs.result }} + branch: ci-update_translation_source + commit-message: Update translation source strings + delete-branch: true + draft: false labels: | CI Translation - draft: false + title: 'Update source strings' - - name: PR Status + - name: "PR Status" if: github.event_name != 'pull_request' shell: bash env: From e674a39b878d020275a5df6645336d271a4d7952 Mon Sep 17 00:00:00 2001 From: Christo Date: Tue, 9 Jun 2026 13:54:01 +0800 Subject: [PATCH 15/50] Fix #6952: prevent deck loss when saving to a full disk (#6978) * Fix #6952: prevent deck loss when saving to a full disk DeckLoader::saveToFile() opened the target with QFile in WriteOnly mode, which truncates the existing file to 0 bytes the moment it is opened. The serializers (DeckList::saveToFile_Native/_Plain) always return true and the result of flush() was ignored, so a write that failed part-way -- e.g. because the disk was full -- left a 0-byte file behind yet was still reported (and logged) as a successful save. The same truncate-then-write pattern in updateLastLoadedTimestamp() could destroy a deck on load. Switch both paths to QSaveFile, which writes to a temporary file and only atomically replaces the target if commit() succeeds. On any write or flush failure commit() returns false, the original deck is left untouched, and the failure is logged instead of being reported as success. * Use QSaveFile in convertToCockatriceFormat() too convertToCockatriceFormat() had the same data-loss pattern: QFile WriteOnly truncated the .cod, saveToFile_Native() always returns true, and the original file was then removed unconditionally -- so a full disk during conversion wrote a 0-byte .cod and then deleted the source deck. Switch to QSaveFile (write + atomic commit), remove the original only after a successful commit, and move the format check ahead of the file open so an already-Cockatrice or unsupported deck never truncates or deletes anything. Raised in review by ZeldaZach. --- .../src/interface/deck_loader/deck_loader.cpp | 137 ++++++++++-------- 1 file changed, 77 insertions(+), 60 deletions(-) diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index e616c5eb5..39a0c1071 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -129,7 +130,10 @@ std::optional DeckLoader::loadFromRemote(const QString &nativeString std::optional DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFormat::Format fmt) { - QFile file(fileName); + // Use QSaveFile so that a failed write (e.g. a full disk) leaves the existing deck untouched + // instead of truncating it to a 0-byte file. The target is only replaced once every byte has + // been flushed successfully in commit(). + QSaveFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(DeckLoaderLog) << "Could not create or open file:" << fileName; return std::nullopt; @@ -145,15 +149,19 @@ DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFo break; } - file.flush(); - file.close(); - - qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt << "-" << success; - if (!success) { + file.cancelWriting(); + qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName; return std::nullopt; } + if (!file.commit()) { + qCWarning(DeckLoaderLog) << "Failed to save deck to " << fileName << ":" << file.errorString(); + return std::nullopt; + } + + qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt; + LoadedDeck::LoadInfo lastLoadInfo = {fileName, fmt}; return lastLoadInfo; } @@ -196,38 +204,44 @@ bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck) QDateTime originalTimestamp = fileInfo.lastModified(); - // Open the file for writing - QFile file(fileName); + // Use QSaveFile so that a failed write (e.g. a full disk) cannot truncate an existing deck to a + // 0-byte file while merely bumping its timestamp. + QSaveFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << fileName; return false; } - bool result = false; - // Perform file modifications deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString()); - result = deck.deckList.saveToFile_Native(&file); - file.close(); // Close the file to ensure changes are flushed - - if (result) { - // Re-open the file and set the original timestamp - if (!file.open(QIODevice::ReadWrite)) { - qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName; - return false; - } - - if (!file.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) { - qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName; - file.close(); - return false; - } - - file.close(); + if (!deck.deckList.saveToFile_Native(&file)) { + file.cancelWriting(); + qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName; + return false; } - return result; + if (!file.commit()) { + qCWarning(DeckLoaderLog) << "Failed to update timestamp for file:" << fileName << ":" << file.errorString(); + return false; + } + + // Re-open the file and restore the original timestamp, so that updating the lastLoadedTimestamp + // does not change the file's modification time. + QFile timestampFile(fileName); + if (!timestampFile.open(QIODevice::ReadWrite)) { + qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName; + return false; + } + + if (!timestampFile.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) { + qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName; + timestampFile.close(); + return false; + } + + timestampFile.close(); + return true; } static QString getDomainForWebsite(DeckLoader::DecklistWebsite website) @@ -444,51 +458,54 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck) return false; } + // Determine the format before touching any file, so an already-converted or + // unsupported deck never truncates or deletes anything. + switch (DeckFileFormat::getFormatFromName(fileName)) { + case DeckFileFormat::PlainText: + break; + case DeckFileFormat::Cockatrice: + qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed."; + return true; + default: + qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName; + return false; + } + // Change the file extension to .cod QFileInfo fileInfo(fileName); QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); - // Open the new file for writing - QFile file(newFileName); + // Use QSaveFile so a failed write (e.g. a full disk) cannot leave a 0-byte .cod + // behind and then delete the original deck. + QSaveFile file(newFileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << newFileName; return false; } - bool result = false; - - // Perform file modifications based on the detected format - switch (DeckFileFormat::getFormatFromName(fileName)) { - case DeckFileFormat::PlainText: - // Save in Cockatrice's native format - result = deck.deckList.saveToFile_Native(&file); - break; - case DeckFileFormat::Cockatrice: - qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed."; - result = true; - break; - default: - qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName; - result = false; - break; + if (!deck.deckList.saveToFile_Native(&file)) { + file.cancelWriting(); + qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << newFileName; + return false; } - file.close(); - - // Delete the old file if conversion was successful - if (result) { - if (!QFile::remove(fileName)) { - qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName; - } else { - qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName; - } - deck.lastLoadInfo = { - .fileName = newFileName, - .fileFormat = DeckFileFormat::Cockatrice, - }; + if (!file.commit()) { + qCWarning(DeckLoaderLog) << "Failed to convert deck to " << newFileName << ":" << file.errorString(); + return false; } - return result; + // Conversion succeeded: delete the original file. + if (!QFile::remove(fileName)) { + qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName; + } else { + qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName; + } + deck.lastLoadInfo = { + .fileName = newFileName, + .fileFormat = DeckFileFormat::Cockatrice, + }; + + return true; } void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node) From 9e03f826165604f89c033fc368c3412652ff92f0 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:05:39 +0200 Subject: [PATCH 16/50] [Game][Player] Split Player into PlayerLogic/PlayerGraphicsItem (#6944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Game][Player] Split Player into PlayerLogic/PlayerGraphicsItem Took 4 minutes Took 48 seconds * Drop early return. Took 1 hour 13 minutes Took 2 minutes Took 1 minute * Delete player view. Took 37 seconds * Restore card counter color in menu. Took 5 minutes --------- Co-authored-by: Lukas Brübach --- .../src/game/board/abstract_card_item.h | 5 + cockatrice/src/game/board/card_item.cpp | 39 ++-- cockatrice/src/game/game_scene.cpp | 101 ++++++++++- cockatrice/src/game/game_scene.h | 15 ++ cockatrice/src/game/player/menu/card_menu.cpp | 133 +++++++------- cockatrice/src/game/player/menu/card_menu.h | 9 +- .../src/game/player/menu/custom_zone_menu.cpp | 10 +- .../src/game/player/menu/custom_zone_menu.h | 6 +- cockatrice/src/game/player/menu/move_menu.cpp | 27 ++- cockatrice/src/game/player/menu/move_menu.h | 4 +- .../src/game/player/menu/player_menu.cpp | 50 +++--- cockatrice/src/game/player/menu/player_menu.h | 13 +- cockatrice/src/game/player/menu/pt_menu.cpp | 34 ++-- cockatrice/src/game/player/menu/pt_menu.h | 4 +- .../src/game/player/menu/utility_menu.cpp | 40 +++-- .../src/game/player/menu/utility_menu.h | 10 +- cockatrice/src/game/player/player_actions.cpp | 167 +++++++++--------- cockatrice/src/game/player/player_actions.h | 66 ++++--- .../src/game/player/player_event_handler.cpp | 13 +- .../src/game/player/player_event_handler.h | 1 + .../src/game/player/player_graphics_item.cpp | 31 ++-- .../src/game/player/player_graphics_item.h | 10 +- cockatrice/src/game/player/player_logic.cpp | 23 +-- cockatrice/src/game/player/player_logic.h | 18 +- .../src/game_graphics/zones/table_zone.cpp | 15 +- .../src/game_graphics/zones/table_zone.h | 4 +- .../src/interface/widgets/tabs/tab_game.cpp | 56 +++--- .../src/interface/widgets/tabs/tab_game.h | 2 +- 28 files changed, 538 insertions(+), 368 deletions(-) diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game/board/abstract_card_item.h index ed545e1ab..863954b73 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game/board/abstract_card_item.h @@ -44,6 +44,11 @@ signals: void deleteCardInfoPopup(QString cardName); void sigPixmapUpdated(); void cardShiftClicked(QString cardName); + void rightClicked(AbstractCardItem *card, QPoint screenPos); + void playSelected(AbstractCardItem *card); + void playSelectedFaceDown(AbstractCardItem *card); + void hideSelected(AbstractCardItem *card); + void selectionChanged(AbstractCardItem *card, bool selected); public: enum diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp index a08194540..16197ae16 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game/board/card_item.cpp @@ -40,7 +40,7 @@ void CardItem::prepareDelete() { if (owner != nullptr) { if (owner->getGame()->getActiveCard() == this) { - owner->getPlayerMenu()->updateCardMenu(nullptr); + emit owner->requestCardMenuUpdate(nullptr); owner->getGame()->setActiveCard(nullptr); } owner = nullptr; @@ -399,8 +399,11 @@ void CardItem::playCard(bool faceDown) emit tz->toggleTapped(); } else { if (SettingsCache::instance().getClickPlaysAllSelected()) { - faceDown ? state->getZone()->getPlayer()->getPlayerActions()->actPlayFacedown() - : state->getZone()->getPlayer()->getPlayerActions()->actPlay(); + if (faceDown) { + emit playSelectedFaceDown(this); + } else { + emit playSelected(this); + } } else { state->getZone()->getPlayer()->getPlayerActions()->playCard(this, faceDown); } @@ -460,7 +463,7 @@ void CardItem::handleClickedToPlay(bool shiftHeld) { if (isUnwritableRevealZone(state->getZone())) { if (SettingsCache::instance().getClickPlaysAllSelected()) { - state->getZone()->getPlayer()->getPlayerActions()->actHide(); + emit hideSelected(this); } else { state->getZone()->removeCard(this); } @@ -471,17 +474,11 @@ void CardItem::handleClickedToPlay(bool shiftHeld) void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if (event->button() == Qt::RightButton) { - - if (owner != nullptr) { - owner->getGame()->setActiveCard(this); - if (QMenu *cardMenu = owner->getPlayerMenu()->updateCardMenu(this)) { - cardMenu->popup(event->screenPos()); - return; - } - } - } else if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && - (!SettingsCache::instance().getDoubleClickToPlay())) { + if (event->button() == Qt::RightButton && owner != nullptr) { + emit rightClicked(this, event->screenPos()); + } + if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && + (!SettingsCache::instance().getDoubleClickToPlay())) { handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); } @@ -531,14 +528,14 @@ bool CardItem::animationEvent() QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value) { if ((change == ItemSelectedHasChanged) && owner != nullptr) { - if (value == true) { - owner->getGame()->setActiveCard(this); - owner->getPlayerMenu()->updateCardMenu(this); - } else if (owner->getGameScene()->selectedItems().isEmpty()) { + bool selected = value.toBool(); - owner->getGame()->setActiveCard(nullptr); - owner->getPlayerMenu()->updateCardMenu(nullptr); + if (selected) { + owner->getGame()->setActiveCard(this); } + + emit selectionChanged(this, selected); } + return AbstractCardItem::itemChange(change, value); } diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 867869a3f..dc55ecfe9 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -4,8 +4,10 @@ #include "../game_graphics/zones/select_zone.h" #include "../game_graphics/zones/view_zone.h" #include "../game_graphics/zones/view_zone_widget.h" +#include "abstract_game.h" #include "board/card_item.h" #include "phases_toolbar.h" +#include "player/player_actions.h" #include "player/player_graphics_item.h" #include "player/player_logic.h" @@ -72,6 +74,80 @@ QList GameScene::selectedCards() const return selectedCards; } +void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool selected) +{ + CardItem *card = qobject_cast(abstractCard); + if (!card || !card->getOwner()) { + return; + } + + auto *owner = card->getOwner(); + + if (selected) { + owner->requestCardMenuUpdate(card); + return; + } + + if (selectedItems().isEmpty()) { + owner->getGame()->setActiveCard(nullptr); + owner->requestCardMenuUpdate(nullptr); + } +} + +void GameScene::onCardRightClicked(AbstractCardItem *abstractCard, QPoint screenPos) +{ + auto *card = qobject_cast(abstractCard); + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + auto *view = playerViews.value(card->getOwner()->getPlayerInfo()->getId()); + if (!view) { + return; + } + + card->getOwner()->getGame()->setActiveCard(card); + + if (auto *menu = view->getPlayerMenu()->updateCardMenu(card)) { + menu->popup(screenPos); + } +} + +void GameScene::playSelected(AbstractCardItem *card) +{ + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + card->getOwner()->getPlayerActions()->actPlay(selectedCards()); +} + +void GameScene::playSelectedFaceDown(AbstractCardItem *card) +{ + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCards()); +} + +void GameScene::hideSelected(AbstractCardItem *card) +{ + if (!card) { + return; + } + if (!card->getOwner()) { + return; + } + card->getOwner()->getPlayerActions()->actHide(selectedCards()); +} + /** * @brief Adds a player to the scene and stores their graphics item. * @param player Player to add. @@ -82,9 +158,11 @@ void GameScene::addPlayer(PlayerLogic *player) { qCInfo(GameScenePlayerAdditionRemovalLog) << "GameScene::addPlayer name=" << player->getPlayerInfo()->getName(); - playerViews.insert(player->getPlayerInfo()->getId(), player->getGraphicsItem()); - addItem(player->getGraphicsItem()); - connect(player->getGraphicsItem(), &PlayerGraphicsItem::sizeChanged, this, &GameScene::rearrange); + auto *view = new PlayerGraphicsItem(player); + + playerViews.insert(player->getPlayerInfo()->getId(), view); + addItem(view); + connect(view, &PlayerGraphicsItem::sizeChanged, this, &GameScene::rearrange); connect(player, &PlayerLogic::concededChanged, this, [this](int id, bool conceded) { if (conceded) { @@ -93,6 +171,8 @@ void GameScene::addPlayer(PlayerLogic *player) rearrange(); }); + connect(player, &PlayerLogic::requestZoneViewToggle, this, &GameScene::toggleZoneView); + connect(player, &PlayerLogic::requestRevealedZoneView, this, &GameScene::addRevealedZoneView); connect(player, &PlayerLogic::arrowDeleted, this, &GameScene::deleteArrow); connect(player, &PlayerLogic::arrowCreateRequested, this, &GameScene::addArrow); connect(player, &PlayerLogic::arrowDeleteRequested, this, &GameScene::requestArrowDeletion); @@ -123,6 +203,7 @@ void GameScene::removePlayer(PlayerLogic *player) } auto *view = playerViews.take(player->getPlayerInfo()->getId()); removeItem(view); + view->deleteLater(); rearrange(); } @@ -204,7 +285,7 @@ QList GameScene::collectActivePlayers(int &firstPlayerIndex) cons bool firstPlayerFound = false; for (auto *pgItem : playerViews.values()) { - PlayerLogic *p = pgItem->getPlayer(); + PlayerLogic *p = pgItem->getLogic(); if (p && !p->getConceded()) { activePlayers.append(p); if (!firstPlayerFound && p->getPlayerInfo()->getLocal()) { @@ -275,12 +356,12 @@ QSizeF GameScene::computeSceneSizeAndPlayerLayout(const QList &pl for (int j = 0; j < rowsInColumn; ++j) { PlayerLogic *player = playersIter.next(); if (col == 0) { - playersByColumn[col].prepend(player->getGraphicsItem()); + playersByColumn[col].prepend(playerViews.value(player->getPlayerInfo()->getId())); } else { - playersByColumn[col].append(player->getGraphicsItem()); + playersByColumn[col].append(playerViews.value(player->getPlayerInfo()->getId())); } - auto *pgItem = player->getGraphicsItem(); + auto *pgItem = playerViews.value(player->getPlayerInfo()->getId()); thisColumnHeight += pgItem->boundingRect().height() + playerAreaSpacing; columnWidth[col] = std::max(columnWidth[col], (int)pgItem->boundingRect().width()); } @@ -375,7 +456,8 @@ void GameScene::addArrow(QSharedPointer data) return; } - auto *startZone = startView->getPlayer()->getZones().value(data->startZone); + PlayerLogic *startLogic = startView->getLogic(); + auto *startZone = startLogic->getZones().value(data->startZone); if (!startZone) { return; } @@ -389,7 +471,8 @@ void GameScene::addArrow(QSharedPointer data) if (data->isPlayerTargeted()) { targetItem = targetView->getPlayerTarget(); } else { - if (auto *zone = targetView->getPlayer()->getZones().value(data->targetZone)) { + auto *zone = targetView->getLogic()->getZones().value(data->targetZone); + if (zone) { targetItem = zone->getCard(data->targetCardId); } } diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h index 567089fc0..0587566d0 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game/game_scene.h @@ -97,6 +97,16 @@ public: */ void removePlayer(PlayerLogic *player); + QMap getPlayers() const + { + return playerViews; + } + + PlayerGraphicsItem *viewForPlayer(int playerId) + { + return playerViews.value(playerId); + } + /** * @brief Adjusts the global rotation offset for player layout. * @param rotationAdjustment Number of positions to rotate. @@ -182,6 +192,11 @@ public: void stopRubberBand(); public slots: + void onCardSelectionChanged(AbstractCardItem *card, bool selected); + void onCardRightClicked(AbstractCardItem *card, QPoint screenPos); + void playSelected(AbstractCardItem *card); + void playSelectedFaceDown(AbstractCardItem *card); + void hideSelected(AbstractCardItem *card); /** @brief Toggles a zone view for a player. */ void toggleZoneView(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed = false); diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index 3b866d4e0..ba925afb0 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -31,93 +31,92 @@ static QIcon createCircleIcon(const QColor &color) return QIcon(pixmap); } -CardMenu::CardMenu(PlayerLogic *_player, const CardItem *_card, bool _shortcutsActive) +template +static QAction *makeAction(QObject *parent, Slot &&slot, bool checkable = false, bool checked = false) +{ + auto *a = new QAction(parent); + a->setCheckable(checkable); + if (checkable) { + a->setChecked(checked); + } + QObject::connect(a, &QAction::triggered, parent, std::forward(slot)); + return a; +} + +CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _shortcutsActive) : player(_player), card(_card), shortcutsActive(_shortcutsActive) { - auto playerActions = player->getPlayerActions(); - - const QList &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const QList &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto playerToAdd : players) { - if (playerToAdd == player) { + if (playerToAdd == player->getLogic()) { continue; } playersInfo.append(qMakePair(playerToAdd->getPlayerInfo()->getName(), playerToAdd->getPlayerInfo()->getId())); } - connect(player->getGame()->getPlayerManager(), &PlayerManager::playerRemoved, this, &CardMenu::removePlayer); + connect(player->getLogic()->getGame()->getPlayerManager(), &PlayerManager::playerRemoved, this, + &CardMenu::removePlayer); - aTap = new QAction(this); - aTap->setData(cmTap); - connect(aTap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); - aDoesntUntap = new QAction(this); - aDoesntUntap->setData(cmDoesntUntap); - aDoesntUntap->setCheckable(true); - aDoesntUntap->setChecked(card != nullptr && card->getDoesntUntap()); - connect(aDoesntUntap, &QAction::triggered, playerActions, &PlayerActions::cardMenuAction); + auto *actions = player->getLogic()->getPlayerActions(); + auto *gameScene = player->getGameScene(); + + // Single selection resolver used by all lambdas — called at trigger time + auto sel = [gameScene]() { return gameScene->selectedCards(); }; + + // Unified dispatcher for card menu actions + auto invoke = [actions, sel](CardMenuActionType type) { + return [actions, sel, type]() { actions->cardMenuAction(sel(), type); }; + }; + + // Actions using invoke (type dispatch, need selection) + aTap = makeAction(this, invoke(cmTap)); + aDoesntUntap = makeAction(this, invoke(cmDoesntUntap), /*checkable=*/true, card && card->getDoesntUntap()); + aFlip = makeAction(this, invoke(cmFlip)); + aPeek = makeAction(this, invoke(cmPeek)); + aClone = makeAction(this, invoke(cmClone)); + + // Actions using selection directly + aUnattach = makeAction(this, [actions, sel]() { actions->actUnattach(sel()); }); + aSetAnnotation = makeAction(this, [actions, sel]() { actions->actSetAnnotation(sel()); }); + aPlay = makeAction(this, [actions, sel]() { actions->actPlay(sel()); }); + aPlayFacedown = makeAction(this, [actions, sel]() { actions->actPlayFacedown(sel()); }); + aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); }); + aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); }); + + // Actions that use activeCard, not selection — direct connection aAttach = new QAction(this); - connect(aAttach, &QAction::triggered, playerActions, &PlayerActions::actAttach); - aUnattach = new QAction(this); - connect(aUnattach, &QAction::triggered, playerActions, &PlayerActions::actUnattach); aDrawArrow = new QAction(this); - connect(aDrawArrow, &QAction::triggered, playerActions, &PlayerActions::actDrawArrow); - aSetAnnotation = new QAction(this); - connect(aSetAnnotation, &QAction::triggered, playerActions, &PlayerActions::actSetAnnotation); - aFlip = new QAction(this); - aFlip->setData(cmFlip); - connect(aFlip, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - aPeek = new QAction(this); - aPeek->setData(cmPeek); - connect(aPeek, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - aClone = new QAction(this); - aClone->setData(cmClone); - connect(aClone, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); aSelectAll = new QAction(this); - connect(aSelectAll, &QAction::triggered, playerActions, &PlayerActions::actSelectAll); aSelectRow = new QAction(this); - connect(aSelectRow, &QAction::triggered, playerActions, &PlayerActions::actSelectRow); aSelectColumn = new QAction(this); - connect(aSelectColumn, &QAction::triggered, playerActions, &PlayerActions::actSelectColumn); - aReduceLifeByPower = new QAction(this); - connect(aReduceLifeByPower, &QAction::triggered, playerActions, &PlayerActions::actReduceLifeByPower); - - aPlay = new QAction(this); - connect(aPlay, &QAction::triggered, playerActions, &PlayerActions::actPlay); - aHide = new QAction(this); - connect(aHide, &QAction::triggered, playerActions, &PlayerActions::actHide); - aPlayFacedown = new QAction(this); - connect(aPlayFacedown, &QAction::triggered, playerActions, &PlayerActions::actPlayFacedown); + connect(aAttach, &QAction::triggered, actions, &PlayerActions::actAttach); + connect(aDrawArrow, &QAction::triggered, actions, &PlayerActions::actDrawArrow); + connect(aSelectAll, &QAction::triggered, actions, &PlayerActions::actSelectAll); + connect(aSelectRow, &QAction::triggered, actions, &PlayerActions::actSelectRow); + connect(aSelectColumn, &QAction::triggered, actions, &PlayerActions::actSelectColumn); aRevealToAll = new QAction(this); mCardCounters = new QMenu; + // Card counters for (int i = 0; i < 6; ++i) { QColor color = SettingsCache::instance().cardCounters().color(i); QIcon circleIcon = createCircleIcon(color); - auto *tempAddCounter = new QAction(this); - tempAddCounter->setIconVisibleInMenu(true); - tempAddCounter->setIcon(circleIcon); + auto *addAction = makeAction(this, [actions, sel, i]() { actions->actAddCardCounter(sel(), i); }); + addAction->setIcon(circleIcon); + aAddCounter.append(addAction); - auto *tempRemoveCounter = new QAction(this); - tempRemoveCounter->setIconVisibleInMenu(true); - tempRemoveCounter->setIcon(circleIcon); + auto *removeAction = makeAction(this, [actions, sel, i]() { actions->actRemoveCardCounter(sel(), i); }); + removeAction->setIcon(circleIcon); + aRemoveCounter.append(removeAction); - auto *tempSetCounter = new QAction(this); - tempSetCounter->setIconVisibleInMenu(true); - tempSetCounter->setIcon(circleIcon); - - aAddCounter.append(tempAddCounter); - aRemoveCounter.append(tempRemoveCounter); - aSetCounter.append(tempSetCounter); - connect(tempAddCounter, &QAction::triggered, playerActions, - [playerActions, i] { playerActions->actAddCardCounter(i); }); - connect(tempRemoveCounter, &QAction::triggered, playerActions, - [playerActions, i] { playerActions->actRemoveCardCounter(i); }); - connect(tempSetCounter, &QAction::triggered, playerActions, - [playerActions, i] { playerActions->actSetCardCounter(i); }); + auto *setAction = makeAction(this, [actions, sel, i]() { actions->actSetCardCounter(sel(), i); }); + setAction->setIcon(circleIcon); + aSetCounter.append(setAction); } setShortcutsActive(); @@ -129,7 +128,7 @@ CardMenu::CardMenu(PlayerLogic *_player, const CardItem *_card, bool _shortcutsA } bool revealedCard = false; - bool writeableCard = player->getPlayerInfo()->getLocalOrJudge(); + bool writeableCard = player->getLogic()->getPlayerInfo()->getLocalOrJudge(); if (auto *view = qobject_cast(card->getZone())) { if (view->getRevealZone()) { if (view->getWriteableRevealZone()) { @@ -313,7 +312,9 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard) initContextualPlayersMenu(revealMenu, aRevealToAll); - connect(revealMenu, &QMenu::triggered, player->getPlayerActions(), &PlayerActions::actReveal); + connect(revealMenu, &QMenu::triggered, this, [this](QAction *action) { + player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCards(), action); + }); addSeparator(); addAction(aClone); @@ -398,8 +399,7 @@ void CardMenu::addRelatedCardView() QAction *viewCard = viewRelatedCards->addAction(relatedCardName); Q_UNUSED(viewCard); - connect(viewCard, &QAction::triggered, player->getGame(), - [this, cardRef] { player->getGame()->getTab()->viewCardInfo(cardRef); }); + connect(viewCard, &QAction::triggered, this, [this, cardRef] { emit cardInfoRequested(cardRef); }); } } @@ -461,7 +461,8 @@ void CardMenu::addRelatedCardActions() auto *createRelated = new QAction(text, this); createRelated->setData(QVariant(index++)); - connect(createRelated, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actCreateRelatedCard); + connect(createRelated, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actCreateRelatedCard); addAction(createRelated); } @@ -470,7 +471,7 @@ void CardMenu::addRelatedCardActions() createRelatedCards->setShortcuts( SettingsCache::instance().shortcuts().getShortcut("Player/aCreateRelatedTokens")); } - connect(createRelatedCards, &QAction::triggered, player->getPlayerActions(), + connect(createRelatedCards, &QAction::triggered, player->getLogic()->getPlayerActions(), &PlayerActions::actCreateAllRelatedCards); addAction(createRelatedCards); } diff --git a/cockatrice/src/game/player/menu/card_menu.h b/cockatrice/src/game/player/menu/card_menu.h index ad3962caf..d67ef3876 100644 --- a/cockatrice/src/game/player/menu/card_menu.h +++ b/cockatrice/src/game/player/menu/card_menu.h @@ -8,15 +8,20 @@ #define COCKATRICE_CARD_MENU_H #include +#include class CardItem; +class PlayerGraphicsItem; class PlayerLogic; class CardMenu : public QMenu { Q_OBJECT +signals: + void cardInfoRequested(const CardRef &cardRef); + public: - explicit CardMenu(PlayerLogic *player, const CardItem *card, bool shortcutsActive); + explicit CardMenu(PlayerGraphicsItem *player, const CardItem *card, bool shortcutsActive); void removePlayer(PlayerLogic *playerToRemove); void createTableMenu(bool canModifyCard); void createStackMenu(bool canModifyCard); @@ -41,7 +46,7 @@ public: QList aAddCounter, aSetCounter, aRemoveCounter; private: - PlayerLogic *player; + PlayerGraphicsItem *player; const CardItem *card; QList> playersInfo; bool shortcutsActive; diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.cpp b/cockatrice/src/game/player/menu/custom_zone_menu.cpp index 88b7f3710..106e646d9 100644 --- a/cockatrice/src/game/player/menu/custom_zone_menu.cpp +++ b/cockatrice/src/game/player/menu/custom_zone_menu.cpp @@ -2,12 +2,12 @@ #include "../player_logic.h" -CustomZoneMenu::CustomZoneMenu(PlayerLogic *_player) : player(_player) +CustomZoneMenu::CustomZoneMenu(PlayerGraphicsItem *_player) : player(_player) { menuAction()->setVisible(false); - connect(player, &PlayerLogic::clearCustomZonesMenu, this, &CustomZoneMenu::clearCustomZonesMenu); - connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this, + connect(player->getLogic(), &PlayerLogic::clearCustomZonesMenu, this, &CustomZoneMenu::clearCustomZonesMenu); + connect(player->getLogic(), &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this, &CustomZoneMenu::addViewCustomZoneActionToCustomZoneMenu); retranslateUi(); @@ -17,7 +17,7 @@ void CustomZoneMenu::retranslateUi() { setTitle(tr("C&ustom Zones")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { for (auto aViewZone : actions()) { aViewZone->setText(tr("View custom zone '%1'").arg(aViewZone->data().toString())); @@ -37,5 +37,5 @@ void CustomZoneMenu::addViewCustomZoneActionToCustomZoneMenu(QString zoneName) QAction *aViewZone = addAction(tr("View custom zone '%1'").arg(zoneName)); aViewZone->setData(zoneName); connect(aViewZone, &QAction::triggered, this, - [zoneName, this]() { player->getGameScene()->toggleZoneView(player, zoneName, -1); }); + [zoneName, this]() { player->getGameScene()->toggleZoneView(player->getLogic(), zoneName, -1); }); } \ No newline at end of file diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.h b/cockatrice/src/game/player/menu/custom_zone_menu.h index e10f6a4f0..46dd58db6 100644 --- a/cockatrice/src/game/player/menu/custom_zone_menu.h +++ b/cockatrice/src/game/player/menu/custom_zone_menu.h @@ -11,12 +11,12 @@ #include -class PlayerLogic; +class PlayerGraphicsItem; class CustomZoneMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit CustomZoneMenu(PlayerLogic *player); + explicit CustomZoneMenu(PlayerGraphicsItem *player); void retranslateUi() override; void setShortcutsActive() override { @@ -26,7 +26,7 @@ public: } private: - PlayerLogic *player; + PlayerGraphicsItem *player; private slots: void clearCustomZonesMenu(); void addViewCustomZoneActionToCustomZoneMenu(QString zoneName); diff --git a/cockatrice/src/game/player/menu/move_menu.cpp b/cockatrice/src/game/player/menu/move_menu.cpp index 3a5ad4da3..4dfdee432 100644 --- a/cockatrice/src/game/player/menu/move_menu.cpp +++ b/cockatrice/src/game/player/menu/move_menu.cpp @@ -4,7 +4,7 @@ #include "../player_actions.h" #include "../player_logic.h" -MoveMenu::MoveMenu(PlayerLogic *player) : QMenu(tr("Move to")) +MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) { aMoveToTopLibrary = new QAction(this); aMoveToTopLibrary->setData(cmMoveToTopLibrary); @@ -20,14 +20,23 @@ MoveMenu::MoveMenu(PlayerLogic *player) : QMenu(tr("Move to")) aMoveToExile = new QAction(this); aMoveToExile->setData(cmMoveToExile); - connect(aMoveToTopLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToBottomLibrary, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToXfromTopOfLibrary, &QAction::triggered, player->getPlayerActions(), - &PlayerActions::actMoveCardXCardsFromTop); - connect(aMoveToTable, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToHand, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToGraveyard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); - connect(aMoveToExile, &QAction::triggered, player->getPlayerActions(), &PlayerActions::cardMenuAction); + auto *actions = player->getLogic()->getPlayerActions(); + + auto invoke = [player](CardMenuActionType type) { + return [type, player]() { + player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCards(), type); + }; + }; + + connect(aMoveToTopLibrary, &QAction::triggered, actions, invoke(cmMoveToTopLibrary)); + connect(aMoveToBottomLibrary, &QAction::triggered, actions, invoke(cmMoveToBottomLibrary)); + connect(aMoveToXfromTopOfLibrary, &QAction::triggered, actions, [player]() { + player->getLogic()->getPlayerActions()->actMoveCardXCardsFromTop(player->getGameScene()->selectedCards()); + }); + connect(aMoveToTable, &QAction::triggered, actions, invoke(cmMoveToTable)); + connect(aMoveToHand, &QAction::triggered, actions, invoke(cmMoveToHand)); + connect(aMoveToGraveyard, &QAction::triggered, actions, invoke(cmMoveToGraveyard)); + connect(aMoveToExile, &QAction::triggered, actions, invoke(cmMoveToExile)); addAction(aMoveToTopLibrary); addAction(aMoveToXfromTopOfLibrary); diff --git a/cockatrice/src/game/player/menu/move_menu.h b/cockatrice/src/game/player/menu/move_menu.h index 4e257b7fb..150bdbd3c 100644 --- a/cockatrice/src/game/player/menu/move_menu.h +++ b/cockatrice/src/game/player/menu/move_menu.h @@ -8,13 +8,13 @@ #define COCKATRICE_MOVE_MENU_H #include -class PlayerLogic; +class PlayerGraphicsItem; class MoveMenu : public QMenu { Q_OBJECT public: - explicit MoveMenu(PlayerLogic *player); + explicit MoveMenu(PlayerGraphicsItem *player); void setShortcutsActive(); void retranslateUi(); diff --git a/cockatrice/src/game/player/menu/player_menu.cpp b/cockatrice/src/game/player/menu/player_menu.cpp index 9e7b91923..6687bbba8 100644 --- a/cockatrice/src/game/player/menu/player_menu.cpp +++ b/cockatrice/src/game/player/menu/player_menu.cpp @@ -10,23 +10,26 @@ #include -PlayerMenu::PlayerMenu(PlayerLogic *_player) : QObject(_player), player(_player) +PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player) { + connect(player->getLogic(), &PlayerLogic::requestCardMenuUpdate, this, &PlayerMenu::updateCardMenu); + connect(this, &PlayerMenu::cardInfoRequested, player, &PlayerGraphicsItem::cardInfoRequested); + playerMenu = new TearOffMenu(); - if (player->getPlayerInfo()->getLocalOrJudge()) { - handMenu = addManagedMenu(player, player->getPlayerActions(), playerMenu); - libraryMenu = addManagedMenu(player, playerMenu); + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + handMenu = addManagedMenu(player->getLogic(), player->getLogic()->getPlayerActions(), playerMenu); + libraryMenu = addManagedMenu(player->getLogic(), playerMenu); } else { handMenu = nullptr; libraryMenu = nullptr; } - graveMenu = addManagedMenu(player, playerMenu); - rfgMenu = addManagedMenu(player, playerMenu); + graveMenu = addManagedMenu(player->getLogic(), playerMenu); + rfgMenu = addManagedMenu(player->getLogic(), playerMenu); - if (player->getPlayerInfo()->getLocalOrJudge()) { - sideboardMenu = addManagedMenu(player, playerMenu); + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + sideboardMenu = addManagedMenu(player->getLogic(), playerMenu); customZonesMenu = addManagedMenu(player); playerMenu->addSeparator(); @@ -40,8 +43,8 @@ PlayerMenu::PlayerMenu(PlayerLogic *_player) : QObject(_player), player(_player) utilityMenu = nullptr; } - if (player->getPlayerInfo()->getLocal()) { - sayMenu = addManagedMenu(player); + if (player->getLogic()->getPlayerInfo()->getLocal()) { + sayMenu = addManagedMenu(player->getLogic()); } else { sayMenu = nullptr; } @@ -55,13 +58,13 @@ PlayerMenu::PlayerMenu(PlayerLogic *_player) : QObject(_player), player(_player) void PlayerMenu::setMenusForGraphicItems() { - player->getGraphicsItem()->getTableZoneGraphicsItem()->setMenu(playerMenu); - player->getGraphicsItem()->getGraveyardZoneGraphicsItem()->setMenu(graveMenu, graveMenu->aViewGraveyard); - player->getGraphicsItem()->getRfgZoneGraphicsItem()->setMenu(rfgMenu, rfgMenu->aViewRfg); - if (player->getPlayerInfo()->getLocalOrJudge()) { - player->getGraphicsItem()->getHandZoneGraphicsItem()->setMenu(handMenu); - player->getGraphicsItem()->getDeckZoneGraphicsItem()->setMenu(libraryMenu, libraryMenu->aDrawCard); - player->getGraphicsItem()->getSideboardZoneGraphicsItem()->setMenu(sideboardMenu); + player->getTableZoneGraphicsItem()->setMenu(playerMenu); + player->getGraveyardZoneGraphicsItem()->setMenu(graveMenu, graveMenu->aViewGraveyard); + player->getRfgZoneGraphicsItem()->setMenu(rfgMenu, rfgMenu->aViewRfg); + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + player->getHandZoneGraphicsItem()->setMenu(handMenu); + player->getDeckZoneGraphicsItem()->setMenu(libraryMenu, libraryMenu->aDrawCard); + player->getSideboardZoneGraphicsItem()->setMenu(sideboardMenu); } } @@ -74,12 +77,14 @@ QMenu *PlayerMenu::updateCardMenu(const CardItem *card) // If is spectator (as spectators don't need card menus), return // only update the menu if the card is actually selected - if ((player->getGame()->getPlayerManager()->isSpectator() && !player->getGame()->getPlayerManager()->isJudge()) || - player->getGame()->getActiveCard() != card) { + if ((player->getLogic()->getGame()->getPlayerManager()->isSpectator() && + !player->getLogic()->getGame()->getPlayerManager()->isJudge()) || + player->getLogic()->getGame()->getActiveCard() != card) { return nullptr; } - QMenu *menu = new CardMenu(player, card, shortcutsActive); + CardMenu *menu = new CardMenu(player, card, shortcutsActive); + connect(menu, &CardMenu::cardInfoRequested, this, &PlayerMenu::cardInfoRequested); emit cardMenuUpdated(menu); return menu; @@ -87,7 +92,7 @@ QMenu *PlayerMenu::updateCardMenu(const CardItem *card) void PlayerMenu::retranslateUi() { - playerMenu->setTitle(tr("Player \"%1\"").arg(player->getPlayerInfo()->getName())); + playerMenu->setTitle(tr("Player \"%1\"").arg(player->getLogic()->getPlayerInfo()->getName())); for (auto *component : managedComponents) { component->retranslateUi(); @@ -104,7 +109,8 @@ void PlayerMenu::refreshShortcuts() { if (shortcutsActive) { // Judges get access to every player's menus but only want shortcuts to be set for their own. - if (player->getPlayerInfo()->getLocalOrJudge() && !player->getPlayerInfo()->getLocal()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge() && + !player->getLogic()->getPlayerInfo()->getLocal()) { setShortcutsInactive(); } else { setShortcutsActive(); diff --git a/cockatrice/src/game/player/menu/player_menu.h b/cockatrice/src/game/player/menu/player_menu.h index d5c19df58..62ba66df7 100644 --- a/cockatrice/src/game/player/menu/player_menu.h +++ b/cockatrice/src/game/player/menu/player_menu.h @@ -8,7 +8,6 @@ #define COCKATRICE_PLAYER_MENU_H #include "../../../interface/widgets/menus/tearoff_menu.h" -#include "../player_logic.h" #include "custom_zone_menu.h" #include "grave_menu.h" #include "hand_menu.h" @@ -23,29 +22,31 @@ #include class CardItem; +class CardMenu; +class PlayerGraphicsItem; class PlayerMenu : public QObject { Q_OBJECT signals: - void cardMenuUpdated(QMenu *cardMenu); + void cardMenuUpdated(CardMenu *cardMenu); + void cardInfoRequested(const CardRef &cardRef); void shortcutsActivated(); void shortcutsDeactivated(); void retranslateRequested(); public slots: void setMenusForGraphicItems(); + QMenu *updateCardMenu(const CardItem *card); private slots: void refreshShortcuts(); public: - explicit PlayerMenu(PlayerLogic *player); + explicit PlayerMenu(PlayerGraphicsItem *player); /** @brief Retranslate all user-visible strings. Called on language change. */ void retranslateUi(); - QMenu *updateCardMenu(const CardItem *card); - [[nodiscard]] QMenu *getPlayerMenu() const { return playerMenu; @@ -77,7 +78,7 @@ public: void setShortcutsInactive(); private: - PlayerLogic *player; + PlayerGraphicsItem *player; TearOffMenu *playerMenu; QMenu *countersMenu; HandMenu *handMenu; diff --git a/cockatrice/src/game/player/menu/pt_menu.cpp b/cockatrice/src/game/player/menu/pt_menu.cpp index 7dc3035c1..846256e24 100644 --- a/cockatrice/src/game/player/menu/pt_menu.cpp +++ b/cockatrice/src/game/player/menu/pt_menu.cpp @@ -3,30 +3,40 @@ #include "../player_actions.h" #include "../player_logic.h" -PtMenu::PtMenu(PlayerLogic *player) : QMenu(tr("Power / toughness")) +PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness")) { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); aIncP = new QAction(this); - connect(aIncP, &QAction::triggered, playerActions, &PlayerActions::actIncP); + connect(aIncP, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCards()); }); aDecP = new QAction(this); - connect(aDecP, &QAction::triggered, playerActions, &PlayerActions::actDecP); + connect(aDecP, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCards()); }); aIncT = new QAction(this); - connect(aIncT, &QAction::triggered, playerActions, &PlayerActions::actIncT); + connect(aIncT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCards()); }); aDecT = new QAction(this); - connect(aDecT, &QAction::triggered, playerActions, &PlayerActions::actDecT); + connect(aDecT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCards()); }); aIncPT = new QAction(this); - connect(aIncPT, &QAction::triggered, playerActions, [playerActions] { playerActions->actIncPT(); }); + connect(aIncPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCards()); }); aDecPT = new QAction(this); - connect(aDecPT, &QAction::triggered, playerActions, &PlayerActions::actDecPT); + connect(aDecPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCards()); }); aFlowP = new QAction(this); - connect(aFlowP, &QAction::triggered, playerActions, &PlayerActions::actFlowP); + connect(aFlowP, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCards()); }); aFlowT = new QAction(this); - connect(aFlowT, &QAction::triggered, playerActions, &PlayerActions::actFlowT); + connect(aFlowT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCards()); }); aSetPT = new QAction(this); - connect(aSetPT, &QAction::triggered, playerActions, &PlayerActions::actSetPT); + connect(aSetPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actSetPT(player->getGameScene()->selectedCards()); }); aResetPT = new QAction(this); - connect(aResetPT, &QAction::triggered, playerActions, &PlayerActions::actResetPT); + connect(aResetPT, &QAction::triggered, playerActions, + [player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCards()); }); addAction(aIncP); addAction(aDecP); diff --git a/cockatrice/src/game/player/menu/pt_menu.h b/cockatrice/src/game/player/menu/pt_menu.h index 645449586..72f828801 100644 --- a/cockatrice/src/game/player/menu/pt_menu.h +++ b/cockatrice/src/game/player/menu/pt_menu.h @@ -8,14 +8,14 @@ #define COCKATRICE_PT_MENU_H #include -class PlayerLogic; +class PlayerGraphicsItem; class PtMenu : public QMenu { Q_OBJECT public: - explicit PtMenu(PlayerLogic *player); + explicit PtMenu(PlayerGraphicsItem *player); void retranslateUi(); void setShortcutsActive(); diff --git a/cockatrice/src/game/player/menu/utility_menu.cpp b/cockatrice/src/game/player/menu/utility_menu.cpp index 6b33d7bde..005b38c3b 100644 --- a/cockatrice/src/game/player/menu/utility_menu.cpp +++ b/cockatrice/src/game/player/menu/utility_menu.cpp @@ -8,11 +8,14 @@ #include #include -UtilityMenu::UtilityMenu(PlayerLogic *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player) +UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player) { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); + connect(playerActions, &PlayerActions::requestEnableAndSetCreateAnotherTokenAction, this, + &UtilityMenu::setAndEnableCreateAnotherTokenAction); + connect(playerActions, &PlayerActions::requestSetLastToken, this, &UtilityMenu::setLastToken); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aUntapAll = new QAction(this); connect(aUntapAll, &QAction::triggered, playerActions, &PlayerActions::actUntapAll); @@ -23,19 +26,22 @@ UtilityMenu::UtilityMenu(PlayerLogic *_player, QMenu *playerMenu) : QMenu(player connect(aFlipCoin, &QAction::triggered, playerActions, &PlayerActions::actFlipCoin); aCreateToken = new QAction(this); - connect(aCreateToken, &QAction::triggered, playerActions, &PlayerActions::actCreateToken); + connect(aCreateToken, &QAction::triggered, playerActions, + [this]() { player->getLogic()->getPlayerActions()->actCreateToken(getPredefinedTokens()); }); aCreateAnotherToken = new QAction(this); connect(aCreateAnotherToken, &QAction::triggered, playerActions, &PlayerActions::actCreateAnotherToken); aCreateAnotherToken->setEnabled(false); aIncrementAllCardCounters = new QAction(this); - connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, - &PlayerActions::actIncrementAllCardCounters); + connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, [this]() { + player->getLogic()->getPlayerActions()->actIncrementAllCardCounters( + player->getGameScene()->selectedCards()); + }); createPredefinedTokenMenu = new QMenu(QString()); createPredefinedTokenMenu->setEnabled(false); - connect(player, &PlayerLogic::deckChanged, this, &UtilityMenu::populatePredefinedTokensMenu); + connect(player->getLogic(), &PlayerLogic::deckChanged, this, &UtilityMenu::populatePredefinedTokensMenu); playerMenu->addAction(aIncrementAllCardCounters); playerMenu->addSeparator(); @@ -66,7 +72,7 @@ void UtilityMenu::populatePredefinedTokensMenu() clear(); setEnabled(false); predefinedTokens.clear(); - const DeckList &deckList = player->getDeck(); + const DeckList &deckList = player->getLogic()->getDeck(); if (deckList.isEmpty()) { return; @@ -84,14 +90,24 @@ void UtilityMenu::populatePredefinedTokensMenu() if (i < 10) { a->setShortcut(QKeySequence("Alt+" + QString::number((i + 1) % 10))); } - connect(a, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actCreatePredefinedToken); + connect(a, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actCreatePredefinedToken); } } } +void UtilityMenu::setLastToken(CardInfoPtr lastToken) +{ + if (!createAnotherTokenActionExists()) { + return; + } + + player->getLogic()->getPlayerActions()->setLastTokenInfo(lastToken); +} + void UtilityMenu::retranslateUi() { - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aIncrementAllCardCounters->setText(tr("Increment all card counters")); aUntapAll->setText(tr("&Untap all permanents")); aRollDie->setText(tr("R&oll die...")); @@ -106,7 +122,7 @@ void UtilityMenu::setShortcutsActive() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aIncrementAllCardCounters->setShortcuts(shortcuts.getShortcut("Player/aIncrementAllCardCounters")); aUntapAll->setShortcuts(shortcuts.getShortcut("Player/aUntapAll")); aRollDie->setShortcuts(shortcuts.getShortcut("Player/aRollDie")); @@ -118,7 +134,7 @@ void UtilityMenu::setShortcutsActive() void UtilityMenu::setShortcutsInactive() { - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aUntapAll->setShortcut(QKeySequence()); aRollDie->setShortcut(QKeySequence()); aFlipCoin->setShortcut(QKeySequence()); diff --git a/cockatrice/src/game/player/menu/utility_menu.h b/cockatrice/src/game/player/menu/utility_menu.h index fab3211ca..bdc2a81a5 100644 --- a/cockatrice/src/game/player/menu/utility_menu.h +++ b/cockatrice/src/game/player/menu/utility_menu.h @@ -10,19 +10,21 @@ #include "abstract_player_component.h" #include +#include -class PlayerLogic; +class PlayerGraphicsItem; class UtilityMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public slots: void populatePredefinedTokensMenu(); + void setLastToken(CardInfoPtr lastToken); void retranslateUi() override; void setShortcutsActive() override; void setShortcutsInactive() override; public: - explicit UtilityMenu(PlayerLogic *player, QMenu *playerMenu); + explicit UtilityMenu(PlayerGraphicsItem *player, QMenu *playerMenu); [[nodiscard]] bool createAnotherTokenActionExists() const { @@ -31,7 +33,7 @@ public: void setAndEnableCreateAnotherTokenAction(QString text) { - aCreateAnotherToken->setText(text); + aCreateAnotherToken->setText(tr("C&reate another %1 token").arg(text)); aCreateAnotherToken->setEnabled(true); } @@ -41,7 +43,7 @@ public: } private: - PlayerLogic *player; + PlayerGraphicsItem *player; QStringList predefinedTokens; QMenu *createPredefinedTokenMenu; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 1706c44dc..7d58be31a 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -39,6 +39,8 @@ static constexpr int MOVE_TOP_CARD_UNTIL_INTERVAL = 100; PlayerActions::PlayerActions(PlayerLogic *_player) : QObject(_player), player(_player), lastTokenTableRow(0), movingCardsUntil(false) { + connect(this, &PlayerActions::requestZoneViewToggle, player, &PlayerLogic::onRequestZoneViewToggle); + moveTopCardTimer = new QTimer(this); moveTopCardTimer->setInterval(MOVE_TOP_CARD_UNTIL_INTERVAL); moveTopCardTimer->setSingleShot(true); @@ -133,12 +135,12 @@ void PlayerActions::playCardToTable(const CardItem *card, bool faceDown) void PlayerActions::actViewLibrary() { - player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, -1); + emit requestZoneViewToggle(ZoneNames::DECK, -1); } void PlayerActions::actViewHand() { - player->getGameScene()->toggleZoneView(player, ZoneNames::HAND, -1); + emit requestZoneViewToggle(ZoneNames::HAND, -1); } /** @@ -170,7 +172,7 @@ void PlayerActions::actSortHand() static QList defaultOptions = {CardList::SortByName, CardList::SortByPrinting}; - player->getGraphicsItem()->getHandZoneGraphicsItem()->sortHand(sortOptions + defaultOptions); + emit requestSortHand(sortOptions + defaultOptions); } void PlayerActions::actViewTopCards() @@ -182,7 +184,7 @@ void PlayerActions::actViewTopCards() deckSize, 1, &ok); if (ok) { defaultNumberTopCards = number; - player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number); + emit requestZoneViewToggle(ZoneNames::DECK, number); } } @@ -195,24 +197,24 @@ void PlayerActions::actViewBottomCards() deckSize, 1, &ok); if (ok) { defaultNumberBottomCards = number; - player->getGameScene()->toggleZoneView(player, ZoneNames::DECK, number, true); + emit requestZoneViewToggle(ZoneNames::DECK, number, true); } } -void PlayerActions::actAlwaysRevealTopCard() +void PlayerActions::actAlwaysRevealTopCard(bool alwaysRevealTopCard) { Command_ChangeZoneProperties cmd; cmd.set_zone_name(ZoneNames::DECK); - cmd.set_always_reveal_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysRevealTopCardChecked()); + cmd.set_always_reveal_top_card(alwaysRevealTopCard); sendGameCommand(cmd); } -void PlayerActions::actAlwaysLookAtTopCard() +void PlayerActions::actAlwaysLookAtTopCard(bool alwaysRevealTopCard) { Command_ChangeZoneProperties cmd; cmd.set_zone_name(ZoneNames::DECK); - cmd.set_always_look_at_top_card(player->getPlayerMenu()->getLibraryMenu()->isAlwaysLookAtTopCardChecked()); + cmd.set_always_look_at_top_card(alwaysRevealTopCard); sendGameCommand(cmd); } @@ -224,17 +226,17 @@ void PlayerActions::actOpenDeckInDeckEditor() void PlayerActions::actViewGraveyard() { - player->getGameScene()->toggleZoneView(player, ZoneNames::GRAVE, -1); + emit requestZoneViewToggle(ZoneNames::GRAVE, -1); } void PlayerActions::actViewRfg() { - player->getGameScene()->toggleZoneView(player, ZoneNames::EXILE, -1); + emit requestZoneViewToggle(ZoneNames::EXILE, -1); } void PlayerActions::actViewSideboard() { - player->getGameScene()->toggleZoneView(player, ZoneNames::SIDEBOARD, -1); + emit requestZoneViewToggle(ZoneNames::SIDEBOARD, -1); } void PlayerActions::actShuffle() @@ -862,9 +864,9 @@ void PlayerActions::actFlipCoin() sendGameCommand(cmd); } -void PlayerActions::actCreateToken() +void PlayerActions::actCreateToken(const QStringList &predefinedTokens) { - DlgCreateToken dlg(player->getPlayerMenu()->getUtilityMenu()->getPredefinedTokens(), player->getGame()->getTab()); + DlgCreateToken dlg(predefinedTokens, player->getGame()->getTab()); if (!dlg.exec()) { return; } @@ -880,8 +882,7 @@ void PlayerActions::actCreateToken() } } - player->getPlayerMenu()->getUtilityMenu()->setAndEnableCreateAnotherTokenAction( - tr("C&reate another %1 token").arg(lastTokenInfo.name)); + emit requestEnableAndSetCreateAnotherTokenAction(lastTokenInfo.name); actCreateAnotherToken(); } @@ -912,8 +913,12 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo) return; } - UtilityMenu *utilityMenu = player->getPlayerMenu()->getUtilityMenu(); - if (utilityMenu == nullptr || !utilityMenu->createAnotherTokenActionExists()) { + emit requestSetLastToken(cardInfo); +} + +void PlayerActions::setLastTokenInfo(CardInfoPtr cardInfo) +{ + if (cardInfo == nullptr) { return; } @@ -927,7 +932,7 @@ void PlayerActions::setLastToken(CardInfoPtr cardInfo) lastTokenTableRow = TableZone::tableRowToGridY(cardInfo->getUiAttributes().tableRow); - utilityMenu->setAndEnableCreateAnotherTokenAction(tr("C&reate another %1 token").arg(lastTokenInfo.name)); + emit requestEnableAndSetCreateAnotherTokenAction(lastTokenInfo.name); } void PlayerActions::actCreatePredefinedToken() @@ -1166,7 +1171,7 @@ void PlayerActions::actSayMessage() sendGameCommand(cmd); } -void PlayerActions::actMoveCardXCardsFromTop() +void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards) { int deckSize = player->getDeckZone()->getCards().size() + 1; // add the card to move to the deck bool ok; @@ -1182,7 +1187,7 @@ void PlayerActions::actMoveCardXCardsFromTop() defaultNumberTopCardsToPlaceBelow = number; - QList cardList = player->getGameScene()->selectedCards(); + QList cardList = selectedCards; if (cardList.isEmpty()) { return; } @@ -1213,12 +1218,12 @@ void PlayerActions::actMoveCardXCardsFromTop() } } -void PlayerActions::actIncPT(int deltaP, int deltaT) +void PlayerActions::actIncPT(QList selectedCards, int deltaP, int deltaT) { int playerid = player->getPlayerInfo()->getId(); QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { QString pt = card->getPT(); const auto ptList = CardItem::parsePT(pt); QString newpt; @@ -1246,11 +1251,11 @@ void PlayerActions::actIncPT(int deltaP, int deltaT) player->getGame()->getGameEventHandler()->sendGameCommand(prepareGameCommand(commandList), playerid); } -void PlayerActions::actResetPT() +void PlayerActions::actResetPT(QList selectedCards) { int playerid = player->getPlayerInfo()->getId(); QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { QString ptString; if (!card->getFaceDown()) { // leave the pt empty if the card is face down ExactCard ec = card->getCard(); @@ -1279,13 +1284,12 @@ void PlayerActions::actResetPT() } } -void PlayerActions::actSetPT() +void PlayerActions::actSetPT(QList selectedCards) { QString oldPT; int playerid = player->getPlayerInfo()->getId(); - auto cards = player->getGameScene()->selectedCards(); - for (auto card : cards) { + for (auto card : selectedCards) { if (!card->getPT().isEmpty()) { oldPT = card->getPT(); } @@ -1303,7 +1307,7 @@ void PlayerActions::actSetPT() bool empty = ptList.isEmpty(); QList commandList; - for (auto card : cards) { + for (auto card : selectedCards) { auto *cmd = new Command_SetCardAttr; QString newpt = QString(); if (!empty) { @@ -1347,47 +1351,47 @@ void PlayerActions::actDrawArrow() } } -void PlayerActions::actIncP() +void PlayerActions::actIncP(QList selectedCards) { - actIncPT(1, 0); + actIncPT(selectedCards, 1, 0); } -void PlayerActions::actDecP() +void PlayerActions::actDecP(QList selectedCards) { - actIncPT(-1, 0); + actIncPT(selectedCards, -1, 0); } -void PlayerActions::actIncT() +void PlayerActions::actIncT(QList selectedCards) { - actIncPT(0, 1); + actIncPT(selectedCards, 0, 1); } -void PlayerActions::actDecT() +void PlayerActions::actDecT(QList selectedCards) { - actIncPT(0, -1); + actIncPT(selectedCards, 0, -1); } -void PlayerActions::actIncPT() +void PlayerActions::actIncPT(QList selectedCards) { - actIncPT(1, 1); + actIncPT(selectedCards, 1, 1); } -void PlayerActions::actDecPT() +void PlayerActions::actDecPT(QList selectedCards) { - actIncPT(-1, -1); + actIncPT(selectedCards, -1, -1); } -void PlayerActions::actFlowP() +void PlayerActions::actFlowP(QList selectedCards) { - actIncPT(1, -1); + actIncPT(selectedCards, 1, -1); } -void PlayerActions::actFlowT() +void PlayerActions::actFlowT(QList selectedCards) { - actIncPT(-1, 1); + actIncPT(selectedCards, -1, 1); } -void PlayerActions::actReduceLifeByPower() +void PlayerActions::actReduceLifeByPower(QList selectedCards) { // find life counter auto lifeCounter = player->getLifeCounter(); @@ -1395,10 +1399,9 @@ void PlayerActions::actReduceLifeByPower() return; } - // calculate total power - auto cards = player->getGameScene()->selectedCards(); + // calculate total power; int total = 0; - for (auto card : cards) { + for (auto card : selectedCards) { QVariantList parsed = CardItem::parsePT(card->getPT()); if (!parsed.isEmpty()) { int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int @@ -1423,11 +1426,10 @@ void AnnotationDialog::keyPressEvent(QKeyEvent *event) QInputDialog::keyPressEvent(event); } -void PlayerActions::actSetAnnotation() +void PlayerActions::actSetAnnotation(QList selectedCards) { QString oldAnnotation; - auto cards = player->getGameScene()->selectedCards(); - for (auto card : cards) { + for (auto card : selectedCards) { if (!card->getAnnotation().isEmpty()) { oldAnnotation = card->getAnnotation(); } @@ -1447,7 +1449,7 @@ void PlayerActions::actSetAnnotation() QString annotation = dialog->textValue().left(MAX_NAME_LENGTH); QList commandList; - for (auto card : cards) { + for (auto card : selectedCards) { auto *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); @@ -1468,10 +1470,10 @@ void PlayerActions::actAttach() card->drawAttachArrow(); } -void PlayerActions::actUnattach() +void PlayerActions::actUnattach(QList selectedCards) { QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { if (!card->getAttachedTo()) { continue; } @@ -1484,20 +1486,20 @@ void PlayerActions::actUnattach() sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actAddCardCounter(int counterId) +void PlayerActions::actAddCardCounter(QList selectedCards, int counterId) { - offsetCardCounter(counterId, 1); + offsetCardCounter(selectedCards, counterId, 1); } -void PlayerActions::actRemoveCardCounter(int counterId) +void PlayerActions::actRemoveCardCounter(QList selectedCards, int counterId) { - offsetCardCounter(counterId, -1); + offsetCardCounter(selectedCards, counterId, -1); } -void PlayerActions::offsetCardCounter(int counterId, int offset) +void PlayerActions::offsetCardCounter(QList selectedCards, int counterId, int offset) { QList commandList; - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { int oldValue = card->getCounters().value(counterId, 0); int newValue = oldValue + offset; @@ -1517,15 +1519,14 @@ void PlayerActions::offsetCardCounter(int counterId, int offset) sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actSetCardCounter(int counterId) +void PlayerActions::actSetCardCounter(QList selectedCards, int counterId) { player->setDialogSemaphore(true); // If a single card is selected, we show the old value in the dialog. Otherwise, we show "x" - QList sel = player->getGameScene()->selectedCards(); QString oldValueForDlg = "x"; - if (sel.size() == 1) { - auto *card = sel.first(); + if (selectedCards.size() == 1) { + auto *card = selectedCards.first(); oldValueForDlg = QString::number(card->getCounters().value(counterId, 0)); } @@ -1541,7 +1542,7 @@ void PlayerActions::actSetCardCounter(int counterId) } QList commandList; - for (auto card : sel) { + for (auto card : selectedCards) { int oldValue = card->getCounters().value(counterId, 0); Expression exp(oldValue); double parsed = exp.parse(dialog.textValue()); @@ -1559,9 +1560,8 @@ void PlayerActions::actSetCardCounter(int counterId) sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actIncrementAllCardCounters() +void PlayerActions::actIncrementAllCardCounters(QList cardsToUpdate) { - auto cardsToUpdate = player->getGameScene()->selectedCards(); if (cardsToUpdate.isEmpty()) { // If no cards selected, update all cards on table cardsToUpdate = static_cast>(player->getTableZone()->getCards()); @@ -1607,10 +1607,8 @@ static bool isUnwritableRevealZone(CardZoneLogic *zone) return false; } -void PlayerActions::playSelectedCards(const bool faceDown) +void PlayerActions::playSelectedCards(QList selectedCards, const bool faceDown) { - QList selectedCards = player->getGameScene()->selectedCards(); - // CardIds will get shuffled downwards when cards leave the deck. // We need to iterate through the cards in reverse order so cardIds don't get changed out from under us as we play // out the cards one-by-one. @@ -1624,19 +1622,19 @@ void PlayerActions::playSelectedCards(const bool faceDown) } } -void PlayerActions::actPlay() +void PlayerActions::actPlay(QList selectedCards) { - playSelectedCards(false); + playSelectedCards(selectedCards, false); } -void PlayerActions::actPlayFacedown() +void PlayerActions::actPlayFacedown(QList selectedCards) { - playSelectedCards(true); + playSelectedCards(selectedCards, true); } -void PlayerActions::actHide() +void PlayerActions::actHide(QList selectedCards) { - for (const auto &item : player->getGameScene()->selectedCards()) { + for (const auto &item : selectedCards) { auto *card = static_cast(item); if (card && isUnwritableRevealZone(card->getZone())) { card->getZone()->removeCard(card); @@ -1644,7 +1642,7 @@ void PlayerActions::actHide() } } -void PlayerActions::actReveal(QAction *action) +void PlayerActions::actReveal(QList selectedCards, QAction *action) { const int otherPlayerId = action->data().toInt(); @@ -1653,7 +1651,7 @@ void PlayerActions::actReveal(QAction *action) cmd.set_player_id(otherPlayerId); } - for (auto card : player->getGameScene()->selectedCards()) { + for (auto card : selectedCards) { if (!cmd.has_zone_name()) { cmd.set_zone_name(card->getZone()->getName().toStdString()); } @@ -1735,15 +1733,14 @@ void PlayerActions::actRevealRandomGraveyardCard(int revealToPlayerId) sendGameCommand(cmd); } -void PlayerActions::cardMenuAction() +void PlayerActions::cardMenuAction(QList selectedCards, CardMenuActionType type) { - auto *a = dynamic_cast(sender()); - QList cardList = player->getGameScene()->selectedCards(); + QList cardList = selectedCards; QList commandList; - if (a->data().toInt() <= (int)cmClone) { + if (type <= cmClone) { for (const auto &card : cardList) { - switch (static_cast(a->data().toInt())) { + switch (type) { // Leaving both for compatibility with server case cmUntap: // fallthrough @@ -1824,7 +1821,7 @@ void PlayerActions::cardMenuAction() idList.add_card()->set_card_id(i->getId()); } - switch (static_cast(a->data().toInt())) { + switch (type) { case cmMoveToTopLibrary: { auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 3b822b61a..940de610f 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -9,6 +9,7 @@ #define COCKATRICE_PLAYER_ACTIONS_H #include "../dialogs/dlg_create_token.h" #include "../dialogs/dlg_move_top_cards_until.h" +#include "card_menu_action_type.h" #include "event_processing_options.h" #include "player_logic.h" @@ -56,15 +57,22 @@ public: return movingCardsUntil; } +signals: + void requestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed = false); + void requestSortHand(const QList &options); + void requestEnableAndSetCreateAnotherTokenAction(const QString &lastTokenName); + void requestSetLastToken(CardInfoPtr lastToken); + public slots: void setLastToken(CardInfoPtr cardInfo); + void setLastTokenInfo(CardInfoPtr cardInfo); void playCard(CardItem *c, bool faceDown); void playCardToTable(const CardItem *c, bool faceDown); void actUntapAll(); void actRollDie(); void actFlipCoin(); - void actCreateToken(); + void actCreateToken(const QStringList &predefinedTokens); void actCreateAnotherToken(); void actShuffle(); void actShuffleTop(); @@ -77,9 +85,9 @@ public slots: void actMulliganMinusOne(); void doMulligan(int number); - void actPlay(); - void actPlayFacedown(); - void actHide(); + void actPlay(QList selectedCards); + void actPlayFacedown(QList selectedCards); + void actHide(QList selectedCards); void actMoveTopCardToPlay(); void actMoveTopCardToPlayFaceDown(); @@ -111,8 +119,8 @@ public slots: void actViewHand(); void actViewTopCards(); void actViewBottomCards(); - void actAlwaysRevealTopCard(); - void actAlwaysLookAtTopCard(); + void actAlwaysRevealTopCard(bool alwaysRevealTopCard); + void actAlwaysLookAtTopCard(bool alwaysRevealTopCard); void actViewGraveyard(); void actLendLibrary(int lendToPlayerId); void actRevealTopCards(int revealToPlayerId, int amount); @@ -127,37 +135,37 @@ public slots: void actCreateRelatedCard(); void actCreateAllRelatedCards(); - void actMoveCardXCardsFromTop(); - void actRemoveCardCounter(int counterId); - void actAddCardCounter(int counterId); - void actSetCardCounter(int counterId); - void actIncrementAllCardCounters(); + void actMoveCardXCardsFromTop(QList selectedCards); + void actRemoveCardCounter(QList selectedCards, int counterId); + void actAddCardCounter(QList selectedCards, int counterId); + void actSetCardCounter(QList selectedCards, int counterId); + void actIncrementAllCardCounters(QList cardsToUpdate); void actAttach(); - void actUnattach(); + void actUnattach(QList selectedCards); void actDrawArrow(); - void actIncPT(int deltaP, int deltaT); - void actResetPT(); - void actSetPT(); - void actIncP(); - void actDecP(); - void actIncT(); - void actDecT(); - void actIncPT(); - void actDecPT(); - void actFlowP(); - void actFlowT(); + void actIncPT(QList selectedCards, int deltaP, int deltaT); + void actResetPT(QList selectedCards); + void actSetPT(QList selectedCards); + void actIncP(QList selectedCards); + void actDecP(QList selectedCards); + void actIncT(QList selectedCards); + void actDecT(QList selectedCards); + void actIncPT(QList selectedCards); + void actDecPT(QList selectedCards); + void actFlowP(QList selectedCards); + void actFlowT(QList selectedCards); - void actReduceLifeByPower(); + void actReduceLifeByPower(QList selectedCards); - void actSetAnnotation(); - void actReveal(QAction *action); + void actSetAnnotation(QList selectedCards); + void actReveal(QList selectedCards, QAction *action); void actRevealHand(int revealToPlayerId); void actRevealRandomHandCard(int revealToPlayerId); void actRevealLibrary(int revealToPlayerId); void actSortHand(); - void cardMenuAction(); + void cardMenuAction(QList selectedCards, CardMenuActionType type); private: PlayerLogic *player; @@ -185,12 +193,12 @@ private: bool persistent = false); bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation); - void playSelectedCards(bool faceDown = false); + void playSelectedCards(QList selectedCards, bool faceDown = false); void cmdSetTopCard(Command_MoveCard &cmd); void cmdSetBottomCard(Command_MoveCard &cmd); - void offsetCardCounter(int counterId, int offset); + void offsetCardCounter(QList selectedCards, int counterId, int offset); }; #endif // COCKATRICE_PLAYER_ACTIONS_H diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index debc6c8f7..aa751170b 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -6,7 +6,6 @@ #include "../board/arrow_item.h" #include "../board/card_item.h" #include "../board/card_list.h" -#include "libcockatrice/utility/color.h" #include "player_actions.h" #include "player_logic.h" @@ -33,10 +32,12 @@ #include #include #include +#include #include PlayerEventHandler::PlayerEventHandler(PlayerLogic *_player) : QObject(_player), player(_player) { + connect(this, &PlayerEventHandler::requestCardMenuUpdate, player, &PlayerLogic::requestCardMenuUpdate); } void PlayerEventHandler::eventGameSay(const Event_GameSay &event) @@ -252,7 +253,7 @@ void PlayerEventHandler::eventSetCardCounter(const Event_SetCardCounter &event) int oldValue = card->getCounters().value(event.counter_id(), 0); card->setCounter(event.counter_id(), event.counter_value()); - player->getPlayerMenu()->updateCardMenu(card); + emit requestCardMenuUpdate(card); emit logSetCardCounter(player, card->getName(), event.counter_id(), event.counter_value(), oldValue); } @@ -370,7 +371,7 @@ void PlayerEventHandler::eventMoveCard(const Event_MoveCard &event, const GameEv targetZone->addCard(card, true, x, y); emit cardZoneChanged(card, startZone == targetZone); - player->getPlayerMenu()->updateCardMenu(card); + emit requestCardMenuUpdate(card); if (player->getPlayerActions()->isMovingCardsUntil() && startZoneString == ZoneNames::DECK && targetZone->getName() == ZoneNames::STACK) { @@ -397,7 +398,7 @@ void PlayerEventHandler::eventFlipCard(const Event_FlipCard &event) emit logFlipCard(player, card->getName(), event.face_down()); card->setFaceDown(event.face_down()); - player->getPlayerMenu()->updateCardMenu(card); + emit requestCardMenuUpdate(card); } void PlayerEventHandler::eventDestroyCard(const Event_DestroyCard &event) @@ -466,7 +467,7 @@ void PlayerEventHandler::eventAttachCard(const Event_AttachCard &event) } else { emit logUnattachCard(player, startCard->getName()); } - player->getPlayerMenu()->updateCardMenu(startCard); + emit requestCardMenuUpdate(startCard); } void PlayerEventHandler::eventDrawCards(const Event_DrawCards &event) @@ -552,7 +553,7 @@ void PlayerEventHandler::eventRevealCards(const Event_RevealCards &event, EventP } if (!options.testFlag(SKIP_REVEAL_WINDOW) && showZoneView && !cardList.isEmpty()) { - player->getGameScene()->addRevealedZoneView(player, zone, cardList, event.grant_write_access()); + emit player->requestRevealedZoneView(player, zone, cardList, event.grant_write_access()); } emit logRevealCards(player, zone, cardId, cardName, otherPlayer, false, diff --git a/cockatrice/src/game/player/player_event_handler.h b/cockatrice/src/game/player/player_event_handler.h index 958dee16b..cfd82933f 100644 --- a/cockatrice/src/game/player/player_event_handler.h +++ b/cockatrice/src/game/player/player_event_handler.h @@ -83,6 +83,7 @@ signals: void logAlwaysRevealTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal); void logAlwaysLookAtTopCard(PlayerLogic *player, CardZoneLogic *zone, bool reveal); void cardZoneChanged(CardItem *card, bool sameZone); + void requestCardMenuUpdate(const CardItem *card); public: PlayerEventHandler(PlayerLogic *player); diff --git a/cockatrice/src/game/player/player_graphics_item.cpp b/cockatrice/src/game/player/player_graphics_item.cpp index 0d4f8c3ed..d86fce86b 100644 --- a/cockatrice/src/game/player/player_graphics_item.cpp +++ b/cockatrice/src/game/player/player_graphics_item.cpp @@ -8,6 +8,9 @@ #include "../board/abstract_card_item.h" #include "../board/counter_general.h" #include "../hand_counter.h" +#include "player_actions.h" + +#include PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) { @@ -16,23 +19,26 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) connect(&SettingsCache::instance(), &SettingsCache::handJustificationChanged, this, &PlayerGraphicsItem::rearrangeZones); connect(player, &PlayerLogic::rearrangeCounters, this, &PlayerGraphicsItem::rearrangeCounters); + connect(player, &PlayerLogic::activeChanged, this, &PlayerGraphicsItem::onPlayerActiveChanged); connect(player, &PlayerLogic::concededChanged, this, [this](int, bool c) { setVisible(!c); }); connect(player, &PlayerLogic::zoneIdChanged, this, [this](int id) { playerArea->setPlayerZoneId(id); }); connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded); connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved); - connect(player->getPlayerMenu(), &PlayerMenu::shortcutsActivated, this, [this]() { + playerMenu = new PlayerMenu(this); + + connect(playerMenu, &PlayerMenu::shortcutsActivated, this, [this]() { for (auto *ctr : counterWidgets) { ctr->setShortcutsActive(); } }); - connect(player->getPlayerMenu(), &PlayerMenu::shortcutsDeactivated, this, [this]() { + connect(playerMenu, &PlayerMenu::shortcutsDeactivated, this, [this]() { for (auto *ctr : counterWidgets) { ctr->setShortcutsInactive(); } }); - connect(player->getPlayerMenu(), &PlayerMenu::retranslateRequested, this, [this]() { + connect(playerMenu, &PlayerMenu::retranslateRequested, this, [this]() { for (auto *ctr : counterWidgets) { ctr->retranslateUi(); } @@ -47,6 +53,8 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) initializeZones(); + playerMenu->setMenusForGraphicItems(); + connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); updateBoundingRect(); @@ -57,7 +65,7 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) void PlayerGraphicsItem::retranslateUi() { - player->getPlayerMenu()->retranslateUi(); + playerMenu->retranslateUi(); QMapIterator zoneIterator(player->getZones()); while (zoneIterator.hasNext()) { @@ -93,14 +101,16 @@ void PlayerGraphicsItem::initializeZones() rfgZoneGraphicsItem = new PileZone(player->getRfgZone(), this); rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10)); - tableZoneGraphicsItem = new TableZone(player->getTableZone(), this); + tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this); connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); + connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored); stackZoneGraphicsItem = new StackZone(player->getStackZone(), static_cast(tableZoneGraphicsItem->boundingRect().height()), this); handZoneGraphicsItem = new HandZone(player->getHandZone(), static_cast(tableZoneGraphicsItem->boundingRect().height()), this); + connect(player->getPlayerActions(), &PlayerActions::requestSortHand, handZoneGraphicsItem, &HandZone::sortHand); connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter, &HandCounter::updateNumber); @@ -145,6 +155,7 @@ void PlayerGraphicsItem::setMirrored(bool _mirrored) { if (mirrored != _mirrored) { mirrored = _mirrored; + emit mirroredChanged(mirrored); rearrangeZones(); } } @@ -159,11 +170,11 @@ void PlayerGraphicsItem::onCounterAdded(CounterState *state) } counterWidgets.insert(state->getId(), widget); - if (player->getPlayerMenu()->getCountersMenu() && widget->getMenu()) { - player->getPlayerMenu()->getCountersMenu()->addMenu(widget->getMenu()); + if (playerMenu->getCountersMenu() && widget->getMenu()) { + playerMenu->getCountersMenu()->addMenu(widget->getMenu()); } - if (player->getPlayerMenu()->getShortcutsActive()) { + if (playerMenu->getShortcutsActive()) { widget->setShortcutsActive(); } @@ -176,8 +187,8 @@ void PlayerGraphicsItem::onCounterRemoved(int counterId) if (!widget) { return; } - if (player->getPlayerMenu()->getCountersMenu() && widget->getMenu()) { - player->getPlayerMenu()->getCountersMenu()->removeAction(widget->getMenu()->menuAction()); + if (playerMenu->getCountersMenu() && widget->getMenu()) { + playerMenu->getCountersMenu()->removeAction(widget->getMenu()->menuAction()); } widget->delCounter(); rearrangeCounters(); diff --git a/cockatrice/src/game/player/player_graphics_item.h b/cockatrice/src/game/player/player_graphics_item.h index e37fe7290..1acb1520f 100644 --- a/cockatrice/src/game/player/player_graphics_item.h +++ b/cockatrice/src/game/player/player_graphics_item.h @@ -55,11 +55,16 @@ public: return static_cast(scene()); } - PlayerLogic *getPlayer() const + PlayerLogic *getLogic() const { return player; } + [[nodiscard]] PlayerMenu *getPlayerMenu() const + { + return playerMenu; + } + PlayerArea *getPlayerArea() const { return playerArea; @@ -111,9 +116,12 @@ public slots: signals: void sizeChanged(); void playerCountChanged(); + void mirroredChanged(bool isMirrored); + void cardInfoRequested(const CardRef &cardRef); private: PlayerLogic *player; + PlayerMenu *playerMenu; PlayerArea *playerArea; PlayerTarget *playerTarget; QMap counterWidgets; diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index 0210aa0c6..b748eb19a 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -35,14 +35,6 @@ PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool conceded(false), zoneId(0), dialogSemaphore(false) { initializeZones(); - - playerMenu = new PlayerMenu(this); - graphicsItem = new PlayerGraphicsItem(this); - playerMenu->setMenusForGraphicItems(); - - connect(this, &PlayerLogic::activeChanged, graphicsItem, &PlayerGraphicsItem::onPlayerActiveChanged); - - connect(this, &PlayerLogic::openDeckEditor, game->getTab(), &TabGame::openDeckEditor); } void PlayerLogic::initializeZones() @@ -68,7 +60,6 @@ PlayerLogic::~PlayerLogic() } zones.clear(); - delete playerMenu; delete getPlayerInfo()->userInfo; } @@ -326,22 +317,16 @@ void PlayerLogic::setActive(bool _active) active = _active; emit activeChanged(active); } +void PlayerLogic::onRequestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed) +{ + emit requestZoneViewToggle(this, zoneName, numberCards, isReversed); +} void PlayerLogic::updateZones() { getTableZone()->reorganizeCards(); } -PlayerGraphicsItem *PlayerLogic::getGraphicsItem() -{ - return graphicsItem; -} - -GameScene *PlayerLogic::getGameScene() -{ - return getGraphicsItem()->getGameScene(); -} - void PlayerLogic::setGameStarted() { if (playerInfo->local) { diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index c3508d069..c83892dea 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -67,8 +67,14 @@ class PlayerLogic : public QObject signals: void openDeckEditor(const LoadedDeck &deck); + void requestZoneViewToggle(PlayerLogic *player, const QString &zoneName, int numberCards, bool isReversed); + void requestRevealedZoneView(PlayerLogic *player, + CardZoneLogic *zone, + const QList &cardList, + bool withWritePermission); void deckChanged(); void newCardAdded(AbstractCardItem *card); + void requestCardMenuUpdate(const CardItem *card); void counterAdded(CounterState *state); void counterRemoved(int counterId); void rearrangeCounters(); @@ -85,6 +91,7 @@ signals: public slots: void setActive(bool _active); + void onRequestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed); public: PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent); @@ -112,10 +119,6 @@ public: return game; } - GameScene *getGameScene(); - - [[nodiscard]] PlayerGraphicsItem *getGraphicsItem(); - [[nodiscard]] PlayerActions *getPlayerActions() const { return playerActions; @@ -131,11 +134,6 @@ public: return playerInfo; } - [[nodiscard]] PlayerMenu *getPlayerMenu() const - { - return playerMenu; - } - void setDeck(const DeckList &_deck); [[nodiscard]] const DeckList &getDeck() const @@ -234,8 +232,6 @@ private: PlayerInfo *playerInfo; PlayerEventHandler *playerEventHandler; PlayerActions *playerActions; - PlayerMenu *playerMenu; - PlayerGraphicsItem *graphicsItem; bool active; bool conceded; diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index ffb4adf5c..245de8281 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -22,7 +22,8 @@ const QColor TableZone::FADE_MASK = QColor(0, 0, 0, 80); const QColor TableZone::GRADIENT_COLOR = QColor(255, 255, 255, 150); const QColor TableZone::GRADIENT_COLORLESS = QColor(255, 255, 255, 0); -TableZone::TableZone(TableZoneLogic *_logic, QGraphicsItem *parent) : SelectZone(_logic, parent), active(false) +TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *parent) + : SelectZone(_logic, parent), active(false), mirrored(_mirrored) { connect(_logic, &TableZoneLogic::contentSizeChanged, this, &TableZone::resizeToContents); connect(_logic, &TableZoneLogic::toggleTapped, this, &TableZone::toggleTapped); @@ -50,12 +51,16 @@ QRectF TableZone::boundingRect() const return QRectF(0, 0, width, height); } +void TableZone::setMirrored(bool isMirrored) +{ + mirrored = isMirrored; + update(); +} + bool TableZone::isInverted() const { - return ((getLogic()->getPlayer()->getGraphicsItem()->getMirrored() && - !SettingsCache::instance().getInvertVerticalCoordinate()) || - (!getLogic()->getPlayer()->getGraphicsItem()->getMirrored() && - SettingsCache::instance().getInvertVerticalCoordinate())); + return ((mirrored && !SettingsCache::instance().getInvertVerticalCoordinate()) || + (!mirrored && SettingsCache::instance().getInvertVerticalCoordinate())); } void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h index f46531520..8a898173b 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.h +++ b/cockatrice/src/game_graphics/zones/table_zone.h @@ -82,6 +82,7 @@ private: If this TableZone is currently active */ bool active = false; + bool mirrored = false; [[nodiscard]] bool isInverted() const; @@ -96,6 +97,7 @@ public slots: Reorganizes CardItems in the TableZone */ void reorganizeCards() override; + void setMirrored(bool isMirrored); public: /** @@ -104,7 +106,7 @@ public: @param _p the Player @param parent defaults to null */ - explicit TableZone(TableZoneLogic *_logic, QGraphicsItem *parent = nullptr); + explicit TableZone(TableZoneLogic *_logic, bool mirrored, QGraphicsItem *parent = nullptr); /** @return a QRectF of the TableZone bounding box. diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index c52f73319..1e2bebd15 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -1,6 +1,7 @@ #include "tab_game.h" #include "../../../client/settings/cache_settings.h" +#include "../../../game/player/menu/card_menu.h" #include "../game/board/arrow_item.h" #include "../game/board/card_item.h" #include "../game/deckview/deck_view_container.h" @@ -363,11 +364,10 @@ void TabGame::retranslateUi() cardInfoFrameWidget->retranslateUi(); - QMapIterator i(game->getPlayerManager()->getPlayers()); - - while (i.hasNext()) { - i.next().value()->getGraphicsItem()->retranslateUi(); + for (auto playerView : scene->getPlayers().values()) { + playerView->retranslateUi(); } + QMapIterator j(deckViewContainers); while (j.hasNext()) { j.next().value()->playerDeckView->retranslateUi(); @@ -654,8 +654,12 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer) scene->addPlayer(newPlayer); + auto *view = scene->viewForPlayer(newPlayer->getPlayerInfo()->getId()); + connect(newPlayer, &PlayerLogic::newCardAdded, this, &TabGame::newCardAdded); - connect(newPlayer->getPlayerMenu(), &PlayerMenu::cardMenuUpdated, this, &TabGame::setCardMenu); + connect(newPlayer, &PlayerLogic::openDeckEditor, this, &TabGame::openDeckEditor); + connect(view->getPlayerMenu(), &PlayerMenu::cardMenuUpdated, this, &TabGame::setCardMenu); + connect(view, &PlayerGraphicsItem::cardInfoRequested, this, &TabGame::viewCardInfo); messageLog->connectToPlayerEventHandler(newPlayer->getPlayerEventHandler()); @@ -668,7 +672,7 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer) addLocalPlayer(newPlayer, newPlayer->getPlayerInfo()->getId()); } - gameMenu->insertMenu(playersSeparator, newPlayer->getPlayerMenu()->getPlayerMenu()); + gameMenu->insertMenu(playersSeparator, view->getPlayerMenu()->getPlayerMenu()); createZoneForPlayer(newPlayer, newPlayer->getPlayerInfo()->getId()); @@ -678,7 +682,7 @@ PlayerLogic *TabGame::addPlayer(PlayerLogic *newPlayer) void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId) { if (game->getGameState()->getClients().size() == 1) { - newPlayer->getPlayerMenu()->setShortcutsActive(); + scene->viewForPlayer(playerId)->getPlayerMenu()->setShortcutsActive(); } auto *deckView = new TabbedDeckViewContainer(playerId, this); @@ -698,27 +702,24 @@ void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId) void TabGame::processPlayerLeave(PlayerLogic *leavingPlayer) { - QString playerName = "@" + leavingPlayer->getPlayerInfo()->getName(); - removePlayerFromAutoCompleteList(playerName); - - scene->removePlayer(leavingPlayer); + removePlayerFromAutoCompleteList("@" + leavingPlayer->getPlayerInfo()->getName()); // When we inserted the playerMenu into the gameMenu earlier, Qt wrapped the playerMenu into a QAction*, which lives // independently and does not get cleaned up when the source menu gets destroyed. We have to manually clean here. - if (leavingPlayer->getPlayerMenu()) { - QMenu *menu = leavingPlayer->getPlayerMenu()->getPlayerMenu(); - if (menu) { - // Find and remove the QAction pointing to this menu - QList actions = gameMenu->actions(); - for (QAction *act : actions) { - if (act->menu() == menu) { - gameMenu->removeAction(act); - delete act; // deletes the QAction wrapper around the submenu - break; - } + auto *view = scene->viewForPlayer(leavingPlayer->getPlayerInfo()->getId()); + if (view) { + // Find and remove the QAction pointing to this menu + QMenu *menu = view->getPlayerMenu()->getPlayerMenu(); + for (QAction *act : gameMenu->actions()) { + if (act->menu() == menu) { + gameMenu->removeAction(act); + delete act; + break; } } } + + scene->removePlayer(leavingPlayer); } void TabGame::processRemotePlayerDeckSelect(QString deckList, int playerId, QString playerName) @@ -869,12 +870,12 @@ PlayerLogic *TabGame::setActivePlayer(int id) if (i.value() == player) { i.value()->setActive(true); if (game->getGameState()->getClients().size() > 1) { - i.value()->getPlayerMenu()->setShortcutsActive(); + scene->viewForPlayer(i.value()->getPlayerInfo()->getId())->getPlayerMenu()->setShortcutsActive(); } } else { i.value()->setActive(false); if (game->getGameState()->getClients().size() > 1) { - i.value()->getPlayerMenu()->setShortcutsInactive(); + scene->viewForPlayer(i.value()->getPlayerInfo()->getId())->getPlayerMenu()->setShortcutsInactive(); } } } @@ -890,8 +891,13 @@ void TabGame::setActivePhase(int phase) void TabGame::newCardAdded(AbstractCardItem *card) { + connect(card, &AbstractCardItem::rightClicked, scene, &GameScene::onCardRightClicked); + connect(card, &AbstractCardItem::playSelected, scene, &GameScene::playSelected); + connect(card, &AbstractCardItem::playSelectedFaceDown, scene, &GameScene::playSelectedFaceDown); + connect(card, &AbstractCardItem::hideSelected, scene, &GameScene::hideSelected); connect(card, &AbstractCardItem::hovered, cardInfoFrameWidget, qOverload(&CardInfoFrameWidget::setCard)); + connect(card, &AbstractCardItem::selectionChanged, scene, &GameScene::onCardSelectionChanged); connect(card, &AbstractCardItem::showCardInfoPopup, this, &TabGame::showCardInfoPopup); connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString))); connect(card, &AbstractCardItem::cardShiftClicked, this, &TabGame::linkCardToChat); @@ -935,7 +941,7 @@ QString TabGame::getTabText() const /** * @param menu The menu to set. Pass in nullptr to set the menu to empty. */ -void TabGame::setCardMenu(QMenu *menu) +void TabGame::setCardMenu(CardMenu *menu) { if (!aCardMenu) { return; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index 7f9392034..ddda4d9b9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -141,7 +141,7 @@ signals: private slots: void adminLockChanged(bool lock); void newCardAdded(AbstractCardItem *card); - void setCardMenu(QMenu *menu); + void setCardMenu(CardMenu *menu); void actGameInfo(); void actConcede(); From 487bb84b6f4473f230871461f4a1bae0de5edc46 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:07:06 +0200 Subject: [PATCH 17/50] [Game][Menus] Make Menus accept PlayerGraphicsItem instead of PlayerLogic (#6945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Game][Player] Split Player into PlayerLogic/PlayerGraphicsItem Took 4 minutes Took 58 seconds Took 2 minutes * [Game][Menus] Make Menus accept PlayerGraphicsItem instead of PlayerLogic Took 7 minutes Took 4 minutes Took 9 seconds Took 2 minutes Took 5 minutes Took 58 seconds --------- Co-authored-by: Lukas Brübach --- cockatrice/src/game/board/card_item.cpp | 1 + cockatrice/src/game/player/menu/card_menu.cpp | 1 + .../src/game/player/menu/grave_menu.cpp | 18 +++--- cockatrice/src/game/player/menu/grave_menu.h | 6 +- cockatrice/src/game/player/menu/hand_menu.cpp | 26 +++++---- cockatrice/src/game/player/menu/hand_menu.h | 6 +- .../src/game/player/menu/library_menu.cpp | 55 +++++++++++-------- .../src/game/player/menu/library_menu.h | 5 +- .../src/game/player/menu/player_menu.cpp | 12 ++-- cockatrice/src/game/player/menu/rfg_menu.cpp | 12 ++-- cockatrice/src/game/player/menu/rfg_menu.h | 6 +- cockatrice/src/game/player/menu/say_menu.cpp | 4 +- cockatrice/src/game/player/menu/say_menu.h | 6 +- .../src/game/player/menu/sideboard_menu.cpp | 7 ++- .../src/game/player/menu/sideboard_menu.h | 6 +- 15 files changed, 93 insertions(+), 78 deletions(-) diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game/board/card_item.cpp index 16197ae16..029822805 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game/board/card_item.cpp @@ -476,6 +476,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event->button() == Qt::RightButton && owner != nullptr) { emit rightClicked(this, event->screenPos()); + return; } if ((event->modifiers() != Qt::AltModifier) && (event->button() == Qt::LeftButton) && (!SettingsCache::instance().getDoubleClickToPlay())) { diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index ba925afb0..150c1c587 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -6,6 +6,7 @@ #include "../../zones/view_zone_logic.h" #include "../card_menu_action_type.h" #include "../player_actions.h" +#include "../player_graphics_item.h" #include "../player_logic.h" #include "move_menu.h" #include "pt_menu.h" diff --git a/cockatrice/src/game/player/menu/grave_menu.cpp b/cockatrice/src/game/player/menu/grave_menu.cpp index 16a5858ca..45762e900 100644 --- a/cockatrice/src/game/player/menu/grave_menu.cpp +++ b/cockatrice/src/game/player/menu/grave_menu.cpp @@ -8,14 +8,14 @@ #include #include -GraveyardMenu::GraveyardMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), player(_player) +GraveyardMenu::GraveyardMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { createMoveActions(); createViewActions(); addAction(aViewGraveyard); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { mRevealRandomGraveyardCard = addMenu(QString()); connect(mRevealRandomGraveyardCard, &QMenu::aboutToShow, this, &GraveyardMenu::populateRevealRandomMenuWithActivePlayers); @@ -36,9 +36,9 @@ GraveyardMenu::GraveyardMenu(PlayerLogic *_player, QWidget *parent) : TearOffMen void GraveyardMenu::createMoveActions() { - auto grave = player->getGraveZone(); + auto grave = player->getLogic()->getGraveZone(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aMoveGraveToTopLibrary = new QAction(this); aMoveGraveToTopLibrary->setData(QList() << ZoneNames::DECK << 0); @@ -60,7 +60,7 @@ void GraveyardMenu::createMoveActions() void GraveyardMenu::createViewActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); aViewGraveyard = new QAction(this); connect(aViewGraveyard, &QAction::triggered, playerActions, &PlayerActions::actViewGraveyard); @@ -76,9 +76,9 @@ void GraveyardMenu::populateRevealRandomMenuWithActivePlayers() mRevealRandomGraveyardCard->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealRandomGraveyardCard->addAction(other->getPlayerInfo()->getName()); @@ -90,7 +90,7 @@ void GraveyardMenu::populateRevealRandomMenuWithActivePlayers() void GraveyardMenu::onRevealRandomTriggered() { if (auto *a = qobject_cast(sender())) { - player->getPlayerActions()->actRevealRandomGraveyardCard(a->data().toInt()); + player->getLogic()->getPlayerActions()->actRevealRandomGraveyardCard(a->data().toInt()); } } @@ -100,7 +100,7 @@ void GraveyardMenu::retranslateUi() aViewGraveyard->setText(tr("&View graveyard")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { moveGraveMenu->setTitle(tr("&Move graveyard to...")); aMoveGraveToTopLibrary->setText(tr("&Top of library")); aMoveGraveToBottomLibrary->setText(tr("&Bottom of library")); diff --git a/cockatrice/src/game/player/menu/grave_menu.h b/cockatrice/src/game/player/menu/grave_menu.h index d3d98802d..116261e9b 100644 --- a/cockatrice/src/game/player/menu/grave_menu.h +++ b/cockatrice/src/game/player/menu/grave_menu.h @@ -13,7 +13,7 @@ #include #include -class PlayerLogic; +class PlayerGraphicsItem; class GraveyardMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT @@ -21,7 +21,7 @@ signals: void newPlayerActionCreated(QAction *action); public: - explicit GraveyardMenu(PlayerLogic *player, QWidget *parent = nullptr); + explicit GraveyardMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); void createMoveActions(); void createViewActions(); void populateRevealRandomMenuWithActivePlayers(); @@ -40,7 +40,7 @@ public: QAction *aMoveGraveToRfg = nullptr; private: - PlayerLogic *player; + PlayerGraphicsItem *player; }; #endif // COCKATRICE_GRAVE_MENU_H diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game/player/menu/hand_menu.cpp index 6ff177655..60899a27a 100644 --- a/cockatrice/src/game/player/menu/hand_menu.cpp +++ b/cockatrice/src/game/player/menu/hand_menu.cpp @@ -5,16 +5,20 @@ #include "../../../game_graphics/zones/hand_zone.h" #include "../../abstract_game.h" #include "../player_actions.h" +#include "../player_graphics_item.h" #include "../player_logic.h" #include #include #include -HandMenu::HandMenu(PlayerLogic *_player, PlayerActions *actions, QWidget *parent) : TearOffMenu(parent), player(_player) +HandMenu::HandMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + auto *actions = player->getLogic()->getPlayerActions(); + + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aViewHand = new QAction(this); + connect(aViewHand, &QAction::triggered, actions, &PlayerActions::actViewHand); addAction(aViewHand); @@ -75,7 +79,7 @@ HandMenu::HandMenu(PlayerLogic *_player, PlayerActions *actions, QWidget *parent mMoveHandMenu = addTearOffMenu(QString()); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aMoveHandToTopLibrary = new QAction(this); aMoveHandToTopLibrary->setData(QList() << ZoneNames::DECK << 0); aMoveHandToBottomLibrary = new QAction(this); @@ -85,7 +89,7 @@ HandMenu::HandMenu(PlayerLogic *_player, PlayerActions *actions, QWidget *parent aMoveHandToRfg = new QAction(this); aMoveHandToRfg->setData(QList() << ZoneNames::EXILE << 0); - auto hand = player->getHandZone(); + auto hand = player->getLogic()->getHandZone(); connect(aMoveHandToTopLibrary, &QAction::triggered, hand, &HandZoneLogic::moveAllToZone); connect(aMoveHandToBottomLibrary, &QAction::triggered, hand, &HandZoneLogic::moveAllToZone); @@ -107,7 +111,7 @@ void HandMenu::retranslateUi() { setTitle(tr("&Hand")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aViewHand->setText(tr("&View hand")); mSortHand->setTitle(tr("Sort hand by...")); @@ -166,9 +170,9 @@ void HandMenu::populateRevealHandMenuWithActivePlayers() mRevealHand->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealHand->addAction(other->getPlayerInfo()->getName()); @@ -185,9 +189,9 @@ void HandMenu::populateRevealRandomHandCardMenuWithActivePlayers() mRevealRandomHandCard->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealRandomHandCard->addAction(other->getPlayerInfo()->getName()); @@ -204,7 +208,7 @@ void HandMenu::onRevealHandTriggered() } const int targetId = action->data().toInt(); - player->getPlayerActions()->actRevealHand(targetId); + player->getLogic()->getPlayerActions()->actRevealHand(targetId); } void HandMenu::onRevealRandomHandCardTriggered() @@ -215,5 +219,5 @@ void HandMenu::onRevealRandomHandCardTriggered() } const int targetId = action->data().toInt(); - player->getPlayerActions()->actRevealRandomHandCard(targetId); + player->getLogic()->getPlayerActions()->actRevealRandomHandCard(targetId); } diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game/player/menu/hand_menu.h index 1e2ddd95a..d5204612b 100644 --- a/cockatrice/src/game/player/menu/hand_menu.h +++ b/cockatrice/src/game/player/menu/hand_menu.h @@ -13,7 +13,7 @@ #include #include -class PlayerLogic; +class PlayerGraphicsItem; class PlayerActions; class HandMenu : public TearOffMenu, public AbstractPlayerComponent @@ -21,7 +21,7 @@ class HandMenu : public TearOffMenu, public AbstractPlayerComponent Q_OBJECT public: - HandMenu(PlayerLogic *player, PlayerActions *actions, QWidget *parent = nullptr); + HandMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); QMenu *revealHandMenu() const { @@ -43,7 +43,7 @@ private slots: void onRevealRandomHandCardTriggered(); private: - PlayerLogic *player; + PlayerGraphicsItem *player; QAction *aViewHand = nullptr; QAction *aMulligan = nullptr; diff --git a/cockatrice/src/game/player/menu/library_menu.cpp b/cockatrice/src/game/player/menu/library_menu.cpp index 8449af05a..cdc45ed7c 100644 --- a/cockatrice/src/game/player/menu/library_menu.cpp +++ b/cockatrice/src/game/player/menu/library_menu.cpp @@ -8,9 +8,10 @@ #include "../player_logic.h" #include +#include #include -LibraryMenu::LibraryMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), player(_player) +LibraryMenu::LibraryMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { createDrawActions(); createShuffleActions(); @@ -75,8 +76,8 @@ LibraryMenu::LibraryMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(pa bottomLibraryMenu->addSeparator(); bottomLibraryMenu->addAction(aShuffleBottomCards); - connect(player, &PlayerLogic::resetTopCardMenuActions, this, &LibraryMenu::resetTopCardMenuActions); - connect(player, &PlayerLogic::deckChanged, this, &LibraryMenu::enableOpenInDeckEditorAction); + connect(player->getLogic(), &PlayerLogic::resetTopCardMenuActions, this, &LibraryMenu::resetTopCardMenuActions); + connect(player->getLogic(), &PlayerLogic::deckChanged, this, &LibraryMenu::enableOpenInDeckEditorAction); retranslateUi(); } @@ -94,9 +95,9 @@ void LibraryMenu::resetTopCardMenuActions() void LibraryMenu::createDrawActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aDrawCard = new QAction(this); connect(aDrawCard, &QAction::triggered, playerActions, &PlayerActions::actDrawCard); aDrawCards = new QAction(this); @@ -112,9 +113,9 @@ void LibraryMenu::createDrawActions() void LibraryMenu::createShuffleActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aShuffle = new QAction(this); connect(aShuffle, &QAction::triggered, playerActions, &PlayerActions::actShuffle); aShuffleTopCards = new QAction(this); @@ -126,9 +127,9 @@ void LibraryMenu::createShuffleActions() void LibraryMenu::createMoveActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aMoveTopToPlay = new QAction(this); connect(aMoveTopToPlay, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardToPlay); aMoveTopToPlayFaceDown = new QAction(this); @@ -181,9 +182,9 @@ void LibraryMenu::createMoveActions() void LibraryMenu::createViewActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); - if (player->getPlayerInfo()->local || player->getPlayerInfo()->judge) { + if (player->getLogic()->getPlayerInfo()->local || player->getLogic()->getPlayerInfo()->judge) { aViewLibrary = new QAction(this); connect(aViewLibrary, &QAction::triggered, playerActions, &PlayerActions::actViewLibrary); @@ -207,7 +208,7 @@ void LibraryMenu::retranslateUi() { setTitle(tr("&Library")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { aViewLibrary->setText(tr("&View library")); aViewTopCards->setText(tr("View &top cards of library...")); aViewBottomCards->setText(tr("View bottom cards of library...")); @@ -263,9 +264,9 @@ void LibraryMenu::populateRevealLibraryMenuWithActivePlayers() mRevealLibrary->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealLibrary->addAction(other->getPlayerInfo()->getName()); @@ -278,9 +279,9 @@ void LibraryMenu::populateLendLibraryMenuWithActivePlayers() { mLendLibrary->clear(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mLendLibrary->addAction(other->getPlayerInfo()->getName()); @@ -299,9 +300,9 @@ void LibraryMenu::populateRevealTopCardMenuWithActivePlayers() mRevealTopCard->addSeparator(); - const auto &players = player->getGame()->getPlayerManager()->getPlayers().values(); + const auto &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values(); for (auto *other : players) { - if (other == player) { + if (other == player->getLogic()) { continue; } QAction *a = mRevealTopCard->addAction(other->getPlayerInfo()->getName()); @@ -313,27 +314,33 @@ void LibraryMenu::populateRevealTopCardMenuWithActivePlayers() void LibraryMenu::onRevealLibraryTriggered() { if (auto *a = qobject_cast(sender())) { - player->getPlayerActions()->actRevealLibrary(a->data().toInt()); + player->getLogic()->getPlayerActions()->actRevealLibrary(a->data().toInt()); } } void LibraryMenu::onLendLibraryTriggered() { if (auto *a = qobject_cast(sender())) { - player->getPlayerActions()->actLendLibrary(a->data().toInt()); + player->getLogic()->getPlayerActions()->actLendLibrary(a->data().toInt()); } } void LibraryMenu::onRevealTopCardTriggered() { + QWidget *parent = nullptr; + if (auto *view = player->scene() ? player->scene()->views().value(0) : nullptr) { + parent = view->window(); + } if (auto *a = qobject_cast(sender())) { - int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Reveal top cards of library"), + + int deckSize = player->getLogic()->getDeckZone()->getCards().size(); + bool ok = true; + int number = QInputDialog::getInt(parent, tr("Reveal top cards of library"), tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, deckSize, 1, &ok); + if (ok) { - player->getPlayerActions()->actRevealTopCards(a->data().toInt(), number); + player->getLogic()->getPlayerActions()->actRevealTopCards(a->data().toInt(), number); defaultNumberTopCards = number; } } diff --git a/cockatrice/src/game/player/menu/library_menu.h b/cockatrice/src/game/player/menu/library_menu.h index a941c54b1..bc0e6fb8e 100644 --- a/cockatrice/src/game/player/menu/library_menu.h +++ b/cockatrice/src/game/player/menu/library_menu.h @@ -13,6 +13,7 @@ #include #include +class PlayerGraphicsItem; class PlayerLogic; class PlayerActions; @@ -24,7 +25,7 @@ public slots: void resetTopCardMenuActions(); public: - LibraryMenu(PlayerLogic *player, QWidget *parent = nullptr); + LibraryMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); void createDrawActions(); void createShuffleActions(); void createMoveActions(); @@ -111,7 +112,7 @@ public: int defaultNumberTopCards = 1; private: - PlayerLogic *player; + PlayerGraphicsItem *player; }; #endif // COCKATRICE_LIBRARY_MENU_H diff --git a/cockatrice/src/game/player/menu/player_menu.cpp b/cockatrice/src/game/player/menu/player_menu.cpp index 6687bbba8..041b41052 100644 --- a/cockatrice/src/game/player/menu/player_menu.cpp +++ b/cockatrice/src/game/player/menu/player_menu.cpp @@ -18,18 +18,18 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_ playerMenu = new TearOffMenu(); if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { - handMenu = addManagedMenu(player->getLogic(), player->getLogic()->getPlayerActions(), playerMenu); - libraryMenu = addManagedMenu(player->getLogic(), playerMenu); + handMenu = addManagedMenu(player, playerMenu); + libraryMenu = addManagedMenu(player, playerMenu); } else { handMenu = nullptr; libraryMenu = nullptr; } - graveMenu = addManagedMenu(player->getLogic(), playerMenu); - rfgMenu = addManagedMenu(player->getLogic(), playerMenu); + graveMenu = addManagedMenu(player, playerMenu); + rfgMenu = addManagedMenu(player, playerMenu); if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { - sideboardMenu = addManagedMenu(player->getLogic(), playerMenu); + sideboardMenu = addManagedMenu(player, playerMenu); customZonesMenu = addManagedMenu(player); playerMenu->addSeparator(); @@ -44,7 +44,7 @@ PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_ } if (player->getLogic()->getPlayerInfo()->getLocal()) { - sayMenu = addManagedMenu(player->getLogic()); + sayMenu = addManagedMenu(player); } else { sayMenu = nullptr; } diff --git a/cockatrice/src/game/player/menu/rfg_menu.cpp b/cockatrice/src/game/player/menu/rfg_menu.cpp index e8aca00cb..79fdebf48 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.cpp +++ b/cockatrice/src/game/player/menu/rfg_menu.cpp @@ -5,14 +5,14 @@ #include -RfgMenu::RfgMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), player(_player) +RfgMenu::RfgMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(parent), player(_player) { createMoveActions(); createViewActions(); addAction(aViewRfg); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { addSeparator(); moveRfgMenu = addTearOffMenu(QString()); moveRfgMenu->addAction(aMoveRfgToTopLibrary); @@ -28,8 +28,8 @@ RfgMenu::RfgMenu(PlayerLogic *_player, QWidget *parent) : TearOffMenu(parent), p void RfgMenu::createMoveActions() { - if (player->getPlayerInfo()->getLocalOrJudge()) { - auto rfg = player->getRfgZone(); + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + auto rfg = player->getLogic()->getRfgZone(); aMoveRfgToTopLibrary = new QAction(this); aMoveRfgToTopLibrary->setData(QList() << ZoneNames::DECK << 0); @@ -49,7 +49,7 @@ void RfgMenu::createMoveActions() void RfgMenu::createViewActions() { - PlayerActions *playerActions = player->getPlayerActions(); + PlayerActions *playerActions = player->getLogic()->getPlayerActions(); aViewRfg = new QAction(this); connect(aViewRfg, &QAction::triggered, playerActions, &PlayerActions::actViewRfg); @@ -61,7 +61,7 @@ void RfgMenu::retranslateUi() aViewRfg->setText(tr("&View exile")); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { moveRfgMenu->setTitle(tr("&Move exile to...")); aMoveRfgToTopLibrary->setText(tr("&Top of library")); aMoveRfgToBottomLibrary->setText(tr("&Bottom of library")); diff --git a/cockatrice/src/game/player/menu/rfg_menu.h b/cockatrice/src/game/player/menu/rfg_menu.h index 9e179f8fd..f5dd888e4 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.h +++ b/cockatrice/src/game/player/menu/rfg_menu.h @@ -13,12 +13,12 @@ #include #include -class PlayerLogic; +class PlayerGraphicsItem; class RfgMenu : public TearOffMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit RfgMenu(PlayerLogic *player, QWidget *parent = nullptr); + explicit RfgMenu(PlayerGraphicsItem *player, QWidget *parent = nullptr); void createMoveActions(); void createViewActions(); void retranslateUi() override; @@ -38,7 +38,7 @@ public: QAction *aMoveRfgToGrave = nullptr; private: - PlayerLogic *player; + PlayerGraphicsItem *player; }; #endif // COCKATRICE_RFG_MENU_H diff --git a/cockatrice/src/game/player/menu/say_menu.cpp b/cockatrice/src/game/player/menu/say_menu.cpp index a2d5ab982..58bbd33aa 100644 --- a/cockatrice/src/game/player/menu/say_menu.cpp +++ b/cockatrice/src/game/player/menu/say_menu.cpp @@ -4,7 +4,7 @@ #include "../player_actions.h" #include "../player_logic.h" -SayMenu::SayMenu(PlayerLogic *_player) : player(_player) +SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player) { connect(&SettingsCache::instance().messages(), &MessageSettings::messageMacrosChanged, this, &SayMenu::initSayMenu); initSayMenu(); @@ -44,7 +44,7 @@ void SayMenu::initSayMenu() for (int i = 0; i < count; ++i) { auto *newAction = new QAction(SettingsCache::instance().messages().getMessageAt(i), this); - connect(newAction, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actSayMessage); + connect(newAction, &QAction::triggered, player->getLogic()->getPlayerActions(), &PlayerActions::actSayMessage); addAction(newAction); } diff --git a/cockatrice/src/game/player/menu/say_menu.h b/cockatrice/src/game/player/menu/say_menu.h index 3de70e85c..3ff160d05 100644 --- a/cockatrice/src/game/player/menu/say_menu.h +++ b/cockatrice/src/game/player/menu/say_menu.h @@ -11,12 +11,12 @@ #include -class PlayerLogic; +class PlayerGraphicsItem; class SayMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit SayMenu(PlayerLogic *player); + explicit SayMenu(PlayerGraphicsItem *player); void retranslateUi() override; void setShortcutsActive() override; @@ -26,7 +26,7 @@ private slots: void initSayMenu(); private: - PlayerLogic *player; + PlayerGraphicsItem *player; bool shortcutsActive = false; }; diff --git a/cockatrice/src/game/player/menu/sideboard_menu.cpp b/cockatrice/src/game/player/menu/sideboard_menu.cpp index f88625a1f..27b50b570 100644 --- a/cockatrice/src/game/player/menu/sideboard_menu.cpp +++ b/cockatrice/src/game/player/menu/sideboard_menu.cpp @@ -3,12 +3,13 @@ #include "../player_actions.h" #include "../player_logic.h" -SideboardMenu::SideboardMenu(PlayerLogic *player, QMenu *playerMenu) : QMenu(playerMenu) +SideboardMenu::SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu) : QMenu(playerMenu) { aViewSideboard = new QAction(this); - connect(aViewSideboard, &QAction::triggered, player->getPlayerActions(), &PlayerActions::actViewSideboard); + connect(aViewSideboard, &QAction::triggered, player->getLogic()->getPlayerActions(), + &PlayerActions::actViewSideboard); - if (player->getPlayerInfo()->getLocalOrJudge()) { + if (player->getLogic()->getPlayerInfo()->getLocalOrJudge()) { addAction(aViewSideboard); } diff --git a/cockatrice/src/game/player/menu/sideboard_menu.h b/cockatrice/src/game/player/menu/sideboard_menu.h index 20a206782..b3b547291 100644 --- a/cockatrice/src/game/player/menu/sideboard_menu.h +++ b/cockatrice/src/game/player/menu/sideboard_menu.h @@ -11,19 +11,19 @@ #include -class PlayerLogic; +class PlayerGraphicsItem; class SideboardMenu : public QMenu, public AbstractPlayerComponent { Q_OBJECT public: - explicit SideboardMenu(PlayerLogic *player, QMenu *playerMenu); + explicit SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu); void retranslateUi() override; void setShortcutsActive() override; void setShortcutsInactive() override; private: - PlayerLogic *player; + PlayerGraphicsItem *player; QAction *aViewSideboard; }; From cbfd28690842eb4348a38889ac83ad3b07cc7c91 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:22:59 +0200 Subject: [PATCH 18/50] [Game][Player] Move dialog creation out of player_actions and into player_dialogs (#6946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Game][Player] Split Player into PlayerLogic/PlayerGraphicsItem Took 4 minutes Took 48 seconds Took 2 minutes * Drop early return. Took 1 hour 13 minutes Took 2 minutes Took 1 minute Took 24 seconds * [Game][Player] Split Player into PlayerLogic/PlayerGraphicsItem Took 4 minutes Took 58 seconds * [Game][Menus] Make Menus accept PlayerGraphicsItem instead of PlayerLogic Took 7 minutes Took 4 minutes Took 9 seconds Took 2 minutes Took 5 minutes Took 58 seconds * [Game][Player] Split Player into PlayerLogic/PlayerGraphicsItem Took 4 minutes Took 2 minutes * [Game][Menus] Make Menus accept PlayerGraphicsItem instead of PlayerLogic Took 7 minutes Took 1 minute Took 57 seconds * [Game][Player] Move dialog creation out of player_actions and into player_dialogs Took 3 minutes Took 1 second * Fix typo. Took 5 minutes * Addressed comments. Took 16 minutes Took 11 seconds * Reintroduce clearCardsToDelete check. Took 3 minutes * Capture cards before semaphore. Took 1 minute --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 1 + cockatrice/src/game/player/menu/card_menu.cpp | 4 +- cockatrice/src/game/player/menu/hand_menu.cpp | 2 +- .../src/game/player/menu/library_menu.cpp | 15 +- cockatrice/src/game/player/menu/move_menu.cpp | 5 +- cockatrice/src/game/player/menu/pt_menu.cpp | 2 +- .../src/game/player/menu/utility_menu.cpp | 7 +- cockatrice/src/game/player/player_actions.cpp | 410 +++++++++--------- cockatrice/src/game/player/player_actions.h | 82 +++- cockatrice/src/game/player/player_dialogs.cpp | 298 +++++++++++++ cockatrice/src/game/player/player_dialogs.h | 62 +++ .../src/game/player/player_graphics_item.cpp | 5 + .../src/game/player/player_graphics_item.h | 2 + 13 files changed, 664 insertions(+), 231 deletions(-) create mode 100644 cockatrice/src/game/player/player_dialogs.cpp create mode 100644 cockatrice/src/game/player/player_dialogs.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 028161ee0..f0e363e18 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -100,6 +100,7 @@ set(cockatrice_SOURCES src/game/player/menu/utility_menu.cpp src/game/player/player_actions.cpp src/game/player/player_area.cpp + src/game/player/player_dialogs.cpp src/game/player/player_event_handler.cpp src/game/player/player_graphics_item.cpp src/game/player/player_info.cpp diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index 150c1c587..c1c33e37d 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -79,7 +79,7 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho // Actions using selection directly aUnattach = makeAction(this, [actions, sel]() { actions->actUnattach(sel()); }); - aSetAnnotation = makeAction(this, [actions, sel]() { actions->actSetAnnotation(sel()); }); + aSetAnnotation = makeAction(this, [actions, sel]() { actions->actRequestSetAnnotationDialog(sel()); }); aPlay = makeAction(this, [actions, sel]() { actions->actPlay(sel()); }); aPlayFacedown = makeAction(this, [actions, sel]() { actions->actPlayFacedown(sel()); }); aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); }); @@ -115,7 +115,7 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho removeAction->setIcon(circleIcon); aRemoveCounter.append(removeAction); - auto *setAction = makeAction(this, [actions, sel, i]() { actions->actSetCardCounter(sel(), i); }); + auto *setAction = makeAction(this, [actions, sel, i]() { actions->actRequestSetCardCounterDialog(sel(), i); }); setAction->setIcon(circleIcon); aSetCounter.append(setAction); } diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game/player/menu/hand_menu.cpp index 60899a27a..64a8c5754 100644 --- a/cockatrice/src/game/player/menu/hand_menu.cpp +++ b/cockatrice/src/game/player/menu/hand_menu.cpp @@ -62,7 +62,7 @@ HandMenu::HandMenu(PlayerGraphicsItem *_player, QWidget *parent) : TearOffMenu(p addSeparator(); aMulligan = new QAction(this); - connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan); + connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actRequestMulliganDialog); addAction(aMulligan); // Mulligan same size diff --git a/cockatrice/src/game/player/menu/library_menu.cpp b/cockatrice/src/game/player/menu/library_menu.cpp index cdc45ed7c..00ab4592f 100644 --- a/cockatrice/src/game/player/menu/library_menu.cpp +++ b/cockatrice/src/game/player/menu/library_menu.cpp @@ -101,13 +101,13 @@ void LibraryMenu::createDrawActions() aDrawCard = new QAction(this); connect(aDrawCard, &QAction::triggered, playerActions, &PlayerActions::actDrawCard); aDrawCards = new QAction(this); - connect(aDrawCards, &QAction::triggered, playerActions, &PlayerActions::actDrawCards); + connect(aDrawCards, &QAction::triggered, playerActions, &PlayerActions::actRequestDrawCardsDialog); aUndoDraw = new QAction(this); connect(aUndoDraw, &QAction::triggered, playerActions, &PlayerActions::actUndoDraw); aDrawBottomCard = new QAction(this); connect(aDrawBottomCard, &QAction::triggered, playerActions, &PlayerActions::actDrawBottomCard); aDrawBottomCards = new QAction(this); - connect(aDrawBottomCards, &QAction::triggered, playerActions, &PlayerActions::actDrawBottomCards); + connect(aDrawBottomCards, &QAction::triggered, playerActions, &PlayerActions::actRequestDrawBottomCardsDialog); } } @@ -119,9 +119,9 @@ void LibraryMenu::createShuffleActions() aShuffle = new QAction(this); connect(aShuffle, &QAction::triggered, playerActions, &PlayerActions::actShuffle); aShuffleTopCards = new QAction(this); - connect(aShuffleTopCards, &QAction::triggered, playerActions, &PlayerActions::actShuffleTop); + connect(aShuffleTopCards, &QAction::triggered, playerActions, &PlayerActions::actRequestShuffleTopDialog); aShuffleBottomCards = new QAction(this); - connect(aShuffleBottomCards, &QAction::triggered, playerActions, &PlayerActions::actShuffleBottom); + connect(aShuffleBottomCards, &QAction::triggered, playerActions, &PlayerActions::actRequestShuffleBottomDialog); } } @@ -150,7 +150,8 @@ void LibraryMenu::createMoveActions() connect(aMoveTopCardsToExileFaceDown, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsToExileFaceDown); aMoveTopCardsUntil = new QAction(this); - connect(aMoveTopCardsUntil, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardsUntil); + connect(aMoveTopCardsUntil, &QAction::triggered, playerActions, + &PlayerActions::actRequestMoveTopCardsUntilDialog); aMoveTopCardToBottom = new QAction(this); connect(aMoveTopCardToBottom, &QAction::triggered, playerActions, &PlayerActions::actMoveTopCardToBottom); @@ -189,9 +190,9 @@ void LibraryMenu::createViewActions() connect(aViewLibrary, &QAction::triggered, playerActions, &PlayerActions::actViewLibrary); aViewTopCards = new QAction(this); - connect(aViewTopCards, &QAction::triggered, playerActions, &PlayerActions::actViewTopCards); + connect(aViewTopCards, &QAction::triggered, playerActions, &PlayerActions::actRequestViewTopCardsDialog); aViewBottomCards = new QAction(this); - connect(aViewBottomCards, &QAction::triggered, playerActions, &PlayerActions::actViewBottomCards); + connect(aViewBottomCards, &QAction::triggered, playerActions, &PlayerActions::actRequestViewBottomCardsDialog); aAlwaysRevealTopCard = new QAction(this); aAlwaysRevealTopCard->setCheckable(true); connect(aAlwaysRevealTopCard, &QAction::triggered, playerActions, &PlayerActions::actAlwaysRevealTopCard); diff --git a/cockatrice/src/game/player/menu/move_menu.cpp b/cockatrice/src/game/player/menu/move_menu.cpp index 4dfdee432..9997aecf3 100644 --- a/cockatrice/src/game/player/menu/move_menu.cpp +++ b/cockatrice/src/game/player/menu/move_menu.cpp @@ -30,9 +30,8 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) connect(aMoveToTopLibrary, &QAction::triggered, actions, invoke(cmMoveToTopLibrary)); connect(aMoveToBottomLibrary, &QAction::triggered, actions, invoke(cmMoveToBottomLibrary)); - connect(aMoveToXfromTopOfLibrary, &QAction::triggered, actions, [player]() { - player->getLogic()->getPlayerActions()->actMoveCardXCardsFromTop(player->getGameScene()->selectedCards()); - }); + connect(aMoveToXfromTopOfLibrary, &QAction::triggered, actions, + &PlayerActions::actRequestMoveCardXCardsFromTopDialog); connect(aMoveToTable, &QAction::triggered, actions, invoke(cmMoveToTable)); connect(aMoveToHand, &QAction::triggered, actions, invoke(cmMoveToHand)); connect(aMoveToGraveyard, &QAction::triggered, actions, invoke(cmMoveToGraveyard)); diff --git a/cockatrice/src/game/player/menu/pt_menu.cpp b/cockatrice/src/game/player/menu/pt_menu.cpp index 846256e24..011271385 100644 --- a/cockatrice/src/game/player/menu/pt_menu.cpp +++ b/cockatrice/src/game/player/menu/pt_menu.cpp @@ -33,7 +33,7 @@ PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness")) [player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCards()); }); aSetPT = new QAction(this); connect(aSetPT, &QAction::triggered, playerActions, - [player, playerActions] { playerActions->actSetPT(player->getGameScene()->selectedCards()); }); + [player, playerActions] { playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCards()); }); aResetPT = new QAction(this); connect(aResetPT, &QAction::triggered, playerActions, [player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCards()); }); diff --git a/cockatrice/src/game/player/menu/utility_menu.cpp b/cockatrice/src/game/player/menu/utility_menu.cpp index 005b38c3b..9769a029e 100644 --- a/cockatrice/src/game/player/menu/utility_menu.cpp +++ b/cockatrice/src/game/player/menu/utility_menu.cpp @@ -20,14 +20,15 @@ UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu connect(aUntapAll, &QAction::triggered, playerActions, &PlayerActions::actUntapAll); aRollDie = new QAction(this); - connect(aRollDie, &QAction::triggered, playerActions, &PlayerActions::actRollDie); + connect(aRollDie, &QAction::triggered, playerActions, &PlayerActions::actRequestRollDieDialog); aFlipCoin = new QAction(this); connect(aFlipCoin, &QAction::triggered, playerActions, &PlayerActions::actFlipCoin); aCreateToken = new QAction(this); - connect(aCreateToken, &QAction::triggered, playerActions, - [this]() { player->getLogic()->getPlayerActions()->actCreateToken(getPredefinedTokens()); }); + connect(aCreateToken, &QAction::triggered, playerActions, [this]() { + player->getLogic()->getPlayerActions()->actRequestCreateTokenDialog(getPredefinedTokens()); + }); aCreateAnotherToken = new QAction(this); connect(aCreateAnotherToken, &QAction::triggered, playerActions, &PlayerActions::actCreateAnotherToken); diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 7d58be31a..2b0428dd8 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -5,7 +5,6 @@ #include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/utility/get_text_with_max.h" #include "../board/card_item.h" -#include "../client/settings/card_counter_settings.h" #include "../dialogs/dlg_move_top_cards_until.h" #include "../dialogs/dlg_roll_dice.h" #include "../zones/view_zone_logic.h" @@ -175,30 +174,26 @@ void PlayerActions::actSortHand() emit requestSortHand(sortOptions + defaultOptions); } -void PlayerActions::actViewTopCards() +void PlayerActions::actRequestViewTopCardsDialog() { - int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("View top cards of library"), - tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, - deckSize, 1, &ok); - if (ok) { - defaultNumberTopCards = number; - emit requestZoneViewToggle(ZoneNames::DECK, number); - } + emit requestViewTopCardsDialog(defaultNumberTopCards, player->getDeckZone()->getCards().size()); } -void PlayerActions::actViewBottomCards() +void PlayerActions::actViewTopCards(int number) { - int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("View bottom cards of library"), - tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberBottomCards, 1, - deckSize, 1, &ok); - if (ok) { - defaultNumberBottomCards = number; - emit requestZoneViewToggle(ZoneNames::DECK, number, true); - } + defaultNumberTopCards = number; + emit requestZoneViewToggle(ZoneNames::DECK, number); +} + +void PlayerActions::actRequestViewBottomCardsDialog() +{ + emit requestViewBottomCardsDialog(defaultNumberBottomCards, player->getDeckZone()->getCards().size()); +} + +void PlayerActions::actViewBottomCards(int number) +{ + defaultNumberBottomCards = number; + emit requestZoneViewToggle(ZoneNames::DECK, number, true); } void PlayerActions::actAlwaysRevealTopCard(bool alwaysRevealTopCard) @@ -244,18 +239,20 @@ void PlayerActions::actShuffle() sendGameCommand(Command_Shuffle()); } -void PlayerActions::actShuffleTop() +void PlayerActions::actRequestShuffleTopDialog() { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Shuffle top cards of library"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestShuffleTopDialog(defaultNumberTopCards, maxCards); +} + +void PlayerActions::actShuffleTop(int number) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -273,18 +270,20 @@ void PlayerActions::actShuffleTop() sendGameCommand(cmd); } -void PlayerActions::actShuffleBottom() +void PlayerActions::actRequestShuffleBottomDialog() { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Shuffle bottom cards of library"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestShuffleBottomDialog(defaultNumberBottomCards, maxCards); +} + +void PlayerActions::actShuffleBottom(int number) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -309,21 +308,18 @@ void PlayerActions::actDrawCard() sendGameCommand(cmd); } -void PlayerActions::actMulligan() +void PlayerActions::actRequestMulliganDialog() { int startSize = SettingsCache::instance().getStartingHandSize(); int handSize = player->getHandZone()->getCards().size(); int deckSize = player->getDeckZone()->getCards().size() + handSize; - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw hand"), - tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + - tr("0 and lower are in comparison to current hand size"), - startSize, -handSize, deckSize, 1, &ok); + emit requestMulliganDialog(startSize, handSize, deckSize); +} - if (!ok) { - return; - } +void PlayerActions::actMulligan(int number) +{ + int handSize = player->getHandZone()->getCards().size(); if (number < 1) { number = handSize + number; @@ -357,19 +353,19 @@ void PlayerActions::doMulligan(int number) sendGameCommand(cmd); } -void PlayerActions::actDrawCards() +void PlayerActions::actRequestDrawCardsDialog() { int deckSize = player->getDeckZone()->getCards().size(); - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw cards"), - tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, - deckSize, 1, &ok); - if (ok) { - defaultNumberTopCards = number; - Command_DrawCards cmd; - cmd.set_number(static_cast(number)); - sendGameCommand(cmd); - } + + emit requestDrawCardsDialog(defaultNumberTopCards, deckSize); +} + +void PlayerActions::actDrawCards(int number) +{ + defaultNumberTopCards = number; + Command_DrawCards cmd; + cmd.set_number(static_cast(number)); + sendGameCommand(cmd); } void PlayerActions::actUndoDraw() @@ -427,36 +423,40 @@ void PlayerActions::actMoveTopCardToExile() void PlayerActions::actMoveTopCardsToGrave() { - moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), false); + actRequestMoveTopCardsToDialog(ZoneNames::GRAVE, tr("grave"), false); } void PlayerActions::actMoveTopCardsToGraveFaceDown() { - moveTopCardsTo(ZoneNames::GRAVE, tr("grave"), true); + actRequestMoveTopCardsToDialog(ZoneNames::GRAVE, tr("grave"), true); } void PlayerActions::actMoveTopCardsToExile() { - moveTopCardsTo(ZoneNames::EXILE, tr("exile"), false); + actRequestMoveTopCardsToDialog(ZoneNames::EXILE, tr("exile"), false); } void PlayerActions::actMoveTopCardsToExileFaceDown() { - moveTopCardsTo(ZoneNames::EXILE, tr("exile"), true); + actRequestMoveTopCardsToDialog(ZoneNames::EXILE, tr("exile"), true); } -void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) +void PlayerActions::actRequestMoveTopCardsToDialog(const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move top cards to %1").arg(zoneDisplayName), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestMoveTopCardsToDialog(defaultNumberTopCards, maxCards, targetZone, zoneDisplayName, faceDown); +} + +void PlayerActions::moveTopCardsTo(int number, const QString &targetZone, bool faceDown) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -483,17 +483,16 @@ void PlayerActions::moveTopCardsTo(const QString &targetZone, const QString &zon sendGameCommand(cmd); } -void PlayerActions::actMoveTopCardsUntil() +void PlayerActions::actRequestMoveTopCardsUntilDialog() { stopMoveTopCardsUntil(); - DlgMoveTopCardsUntil dlg(player->getGame()->getTab(), movingCardsUntilOptions); - if (!dlg.exec()) { - return; - } + emit requestMoveTopCardsUntilDialog(movingCardsUntilOptions); +} - auto expr = dlg.getExpr(); - movingCardsUntilOptions = dlg.getOptions(); +void PlayerActions::moveTopCardsUntil(const QString &expr, MoveTopCardsUntilOptions options) +{ + movingCardsUntilOptions = options; if (player->getDeckZone()->getCards().empty()) { stopMoveTopCardsUntil(); @@ -622,36 +621,40 @@ void PlayerActions::actMoveBottomCardToExile() void PlayerActions::actMoveBottomCardsToGrave() { - moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), false); + actRequestMoveBottomCardsToDialog(ZoneNames::GRAVE, tr("grave"), false); } void PlayerActions::actMoveBottomCardsToGraveFaceDown() { - moveBottomCardsTo(ZoneNames::GRAVE, tr("grave"), true); + actRequestMoveBottomCardsToDialog(ZoneNames::GRAVE, tr("grave"), true); } void PlayerActions::actMoveBottomCardsToExile() { - moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), false); + actRequestMoveBottomCardsToDialog(ZoneNames::EXILE, tr("exile"), false); } void PlayerActions::actMoveBottomCardsToExileFaceDown() { - moveBottomCardsTo(ZoneNames::EXILE, tr("exile"), true); + actRequestMoveBottomCardsToDialog(ZoneNames::EXILE, tr("exile"), true); } -void PlayerActions::moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown) +void PlayerActions::actRequestMoveBottomCardsToDialog(const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Move bottom cards to %1").arg(zoneDisplayName), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestMoveBottomCardsToDialog(defaultNumberBottomCards, maxCards, targetZone, zoneDisplayName, faceDown); +} + +void PlayerActions::moveBottomCardsTo(int number, const QString &targetZone, bool faceDown) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; } @@ -763,20 +766,24 @@ void PlayerActions::actDrawBottomCard() sendGameCommand(cmd); } -void PlayerActions::actDrawBottomCards() +void PlayerActions::actRequestDrawBottomCardsDialog() { const int maxCards = player->getDeckZone()->getCards().size(); if (maxCards == 0) { return; } - bool ok; - int number = QInputDialog::getInt(player->getGame()->getTab(), tr("Draw bottom cards"), - tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, - maxCards, 1, &ok); - if (!ok) { + emit requestDrawBottomCardsDialog(defaultNumberBottomCards, maxCards); +} + +void PlayerActions::actDrawBottomCards(int number) +{ + const int maxCards = player->getDeckZone()->getCards().size(); + if (maxCards == 0) { return; - } else if (number > maxCards) { + } + + if (number > maxCards) { number = maxCards; } defaultNumberBottomCards = number; @@ -843,16 +850,16 @@ void PlayerActions::actUntapAll() sendGameCommand(cmd); } -void PlayerActions::actRollDie() +void PlayerActions::actRequestRollDieDialog() { - DlgRollDice dlg(player->getGame()->getTab()); - if (!dlg.exec()) { - return; - } + emit requestRollDieDialog(); +} +void PlayerActions::actRollDie(int sides, int count) +{ Command_RollDie cmd; - cmd.set_sides(dlg.getDieSideCount()); - cmd.set_count(dlg.getDiceToRollCount()); + cmd.set_sides(sides); + cmd.set_count(count); sendGameCommand(cmd); } @@ -864,14 +871,14 @@ void PlayerActions::actFlipCoin() sendGameCommand(cmd); } -void PlayerActions::actCreateToken(const QStringList &predefinedTokens) +void PlayerActions::actRequestCreateTokenDialog(const QStringList &predefinedTokens) { - DlgCreateToken dlg(predefinedTokens, player->getGame()->getTab()); - if (!dlg.exec()) { - return; - } + emit requestCreateTokenDialog(predefinedTokens); +} - lastTokenInfo = dlg.getTokenInfo(); +void PlayerActions::actCreateToken(TokenInfo tokenToCreate) +{ + lastTokenInfo = tokenToCreate; ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId}); if (correctedCard) { @@ -951,23 +958,17 @@ void PlayerActions::actCreatePredefinedToken() void PlayerActions::actCreateRelatedCard() { const CardItem *sourceCard = player->getGame()->getActiveCard(); + if (!sourceCard) { return; } + auto *action = static_cast(sender()); // If there is a better way of passing a CardRelation through a QAction, please add it here. auto relatedCards = sourceCard->getCardInfo().getAllRelatedCards(); - CardRelation *cardRelation = relatedCards.at(action->data().toInt()); - /* - * If we make a token via "Token: TokenName" - * then let's allow it to be created via "create another token" - */ - if (createRelatedFromRelation(sourceCard, cardRelation) && cardRelation->getCanCreateAnother()) { - ExactCard relatedCard = CardDatabaseManager::query()->getCardFromSameSet(cardRelation->getName(), - sourceCard->getCard().getPrinting()); - setLastToken(relatedCard.getCardPtr()); - } + CardRelation *cardRelation = relatedCards.at(action->data().toInt()); + actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation); } void PlayerActions::actCreateAllRelatedCards() @@ -987,7 +988,9 @@ void PlayerActions::actCreateAllRelatedCards() if (relatedCards.length() == 1) { cardRelation = relatedCards.at(0); - if (createRelatedFromRelation(sourceCard, cardRelation)) { + lastRelatedCreationSucceeded = false; // reset before emit + actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation); + if (lastRelatedCreationSucceeded) { ++tokensTypesCreated; } } else { @@ -999,15 +1002,18 @@ void PlayerActions::actCreateAllRelatedCards() } } switch (nonExcludedRelatedCards.length()) { - case 1: // if nonExcludedRelatedCards == 1 + case 1: cardRelation = nonExcludedRelatedCards.at(0); - if (createRelatedFromRelation(sourceCard, cardRelation)) { + lastRelatedCreationSucceeded = false; // reset before emit + actRequestCreateRelatedFromRelationDialog(sourceCard, cardRelation); + if (lastRelatedCreationSucceeded) { ++tokensTypesCreated; } break; + // If all are marked "Exclude", then treat the situation as if none of them are. // We won't accept "garbage in, garbage out", here. - case 0: // else if nonExcludedRelatedCards == 0 + case 0: for (CardRelation *cardRelationAll : relatedCards) { if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { dbName = cardRelationAll->getName(); @@ -1022,7 +1028,8 @@ void PlayerActions::actCreateAllRelatedCards() } } break; - default: // else + + default: for (CardRelation *cardRelationNotExcluded : nonExcludedRelatedCards) { if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { dbName = cardRelationNotExcluded->getName(); @@ -1050,50 +1057,83 @@ void PlayerActions::actCreateAllRelatedCards() } } -bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation) +void PlayerActions::actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard, + const CardRelation *cardRelation) +{ + emit requestCreateRelatedFromRelationDialog(sourceCard, cardRelation); +} + +bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, + const CardRelation *cardRelation, + int variableCount) { if (sourceCard == nullptr || cardRelation == nullptr) { return false; } - QString dbName = cardRelation->getName(); - bool persistent = cardRelation->getIsPersistent(); + + const QString dbName = cardRelation->getName(); + const bool persistent = cardRelation->getIsPersistent(); + + // Variable relations always use DoesNotAttach, regardless of the count the user + // entered. if (cardRelation->getIsVariable()) { - bool ok; - player->setDialogSemaphore(true); - int count = QInputDialog::getInt(player->getGame()->getTab(), tr("Create tokens"), tr("Number:"), - cardRelation->getDefaultCount(), 1, MAX_TOKENS_PER_DIALOG, 1, &ok); - player->setDialogSemaphore(false); - if (!ok) { + if (variableCount <= 0) { return false; } + for (int i = 0; i < variableCount; ++i) { + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + } + return true; + } + + const int count = cardRelation->getDefaultCount(); + + if (count > 1) { for (int i = 0; i < count; ++i) { createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); } - } else if (cardRelation->getDefaultCount() > 1) { - for (int i = 0; i < cardRelation->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); - } - } else { - CardRelationType attachType; - // do not attempt to attach to another player's cards, this causes the card to attempt to attach to the same - // cardid on the local player's field instead, which is an entirely different card! - if (player->getPlayerInfo()->getLocalOrJudge()) { - attachType = cardRelation->getAttachType(); - } else { - attachType = CardRelationType::DoesNotAttach; - } - - // move card onto table first if attaching from some other zone - // we only do this for AttachTo because cross-zone TransformInto is already handled server-side - if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != ZoneNames::TABLE) { - playCardToTable(sourceCard, false); - } - - createCard(sourceCard, dbName, attachType, persistent); + return true; } + + CardRelationType attachType; + // do not attempt to attach to another player's cards, this causes the card to attempt to attach to the same + // cardid on the local player's field instead, which is an entirely different card! + if (player->getPlayerInfo()->getLocalOrJudge()) { + attachType = cardRelation->getAttachType(); + } else { + attachType = CardRelationType::DoesNotAttach; + } + + // move card onto table first if attaching from some other zone + // we only do this for AttachTo because cross-zone TransformInto is already handled server-side + if (attachType == CardRelationType::AttachTo && sourceCard->getZone()->getName() != ZoneNames::TABLE) { + playCardToTable(sourceCard, false); + } + + createCard(sourceCard, dbName, attachType, persistent); return true; } +void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation) +{ + if (sourceCard == nullptr || cardRelation == nullptr) { + return; + } + + /* + * If we make a token via "Token: TokenName" + * then let's allow it to be created via "create another token" + */ + if (!cardRelation->getCanCreateAnother()) { + return; + } + + ExactCard relatedCard = + CardDatabaseManager::query()->getCardFromSameSet(cardRelation->getName(), sourceCard->getCard().getPrinting()); + + setLastToken(relatedCard.getCardPtr()); +} + void PlayerActions::createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attachType, @@ -1171,35 +1211,29 @@ void PlayerActions::actSayMessage() sendGameCommand(cmd); } -void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards) +void PlayerActions::actRequestMoveCardXCardsFromTopDialog() { int deckSize = player->getDeckZone()->getCards().size() + 1; // add the card to move to the deck - bool ok; - int number = - QInputDialog::getInt(player->getGame()->getTab(), tr("Place card X cards from top of library"), - tr("Which position should this card be placed:") + "\n" + tr("(max. %1)").arg(deckSize), - defaultNumberTopCardsToPlaceBelow, 1, deckSize, 1, &ok); - number -= 1; // indexes start at 0 - if (!ok) { - return; - } + emit requestMoveCardXCardsFromTopDialog(defaultNumberTopCardsToPlaceBelow, deckSize); +} +void PlayerActions::actMoveCardXCardsFromTop(QList selectedCards, int number) +{ defaultNumberTopCardsToPlaceBelow = number; - QList cardList = selectedCards; - if (cardList.isEmpty()) { + if (selectedCards.isEmpty()) { return; } QList commandList; ListOfCardsToMove idList; - for (const auto &i : cardList) { + for (const auto &i : selectedCards) { idList.add_card()->set_card_id(i->getId()); } - int startPlayerId = cardList[0]->getZone()->getPlayer()->getPlayerInfo()->getId(); - QString startZone = cardList[0]->getZone()->getName(); + int startPlayerId = selectedCards[0]->getZone()->getPlayer()->getPlayerInfo()->getId(); + QString startZone = selectedCards[0]->getZone()->getName(); auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); @@ -1284,24 +1318,22 @@ void PlayerActions::actResetPT(QList selectedCards) } } -void PlayerActions::actSetPT(QList selectedCards) +void PlayerActions::actRequestSetPTDialog(QList selectedCards) { QString oldPT; - int playerid = player->getPlayerInfo()->getId(); for (auto card : selectedCards) { if (!card->getPT().isEmpty()) { oldPT = card->getPT(); } } - bool ok; - player->setDialogSemaphore(true); - QString pt = getTextWithMax(player->getGame()->getTab(), tr("Change power/toughness"), tr("Change stats to:"), - QLineEdit::Normal, oldPT, &ok); - player->setDialogSemaphore(false); - if (player->clearCardsToDelete() || !ok) { - return; - } + + emit requestSetPTDialog(oldPT); +} + +void PlayerActions::actSetPT(QList selectedCards, const QString &pt) +{ + int playerid = player->getPlayerInfo()->getId(); const auto ptList = CardItem::parsePT(pt); bool empty = ptList.isEmpty(); @@ -1426,7 +1458,7 @@ void AnnotationDialog::keyPressEvent(QKeyEvent *event) QInputDialog::keyPressEvent(event); } -void PlayerActions::actSetAnnotation(QList selectedCards) +void PlayerActions::actRequestSetAnnotationDialog(QList selectedCards) { QString oldAnnotation; for (auto card : selectedCards) { @@ -1435,19 +1467,11 @@ void PlayerActions::actSetAnnotation(QList selectedCards) } } - player->setDialogSemaphore(true); - AnnotationDialog *dialog = new AnnotationDialog(player->getGame()->getTab()); - dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput); - dialog->setWindowTitle(tr("Set annotation")); - dialog->setLabelText(tr("Please enter the new annotation:")); - dialog->setTextValue(oldAnnotation); - bool ok = dialog->exec(); - player->setDialogSemaphore(false); - if (player->clearCardsToDelete() || !ok) { - return; - } - QString annotation = dialog->textValue().left(MAX_NAME_LENGTH); + emit requestSetAnnotationDialog(oldAnnotation); +} +void PlayerActions::actSetAnnotation(QList selectedCards, const QString &annotation) +{ QList commandList; for (auto card : selectedCards) { auto *cmd = new Command_SetCardAttr; @@ -1519,10 +1543,8 @@ void PlayerActions::offsetCardCounter(QList selectedCards, int count sendGameCommand(prepareGameCommand(commandList)); } -void PlayerActions::actSetCardCounter(QList selectedCards, int counterId) +void PlayerActions::actRequestSetCardCounterDialog(QList selectedCards, int counterId) { - player->setDialogSemaphore(true); - // If a single card is selected, we show the old value in the dialog. Otherwise, we show "x" QString oldValueForDlg = "x"; if (selectedCards.size() == 1) { @@ -1530,22 +1552,16 @@ void PlayerActions::actSetCardCounter(QList selectedCards, int count oldValueForDlg = QString::number(card->getCounters().value(counterId, 0)); } - auto &cardCounterSettings = SettingsCache::instance().cardCounters(); - QString counterName = cardCounterSettings.displayName(counterId); - - AbstractCounterDialog dialog(counterName, oldValueForDlg, player->getGame()->getTab()); - int ok = dialog.exec(); - - player->setDialogSemaphore(false); - if (player->clearCardsToDelete() || !ok) { - return; - } + emit requestSetCardCounterDialog(counterId, oldValueForDlg); +} +void PlayerActions::actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue) +{ QList commandList; for (auto card : selectedCards) { int oldValue = card->getCounters().value(counterId, 0); Expression exp(oldValue); - double parsed = exp.parse(dialog.textValue()); + double parsed = exp.parse(counterValue); // Clamp in double precision first to avoid UB, then cast int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTERS_ON_CARD))); diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 940de610f..2779fa5aa 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -58,6 +58,31 @@ public: } signals: + void requestViewTopCardsDialog(int defaultNumberTopCards, int deckSize); + void requestViewBottomCardsDialog(int defaultNumberBottomCards, int deckSize); + void requestShuffleTopDialog(int defaultNumberTopCards, int maxCards); + void requestShuffleBottomDialog(int defaultNumberBottomCards, int maxCards); + void requestMulliganDialog(int startSize, int handSize, int deckSize); + void requestDrawCardsDialog(int defaultNumberTopCards, int deckSize); + void requestMoveTopCardsToDialog(int defaultNumberTopCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void requestMoveTopCardsUntilDialog(MoveTopCardsUntilOptions options); + void requestMoveBottomCardsToDialog(int defaultNumberBottomCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void requestDrawBottomCardsDialog(int defaultNumberBottomCards, int maxCards); + void requestRollDieDialog(); + void requestCreateTokenDialog(const QStringList &predefinedTokens); + void requestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation); + void requestMoveCardXCardsFromTopDialog(int defaultNumberTopCardsToPlaceBelow, int deckSize); + void requestSetPTDialog(const QString &oldPT); + void requestSetAnnotationDialog(const QString &oldAnnotation); + void requestSetCardCounterDialog(int counterId, const QString &oldValueForDlg); void requestZoneViewToggle(const QString &zoneName, int numberCards, bool isReversed = false); void requestSortHand(const QList &options); void requestEnableAndSetCreateAnotherTokenAction(const QString &lastTokenName); @@ -70,17 +95,30 @@ public slots: void playCardToTable(const CardItem *c, bool faceDown); void actUntapAll(); - void actRollDie(); + void actRequestRollDieDialog(); + void actRollDie(int sides, int count); void actFlipCoin(); - void actCreateToken(const QStringList &predefinedTokens); + void actRequestCreateTokenDialog(const QStringList &predefinedTokens); + void actCreateToken(TokenInfo tokenToCreate); void actCreateAnotherToken(); + void actRequestCreateRelatedFromRelationDialog(const CardItem *sourceCard, const CardRelation *cardRelation); + bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation, int variableCount); + void onRelatedCardCreated(const CardItem *sourceCard, const CardRelation *cardRelation); + void setLastRelatedCreationSucceeded(bool succeeded) + { + lastRelatedCreationSucceeded = succeeded; + } void actShuffle(); - void actShuffleTop(); - void actShuffleBottom(); + void actRequestShuffleTopDialog(); + void actShuffleTop(int number); + void actRequestShuffleBottomDialog(); + void actShuffleBottom(int number); void actDrawCard(); - void actDrawCards(); + void actRequestDrawCardsDialog(); + void actDrawCards(int number); void actUndoDraw(); - void actMulligan(); + void actRequestMulliganDialog(); + void actMulligan(int number); void actMulliganSameSize(); void actMulliganMinusOne(); void doMulligan(int number); @@ -97,10 +135,14 @@ public slots: void actMoveTopCardsToGraveFaceDown(); void actMoveTopCardsToExile(); void actMoveTopCardsToExileFaceDown(); - void actMoveTopCardsUntil(); + void actRequestMoveTopCardsUntilDialog(); + void moveTopCardsUntil(const QString &expr, MoveTopCardsUntilOptions options); void actMoveTopCardToBottom(); + void actRequestMoveTopCardsToDialog(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); + void moveTopCardsTo(int number, const QString &targetZone, bool faceDown); void actDrawBottomCard(); - void actDrawBottomCards(); + void actRequestDrawBottomCardsDialog(); + void actDrawBottomCards(int number); void actMoveBottomCardToPlay(); void actMoveBottomCardToPlayFaceDown(); void actMoveBottomCardToGrave(); @@ -110,6 +152,8 @@ public slots: void actMoveBottomCardsToExile(); void actMoveBottomCardsToExileFaceDown(); void actMoveBottomCardToTop(); + void actRequestMoveBottomCardsToDialog(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); + void moveBottomCardsTo(int number, const QString &targetZone, bool faceDown); void actSelectAll(); void actSelectRow(); @@ -117,8 +161,10 @@ public slots: void actViewLibrary(); void actViewHand(); - void actViewTopCards(); - void actViewBottomCards(); + void actRequestViewTopCardsDialog(); + void actViewTopCards(int number); + void actRequestViewBottomCardsDialog(); + void actViewBottomCards(int number); void actAlwaysRevealTopCard(bool alwaysRevealTopCard); void actAlwaysLookAtTopCard(bool alwaysRevealTopCard); void actViewGraveyard(); @@ -135,17 +181,20 @@ public slots: void actCreateRelatedCard(); void actCreateAllRelatedCards(); - void actMoveCardXCardsFromTop(QList selectedCards); + void actRequestMoveCardXCardsFromTopDialog(); + void actMoveCardXCardsFromTop(QList selectedCards, int number); void actRemoveCardCounter(QList selectedCards, int counterId); void actAddCardCounter(QList selectedCards, int counterId); - void actSetCardCounter(QList selectedCards, int counterId); + void actRequestSetCardCounterDialog(QList selectedCards, int counterId); + void actSetCardCounter(QList selectedCards, int counterId, const QString &counterValue); void actIncrementAllCardCounters(QList cardsToUpdate); void actAttach(); void actUnattach(QList selectedCards); void actDrawArrow(); void actIncPT(QList selectedCards, int deltaP, int deltaT); void actResetPT(QList selectedCards); - void actSetPT(QList selectedCards); + void actRequestSetPTDialog(QList selectedCards); + void actSetPT(QList selectedCards, const QString &pt); void actIncP(QList selectedCards); void actDecP(QList selectedCards); void actIncT(QList selectedCards); @@ -157,7 +206,8 @@ public slots: void actReduceLifeByPower(QList selectedCards); - void actSetAnnotation(QList selectedCards); + void actRequestSetAnnotationDialog(QList selectedCards); + void actSetAnnotation(QList selectedCards, const QString &annotation); void actReveal(QList selectedCards, QAction *action); void actRevealHand(int revealToPlayerId); void actRevealRandomHandCard(int revealToPlayerId); @@ -184,14 +234,12 @@ private: int movingCardsUntilCounter = 0; MoveTopCardsUntilOptions movingCardsUntilOptions; - void moveTopCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); - void moveBottomCardsTo(const QString &targetZone, const QString &zoneDisplayName, bool faceDown); + bool lastRelatedCreationSucceeded = false; void createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attach = CardRelationType::DoesNotAttach, bool persistent = false); - bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation); void playSelectedCards(QList selectedCards, bool faceDown = false); diff --git a/cockatrice/src/game/player/player_dialogs.cpp b/cockatrice/src/game/player/player_dialogs.cpp new file mode 100644 index 000000000..3c26ae1fe --- /dev/null +++ b/cockatrice/src/game/player/player_dialogs.cpp @@ -0,0 +1,298 @@ +#include "player_dialogs.h" + +#include "../../client/settings/card_counter_settings.h" +#include "../../interface/widgets/utility/get_text_with_max.h" +#include "../board/card_item.h" +#include "../dialogs/dlg_roll_dice.h" +#include "../player/player_graphics_item.h" + +#include +#include + +PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions) + : QObject(_player), player(_player), playerActions(_playerActions) +{ + connect(playerActions, &PlayerActions::requestViewTopCardsDialog, this, + &PlayerDialogs::onViewTopCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestViewBottomCardsDialog, this, + &PlayerDialogs::onViewBottomCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestShuffleTopDialog, this, &PlayerDialogs::onShuffleTopDialogRequested); + + connect(playerActions, &PlayerActions::requestShuffleBottomDialog, this, + &PlayerDialogs::onShuffleBottomDialogRequested); + + connect(playerActions, &PlayerActions::requestMulliganDialog, this, &PlayerDialogs::onMulliganDialogRequested); + + connect(playerActions, &PlayerActions::requestDrawCardsDialog, this, &PlayerDialogs::onDrawCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveTopCardsToDialog, this, + &PlayerDialogs::onMoveTopCardsToDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveTopCardsUntilDialog, this, + &PlayerDialogs::onMoveTopCardsUntilDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveBottomCardsToDialog, this, + &PlayerDialogs::onMoveBottomCardsToDialogRequested); + + connect(playerActions, &PlayerActions::requestDrawBottomCardsDialog, this, + &PlayerDialogs::onDrawBottomCardsDialogRequested); + + connect(playerActions, &PlayerActions::requestRollDieDialog, this, &PlayerDialogs::onRollDieDialogRequested); + + connect(playerActions, &PlayerActions::requestCreateTokenDialog, this, + &PlayerDialogs::onCreateTokenDialogRequested); + + connect(playerActions, &PlayerActions::requestCreateRelatedFromRelationDialog, this, + &PlayerDialogs::onCreateRelatedFromRelationDialogRequested); + + connect(playerActions, &PlayerActions::requestMoveCardXCardsFromTopDialog, this, + &PlayerDialogs::onMoveCardXCardsFromTopDialogRequested); + + connect(playerActions, &PlayerActions::requestSetPTDialog, this, &PlayerDialogs::onSetPTDialogRequested); + + connect(playerActions, &PlayerActions::requestSetAnnotationDialog, this, + &PlayerDialogs::onSetAnnotationDialogRequested); + + connect(playerActions, &PlayerActions::requestSetCardCounterDialog, this, + &PlayerDialogs::onSetCardCounterDialogRequested); +} + +void PlayerDialogs::onViewTopCardsDialogRequested(int defaultNumberTopCards, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("View top cards of library"), + tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberTopCards, 1, + deckSize, 1, &ok); + if (ok) { + playerActions->actViewTopCards(number); + } +} + +void PlayerDialogs::onViewBottomCardsDialogRequested(int defaultNumberBottomCards, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("View bottom cards of library"), + tr("Number of cards: (max. %1)").arg(deckSize), defaultNumberBottomCards, 1, + deckSize, 1, &ok); + if (ok) { + playerActions->actViewBottomCards(number); + } +} + +void PlayerDialogs::onShuffleTopDialogRequested(int defaultNumberTopCards, int maxCards) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Shuffle top cards of library"), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->actShuffleTop(number); + } +} + +void PlayerDialogs::onShuffleBottomDialogRequested(int defaultNumberBottomCards, int maxCards) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Shuffle bottom cards of library"), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->actShuffleBottom(number); + } +} + +void PlayerDialogs::onMulliganDialogRequested(int startSize, int handSize, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Draw hand"), + tr("Number of cards: (max. %1)").arg(deckSize) + '\n' + + tr("0 and lower are in comparison to current hand size"), + startSize, -handSize, deckSize, 1, &ok); + + if (ok) { + playerActions->actMulligan(number); + } +} + +void PlayerDialogs::onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Draw cards"), tr("Number of cards: (max. %1)").arg(deckSize), + defaultNumberTopCards, 1, deckSize, 1, &ok); + + if (ok) { + playerActions->actDrawCards(number); + } +} + +void PlayerDialogs::onMoveTopCardsToDialogRequested(int defaultNumberTopCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Move top cards to %1").arg(zoneDisplayName), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberTopCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->moveTopCardsTo(number, targetZone, faceDown); + } +} + +void PlayerDialogs::onMoveTopCardsUntilDialogRequested(MoveTopCardsUntilOptions options) +{ + DlgMoveTopCardsUntil dlg(dialogParent(), options); + if (!dlg.exec()) { + return; + } + playerActions->moveTopCardsUntil(dlg.getExpr(), dlg.getOptions()); +} + +void PlayerDialogs::onMoveBottomCardsToDialogRequested(int defaultNumberBottomCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown) +{ + bool ok; + int number = QInputDialog::getInt(dialogParent(), tr("Move bottom cards to %1").arg(zoneDisplayName), + tr("Number of cards: (max. %1)").arg(maxCards), defaultNumberBottomCards, 1, + maxCards, 1, &ok); + if (ok) { + playerActions->moveBottomCardsTo(number, targetZone, faceDown); + } +} + +void PlayerDialogs::onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards) +{ + bool ok; + int number = + QInputDialog::getInt(dialogParent(), tr("Draw bottom cards"), tr("Number of cards: (max. %1)").arg(maxCards), + defaultNumberBottomCards, 1, maxCards, 1, &ok); + if (ok) { + playerActions->actDrawBottomCards(number); + } +} + +void PlayerDialogs::onRollDieDialogRequested() +{ + DlgRollDice dlg(dialogParent()); + if (!dlg.exec()) { + return; + } + playerActions->actRollDie(dlg.getDieSideCount(), dlg.getDiceToRollCount()); +} + +void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, + const CardRelation *cardRelation) +{ + if (sourceCard == nullptr || cardRelation == nullptr) { + playerActions->setLastRelatedCreationSucceeded(false); + return; + } + + int variableCount = cardRelation->getDefaultCount(); + + if (cardRelation->getIsVariable()) { + bool ok; + + emit requestDialogSemaphore(true); + + variableCount = QInputDialog::getInt(dialogParent(), tr("Create tokens"), tr("Number:"), + cardRelation->getDefaultCount(), 1, MAX_TOKENS_PER_DIALOG, 1, &ok); + + emit requestDialogSemaphore(false); + + if (!ok) { + playerActions->setLastRelatedCreationSucceeded(false); // cancelled + return; + } + } + + const bool succeeded = playerActions->createRelatedFromRelation(sourceCard, cardRelation, variableCount); + + playerActions->setLastRelatedCreationSucceeded(succeeded); + + if (succeeded) { + playerActions->onRelatedCardCreated(sourceCard, cardRelation); // only on confirmed success + } +} + +void PlayerDialogs::onCreateTokenDialogRequested(const QStringList &predefinedTokens) +{ + DlgCreateToken dlg(predefinedTokens, dialogParent()); + if (!dlg.exec()) { + return; + } + + playerActions->actCreateToken(dlg.getTokenInfo()); +} + +void PlayerDialogs::onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize) +{ + bool ok; + int number = + QInputDialog::getInt(dialogParent(), tr("Place card X cards from top of library"), + tr("Which position should this card be placed:") + "\n" + tr("(max. %1)").arg(deckSize), + defaultNumberTopCardsToPlaceBelow, 1, deckSize, 1, &ok); + number -= 1; // indexes start at 0 + + if (ok) { + playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCards(), number); + } +} + +void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT) +{ + bool ok; + auto cards = player->getGameScene()->selectedCards(); + emit requestDialogSemaphore(true); + QString pt = getTextWithMax(dialogParent(), tr("Change power/toughness"), tr("Change stats to:"), QLineEdit::Normal, + oldPT, &ok); + emit requestDialogSemaphore(false); + + if (!ok || player->getLogic()->clearCardsToDelete()) { + return; + } + + playerActions->actSetPT(cards, pt); +} + +void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation) +{ + auto cards = player->getGameScene()->selectedCards(); + emit requestDialogSemaphore(true); + AnnotationDialog *dialog = new AnnotationDialog(dialogParent()); + dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput); + dialog->setWindowTitle(tr("Set annotation")); + dialog->setLabelText(tr("Please enter the new annotation:")); + dialog->setTextValue(oldAnnotation); + bool ok = dialog->exec(); + emit requestDialogSemaphore(false); + if (!ok || player->getLogic()->clearCardsToDelete()) { + return; + } + QString annotation = dialog->textValue().left(MAX_NAME_LENGTH); + playerActions->actSetAnnotation(cards, annotation); +} + +void PlayerDialogs::onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg) +{ + auto cards = player->getGameScene()->selectedCards(); + emit requestDialogSemaphore(true); + + auto &cardCounterSettings = SettingsCache::instance().cardCounters(); + QString counterName = cardCounterSettings.displayName(counterId); + + AbstractCounterDialog dialog(counterName, oldValueForDlg, dialogParent()); + int ok = dialog.exec(); + + emit requestDialogSemaphore(false); + if (!ok || player->getLogic()->clearCardsToDelete()) { + return; + } + playerActions->actSetCardCounter(cards, counterId, dialog.textValue()); +} \ No newline at end of file diff --git a/cockatrice/src/game/player/player_dialogs.h b/cockatrice/src/game/player/player_dialogs.h new file mode 100644 index 000000000..a15c5174f --- /dev/null +++ b/cockatrice/src/game/player/player_dialogs.h @@ -0,0 +1,62 @@ +#ifndef COCKATRICE_PLAYER_DIALOGS_H +#define COCKATRICE_PLAYER_DIALOGS_H +#include "player_actions.h" + +#include +#include + +class PlayerGraphicsItem; +class PlayerDialogs : public QObject +{ + + Q_OBJECT + +public: + explicit PlayerDialogs(PlayerGraphicsItem *player, PlayerActions *playerActions); + +signals: + void requestDialogSemaphore(bool active); + +public slots: + void onViewTopCardsDialogRequested(int defaultNumberTopCards, int deckSize); + void onViewBottomCardsDialogRequested(int defaultNumberBottomCards, int deckSize); + void onShuffleTopDialogRequested(int defaultNumberTopCards, int maxCards); + void onShuffleBottomDialogRequested(int defaultNumberBottomCards, int maxCards); + void onMulliganDialogRequested(int startSize, int handSize, int deckSize); + void onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize); + void onMoveTopCardsToDialogRequested(int defaultNumberTopCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void onMoveTopCardsUntilDialogRequested(MoveTopCardsUntilOptions options); + void onMoveBottomCardsToDialogRequested(int defaultNumberBottomCards, + int maxCards, + const QString &targetZone, + const QString &zoneDisplayName, + bool faceDown); + void onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards); + void onRollDieDialogRequested(); + void onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, const CardRelation *cardRelation); + void onCreateTokenDialogRequested(const QStringList &predefinedTokens); + void onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize); + void onSetPTDialogRequested(const QString &oldPT); + void onSetAnnotationDialogRequested(const QString &oldAnnotation); + void onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg); + +private: + PlayerGraphicsItem *player; + PlayerActions *playerActions; + + QWidget *dialogParent() const + { + if (auto *s = player->scene()) { + if (auto *v = s->views().value(0)) { + return v->window(); + } + } + return nullptr; + } +}; + +#endif // COCKATRICE_PLAYER_DIALOGS_H diff --git a/cockatrice/src/game/player/player_graphics_item.cpp b/cockatrice/src/game/player/player_graphics_item.cpp index d86fce86b..b0a476d5a 100644 --- a/cockatrice/src/game/player/player_graphics_item.cpp +++ b/cockatrice/src/game/player/player_graphics_item.cpp @@ -9,6 +9,7 @@ #include "../board/counter_general.h" #include "../hand_counter.h" #include "player_actions.h" +#include "player_dialogs.h" #include @@ -44,6 +45,10 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) } }); + playerDialogs = new PlayerDialogs(this, player->getPlayerActions()); + + connect(playerDialogs, &PlayerDialogs::requestDialogSemaphore, player, &PlayerLogic::setDialogSemaphore); + playerArea = new PlayerArea(this); playerTarget = new PlayerTarget(player, playerArea); diff --git a/cockatrice/src/game/player/player_graphics_item.h b/cockatrice/src/game/player/player_graphics_item.h index 1acb1520f..c1fcb4ed8 100644 --- a/cockatrice/src/game/player/player_graphics_item.h +++ b/cockatrice/src/game/player/player_graphics_item.h @@ -14,6 +14,7 @@ class HandZone; class PileZone; +class PlayerDialogs; class PlayerTarget; class StackZone; class TableZone; @@ -122,6 +123,7 @@ signals: private: PlayerLogic *player; PlayerMenu *playerMenu; + PlayerDialogs *playerDialogs; PlayerArea *playerArea; PlayerTarget *playerTarget; QMap counterWidgets; From da4ba222c0c9dabd1b70c96aac20bdd8721c6e13 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:51:13 +0200 Subject: [PATCH 19/50] [Game] Move graphics out of game and into game_graphics (#6928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Game][Player] Pull out graphics_items out of player_logic Took 25 seconds Took 9 minutes * [Game] Move graphics files into game_graphics Took 1 minute Took 2 minutes Took 23 seconds Took 1 minute Took 2 seconds * Include. Took 4 minutes Took 3 minutes Took 4 minutes Took 1 minute Took 3 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 74 +++++++++---------- cockatrice/src/game/abstract_game.cpp | 2 +- cockatrice/src/game/abstract_game.h | 9 +-- cockatrice/src/game/arrow_registry.cpp | 2 +- cockatrice/src/game/board/card_list.cpp | 2 +- cockatrice/src/game/game.cpp | 8 +- cockatrice/src/game/game.h | 3 +- cockatrice/src/game/game_event_handler.cpp | 2 +- cockatrice/src/game/player/player_actions.cpp | 7 +- cockatrice/src/game/player/player_actions.h | 9 ++- .../src/game/player/player_event_handler.cpp | 4 +- cockatrice/src/game/player/player_info.h | 2 +- cockatrice/src/game/player/player_logic.cpp | 10 +-- cockatrice/src/game/player/player_logic.h | 5 +- cockatrice/src/game/replay.cpp | 4 +- cockatrice/src/game/replay.h | 2 +- cockatrice/src/game/zones/card_zone_logic.cpp | 2 +- cockatrice/src/game/zones/hand_zone_logic.cpp | 2 +- cockatrice/src/game/zones/pile_zone_logic.cpp | 2 +- .../src/game/zones/stack_zone_logic.cpp | 2 +- .../src/game/zones/table_zone_logic.cpp | 2 +- cockatrice/src/game/zones/view_zone_logic.cpp | 2 +- .../board/abstract_card_drag_item.cpp | 0 .../board/abstract_card_drag_item.h | 0 .../board/abstract_card_item.cpp | 0 .../board/abstract_card_item.h | 2 +- .../board/abstract_counter.cpp | 6 +- .../board/abstract_counter.h | 2 +- .../board/arrow_item.cpp | 6 +- .../board/arrow_item.h | 2 +- .../board/arrow_target.cpp | 2 +- .../board/arrow_target.h | 2 +- .../board/card_drag_item.cpp | 6 +- .../board/card_drag_item.h | 0 .../board/card_item.cpp | 17 ++--- .../{game => game_graphics}/board/card_item.h | 4 +- .../board/counter_general.cpp | 2 +- .../board/counter_general.h | 0 .../board/translate_counter_name.cpp | 0 .../board/translate_counter_name.h | 0 .../{game => game_graphics}/card_dimensions.h | 0 .../deckview/deck_view.cpp | 0 .../deckview/deck_view.h | 0 .../deckview/deck_view_container.cpp | 0 .../deckview/deck_view_container.h | 0 .../deckview/tabbed_deck_view_container.cpp | 0 .../deckview/tabbed_deck_view_container.h | 0 .../dialogs/dlg_create_token.cpp | 0 .../dialogs/dlg_create_token.h | 0 .../dialogs/dlg_move_top_cards_until.cpp | 0 .../dialogs/dlg_move_top_cards_until.h | 0 .../dialogs/dlg_roll_dice.cpp | 0 .../dialogs/dlg_roll_dice.h | 0 .../{game => game_graphics}/game_scene.cpp | 14 ++-- .../src/{game => game_graphics}/game_scene.h | 6 +- .../src/{game => game_graphics}/game_view.cpp | 0 .../src/{game => game_graphics}/game_view.h | 0 .../{game => game_graphics}/hand_counter.cpp | 2 +- .../{game => game_graphics}/hand_counter.h | 4 +- .../log/message_log_widget.cpp | 6 +- .../log/message_log_widget.h | 2 +- .../phases_toolbar.cpp | 0 .../{game => game_graphics}/phases_toolbar.h | 2 +- .../player/card_menu_action_type.h | 0 .../player/menu/abstract_player_component.h | 0 .../player/menu/card_menu.cpp | 6 +- .../player/menu/card_menu.h | 0 .../player/menu/custom_zone_menu.cpp | 3 +- .../player/menu/custom_zone_menu.h | 0 .../player/menu/grave_menu.cpp | 7 +- .../player/menu/grave_menu.h | 0 .../player/menu/hand_menu.cpp | 6 +- .../player/menu/hand_menu.h | 0 .../player/menu/library_menu.cpp | 7 +- .../player/menu/library_menu.h | 0 .../player/menu/move_menu.cpp | 5 +- .../player/menu/move_menu.h | 0 .../player/menu/player_menu.cpp | 1 + .../player/menu/player_menu.h | 0 .../player/menu/pt_menu.cpp | 5 +- .../player/menu/pt_menu.h | 0 .../player/menu/rfg_menu.cpp | 5 +- .../player/menu/rfg_menu.h | 0 .../player/menu/say_menu.cpp | 5 +- .../player/menu/say_menu.h | 0 .../player/menu/sideboard_menu.cpp | 5 +- .../player/menu/sideboard_menu.h | 0 .../player/menu/utility_menu.cpp | 5 +- .../player/menu/utility_menu.h | 0 .../player/player_area.cpp | 0 .../player/player_area.h | 2 +- .../player/player_dialogs.cpp | 0 .../player/player_dialogs.h | 3 +- .../player/player_graphics_item.cpp | 11 +-- .../player/player_graphics_item.h | 3 +- .../player/player_list_widget.cpp | 0 .../player/player_list_widget.h | 2 +- .../player/player_target.cpp | 2 +- .../player/player_target.h | 2 +- .../z_value_layer_manager.h | 0 .../src/{game => game_graphics}/z_values.h | 0 .../src/game_graphics/zones/card_zone.cpp | 2 +- .../src/game_graphics/zones/hand_zone.cpp | 4 +- .../src/game_graphics/zones/pile_zone.cpp | 4 +- .../src/game_graphics/zones/select_zone.cpp | 4 +- .../src/game_graphics/zones/stack_zone.cpp | 6 +- .../src/game_graphics/zones/table_zone.cpp | 8 +- .../src/game_graphics/zones/table_zone.h | 2 +- .../src/game_graphics/zones/view_zone.cpp | 4 +- .../game_graphics/zones/view_zone_widget.cpp | 6 +- .../cards/card_info_display_widget.cpp | 2 +- .../widgets/cards/card_info_frame_widget.cpp | 2 +- .../cards/card_info_picture_widget.cpp | 2 +- .../widgets/cards/card_info_text_widget.cpp | 2 +- .../src/interface/widgets/tabs/tab_game.cpp | 26 ++++--- .../src/interface/widgets/tabs/tab_game.h | 3 +- 116 files changed, 208 insertions(+), 198 deletions(-) rename cockatrice/src/{game => game_graphics}/board/abstract_card_drag_item.cpp (100%) rename cockatrice/src/{game => game_graphics}/board/abstract_card_drag_item.h (100%) rename cockatrice/src/{game => game_graphics}/board/abstract_card_item.cpp (100%) rename cockatrice/src/{game => game_graphics}/board/abstract_card_item.h (98%) rename cockatrice/src/{game => game_graphics}/board/abstract_counter.cpp (97%) rename cockatrice/src/{game => game_graphics}/board/abstract_counter.h (98%) rename cockatrice/src/{game => game_graphics}/board/arrow_item.cpp (99%) rename cockatrice/src/{game => game_graphics}/board/arrow_item.h (98%) rename cockatrice/src/{game => game_graphics}/board/arrow_target.cpp (92%) rename cockatrice/src/{game => game_graphics}/board/arrow_target.h (93%) rename cockatrice/src/{game => game_graphics}/board/card_drag_item.cpp (96%) rename cockatrice/src/{game => game_graphics}/board/card_drag_item.h (100%) rename cockatrice/src/{game => game_graphics}/board/card_item.cpp (98%) rename cockatrice/src/{game => game_graphics}/board/card_item.h (98%) rename cockatrice/src/{game => game_graphics}/board/counter_general.cpp (95%) rename cockatrice/src/{game => game_graphics}/board/counter_general.h (100%) rename cockatrice/src/{game => game_graphics}/board/translate_counter_name.cpp (100%) rename cockatrice/src/{game => game_graphics}/board/translate_counter_name.h (100%) rename cockatrice/src/{game => game_graphics}/card_dimensions.h (100%) rename cockatrice/src/{game => game_graphics}/deckview/deck_view.cpp (100%) rename cockatrice/src/{game => game_graphics}/deckview/deck_view.h (100%) rename cockatrice/src/{game => game_graphics}/deckview/deck_view_container.cpp (100%) rename cockatrice/src/{game => game_graphics}/deckview/deck_view_container.h (100%) rename cockatrice/src/{game => game_graphics}/deckview/tabbed_deck_view_container.cpp (100%) rename cockatrice/src/{game => game_graphics}/deckview/tabbed_deck_view_container.h (100%) rename cockatrice/src/{game => game_graphics}/dialogs/dlg_create_token.cpp (100%) rename cockatrice/src/{game => game_graphics}/dialogs/dlg_create_token.h (100%) rename cockatrice/src/{game => game_graphics}/dialogs/dlg_move_top_cards_until.cpp (100%) rename cockatrice/src/{game => game_graphics}/dialogs/dlg_move_top_cards_until.h (100%) rename cockatrice/src/{game => game_graphics}/dialogs/dlg_roll_dice.cpp (100%) rename cockatrice/src/{game => game_graphics}/dialogs/dlg_roll_dice.h (100%) rename cockatrice/src/{game => game_graphics}/game_scene.cpp (98%) rename cockatrice/src/{game => game_graphics}/game_scene.h (98%) rename cockatrice/src/{game => game_graphics}/game_view.cpp (100%) rename cockatrice/src/{game => game_graphics}/game_view.h (100%) rename cockatrice/src/{game => game_graphics}/hand_counter.cpp (96%) rename cockatrice/src/{game => game_graphics}/hand_counter.h (87%) rename cockatrice/src/{game => game_graphics}/log/message_log_widget.cpp (99%) rename cockatrice/src/{game => game_graphics}/log/message_log_widget.h (99%) rename cockatrice/src/{game => game_graphics}/phases_toolbar.cpp (100%) rename cockatrice/src/{game => game_graphics}/phases_toolbar.h (97%) rename cockatrice/src/{game => game_graphics}/player/card_menu_action_type.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/abstract_player_component.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/card_menu.cpp (99%) rename cockatrice/src/{game => game_graphics}/player/menu/card_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/custom_zone_menu.cpp (93%) rename cockatrice/src/{game => game_graphics}/player/menu/custom_zone_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/grave_menu.cpp (96%) rename cockatrice/src/{game => game_graphics}/player/menu/grave_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/hand_menu.cpp (98%) rename cockatrice/src/{game => game_graphics}/player/menu/hand_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/library_menu.cpp (99%) rename cockatrice/src/{game => game_graphics}/player/menu/library_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/move_menu.cpp (96%) rename cockatrice/src/{game => game_graphics}/player/menu/move_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/player_menu.cpp (99%) rename cockatrice/src/{game => game_graphics}/player/menu/player_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/pt_menu.cpp (96%) rename cockatrice/src/{game => game_graphics}/player/menu/pt_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/rfg_menu.cpp (95%) rename cockatrice/src/{game => game_graphics}/player/menu/rfg_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/say_menu.cpp (91%) rename cockatrice/src/{game => game_graphics}/player/menu/say_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/sideboard_menu.cpp (87%) rename cockatrice/src/{game => game_graphics}/player/menu/sideboard_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/menu/utility_menu.cpp (97%) rename cockatrice/src/{game => game_graphics}/player/menu/utility_menu.h (100%) rename cockatrice/src/{game => game_graphics}/player/player_area.cpp (100%) rename cockatrice/src/{game => game_graphics}/player/player_area.h (94%) rename cockatrice/src/{game => game_graphics}/player/player_dialogs.cpp (100%) rename cockatrice/src/{game => game_graphics}/player/player_dialogs.h (96%) rename cockatrice/src/{game => game_graphics}/player/player_graphics_item.cpp (97%) rename cockatrice/src/{game => game_graphics}/player/player_graphics_item.h (98%) rename cockatrice/src/{game => game_graphics}/player/player_list_widget.cpp (100%) rename cockatrice/src/{game => game_graphics}/player/player_list_widget.h (97%) rename cockatrice/src/{game => game_graphics}/player/player_target.cpp (99%) rename cockatrice/src/{game => game_graphics}/player/player_target.h (95%) rename cockatrice/src/{game => game_graphics}/z_value_layer_manager.h (100%) rename cockatrice/src/{game => game_graphics}/z_values.h (100%) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index f0e363e18..bd99d08bf 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -57,57 +57,57 @@ set(cockatrice_SOURCES src/filters/syntax_help.cpp src/game/abstract_game.cpp src/game/arrow_registry.cpp - src/game/board/abstract_card_drag_item.cpp - src/game/board/abstract_card_item.cpp - src/game/board/abstract_counter.cpp + src/game_graphics/board/abstract_card_drag_item.cpp + src/game_graphics/board/abstract_card_item.cpp + src/game_graphics/board/abstract_counter.cpp src/game/board/arrow_data.cpp - src/game/board/arrow_item.cpp - src/game/board/arrow_target.cpp - src/game/board/card_drag_item.cpp - src/game/board/card_item.cpp + src/game_graphics/board/arrow_item.cpp + src/game_graphics/board/arrow_target.cpp + src/game_graphics/board/card_drag_item.cpp + src/game_graphics/board/card_item.cpp src/game/board/card_list.cpp src/game/board/card_state.cpp - src/game/board/counter_general.cpp + src/game_graphics/board/counter_general.cpp src/game/board/counter_state.cpp - src/game/board/translate_counter_name.cpp - src/game/deckview/deck_view.cpp - src/game/deckview/deck_view_container.cpp - src/game/deckview/tabbed_deck_view_container.cpp - src/game/dialogs/dlg_create_token.cpp - src/game/dialogs/dlg_move_top_cards_until.cpp - src/game/dialogs/dlg_roll_dice.cpp + src/game_graphics/board/translate_counter_name.cpp + src/game_graphics/deckview/deck_view.cpp + src/game_graphics/deckview/deck_view_container.cpp + src/game_graphics/deckview/tabbed_deck_view_container.cpp + src/game_graphics/dialogs/dlg_create_token.cpp + src/game_graphics/dialogs/dlg_move_top_cards_until.cpp + src/game_graphics/dialogs/dlg_roll_dice.cpp src/game/game.cpp src/game/game_event_handler.cpp src/game/game_meta_info.cpp - src/game/game_scene.cpp + src/game_graphics/game_scene.cpp src/game/game_state.cpp - src/game/game_view.cpp - src/game/hand_counter.cpp - src/game/log/message_log_widget.cpp + src/game_graphics/game_view.cpp + src/game_graphics/hand_counter.cpp + src/game_graphics/log/message_log_widget.cpp src/game/phase.cpp - src/game/phases_toolbar.cpp - src/game/player/menu/card_menu.cpp - src/game/player/menu/custom_zone_menu.cpp - src/game/player/menu/grave_menu.cpp - src/game/player/menu/hand_menu.cpp - src/game/player/menu/library_menu.cpp - src/game/player/menu/move_menu.cpp - src/game/player/menu/player_menu.cpp - src/game/player/menu/pt_menu.cpp - src/game/player/menu/rfg_menu.cpp - src/game/player/menu/say_menu.cpp - src/game/player/menu/sideboard_menu.cpp - src/game/player/menu/utility_menu.cpp + src/game_graphics/phases_toolbar.cpp + src/game_graphics/player/menu/card_menu.cpp + src/game_graphics/player/menu/custom_zone_menu.cpp + src/game_graphics/player/menu/grave_menu.cpp + src/game_graphics/player/menu/hand_menu.cpp + src/game_graphics/player/menu/library_menu.cpp + src/game_graphics/player/menu/move_menu.cpp + src/game_graphics/player/menu/player_menu.cpp + src/game_graphics/player/menu/pt_menu.cpp + src/game_graphics/player/menu/rfg_menu.cpp + src/game_graphics/player/menu/say_menu.cpp + src/game_graphics/player/menu/sideboard_menu.cpp + src/game_graphics/player/menu/utility_menu.cpp src/game/player/player_actions.cpp - src/game/player/player_area.cpp - src/game/player/player_dialogs.cpp + src/game_graphics/player/player_area.cpp + src/game_graphics/player/player_dialogs.cpp src/game/player/player_event_handler.cpp - src/game/player/player_graphics_item.cpp + src/game_graphics/player/player_graphics_item.cpp src/game/player/player_info.cpp - src/game/player/player_list_widget.cpp + src/game_graphics/player/player_list_widget.cpp src/game/player/player_logic.cpp src/game/player/player_manager.cpp - src/game/player/player_target.cpp + src/game_graphics/player/player_target.cpp src/game/replay.cpp src/game/zones/card_zone_logic.cpp src/game/zones/hand_zone_logic.cpp diff --git a/cockatrice/src/game/abstract_game.cpp b/cockatrice/src/game/abstract_game.cpp index 5b1b4bff2..c20003ece 100644 --- a/cockatrice/src/game/abstract_game.cpp +++ b/cockatrice/src/game/abstract_game.cpp @@ -3,7 +3,7 @@ #include "../interface/widgets/tabs/tab_game.h" #include "player/player_logic.h" -AbstractGame::AbstractGame(TabGame *_tab) : QObject(_tab), tab(_tab) +AbstractGame::AbstractGame(QObject *_parent) : QObject(_parent) { gameMetaInfo = new GameMetaInfo(this); gameEventHandler = new GameEventHandler(this); diff --git a/cockatrice/src/game/abstract_game.h b/cockatrice/src/game/abstract_game.h index 2441bac2d..5115ed5ca 100644 --- a/cockatrice/src/game/abstract_game.h +++ b/cockatrice/src/game/abstract_game.h @@ -16,26 +16,19 @@ #include class CardItem; -class TabGame; class AbstractGame : public QObject { Q_OBJECT public: - explicit AbstractGame(TabGame *tab); + explicit AbstractGame(QObject *parent); - TabGame *tab; GameMetaInfo *gameMetaInfo; GameState *gameState; GameEventHandler *gameEventHandler; PlayerManager *playerManager; CardItem *activeCard; - TabGame *getTab() const - { - return tab; - } - GameMetaInfo *getGameMetaInfo() { return gameMetaInfo; diff --git a/cockatrice/src/game/arrow_registry.cpp b/cockatrice/src/game/arrow_registry.cpp index e679d2972..286764b3b 100644 --- a/cockatrice/src/game/arrow_registry.cpp +++ b/cockatrice/src/game/arrow_registry.cpp @@ -1,6 +1,6 @@ #include "arrow_registry.h" -#include "board/arrow_item.h" +#include "../game_graphics/board/arrow_item.h" void ArrowRegistry::insert(QSharedPointer data, ArrowItem *arrow) { diff --git a/cockatrice/src/game/board/card_list.cpp b/cockatrice/src/game/board/card_list.cpp index c324ca10a..0080b5ae6 100644 --- a/cockatrice/src/game/board/card_list.cpp +++ b/cockatrice/src/game/board/card_list.cpp @@ -1,6 +1,6 @@ #include "card_list.h" -#include "card_item.h" +#include "../../game_graphics/board/card_item.h" #include #include diff --git a/cockatrice/src/game/game.cpp b/cockatrice/src/game/game.cpp index 38477f7f7..4c8b109c2 100644 --- a/cockatrice/src/game/game.cpp +++ b/cockatrice/src/game/game.cpp @@ -4,16 +4,16 @@ #include -Game::Game(TabGame *_tab, +Game::Game(QObject *_parent, + bool isLocalGame, QList &_clients, const Event_GameJoined &event, const QMap &_roomGameTypes) - : AbstractGame(_tab) + : AbstractGame(_parent) { gameMetaInfo->setFromProto(event.game_info()); gameMetaInfo->setRoomGameTypes(_roomGameTypes); - gameState = new GameState(this, 0, event.host_id(), tab->getTabSupervisor()->getIsLocalGame(), _clients, false, - event.resuming(), -1, false); + gameState = new GameState(this, 0, event.host_id(), isLocalGame, _clients, false, event.resuming(), -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); playerManager = new PlayerManager(this, event.player_id(), event.judge(), event.spectator()); gameMetaInfo->setStarted(false); diff --git a/cockatrice/src/game/game.h b/cockatrice/src/game/game.h index ccdb679df..4f912664c 100644 --- a/cockatrice/src/game/game.h +++ b/cockatrice/src/game/game.h @@ -16,7 +16,8 @@ class Game : public AbstractGame Q_OBJECT public: - Game(TabGame *tab, + Game(QObject *parent, + bool isLocalGame, QList &_clients, const Event_GameJoined &event, const QMap &_roomGameTypes); diff --git a/cockatrice/src/game/game_event_handler.cpp b/cockatrice/src/game/game_event_handler.cpp index 629e2f6a1..4a96eebdb 100644 --- a/cockatrice/src/game/game_event_handler.cpp +++ b/cockatrice/src/game/game_event_handler.cpp @@ -1,8 +1,8 @@ #include "game_event_handler.h" +#include "../game_graphics/log/message_log_widget.h" #include "../interface/widgets/tabs/tab_game.h" #include "abstract_game.h" -#include "log/message_log_widget.h" #include #include diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 2b0428dd8..de909ca5e 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -1,14 +1,13 @@ #include "player_actions.h" +#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h" +#include "../../game_graphics/dialogs/dlg_roll_dice.h" +#include "../../game_graphics/player/card_menu_action_type.h" #include "../../game_graphics/zones/hand_zone.h" #include "../../game_graphics/zones/table_zone.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../../interface/widgets/utility/get_text_with_max.h" -#include "../board/card_item.h" -#include "../dialogs/dlg_move_top_cards_until.h" -#include "../dialogs/dlg_roll_dice.h" #include "../zones/view_zone_logic.h" -#include "card_menu_action_type.h" #include #include diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 2779fa5aa..3f1960892 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -7,9 +7,11 @@ #ifndef COCKATRICE_PLAYER_ACTIONS_H #define COCKATRICE_PLAYER_ACTIONS_H -#include "../dialogs/dlg_create_token.h" -#include "../dialogs/dlg_move_top_cards_until.h" -#include "card_menu_action_type.h" + +#include "../../game_graphics/board/card_item.h" +#include "../../game_graphics/dialogs/dlg_create_token.h" +#include "../../game_graphics/dialogs/dlg_move_top_cards_until.h" +#include "../../game_graphics/player/card_menu_action_type.h" #include "event_processing_options.h" #include "player_logic.h" @@ -26,7 +28,6 @@ class Message; } } // namespace google -class CardItem; class Command_MoveCard; class GameEventContext; class PendingCommand; diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index aa751170b..bc48298f7 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -1,10 +1,10 @@ #include "player_event_handler.h" +#include "../../game_graphics/board/arrow_item.h" +#include "../../game_graphics/board/card_item.h" #include "../../game_graphics/zones/view_zone.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/arrow_data.h" -#include "../board/arrow_item.h" -#include "../board/card_item.h" #include "../board/card_list.h" #include "player_actions.h" #include "player_logic.h" diff --git a/cockatrice/src/game/player/player_info.h b/cockatrice/src/game/player/player_info.h index e67131ceb..4ec39edbd 100644 --- a/cockatrice/src/game/player/player_info.h +++ b/cockatrice/src/game/player/player_info.h @@ -7,7 +7,7 @@ #ifndef COCKATRICE_PLAYER_INFO_H #define COCKATRICE_PLAYER_INFO_H -#include "player_target.h" +#include "../../game_graphics/player/player_target.h" #include #include diff --git a/cockatrice/src/game/player/player_logic.cpp b/cockatrice/src/game/player/player_logic.cpp index b748eb19a..485e2fc5c 100644 --- a/cockatrice/src/game/player/player_logic.cpp +++ b/cockatrice/src/game/player/player_logic.cpp @@ -1,18 +1,18 @@ #include "player_logic.h" +#include "../../game_graphics/board/arrow_item.h" +#include "../../game_graphics/board/card_item.h" +#include "../../game_graphics/board/counter_general.h" +#include "../../game_graphics/game_scene.h" +#include "../../game_graphics/player/player_target.h" #include "../../game_graphics/zones/hand_zone.h" #include "../../game_graphics/zones/pile_zone.h" #include "../../game_graphics/zones/stack_zone.h" #include "../../game_graphics/zones/table_zone.h" #include "../../interface/theme_manager.h" #include "../../interface/widgets/tabs/tab_game.h" -#include "../board/arrow_item.h" -#include "../board/card_item.h" #include "../board/card_list.h" -#include "../board/counter_general.h" -#include "../game_scene.h" #include "player_actions.h" -#include "player_target.h" #include #include diff --git a/cockatrice/src/game/player/player_logic.h b/cockatrice/src/game/player/player_logic.h index c83892dea..a89cb6eed 100644 --- a/cockatrice/src/game/player/player_logic.h +++ b/cockatrice/src/game/player/player_logic.h @@ -7,6 +7,7 @@ #ifndef PLAYER_H #define PLAYER_H +#include "../../game_graphics/player/player_area.h" #include "../../interface/widgets/menus/tearoff_menu.h" #include "../board/arrow_data.h" #include "../interface/deck_loader/loaded_deck.h" @@ -14,10 +15,7 @@ #include "../zones/pile_zone_logic.h" #include "../zones/stack_zone_logic.h" #include "../zones/table_zone_logic.h" -#include "menu/player_menu.h" -#include "player_area.h" #include "player_event_handler.h" -#include "player_graphics_item.h" #include "player_info.h" #include @@ -54,6 +52,7 @@ class PlayerMenu; class QAction; class QMenu; class ServerInfo_Arrow; +class ServerInfo_Card; class ServerInfo_Counter; class ServerInfo_Player; class ServerInfo_User; diff --git a/cockatrice/src/game/replay.cpp b/cockatrice/src/game/replay.cpp index 6886f817a..69f9d8b20 100644 --- a/cockatrice/src/game/replay.cpp +++ b/cockatrice/src/game/replay.cpp @@ -2,9 +2,9 @@ #include "../interface/widgets/tabs/tab_game.h" -Replay::Replay(TabGame *_tab, GameReplay *_replay) : AbstractGame(_tab) +Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent) { - gameState = new GameState(this, 0, -1, tab->getTabSupervisor()->getIsLocalGame(), {}, false, false, -1, false); + gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false); connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged); playerManager = new PlayerManager(this, -1, false, true); loadReplay(_replay); diff --git a/cockatrice/src/game/replay.h b/cockatrice/src/game/replay.h index b837e4b8c..ecb3a10d0 100644 --- a/cockatrice/src/game/replay.h +++ b/cockatrice/src/game/replay.h @@ -15,7 +15,7 @@ class Replay : public AbstractGame Q_OBJECT public: - explicit Replay(TabGame *_tab, GameReplay *_replay); + explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame); }; #endif // COCKATRICE_REPLAY_H diff --git a/cockatrice/src/game/zones/card_zone_logic.cpp b/cockatrice/src/game/zones/card_zone_logic.cpp index aace7097e..7e0585f4e 100644 --- a/cockatrice/src/game/zones/card_zone_logic.cpp +++ b/cockatrice/src/game/zones/card_zone_logic.cpp @@ -1,7 +1,7 @@ #include "card_zone_logic.h" +#include "../../game_graphics/board/card_item.h" #include "../../game_graphics/zones/view_zone.h" -#include "../board/card_item.h" #include "../player/player_actions.h" #include "../player/player_logic.h" #include "view_zone_logic.h" diff --git a/cockatrice/src/game/zones/hand_zone_logic.cpp b/cockatrice/src/game/zones/hand_zone_logic.cpp index 36af11131..3bdd15902 100644 --- a/cockatrice/src/game/zones/hand_zone_logic.cpp +++ b/cockatrice/src/game/zones/hand_zone_logic.cpp @@ -1,6 +1,6 @@ #include "hand_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" #include "card_zone_algorithms.h" HandZoneLogic::HandZoneLogic(PlayerLogic *_player, diff --git a/cockatrice/src/game/zones/pile_zone_logic.cpp b/cockatrice/src/game/zones/pile_zone_logic.cpp index 66edde4b7..0f374fb84 100644 --- a/cockatrice/src/game/zones/pile_zone_logic.cpp +++ b/cockatrice/src/game/zones/pile_zone_logic.cpp @@ -1,6 +1,6 @@ #include "pile_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" PileZoneLogic::PileZoneLogic(PlayerLogic *_player, const QString &_name, diff --git a/cockatrice/src/game/zones/stack_zone_logic.cpp b/cockatrice/src/game/zones/stack_zone_logic.cpp index 2120b9a1d..341d4b0e4 100644 --- a/cockatrice/src/game/zones/stack_zone_logic.cpp +++ b/cockatrice/src/game/zones/stack_zone_logic.cpp @@ -1,6 +1,6 @@ #include "stack_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" #include "card_zone_algorithms.h" StackZoneLogic::StackZoneLogic(PlayerLogic *_player, diff --git a/cockatrice/src/game/zones/table_zone_logic.cpp b/cockatrice/src/game/zones/table_zone_logic.cpp index 3d7ac4297..a4f033819 100644 --- a/cockatrice/src/game/zones/table_zone_logic.cpp +++ b/cockatrice/src/game/zones/table_zone_logic.cpp @@ -1,6 +1,6 @@ #include "table_zone_logic.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" TableZoneLogic::TableZoneLogic(PlayerLogic *_player, const QString &_name, diff --git a/cockatrice/src/game/zones/view_zone_logic.cpp b/cockatrice/src/game/zones/view_zone_logic.cpp index fa4a73d38..8782a1762 100644 --- a/cockatrice/src/game/zones/view_zone_logic.cpp +++ b/cockatrice/src/game/zones/view_zone_logic.cpp @@ -1,7 +1,7 @@ #include "view_zone_logic.h" #include "../../client/settings/cache_settings.h" -#include "../board/card_item.h" +#include "../../game_graphics/board/card_item.h" /** * @param _player the player that the cards are revealed to. diff --git a/cockatrice/src/game/board/abstract_card_drag_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp similarity index 100% rename from cockatrice/src/game/board/abstract_card_drag_item.cpp rename to cockatrice/src/game_graphics/board/abstract_card_drag_item.cpp diff --git a/cockatrice/src/game/board/abstract_card_drag_item.h b/cockatrice/src/game_graphics/board/abstract_card_drag_item.h similarity index 100% rename from cockatrice/src/game/board/abstract_card_drag_item.h rename to cockatrice/src/game_graphics/board/abstract_card_drag_item.h diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp similarity index 100% rename from cockatrice/src/game/board/abstract_card_item.cpp rename to cockatrice/src/game_graphics/board/abstract_card_item.cpp diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h similarity index 98% rename from cockatrice/src/game/board/abstract_card_item.h rename to cockatrice/src/game_graphics/board/abstract_card_item.h index 863954b73..bdb5f7cf1 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -7,9 +7,9 @@ #ifndef ABSTRACTCARDITEM_H #define ABSTRACTCARDITEM_H -#include "../../game_graphics/board/graphics_item_type.h" #include "../card_dimensions.h" #include "arrow_target.h" +#include "graphics_item_type.h" #include #include diff --git a/cockatrice/src/game/board/abstract_counter.cpp b/cockatrice/src/game_graphics/board/abstract_counter.cpp similarity index 97% rename from cockatrice/src/game/board/abstract_counter.cpp rename to cockatrice/src/game_graphics/board/abstract_counter.cpp index 18787a0bc..219dd456e 100644 --- a/cockatrice/src/game/board/abstract_counter.cpp +++ b/cockatrice/src/game_graphics/board/abstract_counter.cpp @@ -1,10 +1,10 @@ #include "abstract_counter.h" #include "../../client/settings/cache_settings.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game_graphics/board/translate_counter_name.h" #include "../../interface/widgets/tabs/tab_game.h" -#include "../player/player_actions.h" -#include "../player/player_logic.h" -#include "translate_counter_name.h" #include #include diff --git a/cockatrice/src/game/board/abstract_counter.h b/cockatrice/src/game_graphics/board/abstract_counter.h similarity index 98% rename from cockatrice/src/game/board/abstract_counter.h rename to cockatrice/src/game_graphics/board/abstract_counter.h index b31bd1aa3..b319a722d 100644 --- a/cockatrice/src/game/board/abstract_counter.h +++ b/cockatrice/src/game_graphics/board/abstract_counter.h @@ -7,9 +7,9 @@ #ifndef COUNTER_H #define COUNTER_H +#include "../../game/board/counter_state.h" #include "../../interface/widgets/menus/tearoff_menu.h" #include "../player/menu/abstract_player_component.h" -#include "counter_state.h" #include #include diff --git a/cockatrice/src/game/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp similarity index 99% rename from cockatrice/src/game/board/arrow_item.cpp rename to cockatrice/src/game_graphics/board/arrow_item.cpp index 0b740bc70..af6a6bf36 100644 --- a/cockatrice/src/game/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -2,11 +2,11 @@ #include "arrow_item.h" #include "../../client/settings/cache_settings.h" -#include "../../game_graphics/zones/card_zone.h" -#include "../player/player_actions.h" -#include "../player/player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player/player_target.h" #include "../z_values.h" +#include "../zones/card_zone.h" #include "card_item.h" #include diff --git a/cockatrice/src/game/board/arrow_item.h b/cockatrice/src/game_graphics/board/arrow_item.h similarity index 98% rename from cockatrice/src/game/board/arrow_item.h rename to cockatrice/src/game_graphics/board/arrow_item.h index 0c04c27f8..1c306e065 100644 --- a/cockatrice/src/game/board/arrow_item.h +++ b/cockatrice/src/game_graphics/board/arrow_item.h @@ -1,7 +1,7 @@ #ifndef ARROWITEM_H #define ARROWITEM_H -#include "arrow_data.h" +#include "../../game/board/arrow_data.h" #include "arrow_target.h" #include diff --git a/cockatrice/src/game/board/arrow_target.cpp b/cockatrice/src/game_graphics/board/arrow_target.cpp similarity index 92% rename from cockatrice/src/game/board/arrow_target.cpp rename to cockatrice/src/game_graphics/board/arrow_target.cpp index edf526e4e..79b21d921 100644 --- a/cockatrice/src/game/board/arrow_target.cpp +++ b/cockatrice/src/game_graphics/board/arrow_target.cpp @@ -1,6 +1,6 @@ #include "arrow_target.h" -#include "../player/player_logic.h" +#include "../../game/player/player_logic.h" #include "arrow_item.h" ArrowTarget::ArrowTarget(PlayerLogic *_owner, QGraphicsItem *parent) : AbstractGraphicsItem(parent), owner(_owner) diff --git a/cockatrice/src/game/board/arrow_target.h b/cockatrice/src/game_graphics/board/arrow_target.h similarity index 93% rename from cockatrice/src/game/board/arrow_target.h rename to cockatrice/src/game_graphics/board/arrow_target.h index 664572705..bf89c5456 100644 --- a/cockatrice/src/game/board/arrow_target.h +++ b/cockatrice/src/game_graphics/board/arrow_target.h @@ -7,7 +7,7 @@ #ifndef ARROWTARGET_H #define ARROWTARGET_H -#include "../../game_graphics/board/abstract_graphics_item.h" +#include "abstract_graphics_item.h" #include diff --git a/cockatrice/src/game/board/card_drag_item.cpp b/cockatrice/src/game_graphics/board/card_drag_item.cpp similarity index 96% rename from cockatrice/src/game/board/card_drag_item.cpp rename to cockatrice/src/game_graphics/board/card_drag_item.cpp index 39fb9a390..49467c5c9 100644 --- a/cockatrice/src/game/board/card_drag_item.cpp +++ b/cockatrice/src/game_graphics/board/card_drag_item.cpp @@ -1,9 +1,9 @@ #include "card_drag_item.h" -#include "../../game_graphics/zones/card_zone.h" -#include "../../game_graphics/zones/table_zone.h" -#include "../../game_graphics/zones/view_zone.h" #include "../game_scene.h" +#include "../zones/card_zone.h" +#include "../zones/table_zone.h" +#include "../zones/view_zone.h" #include "card_item.h" #include diff --git a/cockatrice/src/game/board/card_drag_item.h b/cockatrice/src/game_graphics/board/card_drag_item.h similarity index 100% rename from cockatrice/src/game/board/card_drag_item.h rename to cockatrice/src/game_graphics/board/card_drag_item.h diff --git a/cockatrice/src/game/board/card_item.cpp b/cockatrice/src/game_graphics/board/card_item.cpp similarity index 98% rename from cockatrice/src/game/board/card_item.cpp rename to cockatrice/src/game_graphics/board/card_item.cpp index 029822805..cabe988c2 100644 --- a/cockatrice/src/game/board/card_item.cpp +++ b/cockatrice/src/game_graphics/board/card_item.cpp @@ -1,14 +1,14 @@ #include "card_item.h" #include "../../client/settings/cache_settings.h" -#include "../../game_graphics/zones/table_zone.h" -#include "../../game_graphics/zones/view_zone.h" +#include "../../game/phase.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../game_scene.h" -#include "../phase.h" -#include "../player/player_actions.h" -#include "../player/player_logic.h" -#include "../zones/view_zone_logic.h" +#include "../zones/table_zone.h" +#include "../zones/view_zone.h" #include "arrow_item.h" #include "card_drag_item.h" @@ -482,8 +482,7 @@ void CardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) (!SettingsCache::instance().getDoubleClickToPlay())) { handleClickedToPlay(event->modifiers().testFlag(Qt::ShiftModifier)); } - - if (owner != nullptr) { // cards without owner will be deleted + if (owner != nullptr) { setCursor(Qt::OpenHandCursor); } AbstractCardItem::mouseReleaseEvent(event); @@ -539,4 +538,4 @@ QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value) } return AbstractCardItem::itemChange(change, value); -} +} \ No newline at end of file diff --git a/cockatrice/src/game/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h similarity index 98% rename from cockatrice/src/game/board/card_item.h rename to cockatrice/src/game_graphics/board/card_item.h index 87f9667de..8efcd085d 100644 --- a/cockatrice/src/game/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -7,9 +7,9 @@ #ifndef CARDITEM_H #define CARDITEM_H -#include "../zones/card_zone_logic.h" +#include "../../game/board/card_state.h" +#include "../../game/zones/card_zone_logic.h" #include "abstract_card_item.h" -#include "card_state.h" #include #include diff --git a/cockatrice/src/game/board/counter_general.cpp b/cockatrice/src/game_graphics/board/counter_general.cpp similarity index 95% rename from cockatrice/src/game/board/counter_general.cpp rename to cockatrice/src/game_graphics/board/counter_general.cpp index 5147ede6b..379c6f837 100644 --- a/cockatrice/src/game/board/counter_general.cpp +++ b/cockatrice/src/game_graphics/board/counter_general.cpp @@ -1,7 +1,7 @@ #include "counter_general.h" -#include "../../game_graphics/board/abstract_graphics_item.h" #include "../../interface/pixel_map_generator.h" +#include "abstract_graphics_item.h" #include diff --git a/cockatrice/src/game/board/counter_general.h b/cockatrice/src/game_graphics/board/counter_general.h similarity index 100% rename from cockatrice/src/game/board/counter_general.h rename to cockatrice/src/game_graphics/board/counter_general.h diff --git a/cockatrice/src/game/board/translate_counter_name.cpp b/cockatrice/src/game_graphics/board/translate_counter_name.cpp similarity index 100% rename from cockatrice/src/game/board/translate_counter_name.cpp rename to cockatrice/src/game_graphics/board/translate_counter_name.cpp diff --git a/cockatrice/src/game/board/translate_counter_name.h b/cockatrice/src/game_graphics/board/translate_counter_name.h similarity index 100% rename from cockatrice/src/game/board/translate_counter_name.h rename to cockatrice/src/game_graphics/board/translate_counter_name.h diff --git a/cockatrice/src/game/card_dimensions.h b/cockatrice/src/game_graphics/card_dimensions.h similarity index 100% rename from cockatrice/src/game/card_dimensions.h rename to cockatrice/src/game_graphics/card_dimensions.h diff --git a/cockatrice/src/game/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp similarity index 100% rename from cockatrice/src/game/deckview/deck_view.cpp rename to cockatrice/src/game_graphics/deckview/deck_view.cpp diff --git a/cockatrice/src/game/deckview/deck_view.h b/cockatrice/src/game_graphics/deckview/deck_view.h similarity index 100% rename from cockatrice/src/game/deckview/deck_view.h rename to cockatrice/src/game_graphics/deckview/deck_view.h diff --git a/cockatrice/src/game/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp similarity index 100% rename from cockatrice/src/game/deckview/deck_view_container.cpp rename to cockatrice/src/game_graphics/deckview/deck_view_container.cpp diff --git a/cockatrice/src/game/deckview/deck_view_container.h b/cockatrice/src/game_graphics/deckview/deck_view_container.h similarity index 100% rename from cockatrice/src/game/deckview/deck_view_container.h rename to cockatrice/src/game_graphics/deckview/deck_view_container.h diff --git a/cockatrice/src/game/deckview/tabbed_deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp similarity index 100% rename from cockatrice/src/game/deckview/tabbed_deck_view_container.cpp rename to cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp diff --git a/cockatrice/src/game/deckview/tabbed_deck_view_container.h b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.h similarity index 100% rename from cockatrice/src/game/deckview/tabbed_deck_view_container.h rename to cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.h diff --git a/cockatrice/src/game/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp similarity index 100% rename from cockatrice/src/game/dialogs/dlg_create_token.cpp rename to cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp diff --git a/cockatrice/src/game/dialogs/dlg_create_token.h b/cockatrice/src/game_graphics/dialogs/dlg_create_token.h similarity index 100% rename from cockatrice/src/game/dialogs/dlg_create_token.h rename to cockatrice/src/game_graphics/dialogs/dlg_create_token.h diff --git a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp b/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.cpp similarity index 100% rename from cockatrice/src/game/dialogs/dlg_move_top_cards_until.cpp rename to cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.cpp diff --git a/cockatrice/src/game/dialogs/dlg_move_top_cards_until.h b/cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h similarity index 100% rename from cockatrice/src/game/dialogs/dlg_move_top_cards_until.h rename to cockatrice/src/game_graphics/dialogs/dlg_move_top_cards_until.h diff --git a/cockatrice/src/game/dialogs/dlg_roll_dice.cpp b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp similarity index 100% rename from cockatrice/src/game/dialogs/dlg_roll_dice.cpp rename to cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp diff --git a/cockatrice/src/game/dialogs/dlg_roll_dice.h b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.h similarity index 100% rename from cockatrice/src/game/dialogs/dlg_roll_dice.h rename to cockatrice/src/game_graphics/dialogs/dlg_roll_dice.h diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp similarity index 98% rename from cockatrice/src/game/game_scene.cpp rename to cockatrice/src/game_graphics/game_scene.cpp index dc55ecfe9..b9816a602 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -1,15 +1,17 @@ #include "game_scene.h" #include "../client/settings/cache_settings.h" -#include "../game_graphics/zones/select_zone.h" -#include "../game_graphics/zones/view_zone.h" -#include "../game_graphics/zones/view_zone_widget.h" -#include "abstract_game.h" +#include "../game/abstract_game.h" +#include "../game/player/player_actions.h" +#include "../game/player/player_logic.h" +#include "../game_graphics/player/player_graphics_item.h" #include "board/card_item.h" #include "phases_toolbar.h" -#include "player/player_actions.h" +#include "player/menu/player_menu.h" #include "player/player_graphics_item.h" -#include "player/player_logic.h" +#include "zones/select_zone.h" +#include "zones/view_zone.h" +#include "zones/view_zone_widget.h" #include #include diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game_graphics/game_scene.h similarity index 98% rename from cockatrice/src/game/game_scene.h rename to cockatrice/src/game_graphics/game_scene.h index 0587566d0..74e979556 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -1,10 +1,10 @@ #ifndef GAMESCENE_H #define GAMESCENE_H -#include "arrow_registry.h" -#include "board/arrow_data.h" +#include "../game/arrow_registry.h" +#include "../game/board/arrow_data.h" +#include "../game/zones/card_zone_logic.h" #include "board/arrow_item.h" -#include "zones/card_zone_logic.h" #include #include diff --git a/cockatrice/src/game/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp similarity index 100% rename from cockatrice/src/game/game_view.cpp rename to cockatrice/src/game_graphics/game_view.cpp diff --git a/cockatrice/src/game/game_view.h b/cockatrice/src/game_graphics/game_view.h similarity index 100% rename from cockatrice/src/game/game_view.h rename to cockatrice/src/game_graphics/game_view.h diff --git a/cockatrice/src/game/hand_counter.cpp b/cockatrice/src/game_graphics/hand_counter.cpp similarity index 96% rename from cockatrice/src/game/hand_counter.cpp rename to cockatrice/src/game_graphics/hand_counter.cpp index a853ae2de..35989ff38 100644 --- a/cockatrice/src/game/hand_counter.cpp +++ b/cockatrice/src/game_graphics/hand_counter.cpp @@ -1,6 +1,6 @@ #include "hand_counter.h" -#include "../game_graphics/zones/card_zone.h" +#include "zones/card_zone.h" #include #include diff --git a/cockatrice/src/game/hand_counter.h b/cockatrice/src/game_graphics/hand_counter.h similarity index 87% rename from cockatrice/src/game/hand_counter.h rename to cockatrice/src/game_graphics/hand_counter.h index 41ab3b5b2..9aa65d514 100644 --- a/cockatrice/src/game/hand_counter.h +++ b/cockatrice/src/game_graphics/hand_counter.h @@ -7,8 +7,8 @@ #ifndef HANDCOUNTER_H #define HANDCOUNTER_H -#include "../game_graphics/board/abstract_graphics_item.h" -#include "../game_graphics/board/graphics_item_type.h" +#include "board/abstract_graphics_item.h" +#include "board/graphics_item_type.h" #include diff --git a/cockatrice/src/game/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp similarity index 99% rename from cockatrice/src/game/log/message_log_widget.cpp rename to cockatrice/src/game_graphics/log/message_log_widget.cpp index 906f15c2e..ccd903b04 100644 --- a/cockatrice/src/game/log/message_log_widget.cpp +++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp @@ -1,13 +1,13 @@ #include "message_log_widget.h" +#include "../../client/settings/card_counter_settings.h" #include "../../client/sound_engine.h" +#include "../../game/phase.h" +#include "../../game/player/player_logic.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/card_item.h" #include "../board/translate_counter_name.h" -#include "../phase.h" -#include "../player/player_logic.h" -#include <../../client/settings/card_counter_settings.h> #include #include #include diff --git a/cockatrice/src/game/log/message_log_widget.h b/cockatrice/src/game_graphics/log/message_log_widget.h similarity index 99% rename from cockatrice/src/game/log/message_log_widget.h rename to cockatrice/src/game_graphics/log/message_log_widget.h index 9f1990ac4..a145d358d 100644 --- a/cockatrice/src/game/log/message_log_widget.h +++ b/cockatrice/src/game_graphics/log/message_log_widget.h @@ -7,8 +7,8 @@ #ifndef MESSAGELOGWIDGET_H #define MESSAGELOGWIDGET_H +#include "../../game/zones/card_zone_logic.h" #include "../../interface/widgets/server/chat_view/chat_view.h" -#include "../zones/card_zone_logic.h" class AbstractGame; class CardItem; diff --git a/cockatrice/src/game/phases_toolbar.cpp b/cockatrice/src/game_graphics/phases_toolbar.cpp similarity index 100% rename from cockatrice/src/game/phases_toolbar.cpp rename to cockatrice/src/game_graphics/phases_toolbar.cpp diff --git a/cockatrice/src/game/phases_toolbar.h b/cockatrice/src/game_graphics/phases_toolbar.h similarity index 97% rename from cockatrice/src/game/phases_toolbar.h rename to cockatrice/src/game_graphics/phases_toolbar.h index 6f0931d61..39884ef75 100644 --- a/cockatrice/src/game/phases_toolbar.h +++ b/cockatrice/src/game_graphics/phases_toolbar.h @@ -8,7 +8,7 @@ #ifndef PHASESTOOLBAR_H #define PHASESTOOLBAR_H -#include "../game_graphics/board/abstract_graphics_item.h" +#include "board/abstract_graphics_item.h" #include #include diff --git a/cockatrice/src/game/player/card_menu_action_type.h b/cockatrice/src/game_graphics/player/card_menu_action_type.h similarity index 100% rename from cockatrice/src/game/player/card_menu_action_type.h rename to cockatrice/src/game_graphics/player/card_menu_action_type.h diff --git a/cockatrice/src/game/player/menu/abstract_player_component.h b/cockatrice/src/game_graphics/player/menu/abstract_player_component.h similarity index 100% rename from cockatrice/src/game/player/menu/abstract_player_component.h rename to cockatrice/src/game_graphics/player/menu/abstract_player_component.h diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game_graphics/player/menu/card_menu.cpp similarity index 99% rename from cockatrice/src/game/player/menu/card_menu.cpp rename to cockatrice/src/game_graphics/player/menu/card_menu.cpp index c1c33e37d..aa94c3be7 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/card_menu.cpp @@ -3,11 +3,11 @@ #include "../../../client/settings/card_counter_settings.h" #include "../../../interface/widgets/tabs/tab_game.h" #include "../../board/card_item.h" -#include "../../zones/view_zone_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../../game/zones/view_zone_logic.h" #include "../card_menu_action_type.h" -#include "../player_actions.h" #include "../player_graphics_item.h" -#include "../player_logic.h" #include "move_menu.h" #include "pt_menu.h" diff --git a/cockatrice/src/game/player/menu/card_menu.h b/cockatrice/src/game_graphics/player/menu/card_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/card_menu.h rename to cockatrice/src/game_graphics/player/menu/card_menu.h diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.cpp b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp similarity index 93% rename from cockatrice/src/game/player/menu/custom_zone_menu.cpp rename to cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp index 106e646d9..743746cc8 100644 --- a/cockatrice/src/game/player/menu/custom_zone_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.cpp @@ -1,6 +1,7 @@ #include "custom_zone_menu.h" -#include "../player_logic.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" CustomZoneMenu::CustomZoneMenu(PlayerGraphicsItem *_player) : player(_player) { diff --git a/cockatrice/src/game/player/menu/custom_zone_menu.h b/cockatrice/src/game_graphics/player/menu/custom_zone_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/custom_zone_menu.h rename to cockatrice/src/game_graphics/player/menu/custom_zone_menu.h diff --git a/cockatrice/src/game/player/menu/grave_menu.cpp b/cockatrice/src/game_graphics/player/menu/grave_menu.cpp similarity index 96% rename from cockatrice/src/game/player/menu/grave_menu.cpp rename to cockatrice/src/game_graphics/player/menu/grave_menu.cpp index 45762e900..698481f7a 100644 --- a/cockatrice/src/game/player/menu/grave_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/grave_menu.cpp @@ -1,8 +1,9 @@ #include "grave_menu.h" -#include "../../abstract_game.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include #include diff --git a/cockatrice/src/game/player/menu/grave_menu.h b/cockatrice/src/game_graphics/player/menu/grave_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/grave_menu.h rename to cockatrice/src/game_graphics/player/menu/grave_menu.h diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp similarity index 98% rename from cockatrice/src/game/player/menu/hand_menu.cpp rename to cockatrice/src/game_graphics/player/menu/hand_menu.cpp index 64a8c5754..ba0702f07 100644 --- a/cockatrice/src/game/player/menu/hand_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/hand_menu.cpp @@ -3,10 +3,10 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../../../game_graphics/zones/hand_zone.h" -#include "../../abstract_game.h" -#include "../player_actions.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../player_graphics_item.h" -#include "../player_logic.h" #include #include diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game_graphics/player/menu/hand_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/hand_menu.h rename to cockatrice/src/game_graphics/player/menu/hand_menu.h diff --git a/cockatrice/src/game/player/menu/library_menu.cpp b/cockatrice/src/game_graphics/player/menu/library_menu.cpp similarity index 99% rename from cockatrice/src/game/player/menu/library_menu.cpp rename to cockatrice/src/game_graphics/player/menu/library_menu.cpp index 00ab4592f..4c15e09ec 100644 --- a/cockatrice/src/game/player/menu/library_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/library_menu.cpp @@ -3,9 +3,10 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../../../interface/widgets/tabs/tab_game.h" -#include "../../abstract_game.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/abstract_game.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include #include diff --git a/cockatrice/src/game/player/menu/library_menu.h b/cockatrice/src/game_graphics/player/menu/library_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/library_menu.h rename to cockatrice/src/game_graphics/player/menu/library_menu.h diff --git a/cockatrice/src/game/player/menu/move_menu.cpp b/cockatrice/src/game_graphics/player/menu/move_menu.cpp similarity index 96% rename from cockatrice/src/game/player/menu/move_menu.cpp rename to cockatrice/src/game_graphics/player/menu/move_menu.cpp index 9997aecf3..5b7209a9f 100644 --- a/cockatrice/src/game/player/menu/move_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/move_menu.cpp @@ -1,8 +1,9 @@ #include "move_menu.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" #include "../card_menu_action_type.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../player_graphics_item.h" MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to")) { diff --git a/cockatrice/src/game/player/menu/move_menu.h b/cockatrice/src/game_graphics/player/menu/move_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/move_menu.h rename to cockatrice/src/game_graphics/player/menu/move_menu.h diff --git a/cockatrice/src/game/player/menu/player_menu.cpp b/cockatrice/src/game_graphics/player/menu/player_menu.cpp similarity index 99% rename from cockatrice/src/game/player/menu/player_menu.cpp rename to cockatrice/src/game_graphics/player/menu/player_menu.cpp index 041b41052..17b791222 100644 --- a/cockatrice/src/game/player/menu/player_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/player_menu.cpp @@ -5,6 +5,7 @@ #include "../../../game_graphics/zones/table_zone.h" #include "../../../interface/widgets/tabs/tab_game.h" #include "../../board/card_item.h" +#include "../player_graphics_item.h" #include "card_menu.h" #include "hand_menu.h" diff --git a/cockatrice/src/game/player/menu/player_menu.h b/cockatrice/src/game_graphics/player/menu/player_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/player_menu.h rename to cockatrice/src/game_graphics/player/menu/player_menu.h diff --git a/cockatrice/src/game/player/menu/pt_menu.cpp b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp similarity index 96% rename from cockatrice/src/game/player/menu/pt_menu.cpp rename to cockatrice/src/game_graphics/player/menu/pt_menu.cpp index 011271385..a01be9424 100644 --- a/cockatrice/src/game/player/menu/pt_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/pt_menu.cpp @@ -1,7 +1,8 @@ #include "pt_menu.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness")) { diff --git a/cockatrice/src/game/player/menu/pt_menu.h b/cockatrice/src/game_graphics/player/menu/pt_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/pt_menu.h rename to cockatrice/src/game_graphics/player/menu/pt_menu.h diff --git a/cockatrice/src/game/player/menu/rfg_menu.cpp b/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp similarity index 95% rename from cockatrice/src/game/player/menu/rfg_menu.cpp rename to cockatrice/src/game_graphics/player/menu/rfg_menu.cpp index 79fdebf48..45abadbf7 100644 --- a/cockatrice/src/game/player/menu/rfg_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/rfg_menu.cpp @@ -1,7 +1,8 @@ #include "rfg_menu.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include diff --git a/cockatrice/src/game/player/menu/rfg_menu.h b/cockatrice/src/game_graphics/player/menu/rfg_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/rfg_menu.h rename to cockatrice/src/game_graphics/player/menu/rfg_menu.h diff --git a/cockatrice/src/game/player/menu/say_menu.cpp b/cockatrice/src/game_graphics/player/menu/say_menu.cpp similarity index 91% rename from cockatrice/src/game/player/menu/say_menu.cpp rename to cockatrice/src/game_graphics/player/menu/say_menu.cpp index 58bbd33aa..336b70f0d 100644 --- a/cockatrice/src/game/player/menu/say_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/say_menu.cpp @@ -1,8 +1,9 @@ #include "say_menu.h" #include "../../../client/settings/cache_settings.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player) { diff --git a/cockatrice/src/game/player/menu/say_menu.h b/cockatrice/src/game_graphics/player/menu/say_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/say_menu.h rename to cockatrice/src/game_graphics/player/menu/say_menu.h diff --git a/cockatrice/src/game/player/menu/sideboard_menu.cpp b/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp similarity index 87% rename from cockatrice/src/game/player/menu/sideboard_menu.cpp rename to cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp index 27b50b570..0dd7894d2 100644 --- a/cockatrice/src/game/player/menu/sideboard_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/sideboard_menu.cpp @@ -1,7 +1,8 @@ #include "sideboard_menu.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" SideboardMenu::SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu) : QMenu(playerMenu) { diff --git a/cockatrice/src/game/player/menu/sideboard_menu.h b/cockatrice/src/game_graphics/player/menu/sideboard_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/sideboard_menu.h rename to cockatrice/src/game_graphics/player/menu/sideboard_menu.h diff --git a/cockatrice/src/game/player/menu/utility_menu.cpp b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp similarity index 97% rename from cockatrice/src/game/player/menu/utility_menu.cpp rename to cockatrice/src/game_graphics/player/menu/utility_menu.cpp index 9769a029e..61a822b21 100644 --- a/cockatrice/src/game/player/menu/utility_menu.cpp +++ b/cockatrice/src/game_graphics/player/menu/utility_menu.cpp @@ -1,8 +1,9 @@ #include "utility_menu.h" #include "../../../interface/deck_loader/deck_loader.h" -#include "../player_actions.h" -#include "../player_logic.h" +#include "../../game/player/player_actions.h" +#include "../../game/player/player_logic.h" +#include "../player_graphics_item.h" #include "player_menu.h" #include diff --git a/cockatrice/src/game/player/menu/utility_menu.h b/cockatrice/src/game_graphics/player/menu/utility_menu.h similarity index 100% rename from cockatrice/src/game/player/menu/utility_menu.h rename to cockatrice/src/game_graphics/player/menu/utility_menu.h diff --git a/cockatrice/src/game/player/player_area.cpp b/cockatrice/src/game_graphics/player/player_area.cpp similarity index 100% rename from cockatrice/src/game/player/player_area.cpp rename to cockatrice/src/game_graphics/player/player_area.cpp diff --git a/cockatrice/src/game/player/player_area.h b/cockatrice/src/game_graphics/player/player_area.h similarity index 94% rename from cockatrice/src/game/player/player_area.h rename to cockatrice/src/game_graphics/player/player_area.h index 6ffaf4958..d73547f81 100644 --- a/cockatrice/src/game/player/player_area.h +++ b/cockatrice/src/game_graphics/player/player_area.h @@ -7,7 +7,7 @@ #ifndef COCKATRICE_PLAYER_AREA_H #define COCKATRICE_PLAYER_AREA_H -#include "../../game_graphics/board/graphics_item_type.h" +#include "../board/graphics_item_type.h" #include "QGraphicsItem" /** diff --git a/cockatrice/src/game/player/player_dialogs.cpp b/cockatrice/src/game_graphics/player/player_dialogs.cpp similarity index 100% rename from cockatrice/src/game/player/player_dialogs.cpp rename to cockatrice/src/game_graphics/player/player_dialogs.cpp diff --git a/cockatrice/src/game/player/player_dialogs.h b/cockatrice/src/game_graphics/player/player_dialogs.h similarity index 96% rename from cockatrice/src/game/player/player_dialogs.h rename to cockatrice/src/game_graphics/player/player_dialogs.h index a15c5174f..f87704f2d 100644 --- a/cockatrice/src/game/player/player_dialogs.h +++ b/cockatrice/src/game_graphics/player/player_dialogs.h @@ -1,6 +1,7 @@ #ifndef COCKATRICE_PLAYER_DIALOGS_H #define COCKATRICE_PLAYER_DIALOGS_H -#include "player_actions.h" +#include "../../game/player/player_actions.h" +#include "player_graphics_item.h" #include #include diff --git a/cockatrice/src/game/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp similarity index 97% rename from cockatrice/src/game/player/player_graphics_item.cpp rename to cockatrice/src/game_graphics/player/player_graphics_item.cpp index b0a476d5a..07975ed5e 100644 --- a/cockatrice/src/game/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -1,14 +1,15 @@ #include "player_graphics_item.h" -#include "../../game_graphics/zones/hand_zone.h" -#include "../../game_graphics/zones/pile_zone.h" -#include "../../game_graphics/zones/stack_zone.h" -#include "../../game_graphics/zones/table_zone.h" +#include "../../game/player/player_actions.h" #include "../../interface/widgets/tabs/tab_game.h" #include "../board/abstract_card_item.h" #include "../board/counter_general.h" #include "../hand_counter.h" -#include "player_actions.h" +#include "../zones/hand_zone.h" +#include "../zones/pile_zone.h" +#include "../zones/stack_zone.h" +#include "../zones/table_zone.h" +#include "menu/player_menu.h" #include "player_dialogs.h" #include diff --git a/cockatrice/src/game/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h similarity index 98% rename from cockatrice/src/game/player/player_graphics_item.h rename to cockatrice/src/game_graphics/player/player_graphics_item.h index c1fcb4ed8..0dcc959bd 100644 --- a/cockatrice/src/game/player/player_graphics_item.h +++ b/cockatrice/src/game_graphics/player/player_graphics_item.h @@ -6,15 +6,16 @@ #ifndef COCKATRICE_PLAYER_GRAPHICS_ITEM_H #define COCKATRICE_PLAYER_GRAPHICS_ITEM_H +#include "../../game/player/player_logic.h" #include "../board/abstract_counter.h" #include "../game_scene.h" -#include "player_logic.h" #include class HandZone; class PileZone; class PlayerDialogs; +class PlayerMenu; class PlayerTarget; class StackZone; class TableZone; diff --git a/cockatrice/src/game/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp similarity index 100% rename from cockatrice/src/game/player/player_list_widget.cpp rename to cockatrice/src/game_graphics/player/player_list_widget.cpp diff --git a/cockatrice/src/game/player/player_list_widget.h b/cockatrice/src/game_graphics/player/player_list_widget.h similarity index 97% rename from cockatrice/src/game/player/player_list_widget.h rename to cockatrice/src/game_graphics/player/player_list_widget.h index 842c45873..a53cfa989 100644 --- a/cockatrice/src/game/player/player_list_widget.h +++ b/cockatrice/src/game_graphics/player/player_list_widget.h @@ -7,7 +7,7 @@ #ifndef PLAYERLISTWIDGET_H #define PLAYERLISTWIDGET_H -#include "player_logic.h" +#include "../../game/player/player_logic.h" #include #include diff --git a/cockatrice/src/game/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp similarity index 99% rename from cockatrice/src/game/player/player_target.cpp rename to cockatrice/src/game_graphics/player/player_target.cpp index 97fd51998..567f3d44d 100644 --- a/cockatrice/src/game/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -1,7 +1,7 @@ #include "player_target.h" +#include "../../game/player/player_logic.h" #include "../../interface/pixel_map_generator.h" -#include "player_logic.h" #include #include diff --git a/cockatrice/src/game/player/player_target.h b/cockatrice/src/game_graphics/player/player_target.h similarity index 95% rename from cockatrice/src/game/player/player_target.h rename to cockatrice/src/game_graphics/player/player_target.h index d3facc60d..67e155660 100644 --- a/cockatrice/src/game/player/player_target.h +++ b/cockatrice/src/game_graphics/player/player_target.h @@ -7,9 +7,9 @@ #ifndef PLAYERTARGET_H #define PLAYERTARGET_H -#include "../../game_graphics/board/graphics_item_type.h" #include "../board/abstract_counter.h" #include "../board/arrow_target.h" +#include "../board/graphics_item_type.h" #include diff --git a/cockatrice/src/game/z_value_layer_manager.h b/cockatrice/src/game_graphics/z_value_layer_manager.h similarity index 100% rename from cockatrice/src/game/z_value_layer_manager.h rename to cockatrice/src/game_graphics/z_value_layer_manager.h diff --git a/cockatrice/src/game/z_values.h b/cockatrice/src/game_graphics/z_values.h similarity index 100% rename from cockatrice/src/game/z_values.h rename to cockatrice/src/game_graphics/z_values.h diff --git a/cockatrice/src/game_graphics/zones/card_zone.cpp b/cockatrice/src/game_graphics/zones/card_zone.cpp index 6ba8abe42..3457b681e 100644 --- a/cockatrice/src/game_graphics/zones/card_zone.cpp +++ b/cockatrice/src/game_graphics/zones/card_zone.cpp @@ -1,6 +1,6 @@ #include "card_zone.h" -#include "../../game/board/card_item.h" +#include "../board/card_item.h" #include "view_zone.h" #include diff --git a/cockatrice/src/game_graphics/zones/hand_zone.cpp b/cockatrice/src/game_graphics/zones/hand_zone.cpp index 09e9a5091..5885e3630 100644 --- a/cockatrice/src/game_graphics/zones/hand_zone.cpp +++ b/cockatrice/src/game_graphics/zones/hand_zone.cpp @@ -1,11 +1,11 @@ #include "hand_zone.h" #include "../../client/settings/cache_settings.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../interface/theme_manager.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/pile_zone.cpp b/cockatrice/src/game_graphics/zones/pile_zone.cpp index 302b983d8..7bb0e695a 100644 --- a/cockatrice/src/game_graphics/zones/pile_zone.cpp +++ b/cockatrice/src/game_graphics/zones/pile_zone.cpp @@ -1,10 +1,10 @@ #include "pile_zone.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../game/zones/pile_zone_logic.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" #include "view_zone.h" #include diff --git a/cockatrice/src/game_graphics/zones/select_zone.cpp b/cockatrice/src/game_graphics/zones/select_zone.cpp index 90d53b464..f2e720686 100644 --- a/cockatrice/src/game_graphics/zones/select_zone.cpp +++ b/cockatrice/src/game_graphics/zones/select_zone.cpp @@ -1,8 +1,8 @@ #include "select_zone.h" #include "../../client/settings/cache_settings.h" -#include "../../game/board/card_item.h" -#include "../../game/game_scene.h" +#include "../board/card_item.h" +#include "../game_scene.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/stack_zone.cpp b/cockatrice/src/game_graphics/zones/stack_zone.cpp index 9b0545b1d..46ff099ab 100644 --- a/cockatrice/src/game_graphics/zones/stack_zone.cpp +++ b/cockatrice/src/game_graphics/zones/stack_zone.cpp @@ -1,12 +1,12 @@ #include "stack_zone.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" -#include "../../game/card_dimensions.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../game/zones/stack_zone_logic.h" #include "../../interface/theme_manager.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" +#include "../card_dimensions.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/table_zone.cpp b/cockatrice/src/game_graphics/zones/table_zone.cpp index 245de8281..e886f62e9 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.cpp +++ b/cockatrice/src/game_graphics/zones/table_zone.cpp @@ -1,14 +1,14 @@ #include "table_zone.h" #include "../../client/settings/cache_settings.h" -#include "../../game/board/arrow_item.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" -#include "../../game/z_values.h" #include "../../game/zones/table_zone_logic.h" #include "../../interface/theme_manager.h" +#include "../board/arrow_item.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" +#include "../z_values.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/table_zone.h b/cockatrice/src/game_graphics/zones/table_zone.h index 8a898173b..0d7e58206 100644 --- a/cockatrice/src/game_graphics/zones/table_zone.h +++ b/cockatrice/src/game_graphics/zones/table_zone.h @@ -7,8 +7,8 @@ #ifndef TABLEZONE_H #define TABLEZONE_H -#include "../../game/board/abstract_card_item.h" #include "../../game/zones/table_zone_logic.h" +#include "../board/abstract_card_item.h" #include "select_zone.h" /** diff --git a/cockatrice/src/game_graphics/zones/view_zone.cpp b/cockatrice/src/game_graphics/zones/view_zone.cpp index 805c60638..baf7b8b30 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone.cpp @@ -1,10 +1,10 @@ #include "view_zone.h" -#include "../../game/board/card_drag_item.h" -#include "../../game/board/card_item.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" #include "../../game/zones/view_zone_logic.h" +#include "../board/card_drag_item.h" +#include "../board/card_item.h" #include #include diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 03c6d8925..4a5d064d0 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -2,12 +2,12 @@ #include "../../client/settings/cache_settings.h" #include "../../filters/syntax_help.h" -#include "../../game/board/card_item.h" -#include "../../game/game_scene.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" -#include "../../game/z_values.h" #include "../../interface/pixel_map_generator.h" +#include "../board/card_item.h" +#include "../game_scene.h" +#include "../z_values.h" #include "view_zone.h" #include diff --git a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp index 509a2d92f..577dafe0a 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp @@ -1,6 +1,6 @@ #include "card_info_display_widget.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include "card_info_picture_widget.h" #include "card_info_text_widget.h" diff --git a/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp index 21bee8f54..2e7c62461 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_frame_widget.cpp @@ -1,7 +1,7 @@ #include "card_info_frame_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include "card_info_display_widget.h" #include "card_info_picture_widget.h" #include "card_info_text_widget.h" diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 66ec1c197..3bfd9ce7d 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -1,7 +1,7 @@ #include "card_info_picture_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/widgets/tabs/tab_supervisor.h" #include "../../window_main.h" diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp index 345eb9909..c6af5320b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp @@ -1,6 +1,6 @@ #include "card_info_text_widget.h" -#include "../../../game/board/card_item.h" +#include "../../../game_graphics/board/card_item.h" #include #include diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 1e2bebd15..a81161e83 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -1,19 +1,21 @@ #include "tab_game.h" #include "../../../client/settings/cache_settings.h" -#include "../../../game/player/menu/card_menu.h" -#include "../game/board/arrow_item.h" -#include "../game/board/card_item.h" -#include "../game/deckview/deck_view_container.h" -#include "../game/deckview/tabbed_deck_view_container.h" #include "../game/game.h" -#include "../game/game_scene.h" -#include "../game/game_view.h" -#include "../game/log/message_log_widget.h" -#include "../game/phases_toolbar.h" -#include "../game/player/player_list_widget.h" #include "../game/player/player_logic.h" #include "../game/replay.h" +#include "../game_graphics/board/arrow_item.h" +#include "../game_graphics/board/card_item.h" +#include "../game_graphics/deckview/deck_view_container.h" +#include "../game_graphics/deckview/tabbed_deck_view_container.h" +#include "../game_graphics/game_scene.h" +#include "../game_graphics/game_view.h" +#include "../game_graphics/log/message_log_widget.h" +#include "../game_graphics/phases_toolbar.h" +#include "../game_graphics/player/menu/card_menu.h" +#include "../game_graphics/player/menu/player_menu.h" +#include "../game_graphics/player/player_graphics_item.h" +#include "../game_graphics/player/player_list_widget.h" #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" @@ -48,7 +50,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) { // THIS CTOR IS USED ON REPLAY - game = new Replay(this, _replay); + game = new Replay(this, _replay, tabSupervisor->getIsLocalGame()); createCardInfoDock(true); createPlayerListDock(true); @@ -92,7 +94,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, : Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager()) { // THIS CTOR IS USED ON GAMES - game = new Game(this, _clients, event, _roomGameTypes); + game = new Game(this, tabSupervisor->getIsLocalGame(), _clients, event, _roomGameTypes); createCardInfoDock(); createPlayerListDock(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index ddda4d9b9..b9289432d 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -10,8 +10,8 @@ #define TAB_GAME_H #include "../game/abstract_game.h" -#include "../game/log/message_log_widget.h" #include "../game/player/player_logic.h" +#include "../game_graphics/log/message_log_widget.h" #include "../interface/widgets/menus/tearoff_menu.h" #include "../interface/widgets/replay/replay_manager.h" #include "tab.h" @@ -20,6 +20,7 @@ #include #include +class CardMenu; class ServerInfo_PlayerProperties; class TabbedDeckViewContainer; inline Q_LOGGING_CATEGORY(TabGameLog, "tab_game"); From f72c82d0f9a33828ad9bbaeeb0a91ed8ebe14ffc Mon Sep 17 00:00:00 2001 From: kongwu <167565490+kongwu666@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:46:43 +0800 Subject: [PATCH 20/50] [DeckEditor] Replace mainboard/sideboard with tokensboard for tokens (#6971) * [DeckEditor] Replace mainboard/sideboard with tokensboard for token cards (#6546) * [PrintingSelector] Replace std::tuple with ZoneCounts struct for readability (#6546) --- .../deck_editor/deck_state_manager.cpp | 4 ++ .../all_zones_card_amount_widget.cpp | 37 ++++++++++++-- .../all_zones_card_amount_widget.h | 5 +- .../printing_selector/card_amount_widget.cpp | 48 ++++++++++++++++--- .../printing_selector/card_amount_widget.h | 2 + .../printing_selector/printing_selector.cpp | 17 +++++-- .../printing_selector/printing_selector.h | 11 ++++- .../printing_selector_card_display_widget.cpp | 6 +-- .../printing_selector_card_display_widget.h | 2 +- .../printing_selector_card_overlay_widget.cpp | 10 ++-- .../printing_selector_card_overlay_widget.h | 2 +- 11 files changed, 115 insertions(+), 29 deletions(-) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index 6db8e5623..f8fb450ce 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -255,6 +255,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) } QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + // tokens have no swap target + if (zoneName == DECK_ZONE_TOKENS) { + return false; + } QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; QString reason = tr("Moved to %1 1 × \"%2\" (%3)") // diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp index 36bccbcc3..05e269174 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp @@ -8,7 +8,7 @@ * @brief Constructor for the AllZonesCardAmountWidget class. * * Initializes the widget with its layout and sets up the connections and necessary - * UI elements for managing card counts in both the mainboard and sideboard zones. + * UI elements for managing card counts in all the mainboard, tokensboard and sideboard zones. * * @param parent The parent widget. * @param deckStateManager Pointer to the DeckStateManager @@ -31,13 +31,28 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, buttonBoxMainboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_MAIN); zoneLabelSideboard = new ShadowBackgroundLabel(this, tr("Sideboard")); buttonBoxSideboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_SIDE); + zoneLabelTokensboard = new ShadowBackgroundLabel(this, tr("Tokens")); + buttonBoxTokensboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_TOKENS); layout->addWidget(zoneLabelMainboard, 0, Qt::AlignHCenter | Qt::AlignBottom); layout->addWidget(buttonBoxMainboard, 0, Qt::AlignHCenter | Qt::AlignTop); - layout->addSpacing(25); + layout->addSpacing(12); + layout->addWidget(zoneLabelTokensboard, 0, Qt::AlignHCenter | Qt::AlignBottom); + layout->addWidget(buttonBoxTokensboard, 0, Qt::AlignHCenter | Qt::AlignTop); + layout->addSpacing(13); layout->addWidget(zoneLabelSideboard, 0, Qt::AlignHCenter | Qt::AlignBottom); layout->addWidget(buttonBoxSideboard, 0, Qt::AlignHCenter | Qt::AlignTop); + // Show Tokens buttons for token cards, Mainboard/Sideboard for non-token cards + bool isToken = rootCard.getInfo().getIsToken(); + + zoneLabelMainboard->setVisible(!isToken); + buttonBoxMainboard->setVisible(!isToken); + zoneLabelTokensboard->setVisible(isToken); + buttonBoxTokensboard->setVisible(isToken); + zoneLabelSideboard->setVisible(!isToken); + buttonBoxSideboard->setVisible(!isToken); + connect(cardSizeSlider, &QSlider::valueChanged, this, &AllZonesCardAmountWidget::adjustFontSize); QTimer::singleShot(10, this, [this]() { adjustFontSize(this->cardSizeSlider->value()); }); @@ -67,15 +82,17 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) zoneLabelFont.setPointSize(newFontSize); zoneLabelMainboard->setFont(zoneLabelFont); zoneLabelSideboard->setFont(zoneLabelFont); + zoneLabelTokensboard->setFont(zoneLabelFont); // Repaint the widget (if necessary) repaint(); } -void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount) +void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount) { buttonBoxMainboard->setAmount(mainboardAmount); buttonBoxSideboard->setAmount(sideboardAmount); + buttonBoxTokensboard->setAmount(tokensboardAmount); } /** @@ -99,11 +116,21 @@ int AllZonesCardAmountWidget::getSideboardAmount() } /** - * @brief Checks if the amount is at least one in either the mainboard or sideboard. + * @brief Gets the card count in the tokensboard zone. + * + * @return The number of cards in the tokensboard. + */ +int AllZonesCardAmountWidget::getTokensboardAmount() +{ + return buttonBoxTokensboard->getAmount(); +} + +/** + * @brief Checks if the amount is at least one in either the mainboard or sideboard or tokensboard. */ bool AllZonesCardAmountWidget::isNonZero() { - return getMainboardAmount() > 0 || getSideboardAmount() > 0; + return getMainboardAmount() > 0 || getSideboardAmount() > 0 || getTokensboardAmount() > 0; } /** diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h index 05047d94f..de4a984be 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h @@ -23,6 +23,7 @@ public: const ExactCard &rootCard); int getMainboardAmount(); int getSideboardAmount(); + int getTokensboardAmount(); bool isNonZero(); #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) @@ -33,7 +34,7 @@ public: public slots: void adjustFontSize(int scalePercentage); - void setAmounts(int mainboardAmount, int sideboardAmount); + void setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount); private: QVBoxLayout *layout; @@ -42,6 +43,8 @@ private: CardAmountWidget *buttonBoxMainboard; QLabel *zoneLabelSideboard; CardAmountWidget *buttonBoxSideboard; + QLabel *zoneLabelTokensboard; + CardAmountWidget *buttonBoxTokensboard; }; #endif // ALL_ZONES_CARD_AMOUNT_WIDGET_H diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp index 25222f437..ff47e7b9c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -11,7 +11,7 @@ * @param parent The parent widget. * @param cardSizeSlider Pointer to the QSlider for adjusting font size. * @param rootCard The root card to manage within the widget. - * @param zoneName The zone name (e.g., DECK_ZONE_MAIN or DECK_ZONE_SIDE). + * @param zoneName The zone name (e.g., DECK_ZONE_MAIN , DECK_ZONE_SIDE, or DECK_ZONE_TOKENS). */ CardAmountWidget::CardAmountWidget(QWidget *parent, DeckStateManager *deckStateManager, @@ -36,13 +36,16 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, incrementButton->setFixedSize(parentWidget()->size().width() / 3, parentWidget()->size().height() / 9); decrementButton->setFixedSize(parentWidget()->size().width() / 3, parentWidget()->size().height() / 9); - // Set up connections based on the zone (Mainboard or Sideboard) + // Set up connections based on the zone (Mainboard, Sideboard, or Tokensboard) if (zoneName == DECK_ZONE_MAIN) { connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingMainboard); connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingMainboard); } else if (zoneName == DECK_ZONE_SIDE) { connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingSideboard); connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingSideboard); + } else if (zoneName == DECK_ZONE_TOKENS) { + connect(incrementButton, &QPushButton::clicked, this, &CardAmountWidget::addPrintingTokensboard); + connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingTokensboard); } cardCountInZone = new QLabel(QString::number(amount), this); @@ -137,6 +140,19 @@ void CardAmountWidget::updateCardCount() layout->activate(); } +static QString zoneLogName(const QString &zone) +{ + if (zone == DECK_ZONE_MAIN) { + return "mainboard"; + } else if (zone == DECK_ZONE_SIDE) { + return "sideboard"; + } else if (zone == DECK_ZONE_TOKENS) { + return "tokens"; + } else { + return "unknown"; + } +} + static QModelIndex addAndReplacePrintings(DeckListModel *model, const QModelIndex &existing, const ExactCard &rootCard, @@ -161,9 +177,9 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model, } /** - * @brief Adds a printing of the card to the specified zone (Mainboard or Sideboard). + * @brief Adds a printing of the card to the specified zone (Mainboard, Sideboard, or Tokensboard). * - * @param zone The zone to add the card to (DECK_ZONE_MAIN or DECK_ZONE_SIDE). + * @param zone The zone to add the card to (DECK_ZONE_MAIN, DECK_ZONE_SIDE, or DECK_ZONE_TOKENS). */ void CardAmountWidget::addPrinting(const QString &zone) { @@ -183,12 +199,13 @@ void CardAmountWidget::addPrinting(const QString &zone) } } + QString zoneName = zoneLogName(zone); QString reason = QString("Added %1 copies of '%2 (%3) %4' to %5 [ProviderID: %6]%7") .arg(1 + extraCopies) .arg(rootCard.getName()) .arg(rootCard.getPrinting().getSet()->getShortName()) .arg(rootCard.getPrinting().getProperty("num")) - .arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard") + .arg(zoneName) .arg(rootCard.getPrinting().getUuid()) .arg(replacingProviderless ? " (replaced providerless printings)" : ""); @@ -218,6 +235,14 @@ void CardAmountWidget::addPrintingSideboard() addPrinting(DECK_ZONE_SIDE); } +/** + * @brief Adds a printing to the tokens zone. + */ +void CardAmountWidget::addPrintingTokensboard() +{ + addPrinting(DECK_ZONE_TOKENS); +} + /** * @brief Removes a printing from the mainboard zone. */ @@ -234,18 +259,27 @@ void CardAmountWidget::removePrintingSideboard() decrementCardHelper(DECK_ZONE_SIDE); } +/** + * @brief Removes a printing from the tokens zone. + */ +void CardAmountWidget::removePrintingTokensboard() +{ + decrementCardHelper(DECK_ZONE_TOKENS); +} + /** * @brief Helper function to decrement the card count for a given zone. * - * @param zone The zone from which to remove the card (DECK_ZONE_MAIN or DECK_ZONE_SIDE). + * @param zone The zone from which to remove the card (DECK_ZONE_MAIN, DECK_ZONE_SIDE, or DECK_ZONE_TOKENS). */ void CardAmountWidget::decrementCardHelper(const QString &zone) { + QString zoneName = zoneLogName(zone); QString reason = QString("Removed 1 copy of '%1 (%2) %3' from %4 [ProviderID: %5]") .arg(rootCard.getName()) .arg(rootCard.getPrinting().getSet()->getShortName()) .arg(rootCard.getPrinting().getProperty("num")) - .arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard") + .arg(zoneName) .arg(rootCard.getPrinting().getUuid()); deckStateManager->modifyDeck(reason, [this, &zone](auto model) { diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h index f0f2128f0..2780e3ad2 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -60,8 +60,10 @@ private: private slots: void addPrintingMainboard(); void addPrintingSideboard(); + void addPrintingTokensboard(); void removePrintingMainboard(); void removePrintingSideboard(); + void removePrintingTokensboard(); void adjustFontSize(int scalePercentage); }; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index 71b93b297..76a416587 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -105,23 +105,30 @@ void PrintingSelector::printingsInDeckChanged() } /** - * @return A map of uuid to amounts (main, side). + * @return A map of uuid to amounts (main, side, tokens). */ -static QMap> tallyUuidCounts(const DeckListModel *model, const QString &cardName) +static QMap tallyUuidCounts(const DeckListModel *model, const QString &cardName) { - QMap> map; + QMap map; auto mainNodes = model->getCardNodesForZone(DECK_ZONE_MAIN); for (auto &node : mainNodes) { if (node->getName() == cardName) { - map[node->getCardProviderId()].first += node->getNumber(); + map[node->getCardProviderId()].mainboard += node->getNumber(); } } auto sideNodes = model->getCardNodesForZone(DECK_ZONE_SIDE); for (auto &node : sideNodes) { if (node->getName() == cardName) { - map[node->getCardProviderId()].second += node->getNumber(); + map[node->getCardProviderId()].sideboard += node->getNumber(); + } + } + + auto tokensNodes = model->getCardNodesForZone(DECK_ZONE_TOKENS); + for (auto &node : tokensNodes) { + if (node->getName() == cardName) { + map[node->getCardProviderId()].tokensboard += node->getNumber(); } } diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index b9e6723f2..14d73f836 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -22,6 +22,13 @@ #define BATCH_SIZE 10 +struct ZoneCounts +{ + int mainboard = 0; + int sideboard = 0; + int tokensboard = 0; +}; + class DeckStateManager; class PrintingSelectorCardSearchWidget; class PrintingSelectorCardSelectionWidget; @@ -59,9 +66,9 @@ signals: /** * The amounts of the printings in the deck has changed - * @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard) in the deck + * @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard, tokensboard) in the deck */ - void cardAmountsChanged(const QMap> &uuidToAmounts); + void cardAmountsChanged(const QMap &uuidToAmounts); private: QVBoxLayout *layout; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp index 7d0b4882f..edeba86d1 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.cpp @@ -67,10 +67,10 @@ void PrintingSelectorCardDisplayWidget::clampSetNameToPicture() update(); } -void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap> &uuidToAmounts) +void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap &uuidToAmounts) { - auto [main, side] = uuidToAmounts.value(rootCard.getPrinting().getUuid()); - overlayWidget->updateCardAmounts(main, side); + auto counts = uuidToAmounts.value(rootCard.getPrinting().getUuid()); + overlayWidget->updateCardAmounts(counts.mainboard, counts.sideboard, counts.tokensboard); } void PrintingSelectorCardDisplayWidget::resizeEvent(QResizeEvent *event) diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h index b708bd973..4de561f4f 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_display_widget.h @@ -27,7 +27,7 @@ public: public slots: void clampSetNameToPicture(); - void updateCardAmounts(const QMap> &uuidToAmounts); + void updateCardAmounts(const QMap &uuidToAmounts); void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index 69334d6f3..dd5f6dd7f 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -116,9 +116,11 @@ void PrintingSelectorCardOverlayWidget::enterEvent(QEvent *event) updateVisibility(); } -void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount, int sideboardAmount) +void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount, + int sideboardAmount, + int tokensboardAmount) { - allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount); + allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount, tokensboardAmount); updateVisibility(); } @@ -173,8 +175,8 @@ void PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility() /** * @brief Handles the mouse leave event when the cursor leaves the overlay widget area. * - * When the cursor leaves the widget, the card amount widget is hidden if both the mainboard and sideboard - * amounts are zero. + * When the cursor leaves the widget, the card amount widget is hidden if all of the mainboard, sideboard, and + * tokensboard amounts are zero. * * @param event The event triggered when the mouse leaves the widget. */ diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h index 2fdf5ab74..52a43d220 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h @@ -39,7 +39,7 @@ signals: void cardPreferenceChanged(); public slots: - void updateCardAmounts(int mainboardAmount, int sideboardAmount); + void updateCardAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount); private slots: void updateVisibility(); From 6d0a423dcfa842c14463848879f74dfac8280edf Mon Sep 17 00:00:00 2001 From: kongwu <167565490+kongwu666@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:49:29 +0800 Subject: [PATCH 21/50] [Messages] Add option to ignore private messages from non-buddy users (#6966) * [Messages] Add option to ignore private messages from non-buddy users * [Messages] Exclude Moderator/Admin from non-buddy ignore filter Moderator and Admin messages should not be filtered out when the 'Ignore private messages from non-buddies' setting is enabled, to ensure that important warnings from server staff reach users. --- cockatrice/src/client/settings/cache_settings.cpp | 7 +++++++ cockatrice/src/client/settings/cache_settings.h | 7 +++++++ .../widgets/settings_page/messages_settings_page.cpp | 12 +++++++++--- .../widgets/settings_page/messages_settings_page.h | 1 + .../src/interface/widgets/tabs/tab_supervisor.cpp | 6 ++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 64416e5ee..73e5a98a1 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -388,6 +388,7 @@ SettingsCache::SettingsCache() ignoreUnregisteredUsers = settings->value("chat/ignore_unregistered", false).toBool(); ignoreUnregisteredUserMessages = settings->value("chat/ignore_unregistered_messages", false).toBool(); + ignoreNonBuddyUserMessages = settings->value("chat/ignore_nonbuddy_messages", false).toBool(); scaleCards = settings->value("cards/scaleCards", true).toBool(); verticalCardOverlapPercent = settings->value("cards/verticalCardOverlapPercent", 33).toInt(); @@ -1117,6 +1118,12 @@ void SettingsCache::setIgnoreUnregisteredUserMessages(QT_STATE_CHANGED_T _ignore settings->setValue("chat/ignore_unregistered_messages", ignoreUnregisteredUserMessages); } +void SettingsCache::setIgnoreNonBuddyUserMessages(QT_STATE_CHANGED_T _ignoreNonBuddyUserMessages) +{ + ignoreNonBuddyUserMessages = static_cast(_ignoreNonBuddyUserMessages); + settings->setValue("chat/ignore_nonbuddy_messages", ignoreNonBuddyUserMessages); +} + void SettingsCache::setPixmapCacheSize(const int _pixmapCacheSize) { pixmapCacheSize = _pixmapCacheSize; diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index b1197e267..8ee372766 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -183,6 +183,7 @@ signals: void soundThemeChanged(); void ignoreUnregisteredUsersChanged(); void ignoreUnregisteredUserMessagesChanged(); + void ignoreNonBuddyUserMessagesChanged(); void pixmapCacheSizeChanged(int newSizeInMBs); void networkCacheSizeChanged(int newSizeInMBs); void redirectCacheTtlChanged(int newTtl); @@ -294,6 +295,7 @@ private: QString soundThemeName; bool ignoreUnregisteredUsers; bool ignoreUnregisteredUserMessages; + bool ignoreNonBuddyUserMessages; QString picUrl; QString picUrlFallback; QString clientID; @@ -788,6 +790,10 @@ public: { return ignoreUnregisteredUserMessages; } + [[nodiscard]] bool getIgnoreNonBuddyUserMessages() const + { + return ignoreNonBuddyUserMessages; + } [[nodiscard]] int getPixmapCacheSize() const { return pixmapCacheSize; @@ -1111,6 +1117,7 @@ public slots: void setSoundThemeName(const QString &_soundThemeName); void setIgnoreUnregisteredUsers(QT_STATE_CHANGED_T _ignoreUnregisteredUsers); void setIgnoreUnregisteredUserMessages(QT_STATE_CHANGED_T _ignoreUnregisteredUserMessages); + void setIgnoreNonBuddyUserMessages(QT_STATE_CHANGED_T _ignoreNonBuddyUserMessages); void setPixmapCacheSize(const int _pixmapCacheSize); void setCardImageCacheMethod(CardPictureLoaderCacheMethod::CacheMethod _cardImageCachingMethod); void setNetworkCacheSizeInMB(const int _networkCacheSize); diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index f64398fe5..1e6f99245 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -22,10 +22,14 @@ MessagesSettingsPage::MessagesSettingsPage() ignoreUnregUsersMainChat.setChecked(SettingsCache::instance().getIgnoreUnregisteredUsers()); ignoreUnregUserMessages.setChecked(SettingsCache::instance().getIgnoreUnregisteredUserMessages()); + ignoreNonBuddyUserMessages.setChecked(SettingsCache::instance().getIgnoreNonBuddyUserMessages()); + connect(&ignoreUnregUsersMainChat, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setIgnoreUnregisteredUsers); connect(&ignoreUnregUserMessages, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setIgnoreUnregisteredUserMessages); + connect(&ignoreNonBuddyUserMessages, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setIgnoreNonBuddyUserMessages); invertMentionForeground.setChecked(SettingsCache::instance().getChatMentionForeground()); connect(&invertMentionForeground, &QCheckBox::QT_STATE_CHANGED, this, &MessagesSettingsPage::updateTextColor); @@ -62,9 +66,10 @@ MessagesSettingsPage::MessagesSettingsPage() chatGrid->addWidget(&ignoreUnregUsersMainChat, 2, 0); chatGrid->addWidget(&hexLabel, 1, 2); chatGrid->addWidget(&ignoreUnregUserMessages, 3, 0); - chatGrid->addWidget(&messagePopups, 4, 0); - chatGrid->addWidget(&mentionPopups, 5, 0); - chatGrid->addWidget(&roomHistory, 6, 0); + chatGrid->addWidget(&ignoreNonBuddyUserMessages, 4, 0); + chatGrid->addWidget(&messagePopups, 5, 0); + chatGrid->addWidget(&mentionPopups, 6, 0); + chatGrid->addWidget(&roomHistory, 7, 0); chatGroupBox = new QGroupBox; chatGroupBox->setLayout(chatGrid); @@ -237,6 +242,7 @@ void MessagesSettingsPage::retranslateUi() QString("%2").arg(WIKI_CUSTOM_SHORTCUTS).arg(tr("How to use in-game message macros"))); ignoreUnregUsersMainChat.setText(tr("Ignore chat room messages sent by unregistered users")); ignoreUnregUserMessages.setText(tr("Ignore private messages sent by unregistered users")); + ignoreNonBuddyUserMessages.setText(tr("Ignore private messages sent by non-buddy users")); invertMentionForeground.setText(tr("Invert text color")); invertHighlightForeground.setText(tr("Invert text color")); messagePopups.setText(tr("Enable desktop notifications for private messages")); diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h index e8a4a8aa4..e98ae0592 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h @@ -36,6 +36,7 @@ private: QCheckBox invertHighlightForeground; QCheckBox ignoreUnregUsersMainChat; QCheckBox ignoreUnregUserMessages; + QCheckBox ignoreNonBuddyUserMessages; QCheckBox messagePopups; QCheckBox mentionPopups; QCheckBox roomHistory; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 3566d6939..e7075f78f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -1019,6 +1019,12 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event) !userLevel.testFlag(ServerInfo_User::IsRegistered)) { // Flags are additive, so reg/mod/admin are all IsRegistered return; + } else if (SettingsCache::instance().getIgnoreNonBuddyUserMessages() && + !userListManager->isUserBuddy(senderName) && !userLevel.testFlag(ServerInfo_User::IsModerator) && + !userLevel.testFlag(ServerInfo_User::IsAdmin)) { + // Ignore private messages from non-buddies + // Moderator/Admin messages are exempt to ensure warnings reach users + return; } } tab = addMessageTab(QString::fromStdString(event.sender_name()), false); From 6be9cec6e20eb5928c2f8888ee968579d4a6edab Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 10 Jun 2026 08:35:00 +0200 Subject: [PATCH 22/50] do not save a const reference to the user data in the info dialog (#6974) * do not save a const reference to the user data in the info dialog * cmake format --- .../widgets/server/user/user_info_box.cpp | 24 ++++++------------- .../widgets/server/user/user_info_box.h | 14 ++++------- .../utility/days_years_between.h | 8 +++++++ tests/CMakeLists.txt | 4 +++- tests/test_age_formatting.cpp | 13 +++++----- 5 files changed, 29 insertions(+), 34 deletions(-) create mode 100644 libcockatrice_utility/libcockatrice/utility/days_years_between.h diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index a9955ff3d..e41ae6e75 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -85,24 +85,15 @@ void UserInfoBox::retranslateUi() avatarButton.setText(tr("Change avatar")); } -/** - * Creates the default profile pic that is used when the user doesn't have a custom pic - */ -static QPixmap createDefaultAvatar(int height, const ServerInfo_User &user) -{ - return UserLevelPixmapGenerator::generatePixmap(height, UserLevelFlags(user.user_level()), user.pawn_colors(), - false, QString::fromStdString(user.privlevel())); -} - void UserInfoBox::updateInfo(const ServerInfo_User &user) { - currentUserInfo = &user; - - const UserLevelFlags userLevel(user.user_level()); + userLevel = UserLevelFlags(user.user_level()); + pawnColors = user.pawn_colors(); + privLevel = QString::fromStdString(user.privlevel()); const std::string &bmp = user.avatar_bmp(); if (!avatarPixmap.loadFromData((const uchar *)bmp.data(), static_cast(bmp.size()))) { - avatarPixmap = createDefaultAvatar(64, user); + avatarPixmap = UserLevelPixmapGenerator::generatePixmap(64, userLevel, pawnColors, false, privLevel); hasAvatar = false; } else { hasAvatar = true; @@ -120,8 +111,7 @@ void UserInfoBox::updateInfo(const ServerInfo_User &user) countryLabel3.setText(""); } - userLevelIcon.setPixmap(UserLevelPixmapGenerator::generatePixmap(15, userLevel, user.pawn_colors(), false, - QString::fromStdString(user.privlevel()))); + userLevelIcon.setPixmap(UserLevelPixmapGenerator::generatePixmap(15, userLevel, pawnColors, false, privLevel)); QString userLevelText; if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { userLevelText = tr("Administrator"); @@ -373,7 +363,7 @@ void UserInfoBox::processAvatarResponse(const Response &r) break; case Response::RespInternalError: default: - QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to updater your avatar.")); + QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to update your avatar.")); break; } } @@ -385,7 +375,7 @@ void UserInfoBox::resizeEvent(QResizeEvent *event) resizedPixmap = avatarPixmap.scaled(avatarPic.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); } else { int height = qMin(avatarPic.size().width(), avatarPic.size().height()); - resizedPixmap = createDefaultAvatar(height, *currentUserInfo); + resizedPixmap = UserLevelPixmapGenerator::generatePixmap(height, userLevel, pawnColors, false, privLevel); } avatarPic.setPixmap(resizedPixmap); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.h b/cockatrice/src/interface/widgets/server/user/user_info_box.h index 299deed2f..055ac0096 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.h @@ -11,8 +11,9 @@ #include #include #include +#include +#include -class ServerInfo_User; class AbstractClient; class Response; @@ -27,20 +28,15 @@ private: QPushButton editButton, passwordButton, avatarButton; QPixmap avatarPixmap; bool hasAvatar; - const ServerInfo_User *currentUserInfo; + UserLevelFlags userLevel; + ServerInfo_User::PawnColorsOverride pawnColors; + QString privLevel; static QString getAgeString(int ageSeconds); public: UserInfoBox(AbstractClient *_client, bool editable, QWidget *parent = nullptr, Qt::WindowFlags flags = {}); void retranslateUi(); - - inline static QPair getDaysAndYearsBetween(const QDate &then, const QDate &now) - { - int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo - int days = then.addYears(years).daysTo(now); - return {days, years}; - } private slots: void processResponse(const Response &r); void processEditResponse(const Response &r); diff --git a/libcockatrice_utility/libcockatrice/utility/days_years_between.h b/libcockatrice_utility/libcockatrice/utility/days_years_between.h new file mode 100644 index 000000000..c0f5da23a --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/days_years_between.h @@ -0,0 +1,8 @@ +#include + +inline static QPair getDaysAndYearsBetween(const QDate &then, const QDate &now) +{ + int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo + int days = then.addYears(years).daysTo(now); + return {days, years}; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 00eba288e..04ac7fcee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -59,7 +59,9 @@ endif() include_directories(${GTEST_INCLUDE_DIRS}) target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES}) target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) -target_link_libraries(test_age_formatting Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries( + test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) target_link_libraries( password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) diff --git a/tests/test_age_formatting.cpp b/tests/test_age_formatting.cpp index e4fc64cf9..6a9d5d4af 100644 --- a/tests/test_age_formatting.cpp +++ b/tests/test_age_formatting.cpp @@ -1,6 +1,5 @@ -#include "../cockatrice/src/interface/widgets/server/user/user_info_box.h" - #include "gtest/gtest.h" +#include namespace { @@ -8,31 +7,31 @@ using dayyear = QPair; TEST(AgeFormatting, Zero) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2000, 1, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2000, 1, 1)); ASSERT_EQ(got, dayyear(0, 0)) << "these are the same day"; } TEST(AgeFormatting, LeapDay) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2000, 3, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2000, 3, 1)); ASSERT_EQ(got, dayyear(2, 0)) << "there is a leap day in between these days"; } TEST(AgeFormatting, LeapYear) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2001, 1, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 1, 1), QDate(2001, 1, 1)); ASSERT_EQ(got, dayyear(0, 1)) << "there is a leap day in between these dates, but that's fine"; } TEST(AgeFormatting, LeapDayWithYear) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2001, 3, 1)); + auto got = getDaysAndYearsBetween(QDate(2000, 2, 28), QDate(2001, 3, 1)); ASSERT_EQ(got, dayyear(1, 1)) << "there is a leap day in between these days but not in the last year"; } TEST(AgeFormatting, LeapDayThisYear) { - auto got = UserInfoBox::getDaysAndYearsBetween(QDate(2003, 2, 28), QDate(2004, 3, 1)); + auto got = getDaysAndYearsBetween(QDate(2003, 2, 28), QDate(2004, 3, 1)); ASSERT_EQ(got, dayyear(2, 1)) << "there is a leap day in between these days this year"; } } // namespace From b17d879da88f537cd83609f5a4cb42715ef9eb4c Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:58:28 +0200 Subject: [PATCH 23/50] [Game][Graphics][Player] Add named zone lookup-map to player graphics. (#6984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 16 minutes Co-authored-by: Lukas Brübach --- .../player/player_graphics_item.cpp | 16 ++++++++++++++++ .../game_graphics/player/player_graphics_item.h | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 07975ed5e..e0194abda 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -59,6 +59,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player) initializeZones(); + connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this, + &PlayerGraphicsItem::onCustomZoneAdded); + playerMenu->setMenusForGraphicItems(); connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect); @@ -121,6 +124,19 @@ void PlayerGraphicsItem::initializeZones() connect(handZoneGraphicsItem->getLogic(), &HandZoneLogic::cardCountChanged, handCounter, &HandCounter::updateNumber); connect(handCounter, &HandCounter::showContextMenu, handZoneGraphicsItem, &HandZone::showContextMenu); + + zoneGraphicsItems.insert(player->getDeckZone()->getName(), deckZoneGraphicsItem); + zoneGraphicsItems.insert(player->getGraveZone()->getName(), graveyardZoneGraphicsItem); + zoneGraphicsItems.insert(player->getRfgZone()->getName(), rfgZoneGraphicsItem); + zoneGraphicsItems.insert(player->getSideboardZone()->getName(), sideboardGraphicsItem); + zoneGraphicsItems.insert(player->getTableZone()->getName(), tableZoneGraphicsItem); + zoneGraphicsItems.insert(player->getStackZone()->getName(), stackZoneGraphicsItem); + zoneGraphicsItems.insert(player->getHandZone()->getName(), handZoneGraphicsItem); +} + +void PlayerGraphicsItem::onCustomZoneAdded(QString customZoneName) +{ + zoneGraphicsItems.insert(customZoneName, nullptr); // Custom zone view goes here, if we ever implement it. } QRectF PlayerGraphicsItem::boundingRect() const diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.h b/cockatrice/src/game_graphics/player/player_graphics_item.h index 0dcc959bd..d02234ded 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.h +++ b/cockatrice/src/game_graphics/player/player_graphics_item.h @@ -77,6 +77,11 @@ public: return playerTarget; } + CardZone *getZoneGraphicsItem(const QString &name) const + { + return zoneGraphicsItems.value(name, nullptr); + } + [[nodiscard]] PileZone *getDeckZoneGraphicsItem() const { return deckZoneGraphicsItem; @@ -110,6 +115,7 @@ public: public slots: void onPlayerActiveChanged(bool _active); + void onCustomZoneAdded(QString customZoneName); void onCounterAdded(CounterState *state); void onCounterRemoved(int counterId); void rearrangeCounters(); @@ -128,6 +134,7 @@ private: PlayerArea *playerArea; PlayerTarget *playerTarget; QMap counterWidgets; + QMap zoneGraphicsItems; PileZone *deckZoneGraphicsItem; PileZone *sideboardGraphicsItem; PileZone *graveyardZoneGraphicsItem; From 694adc9e64729e3fb5bf9f0d1940a896f5594f0d Mon Sep 17 00:00:00 2001 From: tooomm Date: Thu, 11 Jun 2026 10:45:17 +0200 Subject: [PATCH 24/50] Use Visual Studio 2026 (#6985) --- .github/workflows/desktop-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 19c9a15e3..179fd824f 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -331,7 +331,7 @@ jobs: target: 10 runner: windows-2025 - cmake_generator: "Visual Studio 17 2022" + cmake_generator: "Visual Studio 18 2026" cmake_generator_platform: x64 make_package: 1 package_suffix: "-Win10" From 7aaacbf34701f0c051fe30b3e57ecc24515457b8 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:17:28 +0200 Subject: [PATCH 25/50] [Update][NSIS] Use single string shell invocation (#6986) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 18 minutes Took 2 minutes Co-authored-by: Lukas Brübach --- cockatrice/src/interface/widgets/dialogs/dlg_update.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index 15735168f..f12550fa8 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -220,9 +220,8 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath) { setLabel(tr("Installing...")); // Try to open the installer. If it opens, quit Cockatrice - if (QProcess::startDetached(filepath.toLocalFile(), - QStringList() - << "/R" << QString("/D=%1").arg(QCoreApplication::applicationDirPath()))) { + if (QProcess::startDetached( + QString("\"%1\" /R /D=\"%2\"").arg(filepath.toLocalFile(), QCoreApplication::applicationDirPath()))) { QMetaObject::invokeMethod(static_cast(parent()), "close", Qt::QueuedConnection); qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice"; close(); From f28ede7ae36916bf2bc5248fc46eed9ca44c5edb Mon Sep 17 00:00:00 2001 From: kongwu <167565490+kongwu666@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:42:55 +0800 Subject: [PATCH 26/50] [UserContextMenu] Add confirmation dialog before kicking a player (#6987) --- .../widgets/server/user/user_context_menu.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index 195b1cc8d..faa96fa1f 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -476,10 +476,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos, client->sendCommand(client->prepareSessionCommand(cmd)); } else if (actionClicked == aKick) { - Command_KickFromGame cmd; - cmd.set_player_id(playerId); + auto result = QMessageBox::question(static_cast(parent()), tr("Kick Player"), + tr("Are you sure you want to kick this player from the game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (result == QMessageBox::Yes) { + Command_KickFromGame cmd; + cmd.set_player_id(playerId); - game->getGameEventHandler()->sendGameCommand(cmd); + game->getGameEventHandler()->sendGameCommand(cmd); + } } else if (actionClicked == aBan) { Command_GetUserInfo cmd; cmd.set_user_name(userName.toStdString()); From 5ffe344779987b572ec78f4f11cbcb13da6134e9 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 14 Jun 2026 03:21:17 -0700 Subject: [PATCH 27/50] [Game] Fix facedown predefined tokens leaking tablerow (#7000) --- cockatrice/src/game/player/player_actions.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index de909ca5e..fffd23ccf 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -882,7 +882,8 @@ void PlayerActions::actCreateToken(TokenInfo tokenToCreate) ExactCard correctedCard = CardDatabaseManager::query()->guessCard({lastTokenInfo.name, lastTokenInfo.providerId}); if (correctedCard) { lastTokenInfo.name = correctedCard.getName(); - lastTokenTableRow = TableZone::tableRowToGridY(correctedCard.getInfo().getUiAttributes().tableRow); + int tableRow = lastTokenInfo.faceDown ? 2 : correctedCard.getInfo().getUiAttributes().tableRow; + lastTokenTableRow = TableZone::tableRowToGridY(tableRow); if (lastTokenInfo.pt.isEmpty()) { lastTokenInfo.pt = correctedCard.getInfo().getPowTough(); } From 0f3e6fbe2605c896c628cf55c35f6629e678d612 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 14 Jun 2026 04:46:18 -0700 Subject: [PATCH 28/50] [CardDatabase] Pass CardInfoPtr by const ref (#6998) * [CardDatabase] Pass CardInfoPtr by const ref * trailing newline --- .../libcockatrice/card/database/card_database.cpp | 10 +++++----- .../libcockatrice/card/database/card_database.h | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp index 951381aa4..edad46174 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -92,7 +92,7 @@ void CardDatabase::refreshCachedReverseRelatedCards() } } -void CardDatabase::addCard(CardInfoPtr card) +void CardDatabase::addCard(const CardInfoPtr &card) { if (card == nullptr) { qCWarning(CardDatabaseLog) << "CardDatabase::addCard(nullptr)"; @@ -118,7 +118,7 @@ void CardDatabase::addCard(CardInfoPtr card) emit cardAdded(card); } -void CardDatabase::removeCard(CardInfoPtr card) +void CardDatabase::removeCard(const CardInfoPtr &card) { if (card.isNull()) { qCWarning(CardDatabaseLog) << "CardDatabase::removeCard(nullptr)"; @@ -143,7 +143,7 @@ void CardDatabase::removeCard(CardInfoPtr card) emit cardRemoved(card); } -void CardDatabase::addSet(CardSetPtr set) +void CardDatabase::addSet(const CardSetPtr &set) { sets.insert(set->getShortName(), set); } @@ -215,7 +215,7 @@ void CardDatabase::notifyEnabledSetsChanged() emit cardDatabaseEnabledSetsChanged(); } -void CardDatabase::addFormat(FormatRulesPtr format) +void CardDatabase::addFormat(const FormatRulesPtr &format) { formats.insert(format->formatName.toLower(), format); -} \ No newline at end of file +} diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.h b/libcockatrice_card/libcockatrice/card/database/card_database.h index 521be8fbc..44838962d 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database.h @@ -88,7 +88,7 @@ public: * @brief Removes a card from the database. * @param card Pointer to the card to remove. */ - void removeCard(CardInfoPtr card); + void removeCard(const CardInfoPtr &card); /** @brief Clears all cards, sets, and internal state. */ void clear(); @@ -140,15 +140,15 @@ public slots: * @brief Adds a card to the database. * @param card CardInfoPtr to add. */ - void addCard(CardInfoPtr card); + void addCard(const CardInfoPtr &card); /** * @brief Adds a set to the database. * @param set Pointer to CardSet to add. */ - void addSet(CardSetPtr set); + void addSet(const CardSetPtr &set); - void addFormat(FormatRulesPtr format); + void addFormat(const FormatRulesPtr &format); /** @brief Loads card databases from configured paths. */ void loadCardDatabases(); From dfbe944c31a8339ea886f6726f732f785404e5d1 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:23:18 +0200 Subject: [PATCH 29/50] [App][Windows][NSIS] Use QProcess::setNativeArguments on Windows, properly order portable detection (#6989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- cmake/NSIS.template.in | 9 +++++---- .../interface/widgets/dialogs/dlg_update.cpp | 20 +++++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 7b52b7bcc..5af116470 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -117,21 +117,22 @@ ${If} $InstDir == "" ; we need to set a default based on the install mode StrCpy $InstDir $0 ${EndIf} -Call SetModeDestinationFromInstdir -; --- Detect portable install when using /R --- +; --- Detect portable install when using /R (must come BEFORE SetModeDestinationFromInstdir) --- ${If} $ReinstallMode = 1 IfFileExists "$InstDir\portable.dat" 0 not_portable StrCpy $PortableMode 1 Goto portable_done - not_portable: StrCpy $PortableMode 0 - portable_done: ${EndIf} +; Now that $PortableMode reflects reality, commit InstDir into the correct slot +Call SetModeDestinationFromInstdir + ${If} $ReinstallMode = 1 +${AndIf} $PortableMode = 0 Call AutoUninstallIfNeeded ${EndIf} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index f12550fa8..ee2149309 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -219,9 +219,25 @@ void DlgUpdate::downloadError(const QString &errorString) void DlgUpdate::downloadSuccessful(const QUrl &filepath) { setLabel(tr("Installing...")); + + QString installerPath = filepath.toLocalFile(); + + QString appDir = QDir::toNativeSeparators(QCoreApplication::applicationDirPath()); + QProcess process; + process.setProgram(installerPath); + + // NSIS needs the /D= argument to be an UNQUOTED string, even if it contains spaces. Qt likes to quote arguments if + // they contain spaces, so we use the windows exclusive QProcess::setNativeArguments in the only case where this is + // relevant, which preserves the argument unquoted. +#ifdef Q_OS_WIN + process.setNativeArguments(QString("/R /D=%1").arg(appDir)); +#else + // Linux/macOS: normal argument passing (not relevant since they update differently.) + process.setArguments({"/R", QString("/D=%1").arg(appDir)}); +#endif + // Try to open the installer. If it opens, quit Cockatrice - if (QProcess::startDetached( - QString("\"%1\" /R /D=\"%2\"").arg(filepath.toLocalFile(), QCoreApplication::applicationDirPath()))) { + if (process.startDetached()) { QMetaObject::invokeMethod(static_cast(parent()), "close", Qt::QueuedConnection); qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice"; close(); From 0c4cc3f82450dcf2952d3ab51f909d43b62cda00 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:49:47 +0200 Subject: [PATCH 30/50] [DeckView][DeckEditor] Implement shortcut for load deck from website (default: Ctrl+Shift+O) (#7002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 12 minutes Co-authored-by: Lukas Brübach --- cockatrice/src/client/settings/shortcuts_settings.h | 8 ++++++++ .../src/game_graphics/deckview/deck_view_container.cpp | 1 + .../src/interface/widgets/menus/deck_editor_menu.cpp | 2 ++ 3 files changed, 11 insertions(+) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 45e2c4fca..95155b8d1 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -223,6 +223,10 @@ private: {"TabDeckEditor/aLoadDeck", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck..."), parseSequenceString("Ctrl+O"), ShortcutGroup::Deck_Editor)}, + {"TabDeckEditor/aLoadDeckFromWebsite", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load deck from online service..."), + parseSequenceString("Ctrl+Shift+O"), + ShortcutGroup::Deck_Editor)}, {"TabDeckEditor/aLoadDeckFromClipboard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."), parseSequenceString("Ctrl+Shift+V"), @@ -283,6 +287,10 @@ private: ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."), parseSequenceString("Ctrl+Shift+V"), ShortcutGroup::Game_Lobby)}, + {"DeckViewContainer/loadFromWebsiteButton", + ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load from website..."), + parseSequenceString("Ctrl+Shift+O"), + ShortcutGroup::Game_Lobby)}, {"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"), parseSequenceString("Ctrl+Alt+U"), ShortcutGroup::Game_Lobby)}, diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index cbd6c2bad..21284c517 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -209,6 +209,7 @@ void DeckViewContainer::refreshShortcuts() loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton")); loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton")); loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton")); + loadFromWebsiteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromWebsiteButton")); unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton")); readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton")); sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton")); diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp index 23d19abbb..d6df694df 100644 --- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp +++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp @@ -193,6 +193,8 @@ void DeckEditorMenu::refreshShortcuts() aEditDeckInClipboardRaw->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aEditDeckInClipboardRaw")); aPrintDeck->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aPrintDeck")); + aLoadDeckFromWebsite->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aLoadDeckFromWebsite")); + aExportDeckDecklist->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklist")); aExportDeckDecklistXyz->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aExportDeckDecklistXyz")); aAnalyzeDeckDeckstats->setShortcuts(shortcuts.getShortcut("TabDeckEditor/aAnalyzeDeck")); From 309e4730a3b56eef4fb4bbc35364e67ab5e081ff Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:50:04 +0200 Subject: [PATCH 31/50] [Lobby][DeckView] Always at minimum create main and side deck zone containers. (#7001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 12 minutes Took 3 seconds Co-authored-by: Lukas Brübach --- cockatrice/src/game_graphics/deckview/deck_view.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cockatrice/src/game_graphics/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp index ced02c8db..a5d0fa3bc 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp @@ -360,6 +360,16 @@ void DeckViewScene::rebuildTree() return; } + QStringList requiredZones = {DECK_ZONE_MAIN, DECK_ZONE_SIDE}; + + for (const QString &zoneName : requiredZones) { + if (!cardContainers.contains(zoneName)) { + auto *container = new DeckViewCardContainer(zoneName); + cardContainers.insert(zoneName, container); + addItem(container); + } + } + for (auto *currentZone : deck->getZoneNodes()) { DeckViewCardContainer *container = cardContainers.value(currentZone->getName(), 0); if (!container) { From 45d0cedb5c071ff8ab7b2f14d8390ccf3b3955b5 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 15 Jun 2026 08:04:30 -0700 Subject: [PATCH 32/50] [Game] Setting to restore old chat autofocus behavior (#6992) * [Game] Setting to restore old chat autofocus behavior * fixes --- cockatrice/src/client/settings/cache_settings.cpp | 8 ++++++++ cockatrice/src/client/settings/cache_settings.h | 7 +++++++ cockatrice/src/game_graphics/game_view.cpp | 13 ++++++++++++- cockatrice/src/game_graphics/game_view.h | 1 + .../src/game_graphics/zones/view_zone_widget.cpp | 5 +++++ .../settings_page/user_interface_settings_page.cpp | 8 ++++++++ .../settings_page/user_interface_settings_page.h | 1 + 7 files changed, 42 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 73e5a98a1..cc34e1707 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -309,6 +309,7 @@ SettingsCache::SettingsCache() cardViewExpandedRowsMax = settings->value("interface/cardViewExpandedRowsMax", 20).toInt(); closeEmptyCardView = settings->value("interface/closeEmptyCardView", true).toBool(); focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); + keepGameChatFocus = settings->value("interface/keepGameChatFocus", false).toBool(); showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool(); showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool(); @@ -457,6 +458,13 @@ void SettingsCache::setFocusCardViewSearchBar(QT_STATE_CHANGED_T value) settings->setValue("interface/focusCardViewSearchBar", focusCardViewSearchBar); } +void SettingsCache::setKeepGameChatFocus(QT_STATE_CHANGED_T value) +{ + keepGameChatFocus = value; + settings->setValue("interface/keepGameChatFocus", keepGameChatFocus); + emit keepGameChatFocusChanged(keepGameChatFocus); +} + void SettingsCache::setKnownMissingFeatures(const QString &_knownMissingFeatures) { knownMissingFeatures = _knownMissingFeatures; diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 8ee372766..a166917c1 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -195,6 +195,7 @@ signals: void downloadSpoilerStatusChanged(); void useTearOffMenusChanged(bool state); void roundCardCornersChanged(bool roundCardCorners); + void keepGameChatFocusChanged(bool value); private: QSettings *settings; @@ -306,6 +307,7 @@ private: int cardViewExpandedRowsMax; bool closeEmptyCardView; bool focusCardViewSearchBar; + bool keepGameChatFocus; int pixmapCacheSize; int networkCacheSize; int redirectCacheTtl; @@ -935,6 +937,7 @@ public: void setCardViewExpandedRowsMax(int value); void setCloseEmptyCardView(QT_STATE_CHANGED_T value); void setFocusCardViewSearchBar(QT_STATE_CHANGED_T value); + void setKeepGameChatFocus(QT_STATE_CHANGED_T value); QString getClientID() override { return clientID; @@ -967,6 +970,10 @@ public: { return focusCardViewSearchBar; } + [[nodiscard]] bool getKeepGameChatFocus() const + { + return keepGameChatFocus; + } [[nodiscard]] ShortcutsSettings &shortcuts() const { return *shortcutsSettings; diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index 4ba41cffb..41befd9a4 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -34,7 +34,6 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par { setBackgroundBrush(QBrush(QColor(0, 0, 0))); setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing); - setFocusPolicy(Qt::ClickFocus); setViewportUpdateMode(BoundingRectViewportUpdate); connect(scene, &GameScene::sceneRectChanged, this, &GameView::updateSceneRect); @@ -44,6 +43,9 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); + setFocusDisabled(SettingsCache::instance().getKeepGameChatFocus()); + connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, this, &GameView::setFocusDisabled); + aCloseMostRecentZoneView = new QAction(this); connect(aCloseMostRecentZoneView, &QAction::triggered, scene, &GameScene::closeMostRecentZoneView); @@ -186,3 +188,12 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize) totalCountLabel->hide(); } } + +/** + * Disabling focus on the game view will allow chat to maintain the autofocusing behavior of pre 2.10.3, + * at the cost of disabling the zone view search bar. + */ +void GameView::setFocusDisabled(bool disabled) +{ + setFocusPolicy(disabled ? Qt::NoFocus : Qt::ClickFocus); +} diff --git a/cockatrice/src/game_graphics/game_view.h b/cockatrice/src/game_graphics/game_view.h index 15abad9af..80e8e96b5 100644 --- a/cockatrice/src/game_graphics/game_view.h +++ b/cockatrice/src/game_graphics/game_view.h @@ -31,6 +31,7 @@ private slots: void stopRubberBand(); void refreshShortcuts(); void updateTotalSelectionCount(const QSize &viewSize = QSize()); + void setFocusDisabled(bool disabled); public slots: void updateSceneRect(const QRectF &rect); diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index 4a5d064d0..14537a826 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -75,6 +75,11 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player, searchEditProxy->setZValue(ZValues::DRAG_ITEM); vbox->addItem(searchEditProxy); + // hide search bar if chat autofocus setting is enabled, since typing into it will no longer work anyway + searchEditProxy->setVisible(!SettingsCache::instance().getKeepGameChatFocus()); + connect(&SettingsCache::instance(), &SettingsCache::keepGameChatFocusChanged, searchEditProxy, + [searchEditProxy](bool keepFocus) { searchEditProxy->setVisible(!keepFocus); }); + // top row QGraphicsLinearLayout *hTopRow = new QGraphicsLinearLayout(Qt::Horizontal); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index dfa736a1a..6039e3758 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -72,6 +72,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); + keepGameChatFocusCheckBox.setChecked(SettingsCache::instance().getKeepGameChatFocus()); + connect(&keepGameChatFocusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setKeepGameChatFocus); + auto *generalGrid = new QGridLayout; generalGrid->addWidget(&doubleClickToPlayCheckBox, 0, 0); generalGrid->addWidget(&clickPlaysAllSelectedCheckBox, 1, 0); @@ -83,6 +87,7 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0); generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0); generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0); + generalGrid->addWidget(&keepGameChatFocusCheckBox, 10, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -207,6 +212,9 @@ void UserInterfaceSettingsPage::retranslateUi() showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection")); showTotalSelectionCountCheckBox.setText(tr("Show total selection counter")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); + keepGameChatFocusCheckBox.setText( + tr("Keep game chat focused when clicking in game (Note: disables card view search bar)")); + notificationsGroupBox->setTitle(tr("Notifications settings")); notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar")); specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating")); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index 6dd43ceae..e10ed2a06 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -30,6 +30,7 @@ private: QCheckBox showDragSelectionCountCheckBox; QCheckBox showTotalSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; + QCheckBox keepGameChatFocusCheckBox; QCheckBox tapAnimationCheckBox; QCheckBox openDeckInNewTabCheckBox; QLabel visualDeckStoragePromptForConversionLabel; From 0c140d866fe964993444ff01d2f7de2c777f8f9e Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Wed, 17 Jun 2026 08:09:05 -0400 Subject: [PATCH 33/50] [Card] Respect disabled sets when loading v3 card databases (#6882) (#7004) --- .../card/database/parser/cockatrice_xml_3.cpp | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index ba27d63c4..b6c3afc57 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -217,27 +217,32 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) // NOTE: attributes must be read before readElementText() QXmlStreamAttributes attrs = xml.attributes(); QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements); - PrintingInfo setInfo(internalAddSet(setName)); - if (attrs.hasAttribute("muId")) { - setInfo.setProperty("muid", attrs.value("muId").toString()); - } + auto set = internalAddSet(setName); + // Only load printings from sets the user has enabled, matching the v4 loader's + // behaviour. Without this check, disabling a set has no effect on v3 databases. + if (set->getEnabled()) { + PrintingInfo setInfo(set); + if (attrs.hasAttribute("muId")) { + setInfo.setProperty("muid", attrs.value("muId").toString()); + } - if (attrs.hasAttribute("muId")) { - setInfo.setProperty("uuid", attrs.value("uuId").toString()); - } + if (attrs.hasAttribute("uuId")) { + setInfo.setProperty("uuid", attrs.value("uuId").toString()); + } - if (attrs.hasAttribute("picURL")) { - setInfo.setProperty("picurl", attrs.value("picURL").toString()); - } + if (attrs.hasAttribute("picURL")) { + setInfo.setProperty("picurl", attrs.value("picURL").toString()); + } - if (attrs.hasAttribute("num")) { - setInfo.setProperty("num", attrs.value("num").toString()); - } + if (attrs.hasAttribute("num")) { + setInfo.setProperty("num", attrs.value("num").toString()); + } - if (attrs.hasAttribute("rarity")) { - setInfo.setProperty("rarity", attrs.value("rarity").toString()); + if (attrs.hasAttribute("rarity")) { + setInfo.setProperty("rarity", attrs.value("rarity").toString()); + } + _sets[setName].append(setInfo); } - _sets[setName].append(setInfo); // related cards } else if (xmlName == "related" || xmlName == "reverse-related") { CardRelationType attach = CardRelationType::DoesNotAttach; From e28f31c93e000e5eaf4f41db84e0d4c6a9f9adb5 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:16:26 +0200 Subject: [PATCH 34/50] Translate oracle/oracle_en@source.ts in es (#7006) --- oracle/translations/oracle_es.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/oracle/translations/oracle_es.ts b/oracle/translations/oracle_es.ts index eeb9f71bd..33dbbdc3a 100644 --- a/oracle/translations/oracle_es.ts +++ b/oracle/translations/oracle_es.ts @@ -63,7 +63,7 @@ Sets file (%1) Sets JSON file (%1) - + Archivo de ediciones (%1) @@ -172,7 +172,7 @@ spoiler - + spoiler @@ -192,7 +192,7 @@ Local file: - + Archivo local: @@ -202,7 +202,7 @@ Choose file... - + Elegir archivo... @@ -230,7 +230,7 @@ tokens - + fichas @@ -250,7 +250,7 @@ Local file: - + Archivo local: @@ -260,7 +260,7 @@ Choose file... - + Elegir archivo... @@ -391,12 +391,12 @@ Load %1 file - + Cargar archivo de %1 %1 file (%1) - + archivo de %1 (%1) @@ -420,12 +420,12 @@ Please choose a file. - + Por favor elija un archivo. Cannot open file '%1'. - + No se puede abrir el archivo '%1'. @@ -602,7 +602,7 @@ Run in no-confirm background mode - + Ejecutar en modo del segundo plano sin confirmación \ No newline at end of file From 687e6644bc1008ce51c795ec04f8b08fa22af0a6 Mon Sep 17 00:00:00 2001 From: Phred Lane Date: Fri, 19 Jun 2026 09:42:15 -0500 Subject: [PATCH 35/50] replaced dynamically injected variables with environment variables (#6996) This prevents some injection attacks and makes all the scripts valid Bash. --- .github/workflows/desktop-build.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 179fd824f..11b5b2213 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -211,9 +211,10 @@ jobs: if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit continue-on-error: true env: + CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | - if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then + if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then echo "Cache deleted successfully" fi @@ -256,8 +257,9 @@ jobs: if: steps.attestation.outcome == 'success' shell: bash env: + BUILD_PATH: ${{ steps.build.outputs.path }} GH_TOKEN: ${{ github.token }} - run: gh attestation verify "${{ steps.build.outputs.path }}" --repo Cockatrice/Cockatrice + run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice build-vcpkg: strategy: @@ -381,9 +383,11 @@ jobs: # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases - name: "Resolve latest Qt patch version" + env: + QT_VERSION: ${{ matrix.qt_version }} id: resolve_qt_version shell: bash - run: .ci/resolve_latest_aqt_qt_version.sh "${{ matrix.qt_version }}" + run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION" - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" if: matrix.os == 'macOS' @@ -465,9 +469,10 @@ jobs: if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit continue-on-error: true env: + CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }} GH_TOKEN: ${{ github.token }} run: | - if gh cache delete --repo ${{ github.repository }} ${{ steps.ccache_restore.outputs.cache-primary-key }}; then + if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then echo "Cache deleted successfully" fi @@ -482,18 +487,20 @@ jobs: if: matrix.os == 'macOS' && matrix.make_package && needs.configure.outputs.tag != null id: sign_macos env: + BUILD_PATH: ${{ steps.build.outputs.path }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} run: | if [[ -n "$MACOS_CERTIFICATE_NAME" ]] then security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "${{ steps.build.outputs.path }}" + /usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose "$BUILD_PATH" fi - name: "[macOS] Notarize app bundle" if: matrix.os == 'macOS' && steps.sign_macos.outcome == 'success' env: + BUILD_PATH: ${{ steps.build.outputs.path }} MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} @@ -508,7 +515,7 @@ jobs: # Therefore, we create a zip file containing our app bundle, so that we can send it to the # notarization service echo "Creating temp notarization archive" - ditto -c -k --keepParent "${{ steps.build.outputs.path }}" "notarization.zip" + ditto -c -k --keepParent "$BUILD_PATH" "notarization.zip" # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App @@ -520,7 +527,7 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" - xcrun stapler staple "${{ steps.build.outputs.path }}" + xcrun stapler staple "$BUILD_PATH" fi - name: "Upload artifact" @@ -566,5 +573,6 @@ jobs: if: steps.attestation.outcome == 'success' shell: bash env: + BUILD_PATH: ${{ steps.build.outputs.path }} GH_TOKEN: ${{ github.token }} - run: gh attestation verify "${{ steps.build.outputs.path }}" --repo Cockatrice/Cockatrice + run: gh attestation verify "$BUILD_PATH" --repo Cockatrice/Cockatrice From 2a3c4a2455431dd2115e6e4fc59be709fa6791ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:11:10 +0200 Subject: [PATCH 36/50] Bump actions/checkout from 6 to 7 (#7011) --- .github/workflows/desktop-build.yml | 6 +++--- .github/workflows/desktop-lint.yml | 2 +- .github/workflows/docker-release.yml | 2 +- .github/workflows/documentation-build.yml | 2 +- .github/workflows/translations-pull.yml | 2 +- .github/workflows/translations-push.yml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 11b5b2213..7af521b39 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -66,7 +66,7 @@ jobs: - name: "Checkout" if: steps.configure.outputs.tag != null - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 # fetch all history for all branches and tags @@ -163,7 +163,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: "Restore compiler cache (ccache)" id: ccache_restore @@ -352,7 +352,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml index 54931933c..5f31ea59c 100644 --- a/.github/workflows/desktop-lint.yml +++ b/.github/workflows/desktop-lint.yml @@ -22,7 +22,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 20 # should be enough to find merge base diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index d9ff06282..b479322d0 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -31,7 +31,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: "Docker metadata" id: metadata diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index 717999d5a..4b9ca79ab 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -21,7 +21,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index 057381f8a..7b082e9cd 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Checkout repo" - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: "Pull translated strings from Transifex" uses: transifex/cli-action@v2 diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml index 4adcaf4a4..5a6ca342b 100644 --- a/.github/workflows/translations-push.yml +++ b/.github/workflows/translations-push.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Checkout repo" - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: "Install lupdate" shell: bash From e99a55ccabd1f81e2f15da1d53a3c869d7f53dd2 Mon Sep 17 00:00:00 2001 From: Phred Lane Date: Sat, 20 Jun 2026 15:44:37 -0500 Subject: [PATCH 37/50] CI: Simplified build workflow (#6995) * simplified build workflow * cleaned up translation workflows * fixed typo * updated format based on code review Co-authored-by: tooomm * restored quotes around `group` string --------- Co-authored-by: tooomm --- .github/workflows/desktop-build.yml | 47 +++++++++++-------------- .github/workflows/translations-pull.yml | 16 ++++----- .github/workflows/translations-push.yml | 21 ++++++----- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 7af521b39..74f905351 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -47,22 +47,17 @@ jobs: tag: ${{ steps.configure.outputs.tag }} sha: ${{ steps.configure.outputs.sha }} - steps: + steps: - name: "Configure" + env: + RESOLVED_SHA: ${{ case(github.event_name == 'pull_request', github.event.pull_request.head.sha, github.sha) }} id: configure shell: bash run: | - tag_regex='^refs/tags/' - if [[ $GITHUB_EVENT_NAME == pull-request ]]; then # pull request - sha="${{github.event.pull_request.head.sha}}" - elif [[ $GITHUB_REF =~ $tag_regex ]]; then # release - sha="$GITHUB_SHA" - tag="${GITHUB_REF/refs\/tags\//}" - echo "tag=$tag" >>"$GITHUB_OUTPUT" - else # push to branch - sha="$GITHUB_SHA" + if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release + echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" fi - echo "sha=$sha" >>"$GITHUB_OUTPUT" + echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT" - name: "Checkout" if: steps.configure.outputs.tag != null @@ -92,7 +87,7 @@ jobs: run: | args=() [[ $prerelease == yes ]] && args+=(--prerelease) - + gh release create "$tag_name" --verify-tag --draft "${args[@]}" \ --target "$target" \ --title "$release_name" \ @@ -105,48 +100,48 @@ jobs: # The files in ".ci/$distro$version" correspond to the values given here include: - distro: Arch - + allow-failure: yes package: skip # We are packaged in Arch already - distro: Servatrice_Debian version: 12 - + package: DEB server_only: yes test: skip - distro: Debian version: 12 - + package: DEB test: skip # Running tests on all distros is superfluous - distro: Debian version: 13 - + package: DEB - distro: Fedora version: 43 - + package: RPM test: skip # Running tests on all distros is superfluous - distro: Fedora version: 44 - + package: RPM - distro: Ubuntu version: 24.04 - + package: DEB test: skip # Running tests on all distros is superfluous - distro: Ubuntu version: 26.04 - + package: DEB name: ${{ matrix.distro }} ${{ matrix.version }} @@ -203,7 +198,7 @@ jobs: args+=(--ccache "$CCACHE_SIZE") args+=(--cmake-generator "$CMAKE_GENERATOR") args+=(--suffix "$SUFFIX") - + RUN --server --release --package "$package" "${args[@]}" # Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342 @@ -269,7 +264,7 @@ jobs: - os: macOS target: 13 runner: macos-15-intel - + ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 @@ -286,7 +281,7 @@ jobs: - os: macOS target: 14 runner: macos-14 - + ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 @@ -302,7 +297,7 @@ jobs: - os: macOS target: 15 runner: macos-15 - + ccache_eviction_age: 7d cmake_generator: Ninja make_package: 1 @@ -318,7 +313,7 @@ jobs: - os: macOS target: 15 runner: macos-15 - + ccache_eviction_age: 7d cmake_generator: Ninja qt_version: 6.11.0 @@ -332,7 +327,7 @@ jobs: - os: Windows target: 10 runner: windows-2025 - + cmake_generator: "Visual Studio 18 2026" cmake_generator_platform: x64 make_package: 1 diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index 7b082e9cd..57df31bf0 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -41,11 +41,11 @@ jobs: author: github-actions # owner of the commit body: | Pulled all translated strings from [Transifex][1]. - + --- *This PR is automatically generated and updated by the workflow at `.github/workflows/translations-pull.yml`. Review [action runs][2].*
*After merging, all new languages and translations are available in the next build.* - + [1]: https://explore.transifex.com/cockatrice/cockatrice/ [2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster branch: ci-update_translations @@ -61,11 +61,9 @@ jobs: if: github.event_name != 'pull_request' shell: bash env: - STATUS: ${{ steps.create_pr.outputs.pull-request-operation }} + PR_NUMBER: ${{ steps.create_pr.outputs.pull-request-number }} + PR_URL: ${{ steps.create_pr.outputs.pull-request-url }} + STATUS: ${{ case(steps.create_pr.outputs.pull-request-operation == 'none', 'unchanged', steps.create_pr.outputs.pull-request-operation) }} run: | - if [[ "$STATUS" == "none" ]]; then - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY - else - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY - fi - echo "URL: ${{ steps.create_pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY + echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY" + echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml index 5a6ca342b..c4d3f61fb 100644 --- a/.github/workflows/translations-push.yml +++ b/.github/workflows/translations-push.yml @@ -29,12 +29,13 @@ jobs: sudo apt-get install -y --no-install-recommends qttools5-dev-tools - name: "Update Cockatrice translation source" + env: + FILE: cockatrice/cockatrice_en@source.ts id: cockatrice shell: bash - run: | - FILE="cockatrice/cockatrice_en@source.ts" - export DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')" - FILE="$FILE" DIRS="$DIRS" .ci/update_translation_source_strings.sh + run: > + DIRS="cockatrice/src $(find . -maxdepth 1 -type d -name 'libcockatrice_*')" + .ci/update_translation_source_strings.sh - name: "Update Oracle translation source" id: oracle @@ -77,11 +78,9 @@ jobs: if: github.event_name != 'pull_request' shell: bash env: - STATUS: ${{ steps.create_pr.outputs.pull-request-operation }} + PR_NUMBER: ${{ steps.create_pr.outputs.pull-request-number }} + PR_URL: ${{ steps.create_pr.outputs.pull-request-url }} + STATUS: ${{ case(steps.create_pr.outputs.pull-request-operation == 'none', 'unchanged', steps.create_pr.outputs.pull-request-operation) }} run: | - if [[ "$STATUS" == "none" ]]; then - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} unchanged!" >> $GITHUB_STEP_SUMMARY - else - echo "PR #${{ steps.create_pr.outputs.pull-request-number }} $STATUS!" >> $GITHUB_STEP_SUMMARY - fi - echo "URL: ${{ steps.create_pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY + echo "PR #$PR_NUMBER $STATUS!" >> "$GITHUB_STEP_SUMMARY" + echo "URL: $PR_URL" >> "$GITHUB_STEP_SUMMARY" From 80426d77bc6f5ca0d67e9c3927fcbb51e60db670 Mon Sep 17 00:00:00 2001 From: Zach H Date: Sun, 21 Jun 2026 01:09:57 -0400 Subject: [PATCH 38/50] Add Discord account registration monitor (#7013) A read-only cron script that posts new Servatrice account registrations to a Discord channel via webhook. Dedups by an auto-increment id high-water-mark (single-integer state, no duplicates, nothing missed across downtime). Reads DB credentials and the webhook from a servatrice-style ini via --config. --- servatrice/scripts/account_monitor/.gitignore | 5 + servatrice/scripts/account_monitor/README.md | 163 ++++++++ .../account_monitor/account_monitor.py | 350 ++++++++++++++++++ .../scripts/account_monitor/requirements.txt | 1 + 4 files changed, 519 insertions(+) create mode 100644 servatrice/scripts/account_monitor/.gitignore create mode 100644 servatrice/scripts/account_monitor/README.md create mode 100755 servatrice/scripts/account_monitor/account_monitor.py create mode 100644 servatrice/scripts/account_monitor/requirements.txt diff --git a/servatrice/scripts/account_monitor/.gitignore b/servatrice/scripts/account_monitor/.gitignore new file mode 100644 index 000000000..40cd3839a --- /dev/null +++ b/servatrice/scripts/account_monitor/.gitignore @@ -0,0 +1,5 @@ +# Local state - never commit these +state.json +state.json.tmp +venv/ +__pycache__/ diff --git a/servatrice/scripts/account_monitor/README.md b/servatrice/scripts/account_monitor/README.md new file mode 100644 index 000000000..e2c01d0ba --- /dev/null +++ b/servatrice/scripts/account_monitor/README.md @@ -0,0 +1,163 @@ +# Account registration monitor + +Posts a Discord message whenever a new account is registered in Servatrice +(`cockatrice_users`). Each message includes the username, real name (if set), +email, and registration time. + +It runs as a periodic read-only query against the production database. It does +not modify the database and does not touch the running Servatrice process. + +## How it decides what is "new" + +Accounts get an auto-increment `id`, so "new since last time" is just +`id > last_seen_id`. The monitor stores that single high-water-mark id in its +state file. Each run it posts every account above the mark, oldest first, then +advances the mark to the highest id it posted. + +Because the mark only moves forward and an id is posted exactly once, there are +no duplicate messages and nothing is missed, even if the monitor is down for a +while. The state file holds a single number, so it never grows. + +The first run (when no state file exists yet) records the current maximum id as +the baseline and posts nothing. This prevents the entire existing user base from +being dumped into the channel. Only accounts registered after that baseline are +posted. + +If a post to Discord fails, the monitor stops there without advancing the mark +past it, so that account and everything after it are retried on the next run. + +## Privacy note + +Messages contain personal data (real name and email). Discord stores message +content on their servers, so post only to a private channel that the right +people can see, and treat the webhook URL as a secret. It lives in the config +ini alongside the database password, so keep that file readable only by the user +that runs the monitor. + +## Setup + +The monitor reads its database credentials and the webhook from a +servatrice-style ini file passed with `--config` (or the `CONFIG_FILE` env var). +You can point it at your existing `servatrice.ini`, or keep a small separate ini +just for the monitor. + +### 1. Create a read-only database user + +Run as a DB admin. Adjust the host (`'%'` allows any host; restrict it to the +machine running the monitor if you can) and the table prefix if yours is not the +default `cockatrice`. + +```sql +CREATE USER 'account_monitor'@'%' IDENTIFIED BY 'a-strong-password'; +GRANT SELECT (id, name, realname, email, registrationDate) + ON servatrice.cockatrice_users TO 'account_monitor'@'%'; +FLUSH PRIVILEGES; +``` + +Using a read-only user is recommended over pointing `--config` at the real +`servatrice.ini`, because Servatrice's own DB account usually has write access +the monitor does not need. + +### 2. Create the Discord webhook and add it to the config + +In Discord: open the target channel, then Edit Channel -> Integrations -> +Webhooks -> New Webhook. Name it, pick the channel, and copy the webhook URL. + +Add a `[discord]` section with the URL to the ini you will pass to `--config`. +If you want the read-only user above, set the `[database]` section to use it. A +small dedicated `monitor.ini` looks like this: + +```ini +[database] +hostname=127.0.0.1 +database=servatrice +user=account_monitor +password=a-strong-password +prefix=cockatrice + +[discord] +new_user_activation_webhook=https://discord.com/api/webhooks/XXXX/YYYY +``` + +If you would rather use one file, add the `[discord]` section to the real +`servatrice.ini` instead. Servatrice ignores sections it does not use. Note that +Servatrice (a Qt app) rewrites ini values it touches in quoted, backslash-escaped +form, for example `"https\://..."`. The monitor strips that encoding from the +webhook automatically, so either the plain or the escaped form works. + +### 3. Install + +```bash +cd servatrice/scripts/account_monitor +python3 -m venv venv +./venv/bin/pip install -r requirements.txt +``` + +### 4. Verify before scheduling + +```bash +# Confirm the webhook works (sends one test message to the channel) +./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --test-webhook + +# Confirm DB access and see what it would do, without posting or writing state +./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini --dry-run --verbose +``` + +The first real run seeds the baseline and posts nothing: + +```bash +./venv/bin/python ./account_monitor.py --config /path/to/monitor.ini +``` + +After that, test it end to end by registering a throwaway account and confirming +a message appears on the next run. + +## Run it every 2 minutes with cron + +Edit the crontab of the user that owns the script directory (`crontab -e`) and +add one line. This runs the monitor every 2 minutes, using the venv's Python and +your config ini, and appends output to a log: + +```cron +*/2 * * * * cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1 +``` + +Adjust the three paths to your install: the script directory after `cd`, and the +`--config` and log paths. The `*/2` field is what makes it run every 2 minutes; +change it to `*/5` for every 5, and so on. + +By default the high-water-mark is stored in `state.json` next to the script, so +the directory must be writable by the cron user. To put it elsewhere, set +`STATE_FILE`: + +```cron +*/2 * * * * STATE_FILE=/var/lib/account_monitor/state.json cd /opt/cockatrice/servatrice/scripts/account_monitor && ./venv/bin/python ./account_monitor.py --config /etc/servatrice/servatrice.ini >> /var/log/account_monitor.log 2>&1 +``` + +The interval only controls how often it checks; it is not a lookback window, so +a longer interval never causes missed accounts. The query is cheap: an indexed +range scan on the primary key for `id > last_seen`. + +## Options + +- `--config PATH` / `-c PATH` — read DB settings from `[database]` and the webhook from `[discord] new_user_activation_webhook` of a servatrice-style ini (falls back to the `CONFIG_FILE` env var). +- `--dry-run` — query and log what would be posted; no Discord posts, no state write. +- `--test-webhook` — send one test message to the webhook and exit (does not need DB credentials). +- `--verbose` — debug logging. + +## Configuration reference + +Settings come from the `--config` ini, with environment variables available as +overrides if you need them (env takes precedence over the ini). + +| Setting | ini (`--config`) | Environment override | +| --- | --- | --- | +| DB host | `[database] hostname` | `DB_HOST` | +| DB port | `[database] port` (optional) | `DB_PORT` | +| DB name | `[database] database` | `DB_NAME` | +| DB user | `[database] user` | `DB_USER` | +| DB password | `[database] password` | `DB_PASSWORD` | +| Table prefix | `[database] prefix` | `DB_TABLE_PREFIX` | +| Webhook URL | `[discord] new_user_activation_webhook` | `DISCORD_WEBHOOK_URL` | +| DB TLS | — | `DB_SSL` / `DB_SSL_CA` | +| State file path | — | `STATE_FILE` (default: `state.json` next to the script) | diff --git a/servatrice/scripts/account_monitor/account_monitor.py b/servatrice/scripts/account_monitor/account_monitor.py new file mode 100755 index 000000000..47cf23fd3 --- /dev/null +++ b/servatrice/scripts/account_monitor/account_monitor.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +"""Post a Discord message when a new Servatrice account is registered. + +Accounts get an auto-increment `id`, so "what is new since last time" is simply +`id > last_seen_id`. The monitor stores that single high-water-mark id in a +small state file. Each run it posts every account above the mark (oldest +first), then advances the mark. This means no duplicate posts, nothing missed +across downtime, and a state file that never grows (it holds one number). + +On the very first run (no state file yet) it records the current maximum id as +the baseline and posts nothing, so existing users are not dumped into the +channel. From then on only newly-registered accounts are posted. + +Intended to be run on a schedule (cron). Pass a servatrice-style ini with +--config (or CONFIG_FILE) for the database credentials and webhook; see +README.md. +""" + +import argparse +import configparser +import json +import logging +import os +import re +import sys +import time +import urllib.error +import urllib.request + +import pymysql + +log = logging.getLogger("account_monitor") + +# Columns we use. `id` drives the high-water-mark; `name` is the login/username, +# `realname` is the optional display name, `registrationDate` is when the account +# row was created. +NEW_ACCOUNTS_QUERY = ( + "SELECT id, name, realname, email, registrationDate " + "FROM `{prefix}_users` WHERE id > %s ORDER BY id ASC" +) + +EMBED_COLOR = 0x5865F2 # discord blurple +DISCORD_MAX_EMBED_FIELD = 1024 +POST_DELAY_SECONDS = 1.0 # gap between webhook posts to stay under rate limits +MAX_RATELIMIT_RETRIES = 5 + + +def _clean_ini_value(value): + """Undo Qt QSettings ini encoding of a value. + + Qt apps (including Servatrice) write ini values that contain special + characters wrapped in double quotes and backslash-escaped, e.g. a webhook + URL stored as "https\\://...". configparser returns that text literally, so + strip the wrapping quotes and remove the backslash escapes. This is applied + to the webhook URL only, where it is safe (URLs contain no quotes or + backslashes); DB values are left untouched so passwords are never altered. + """ + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": + value = value[1:-1] + return re.sub(r"\\(.)", r"\1", value) + + +def load_config_file(path): + """Read DB and Discord settings from a servatrice-style ini. + + Pulls the [database] section (hostname/database/user/password/prefix/port) + and the Discord webhook from [discord] new_user_activation_webhook. Returns + a dict using this module's internal config keys; only keys actually present + (and non-empty) in the file are returned, so missing values fall back to + defaults or the environment. + """ + # interpolation=None so a '%' in a password is not treated as a token. + parser = configparser.ConfigParser(interpolation=None) + if not parser.read(path): + log.error("Config file not found or unreadable: %s", path) + sys.exit(2) + + result = {} + if parser.has_section("database"): + db = parser["database"] + db_mapping = { + "hostname": "db_host", + "database": "db_name", + "user": "db_user", + "password": "db_password", + "prefix": "db_prefix", + "port": "db_port", # not in stock servatrice.ini, but honored if present + } + result.update({cfg_key: db[ini_key] for ini_key, cfg_key in db_mapping.items() if db.get(ini_key)}) + + if parser.has_section("discord") and parser["discord"].get("new_user_activation_webhook"): + result["webhook_url"] = _clean_ini_value(parser["discord"]["new_user_activation_webhook"]) + + return result + + +def get_config(config_path=None, require_db=True): + """Build configuration from defaults, an optional ini file, then env vars. + + Precedence, highest first: environment variables, the ini file, built-in + defaults. Database credentials and the Discord webhook may come from either + the ini file or the environment; the state file is environment-only. + """ + cfg = { + "db_host": "localhost", + "db_port": 3306, + "db_name": "servatrice", + "db_user": None, + "db_password": None, + "db_prefix": "cockatrice", + "db_ssl": False, + "db_ssl_ca": None, + "webhook_url": None, + "state_file": os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"), + } + + if config_path: + cfg.update(load_config_file(config_path)) + + env_map = { + "DB_HOST": "db_host", + "DB_PORT": "db_port", + "DB_NAME": "db_name", + "DB_USER": "db_user", + "DB_PASSWORD": "db_password", + "DB_TABLE_PREFIX": "db_prefix", + "DB_SSL_CA": "db_ssl_ca", + "DISCORD_WEBHOOK_URL": "webhook_url", + "STATE_FILE": "state_file", + } + for env_key, cfg_key in env_map.items(): + if os.environ.get(env_key): + cfg[cfg_key] = os.environ[env_key] + if os.environ.get("DB_SSL"): + cfg["db_ssl"] = os.environ["DB_SSL"].lower() in ("1", "true", "yes") + + cfg["db_port"] = int(cfg["db_port"]) + + required = {"webhook_url": "DISCORD_WEBHOOK_URL or [discord] new_user_activation_webhook"} + if require_db: + required["db_user"] = "DB_USER or [database] user" + required["db_password"] = "DB_PASSWORD or [database] password" + missing = [label for key, label in required.items() if not cfg[key]] + if missing: + log.error("Missing required configuration: %s", "; ".join(missing)) + sys.exit(2) + + if cfg["webhook_url"] and not cfg["webhook_url"].lower().startswith(("http://", "https://")): + log.error("Webhook URL does not look like an http(s) URL: %r", cfg["webhook_url"]) + sys.exit(2) + return cfg + + +def connect(cfg): + """Open a read-only connection to the Servatrice database.""" + ssl = None + if cfg["db_ssl"]: + ssl = {"ca": cfg["db_ssl_ca"]} if cfg["db_ssl_ca"] else {} + return pymysql.connect( + host=cfg["db_host"], + port=cfg["db_port"], + user=cfg["db_user"], + password=cfg["db_password"], + database=cfg["db_name"], + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + connect_timeout=15, + read_timeout=30, + ssl=ssl, + ) + + +def fetch_max_id(conn, prefix): + """Return the highest account id currently in the table, or 0 if empty.""" + with conn.cursor() as cur: + cur.execute("SELECT MAX(id) AS max_id FROM `{prefix}_users`".format(prefix=prefix)) + row = cur.fetchone() + return int(row["max_id"]) if row and row["max_id"] is not None else 0 + + +def fetch_new_accounts(conn, prefix, last_id): + """Return detail rows for accounts with id > last_id, oldest first.""" + with conn.cursor() as cur: + cur.execute(NEW_ACCOUNTS_QUERY.format(prefix=prefix), (last_id,)) + return cur.fetchall() + + +def load_state(path): + """Load the high-water-mark id. Returns (last_id, is_first_run).""" + if not os.path.exists(path): + return 0, True + with open(path, "r", encoding="utf-8") as fh: + data = json.load(fh) + return int(data.get("last_id", 0)), False + + +def save_state(path, last_id): + """Atomically persist the high-water-mark id.""" + directory = os.path.dirname(path) + if directory: + os.makedirs(directory, exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as fh: + json.dump({"version": 2, "last_id": int(last_id)}, fh) + os.replace(tmp, path) + + +def build_embed(row): + """Build a Discord embed dict for one newly-registered account.""" + fields = [ + {"name": "Username", "value": str(row["name"]) or "(none)", "inline": False}, + ] + realname = (row.get("realname") or "").strip() + if realname: + fields.append({"name": "Real name", "value": realname[:DISCORD_MAX_EMBED_FIELD], "inline": False}) + fields.append({"name": "Email", "value": str(row.get("email") or "(none)"), "inline": False}) + reg = row.get("registrationDate") + fields.append({"name": "Reg time", "value": str(reg) if reg is not None else "(unknown)", "inline": False}) + return { + "title": "New account registered", + "color": EMBED_COLOR, + "fields": fields, + } + + +def post_embed(webhook_url, embed): + """POST a single embed to the Discord webhook, honoring 429 rate limits.""" + payload = json.dumps({"embeds": [embed]}).encode("utf-8") + for attempt in range(MAX_RATELIMIT_RETRIES): + req = urllib.request.Request( + webhook_url, + data=payload, + headers={"Content-Type": "application/json", "User-Agent": "servatrice-account-monitor/1.0"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + if resp.status in (200, 204): + return True + log.warning("Unexpected Discord status %s", resp.status) + return False + except urllib.error.HTTPError as err: + if err.code == 429: + retry_after = _retry_after_seconds(err) + log.warning("Rate limited by Discord; sleeping %.2fs", retry_after) + time.sleep(retry_after) + continue + log.error("Discord webhook HTTP %s: %s", err.code, err.read().decode("utf-8", "replace")[:500]) + return False + except urllib.error.URLError as err: + log.error("Discord webhook connection error: %s", err) + return False + log.error("Gave up posting after %d rate-limit retries", MAX_RATELIMIT_RETRIES) + return False + + +def _retry_after_seconds(err): + """Extract the retry delay (seconds) from a Discord 429 response.""" + header = err.headers.get("Retry-After") + if header: + try: + return float(header) + except ValueError: + pass + try: + body = json.loads(err.read().decode("utf-8", "replace")) + return float(body.get("retry_after", 1.0)) + except (ValueError, json.JSONDecodeError): + return 1.0 + + +def main(): + parser = argparse.ArgumentParser(description="Post new Servatrice account registrations to Discord.") + parser.add_argument( + "--config", "-c", + help="Path to a servatrice-style ini; reads DB settings from its [database] " + "section (hostname/database/user/password/prefix) and the webhook from " + "[discord] new_user_activation_webhook. Defaults to the CONFIG_FILE env " + "var if set.", + ) + parser.add_argument("--dry-run", action="store_true", help="Log what would be posted; do not post or write state.") + parser.add_argument("--test-webhook", action="store_true", help="Send a single test message to the webhook and exit.") + parser.add_argument("--verbose", action="store_true", help="Enable debug logging.") + args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + ) + + config_path = args.config or os.environ.get("CONFIG_FILE") + + if args.test_webhook: + cfg = get_config(config_path, require_db=False) + ok = post_embed( + cfg["webhook_url"], + {"title": "Account monitor test", "color": EMBED_COLOR, + "description": "If you can see this, the webhook is configured correctly."}, + ) + sys.exit(0 if ok else 1) + + cfg = get_config(config_path) + + try: + conn = connect(cfg) + except pymysql.MySQLError as err: + log.error("Database connection failed: %s", err) + sys.exit(1) + + try: + last_id, first_run = load_state(cfg["state_file"]) + + if first_run: + baseline = fetch_max_id(conn, cfg["db_prefix"]) + log.info("First run: seeding high-water-mark at id=%d; posting nothing.", baseline) + if not args.dry_run: + save_state(cfg["state_file"], baseline) + return + + rows = fetch_new_accounts(conn, cfg["db_prefix"], last_id) + if not rows: + log.info("No new accounts since id=%d.", last_id) + return + + log.info("Found %d new account(s) since id=%d.", len(rows), last_id) + + # Post oldest first. Advance the mark only past accounts we successfully + # posted; on the first failure, stop so nothing after it is posted out of + # order or skipped. The failed account (and the rest) retry next run. + for row in rows: + if args.dry_run: + log.info("[dry-run] would post: id=%s name=%s email=%s reg=%s", + row["id"], row["name"], row.get("email"), row.get("registrationDate")) + continue + if not post_embed(cfg["webhook_url"], build_embed(row)): + log.error("Failed to post account id=%s; stopping. Will retry from here next run.", row["id"]) + break + last_id = row["id"] + log.info("Posted account id=%s (%s)", row["id"], row["name"]) + time.sleep(POST_DELAY_SECONDS) + + if not args.dry_run: + save_state(cfg["state_file"], last_id) + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/servatrice/scripts/account_monitor/requirements.txt b/servatrice/scripts/account_monitor/requirements.txt new file mode 100644 index 000000000..24d93c7a7 --- /dev/null +++ b/servatrice/scripts/account_monitor/requirements.txt @@ -0,0 +1 @@ +PyMySQL==1.2.0 From c9ebdb451f886408aed55086d522d5e876e5e673 Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 26 Jun 2026 16:39:33 +0200 Subject: [PATCH 39/50] CI: Avoid failing tx step if secret can not be accessed (#7012) --- .github/workflows/translations-pull.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index 57df31bf0..a3db5f86d 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -23,6 +23,8 @@ jobs: uses: actions/checkout@v7 - name: "Pull translated strings from Transifex" + # Do not run this step for PR's from forks, they don't have access to the secret + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false uses: transifex/cli-action@v2 with: # Used config file: https://github.com/Cockatrice/Cockatrice/blob/master/.tx/config From 2914874720ebb697c57bcd5dd23485d6d7ec0555 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:52:24 -0400 Subject: [PATCH 40/50] [Room][UserList] Introduce style delegate (#6981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Room] Additionally show a tab for friends and ignored users instead of just all online users. Took 21 minutes Took 12 minutes * [Room][UserList] Introduce style delegate for user list - Allow users to set a card name and parameters as their background banner - Allow mods to white/blacklist cards - Allow toggling back to the old display style Took 7 minutes Took 28 seconds Took 2 minutes Took 2 minutes * Right checkstate. Took 14 minutes Took 2 minutes * Utility for test. Took 9 minutes Took 8 seconds Took 2 seconds * Lint. Took 10 minutes * Algorithm for sql schema migration Took 13 minutes * Use {prefix}, bound card name, return errors. Took 27 seconds * Convert queue to while loop. Took 19 seconds * Hover popup. Took 36 minutes Took 1 minute * More granular signals, popup for user info. Took 25 minutes Took 8 seconds Took 16 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 7 + .../src/client/settings/cache_settings.cpp | 8 + .../src/client/settings/cache_settings.h | 7 + .../server/user/user_avatar_provider.cpp | 48 ++ .../server/user/user_avatar_provider.h | 30 + .../server/user/user_card_art_provider.cpp | 146 ++++ .../server/user/user_card_art_provider.h | 39 ++ .../server/user/user_card_settings_dialog.cpp | 264 +++++++ .../server/user/user_card_settings_dialog.h | 70 ++ .../widgets/server/user/user_context_menu.cpp | 110 +++ .../widgets/server/user/user_context_menu.h | 21 + .../widgets/server/user/user_info_box.cpp | 51 +- .../widgets/server/user/user_info_box.h | 7 +- .../widgets/server/user/user_info_popup.cpp | 656 ++++++++++++++++++ .../widgets/server/user/user_info_popup.h | 181 +++++ .../widgets/server/user/user_list_manager.cpp | 89 ++- .../widgets/server/user/user_list_manager.h | 30 +- .../widgets/server/user/user_list_painter.cpp | 342 +++++++++ .../widgets/server/user/user_list_painter.h | 86 +++ .../widgets/server/user/user_list_widget.cpp | 489 ++++++++++++- .../widgets/server/user/user_list_widget.h | 44 +- .../appearance_settings_page.cpp | 13 + .../settings_page/appearance_settings_page.h | 2 + .../widgets/tabs/tab_card_art_rules.cpp | 246 +++++++ .../widgets/tabs/tab_card_art_rules.h | 89 +++ .../src/interface/widgets/tabs/tab_room.cpp | 22 +- .../src/interface/widgets/tabs/tab_room.h | 2 + .../interface/widgets/tabs/tab_supervisor.cpp | 31 + .../interface/widgets/tabs/tab_supervisor.h | 9 +- .../network/server/remote/server.cpp | 19 + .../network/server/remote/server.h | 1 + .../libcockatrice/protocol/pb/CMakeLists.txt | 1 + .../protocol/pb/moderator_commands.proto | 27 + .../libcockatrice/protocol/pb/response.proto | 1 + .../pb/response_card_art_rule_entry.proto | 15 + .../protocol/pb/serverinfo_user.proto | 11 +- .../protocol/pb/session_commands.proto | 12 + libcockatrice_utility/CMakeLists.txt | 1 + .../utility/days_years_between.h | 5 + .../migrations/servatrice_0034_to_0035.sql | 18 + servatrice/servatrice.sql | 18 +- .../src/servatrice_database_interface.cpp | 28 +- .../src/servatrice_database_interface.h | 2 +- servatrice/src/serversocketinterface.cpp | 167 +++++ servatrice/src/serversocketinterface.h | 5 + 45 files changed, 3387 insertions(+), 83 deletions(-) create mode 100644 cockatrice/src/interface/widgets/server/user/user_avatar_provider.cpp create mode 100644 cockatrice/src/interface/widgets/server/user/user_avatar_provider.h create mode 100644 cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp create mode 100644 cockatrice/src/interface/widgets/server/user/user_card_art_provider.h create mode 100644 cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp create mode 100644 cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h create mode 100644 cockatrice/src/interface/widgets/server/user/user_info_popup.cpp create mode 100644 cockatrice/src/interface/widgets/server/user/user_info_popup.h create mode 100644 cockatrice/src/interface/widgets/server/user/user_list_painter.cpp create mode 100644 cockatrice/src/interface/widgets/server/user/user_list_painter.h create mode 100644 cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h create mode 100644 libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto create mode 100644 servatrice/migrations/servatrice_0034_to_0035.sql diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index bd99d08bf..18679664b 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -236,10 +236,14 @@ set(cockatrice_SOURCES src/interface/widgets/server/handle_public_servers.cpp src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp src/interface/widgets/server/remote/remote_replay_list_tree_widget.cpp + src/interface/widgets/server/user/user_avatar_provider.cpp + src/interface/widgets/server/user/user_card_art_provider.cpp + src/interface/widgets/server/user/user_card_settings_dialog.cpp src/interface/widgets/server/user/user_context_menu.cpp src/interface/widgets/server/user/user_info_box.cpp src/interface/widgets/server/user/user_info_connection.cpp src/interface/widgets/server/user/user_list_manager.cpp + src/interface/widgets/server/user/user_list_painter.cpp src/interface/widgets/server/user/user_list_widget.cpp src/interface/widgets/settings_page/appearance_settings_page.cpp src/interface/widgets/settings_page/deck_editor_settings_page.cpp @@ -327,6 +331,7 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/tab.cpp src/interface/widgets/tabs/tab_account.cpp src/interface/widgets/tabs/tab_admin.cpp + src/interface/widgets/tabs/tab_card_art_rules.cpp src/interface/widgets/tabs/tab_deck_editor.cpp src/interface/widgets/tabs/tab_deck_storage.cpp src/interface/widgets/tabs/tab_game.cpp @@ -349,6 +354,8 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h src/interface/widgets/utility/compact_push_button.cpp src/interface/widgets/utility/compact_push_button.h + src/interface/widgets/server/user/user_info_popup.cpp + src/interface/widgets/server/user/user_info_popup.h ) add_subdirectory(sounds) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index cc34e1707..28e5eb187 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -371,6 +371,7 @@ SettingsCache::SettingsCache() openDeckInNewTab = settings->value("editor/openDeckInNewTab", false).toBool(); rewindBufferingMs = settings->value("replay/rewindBufferingMs", 200).toInt(); + styleUserList = settings->value("appearance/styleUserList", true).toBool(); chatMention = settings->value("chat/mention", true).toBool(); chatMentionCompleter = settings->value("chat/mentioncompleter", true).toBool(); chatMentionForeground = settings->value("chat/mentionforeground", true).toBool(); @@ -1045,6 +1046,13 @@ void SettingsCache::setRewindBufferingMs(int _rewindBufferingMs) settings->setValue("replay/rewindBufferingMs", rewindBufferingMs); } +void SettingsCache::setStyleUserList(QT_STATE_CHANGED_T _styleUserList) +{ + styleUserList = static_cast(_styleUserList); + settings->setValue("appearance/styleUserList", styleUserList); + emit styleUserListChanged(); +} + void SettingsCache::setChatMention(QT_STATE_CHANGED_T _chatMention) { chatMention = static_cast(_chatMention); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index a166917c1..5a5e0c546 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -190,6 +190,7 @@ signals: void cardPictureLoaderCacheMethodChanged(int cardPictureLoaderCacheMethod); void localCardImageStorageNamingSchemeChanged(int localCardImageStorageNamingScheme); void masterVolumeChanged(int value); + void styleUserListChanged(); void chatMentionCompleterChanged(); void downloadSpoilerTimeIndexChanged(); void downloadSpoilerStatusChanged(); @@ -284,6 +285,7 @@ private: bool autoRotateSidewaysLayoutCards; bool openDeckInNewTab; int rewindBufferingMs; + bool styleUserList; bool chatMention; bool chatMentionCompleter; QString chatMentionColor; @@ -738,6 +740,10 @@ public: { return rewindBufferingMs; } + [[nodiscard]] bool getStyleUserList() const + { + return styleUserList; + } [[nodiscard]] bool getChatMention() const { return chatMention; @@ -1113,6 +1119,7 @@ public slots: void setAutoRotateSidewaysLayoutCards(QT_STATE_CHANGED_T _autoRotateSidewaysLayoutCards); void setOpenDeckInNewTab(QT_STATE_CHANGED_T _openDeckInNewTab); void setRewindBufferingMs(int _rewindBufferingMs); + void setStyleUserList(QT_STATE_CHANGED_T _styleUserList); void setChatMention(QT_STATE_CHANGED_T _chatMention); void setChatMentionCompleter(QT_STATE_CHANGED_T _chatMentionCompleter); void setChatMentionForeground(QT_STATE_CHANGED_T _chatMentionForeground); diff --git a/cockatrice/src/interface/widgets/server/user/user_avatar_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.cpp new file mode 100644 index 000000000..c115caa47 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.cpp @@ -0,0 +1,48 @@ +#include "user_avatar_provider.h" + +#include +#include +#include + +UserAvatarProvider::UserAvatarProvider(AbstractClient *client, QObject *parent) : QObject(parent), client(client) +{ +} + +const QMap &UserAvatarProvider::cache() const +{ + return avatarCache; +} + +void UserAvatarProvider::requestAvatar(const QString &userName) +{ + if (avatarCache.contains(userName) || pending.contains(userName)) { + return; + } + + pending.insert(userName); + + Command_GetUserInfo cmd; + cmd.set_user_name(userName.toStdString()); + + PendingCommand *pend = client->prepareSessionCommand(cmd); + + connect(pend, &PendingCommand::finished, this, [this, userName](const Response &r) { + pending.remove(userName); + + const auto &response = r.GetExtension(Response_GetUserInfo::ext); + const auto &user = response.user_info(); + const std::string &bmp = user.avatar_bmp(); + + QPixmap avatar; + if (!bmp.empty() && + avatar.loadFromData(reinterpret_cast(bmp.data()), static_cast(bmp.size()))) { + avatarCache.insert(userName, avatar); + } else { + avatarCache.insert(userName, QPixmap()); + } + + emit avatarUpdated(userName); + }); + + client->sendCommand(pend); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_avatar_provider.h b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.h new file mode 100644 index 000000000..44491e544 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_avatar_provider.h @@ -0,0 +1,30 @@ +#ifndef COCKATRICE_USER_AVATAR_PROVIDER_H +#define COCKATRICE_USER_AVATAR_PROVIDER_H + +#include +#include +#include +#include + +class AbstractClient; + +class UserAvatarProvider : public QObject +{ + Q_OBJECT + +public: + explicit UserAvatarProvider(AbstractClient *client, QObject *parent = nullptr); + + void requestAvatar(const QString &userName); + const QMap &cache() const; + +signals: + void avatarUpdated(const QString &userName); + +private: + AbstractClient *client; + QMap avatarCache; + QSet pending; +}; + +#endif // COCKATRICE_USER_AVATAR_PROVIDER_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp new file mode 100644 index 000000000..70a56375e --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -0,0 +1,146 @@ +#include "user_card_art_provider.h" + +#include "../../../card_picture_loader/card_picture_loader.h" + +#include +#include + +static QString makeKey(const QString &user, const QString &card) +{ + return user + u'|' + card; +} + +UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent) +{ + dbReady = (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok); + + if (!dbReady) { + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, + &UserCardArtProvider::onDatabaseReady); + } +} + +void UserCardArtProvider::onDatabaseReady() +{ + dbReady = true; + processQueue(); +} + +const QMap &UserCardArtProvider::cache() const +{ + return cardArtCache; +} + +void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName) +{ + if (cardName.isEmpty()) { + return; + } + + const QString key = makeKey(userName, cardName); + + if (cardArtCache.contains(key) || pending.contains(key)) { + return; + } + + pending.insert(key); + queue.enqueue(key); + + processQueue(); +} + +QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes) +{ + const QSize sz = fullRes.size(); + const int marginX = sz.width() * 0.07; + const int topMargin = sz.height() * 0.11; + const int bottomMargin = sz.height() * 0.45; + + const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); + + return fullRes.copy(foilRect.intersected(fullRes.rect())); +} + +void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap) +{ + if (!cardArtCache.contains(key)) { + cacheInsertionOrder.append(key); + while (cacheInsertionOrder.size() > MaxCacheEntries) { + const QString evicted = cacheInsertionOrder.takeFirst(); + cardArtCache.remove(evicted); + } + } + cardArtCache.insert(key, pixmap); +} + +void UserCardArtProvider::processQueue() +{ + if (!dbReady) { + return; + } + + while (!queue.isEmpty()) { + const QString key = queue.dequeue(); + + const QStringList parts = key.split(u'|'); + if (parts.size() != 2) { + pending.remove(key); + continue; + } + + const QString userName = parts.at(0); + const QString cardName = parts.at(1); + + ExactCard card = CardDatabaseManager::query()->getCard({cardName}); + + if (!card) { + pending.remove(key); + continue; + } + + QPixmap fullRes; + CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); + + // Synchronous hit (already loaded/on disk) + if (!fullRes.isNull()) { + insertIntoCache(key, cropCardArt(fullRes)); + pending.remove(key); + + emit cardArtUpdated(userName); + continue; + } + + // Async load required. + QPointer self(this); + + auto conn = std::make_shared(); + + *conn = connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, + [self, key, userName, card, conn]() mutable { + if (!self) { + return; + } + + QObject::disconnect(*conn); + + QPixmap fullRes; + CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); + + if (!fullRes.isNull()) { + self->insertIntoCache(key, self->cropCardArt(fullRes)); + } else { + self->insertIntoCache(key, QPixmap()); + } + + self->pending.remove(key); + + emit self->cardArtUpdated(userName); + + // Resume processing remaining queued items. + self->processQueue(); + }); + + // Stop here. We'll continue when the async load finishes. + return; + } +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h new file mode 100644 index 000000000..a3ab874b7 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -0,0 +1,39 @@ +#ifndef COCKATRICE_USER_CARD_ART_PROVIDER_H +#define COCKATRICE_USER_CARD_ART_PROVIDER_H + +#include +#include +#include +#include +#include + +class UserCardArtProvider : public QObject +{ + Q_OBJECT + +public: + explicit UserCardArtProvider(QObject *parent = nullptr); + + void requestCardArt(const QString &userName, const QString &cardName); + const QMap &cache() const; + static QPixmap cropCardArt(const QPixmap &fullRes); + +signals: + void cardArtUpdated(const QString &userName); + +public slots: + void onDatabaseReady(); + +private: + bool dbReady = false; + static constexpr int MaxCacheEntries = 300; + QList cacheInsertionOrder; // FIFO eviction + QMap cardArtCache; + QSet pending; + QQueue queue; + + void processQueue(); + void insertIntoCache(const QString &key, const QPixmap &pixmap); +}; + +#endif // COCKATRICE_USER_CARD_ART_PROVIDER_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp new file mode 100644 index 000000000..335ee097e --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -0,0 +1,264 @@ +#include "user_card_settings_dialog.h" + +#include "../../../card_picture_loader/card_picture_loader.h" +#include "card/card_completer_proxy_model.h" +#include "card/card_search_model.h" +#include "card_database_display_model.h" +#include "card_database_model.h" +#include "user_card_art_provider.h" +#include "user_list_painter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CardArtPreviewWidget::CardArtPreviewWidget(QWidget *parent) : QWidget(parent) +{ + setMinimumSize(400, 72); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +void CardArtPreviewWidget::setPixmap(const QPixmap &pixmap) +{ + sourcePixmap = pixmap; + update(); +} + +void CardArtPreviewWidget::setParams(const CardArtParams &p) +{ + params = p; + update(); +} + +void CardArtPreviewWidget::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); + + const QRect rect = this->rect(); + + const QColor accentColor(100, 116, 139); + const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); + + QLinearGradient bg(cardRect.topLeft(), cardRect.topRight()); + bg.setColorAt(0, accentColor.darker(320)); + bg.setColorAt(1, QColor(18, 22, 30)); + painter.setPen(Qt::NoPen); + painter.setBrush(bg); + painter.drawRoundedRect(cardRect, 6, 6); + painter.setBrush(accentColor); + painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); + + if (sourcePixmap.isNull()) { + painter.setPen(QColor(150, 150, 150)); + painter.drawText(rect, Qt::AlignCenter, tr("No card selected")); + return; + } + + UserListPainter::drawCardArt(&painter, rect, rect.right() - 4, + QString(), // userName not needed for override path + nullptr, // no cache + params, + &sourcePixmap // 👈 direct pixmap + ); + + // Avatar placeholder so the left-margin interaction is visible + const int avatarX = rect.left() + 14; + const int avatarY = rect.top() + (rect.height() - 36) / 2; + const QRect avatarRect(avatarX, avatarY, 36, 36); + + QPainterPath clip; + clip.addEllipse(avatarRect); + painter.save(); + painter.setClipPath(clip); + painter.setBrush(accentColor.darker(200)); + painter.setPen(Qt::NoPen); + painter.drawEllipse(avatarRect); + painter.restore(); + + painter.setPen(QPen(QColor(70, 80, 95), 2)); + painter.setBrush(Qt::NoBrush); + painter.drawEllipse(avatarRect.adjusted(-1, -1, 1, 1)); +} + +UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initial, QWidget *parent) + : QDialog(parent), currentParams(initial) +{ + setWindowTitle(tr("Card Art Settings")); + setMinimumWidth(500); + setupUi(); + + // Seed UI from initial params + if (!initial.cardName.isEmpty()) { + searchBar->setText(initial.cardName); + onCardNameChanged(initial.cardName); + } + marginLSpin->setValue(initial.marginPctL); + marginRSpin->setValue(initial.marginPctR); + verticalOffsetSpin->setValue(initial.verticalOffset); + zoomSpin->setValue(initial.zoom); +} + +CardArtParams UserCardArtSettingsDialog::params() const +{ + return currentParams; +} + +QDoubleSpinBox *UserCardArtSettingsDialog::makeSpinBox(double min, double max, double value, double step) +{ + auto *spin = new QDoubleSpinBox; + spin->setRange(min, max); + spin->setSingleStep(step); + spin->setDecimals(3); + spin->setValue(value); + return spin; +} + +void UserCardArtSettingsDialog::initializeSearchBar() +{ + searchBar = new QLineEdit; + searchBar->setPlaceholderText(tr("Type a card name...")); + + cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); + cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); + searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); + + proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(15); + searchBar->setCompleter(completer); + + connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) { + const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + completer->complete(); + } + }); + + connect(completer, static_cast(&QCompleter::activated), this, + [this](const QString &completion) { + if (searchBar->text() != completion) { + searchBar->setText(completion); + searchBar->setCursorPosition(searchBar->text().length()); + } + onCardNameChanged(completion); + }); + + // Also trigger a load when the user hits Return on a typed name + connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); }); +} + +void UserCardArtSettingsDialog::setupUi() +{ + initializeSearchBar(); + + marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01); + marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01); + verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01); + zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05); + + auto *form = new QFormLayout; + form->addRow(tr("Card name:"), searchBar); + form->addRow(tr("Left margin (%):"), marginLSpin); + form->addRow(tr("Right margin (%):"), marginRSpin); + form->addRow(tr("Vertical offset:"), verticalOffsetSpin); + form->addRow(tr("Zoom:"), zoomSpin); + + auto *controlsGroup = new QGroupBox(tr("Parameters")); + controlsGroup->setLayout(form); + + preview = new CardArtPreviewWidget; + + auto *previewLayout = new QVBoxLayout; + previewLayout->addWidget(preview); + auto *previewGroup = new QGroupBox(tr("Preview")); + previewGroup->setLayout(previewLayout); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + auto *removeBtn = new QPushButton(tr("Remove Banner Card")); + buttons->addButton(removeBtn, QDialogButtonBox::ResetRole); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(removeBtn, &QPushButton::clicked, this, [this]() { + currentParams = CardArtParams{}; // empty cardName signals removal + accept(); + }); + + auto *root = new QVBoxLayout; + root->addWidget(controlsGroup); + root->addWidget(previewGroup); + root->addWidget(buttons); + setLayout(root); + + connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); + connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); + connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); + connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); +} + +void UserCardArtSettingsDialog::onCardNameChanged(const QString &name) +{ + if (name.isEmpty()) { + currentPixmap = QPixmap(); + preview->setPixmap(currentPixmap); + return; + } + + const ExactCard card = CardDatabaseManager::query()->getCard({name}); + if (!card) { + currentPixmap = QPixmap(); + preview->setPixmap(currentPixmap); + return; + } + + currentParams.cardName = name; + + QPixmap fullRes; + CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); + + if (fullRes.isNull()) { + connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this, card](const PrintingInfo &) { + disconnect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, nullptr); + QPixmap loaded; + CardPictureLoader::getPixmap(loaded, card, QSize(745, 1040)); + currentPixmap = UserCardArtProvider::cropCardArt(loaded); + preview->setPixmap(currentPixmap); + }); + return; + } + + currentPixmap = UserCardArtProvider::cropCardArt(fullRes); + preview->setPixmap(currentPixmap); +} + +void UserCardArtSettingsDialog::onParamChanged() +{ + currentParams.marginPctL = marginLSpin->value(); + currentParams.marginPctR = marginRSpin->value(); + currentParams.verticalOffset = verticalOffsetSpin->value(); + currentParams.zoom = zoomSpin->value(); + preview->setParams(currentParams); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h new file mode 100644 index 000000000..cac26c919 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h @@ -0,0 +1,70 @@ +#ifndef COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H +#define COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H + +#include "user_list_painter.h" + +#include +#include + +class QCompleter; +class QLineEdit; +class QDoubleSpinBox; +class CardDatabaseModel; +class CardDatabaseDisplayModel; +class CardSearchModel; +class CardCompleterProxyModel; + +class CardArtPreviewWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CardArtPreviewWidget(QWidget *parent = nullptr); + + void setPixmap(const QPixmap &pixmap); + void setParams(const CardArtParams ¶ms); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QPixmap sourcePixmap; + CardArtParams params; +}; + +class UserCardArtSettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit UserCardArtSettingsDialog(const CardArtParams &initial = {}, QWidget *parent = nullptr); + + CardArtParams params() const; + +private slots: + void onCardNameChanged(const QString &name); + void onParamChanged(); + +private: + void setupUi(); + void initializeSearchBar(); + QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step); + + QLineEdit *searchBar; + QCompleter *completer; + CardDatabaseModel *cardDatabaseModel; + CardDatabaseDisplayModel *cardDatabaseDisplayModel; + CardSearchModel *searchModel; + CardCompleterProxyModel *proxyModel; + + QDoubleSpinBox *marginLSpin; + QDoubleSpinBox *marginRSpin; + QDoubleSpinBox *verticalOffsetSpin; + QDoubleSpinBox *zoomSpin; + CardArtPreviewWidget *preview; + + QPixmap currentPixmap; + CardArtParams currentParams; +}; + +#endif // COCKATRICE_USER_CARD_ART_SETTINGS_DIALOG_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index faa96fa1f..11fd02d80 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -542,3 +542,113 @@ void UserContextMenu::showContextMenu(const QPoint &pos, delete menu; } + +void UserContextMenu::execChat(const QString &userName) +{ + emit openMessageDialog(userName, true); +} + +void UserContextMenu::execDetails(const QString &userName) +{ + auto *w = new UserInfoBox(client, false, static_cast(parent()), + Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint); + w->setAttribute(Qt::WA_DeleteOnClose); + w->updateInfo(userName); +} + +void UserContextMenu::execShowGames(const QString &userName) +{ + Command_GetGamesOfUser cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::gamesOfUserReceived); + client->sendCommand(pend); +} + +void UserContextMenu::execAddToBuddy(const QString &userName) +{ + Command_AddToList cmd; + cmd.set_list("buddy"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execRemoveFromBuddy(const QString &userName) +{ + Command_RemoveFromList cmd; + cmd.set_list("buddy"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execAddToIgnore(const QString &userName) +{ + Command_AddToList cmd; + cmd.set_list("ignore"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execRemoveFromIgnore(const QString &userName) +{ + Command_RemoveFromList cmd; + cmd.set_list("ignore"); + cmd.set_user_name(userName.toStdString()); + client->sendCommand(client->prepareSessionCommand(cmd)); +} + +void UserContextMenu::execBan(const QString &userName) +{ + Command_GetUserInfo cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUser_processUserInfoResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execWarn(const QString &userName) +{ + Command_GetUserInfo cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUser_processUserInfoResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execBanHistory(const QString &userName) +{ + Command_GetBanHistory cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUserHistory_processResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execWarnHistory(const QString &userName) +{ + Command_GetWarnHistory cmd; + cmd.set_user_name(userName.toStdString()); + PendingCommand *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUserHistory_processResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execAdminNotes(const QString &userName) +{ + Command_GetAdminNotes cmd; + cmd.set_user_name(userName.toStdString()); + auto *pend = client->prepareModeratorCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::getAdminNotes_processResponse); + client->sendCommand(pend); +} + +void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge) +{ + Command_AdjustMod cmd; + cmd.set_user_name(userName.toStdString()); + cmd.set_should_be_mod(shouldBeMod); + cmd.set_should_be_judge(shouldBeJudge); + PendingCommand *pend = client->prepareAdminCommand(cmd); + connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse); + client->sendCommand(pend); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h index b0ff89816..28173bfbc 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h @@ -74,6 +74,27 @@ public: int playerId, const QString &deckHash, ChatView *chatView = nullptr); + + const UserListProxy *getUserListProxy() const + { + return userListProxy; + } + + // Individual action entry points — used by UserInfoPopup to trigger + // actions without re-running the full context menu flow. + void execChat(const QString &userName); + void execDetails(const QString &userName); + void execShowGames(const QString &userName); + void execAddToBuddy(const QString &userName); + void execRemoveFromBuddy(const QString &userName); + void execAddToIgnore(const QString &userName); + void execRemoveFromIgnore(const QString &userName); + void execBan(const QString &userName); + void execWarn(const QString &userName); + void execBanHistory(const QString &userName); + void execWarnHistory(const QString &userName); + void execAdminNotes(const QString &userName); + void execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge); }; #endif diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index e41ae6e75..e6cf38787 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -5,6 +5,7 @@ #include "../../interface/widgets/dialogs/dlg_edit_password.h" #include "../../interface/widgets/dialogs/dlg_edit_user.h" #include "../../interface/widgets/utility/get_text_with_max.h" +#include "user_card_settings_dialog.h" #include #include @@ -61,11 +62,13 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren buttonsLayout->addWidget(&editButton); buttonsLayout->addWidget(&passwordButton); buttonsLayout->addWidget(&avatarButton); + buttonsLayout->addWidget(&bannerCardButton); mainLayout->addLayout(buttonsLayout, 7, 0, 1, 3); connect(&editButton, &QPushButton::clicked, this, &UserInfoBox::actEdit); connect(&passwordButton, &QPushButton::clicked, this, &UserInfoBox::actPassword); connect(&avatarButton, &QPushButton::clicked, this, &UserInfoBox::actAvatar); + connect(&bannerCardButton, &QPushButton::clicked, this, &UserInfoBox::actBannerCard); } setWindowTitle(tr("User Information")); @@ -83,11 +86,15 @@ void UserInfoBox::retranslateUi() editButton.setText(tr("Edit")); passwordButton.setText(tr("Change password")); avatarButton.setText(tr("Change avatar")); + bannerCardButton.setText(tr("Edit Banner Card")); } void UserInfoBox::updateInfo(const ServerInfo_User &user) { - userLevel = UserLevelFlags(user.user_level()); + currentUserInfo = user; + hasUserInfo = true; + + const UserLevelFlags userLevel(user.user_level()); pawnColors = user.pawn_colors(); privLevel = QString::fromStdString(user.privlevel()); @@ -306,6 +313,48 @@ void UserInfoBox::actAvatar() client->sendCommand(pend); } +void UserInfoBox::actBannerCard() +{ + CardArtParams initial; + if (hasUserInfo && currentUserInfo.has_card_art_params()) { + const auto &cap = currentUserInfo.card_art_params(); + initial.cardName = QString::fromStdString(cap.card_name()); + initial.marginPctL = cap.margin_pct_l(); + initial.marginPctR = cap.margin_pct_r(); + initial.verticalOffset = cap.vertical_offset(); + initial.zoom = cap.zoom(); + } + + UserCardArtSettingsDialog dlg(initial, this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + const CardArtParams p = dlg.params(); + + Command_SetCardArtParams cmd; + cmd.set_card_name(p.cardName.toStdString()); + if (!p.cardName.isEmpty()) { + cmd.set_margin_pct_l(p.marginPctL); + cmd.set_margin_pct_r(p.marginPctR); + cmd.set_vertical_offset(p.verticalOffset); + cmd.set_zoom(p.zoom); + } + + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, [p, this](const Response &r) { + if (r.response_code() != Response::RespOk) { + QMessageBox::critical(this, tr("Error"), + tr("The selected card is blacklisted on this server or another error occurred.")); + } else { + updateInfo(nameLabel.text()); // re-fetch so currentUserInfo reflects the change + QMessageBox::information(this, tr("Information"), + p.cardName.isEmpty() ? tr("Banner card removed.") : tr("Banner card updated.")); + } + }); + client->sendCommand(pend); +} + void UserInfoBox::processEditResponse(const Response &r) { switch (r.response_code()) { diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.h b/cockatrice/src/interface/widgets/server/user/user_info_box.h index 055ac0096..955cb9d3d 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.h @@ -12,6 +12,7 @@ #include #include #include +#include #include class AbstractClient; @@ -25,9 +26,11 @@ private: bool editable; QLabel avatarPic, userLevelIcon, nameLabel, realNameLabel1, realNameLabel2, countryLabel1, countryLabel2, countryLabel3, userLevelLabel1, userLevelLabel2, accountAgeLabel1, accountAgeLabel2; - QPushButton editButton, passwordButton, avatarButton; + QPushButton editButton, passwordButton, avatarButton, bannerCardButton; QPixmap avatarPixmap; bool hasAvatar; + ServerInfo_User currentUserInfo; + bool hasUserInfo = false; UserLevelFlags userLevel; ServerInfo_User::PawnColorsOverride pawnColors; QString privLevel; @@ -37,6 +40,7 @@ private: public: UserInfoBox(AbstractClient *_client, bool editable, QWidget *parent = nullptr, Qt::WindowFlags flags = {}); void retranslateUi(); + private slots: void processResponse(const Response &r); void processEditResponse(const Response &r); @@ -47,6 +51,7 @@ private slots: void actEditInternal(const Response &r); void actPassword(); void actAvatar(); + void actBannerCard(); public slots: void updateInfo(const ServerInfo_User &user); void updateInfo(const QString &userName); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp new file mode 100644 index 000000000..fd62d5ddf --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -0,0 +1,656 @@ +#include "user_info_popup.h" + +#include "../../interface/pixel_map_generator.h" +#include "../../interface/widgets/tabs/tab_supervisor.h" +#include "user_list_painter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ── Compact game row delegate ───────────────────────────────────────────────── + +class PopupGameDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + + QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override + { + return QSize(0, 38); + } + + void paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override + { + const QVariant var = index.data(PopupRoles::GameData); + if (!var.isValid()) { + QStyledItemDelegate::paint(p, option, index); + return; + } + + p->save(); + p->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); + + const QRect rect = option.rect; + const ServerInfo_Game game = var.value(); + const bool selected = option.state & QStyle::State_Selected; + + p->fillRect(rect, selected ? QColor(35, 45, 62) : QColor(14, 18, 26)); + + // State colour dot + const QColor dot = game.started() ? QColor(239, 68, 68) + : (game.player_count() >= game.max_players()) ? QColor(249, 115, 22) + : game.with_password() ? QColor(59, 130, 246) + : QColor(34, 197, 94); + p->setPen(Qt::NoPen); + p->setBrush(dot); + p->drawEllipse(QRectF(rect.left() + 9, rect.top() + (rect.height() - 8) / 2.0, 8, 8)); + + // Game title (bold, elided) + QFont tf = option.font; + tf.setBold(true); + p->setFont(tf); + p->setPen(QColor(205, 215, 230)); + const int textX = rect.left() + 26; + const int countW = 52; + const int titleW = rect.width() - textX - countW - 6; + p->drawText(QRect(textX, rect.top(), titleW, rect.height()), Qt::AlignVCenter | Qt::AlignLeft, + QFontMetrics(tf).elidedText(QString::fromStdString(game.description()), Qt::ElideRight, titleW)); + + // Player count + const bool full = game.player_count() >= game.max_players(); + p->setFont(option.font); + p->setPen(full ? QColor(249, 115, 22) : QColor(110, 128, 150)); + p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()), + Qt::AlignVCenter | Qt::AlignRight, + QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players())); + + // Row separator + p->setPen(QColor(24, 32, 44)); + p->drawLine(rect.bottomLeft(), rect.bottomRight()); + + p->restore(); + } +}; + +// ── UserInfoHeaderWidget ────────────────────────────────────────────────────── + +UserInfoHeaderWidget::UserInfoHeaderWidget(QWidget *parent) : QWidget(parent) +{ + setFixedHeight(HeaderHeight); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +void UserInfoHeaderWidget::setUserData(const ServerInfo_User &user, + bool online, + const QPixmap &avatar, + const QPixmap &cardArt, + const CardArtParams ¶ms) +{ + m_user = user; + m_online = online; + m_avatar = avatar; + m_cardArt = cardArt; + m_params = params; + update(); +} + +void UserInfoHeaderWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); + + const QRect rect = this->rect(); + const UserLevelFlags level(m_user.user_level()); + const QString userName = QString::fromStdString(m_user.name()); + const QString privLevel = QString::fromStdString(m_user.privlevel()); + + // Dark base + p.fillRect(rect, QColor(14, 18, 26)); + + // ── Card art background ─────────────────────────────────────────────────── + if (!m_cardArt.isNull()) { + const int w = rect.width(); + const int h = rect.height(); + const int mL = qRound(w * m_params.marginPctL); + const int mR = qRound(w * m_params.marginPctR); + const int dW = w - mL - mR; + + const double base = qMax(double(dW) / m_cardArt.width(), double(h) / m_cardArt.height()); + const double scale = base * m_params.zoom; + const int sW = qRound(m_cardArt.width() * scale); + const int sH = qRound(m_cardArt.height() * scale); + + const QPixmap scaled = m_cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + const int srcX = (sW - dW) / 2; + const int srcY = qBound(0, qRound((sH - h) * m_params.verticalOffset), qMax(0, sH - h)); + + QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + { + QPainter mask(&img); + mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); + QLinearGradient g(0, 0, img.width(), 0); + g.setColorAt(0.00, Qt::transparent); + g.setColorAt(0.18, Qt::white); + g.setColorAt(0.82, Qt::white); + g.setColorAt(1.00, Qt::transparent); + mask.fillRect(img.rect(), g); + } + p.setOpacity(0.48); + p.drawImage(mL, 0, img); + p.setOpacity(1.0); + } + + // Bottom gradient overlay so avatar and text are always legible + { + QLinearGradient ov(0, 0, 0, rect.height()); + ov.setColorAt(0.0, QColor(14, 18, 26, 0)); + ov.setColorAt(0.55, QColor(14, 18, 26, 110)); + ov.setColorAt(1.0, QColor(14, 18, 26, 230)); + p.fillRect(rect, ov); + } + + // ── Avatar ──────────────────────────────────────────────────────────────── + const QColor accent = [&]() -> QColor { + if (level.testFlag(ServerInfo_User::IsAdmin)) { + return QColor(245, 158, 11); + } + if (level.testFlag(ServerInfo_User::IsModerator)) { + return QColor(59, 130, 246); + } + if (level.testFlag(ServerInfo_User::IsJudge)) { + return QColor(168, 85, 247); + } + return QColor(100, 116, 139); + }(); + + const int ax = LeftPad; + const int ay = rect.height() - AvatarSize - 10; + const QRect ar(ax, ay, AvatarSize, AvatarSize); + + QPainterPath clip; + clip.addEllipse(ar); + p.save(); + p.setClipPath(clip); + + if (!m_avatar.isNull()) { + p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + } else { + p.setPen(Qt::NoPen); + p.setBrush(accent.darker(200)); + p.drawEllipse(ar); + const QPixmap pawn = + UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, m_user.pawn_colors(), false, privLevel); + p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn); + } + p.restore(); + + // Status ring + p.setPen(QPen(m_online ? QColor(34, 197, 94) : QColor(70, 80, 95), 2.5)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25)); + + // ── Username + badge ────────────────────────────────────────────────────── + const int tx = ax + AvatarSize + AvatarToTextGap; + const int tw = rect.width() - tx - 8; + + QFont nf = font(); + nf.setBold(true); + nf.setPointSizeF(nf.pointSizeF() * 1.12); + p.setFont(nf); + p.setPen(m_online ? QColor(220, 228, 240) : QColor(90, 100, 115)); + p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft, + QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw)); + + // Level / priv badge + struct + { + QString text; + QColor color; + } badge; + if (level.testFlag(ServerInfo_User::IsAdmin)) { + badge = {"ADMIN", QColor(245, 158, 11)}; + } else if (level.testFlag(ServerInfo_User::IsModerator)) { + badge = {"MOD", QColor(59, 130, 246)}; + } else if (level.testFlag(ServerInfo_User::IsJudge)) { + badge = {"JUDGE", QColor(168, 85, 247)}; + } else if (privLevel == "VIP") { + badge = {"VIP", QColor(20, 184, 166)}; + } else if (privLevel == "DONATOR") { + badge = {"DONATOR", QColor(249, 115, 22)}; + } + + if (!badge.text.isEmpty()) { + QFont bf = font(); + bf.setPointSizeF(bf.pointSizeF() * 0.70); + bf.setBold(true); + p.setFont(bf); + const QFontMetrics bfm(bf); + const int bw = bfm.horizontalAdvance(badge.text) + 10; + const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15); + p.setPen(Qt::NoPen); + p.setBrush(badge.color.darker(160)); + p.drawRoundedRect(br, 3, 3); + p.setPen(badge.color.lighter(150)); + p.drawText(br, Qt::AlignCenter, badge.text); + } +} + +// ── UserInfoPopup ───────────────────────────────────────────────────────────── + +UserInfoPopup::UserInfoPopup(TabSupervisor *ts, + AbstractClient *client, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap, + QWidget *parent) + : QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), m_ts(ts), m_client(client), m_avatarCache(avatarCache), + m_cardArtCache(cardArtCache), m_cardArtParamsMap(cardArtParamsMap) +{ + setAttribute(Qt::WA_ShowWithoutActivating); + setFixedWidth(PopupWidth); + setFrameShape(QFrame::NoFrame); + buildUi(); +} + +void UserInfoPopup::buildUi() +{ + setStyleSheet(QStringLiteral("UserInfoPopup {" + " background:#0e1218;" + " border:1px solid #1e2838;" + " border-radius:8px;" + "}")); + + auto *root = new QVBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); + + // Header + m_header = new UserInfoHeaderWidget(this); + root->addWidget(m_header); + + // Action area — rebuilt per user + m_actionArea = new QWidget(this); + m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;")); + root->addWidget(m_actionArea); + + // Thin separator + auto *sep = new QFrame(this); + sep->setFrameShape(QFrame::HLine); + sep->setStyleSheet(QStringLiteral("color:#1a2434; margin: 0 8px;")); + root->addWidget(sep); + + // Games header row + auto *gh = new QHBoxLayout; + gh->setContentsMargins(10, 4, 8, 2); + auto *gl = new QLabel(tr("Games"), this); + gl->setStyleSheet(QStringLiteral("color:#6882a0; font-size:11px; font-weight:bold; background:transparent;")); + gh->addWidget(gl); + gh->addStretch(); + m_refreshBtn = new QPushButton(QStringLiteral("↻"), this); + m_refreshBtn->setFixedSize(20, 20); + m_refreshBtn->setFlat(true); + m_refreshBtn->setStyleSheet( + QStringLiteral("QPushButton{color:#6882a0;border:none;font-size:14px;background:transparent;}" + "QPushButton:hover{color:white;}")); + connect(m_refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames); + gh->addWidget(m_refreshBtn); + root->addLayout(gh); + + // Status label + m_gamesStatus = new QLabel(this); + m_gamesStatus->setAlignment(Qt::AlignCenter); + m_gamesStatus->setStyleSheet( + QStringLiteral("color:#3a4a5e; font-size:11px; padding:10px; background:transparent;")); + root->addWidget(m_gamesStatus); + + // Games list + m_gamesModel = new QStandardItemModel(this); + m_gamesView = new QListView(this); + m_gamesView->setModel(m_gamesModel); + m_gamesView->setItemDelegate(new PopupGameDelegate(m_gamesView)); + m_gamesView->setFrameShape(QFrame::NoFrame); + m_gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_gamesView->setMaximumHeight(220); + m_gamesView->setStyleSheet(QStringLiteral("QListView{background:#0e1218;border:none;}" + "QListView::item:selected{background:#232e42;}")); + m_gamesView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu); + + root->addWidget(m_gamesView); + + // Close button — positioned absolutely in the top-right corner + m_closeBtn = new QPushButton(QStringLiteral("✕"), this); + m_closeBtn->setFixedSize(22, 22); + m_closeBtn->setFlat(true); + m_closeBtn->setStyleSheet(QStringLiteral("QPushButton{background:rgba(14,18,26,180);color:#607080;" + "border:none;border-radius:11px;font-size:10px;}" + "QPushButton:hover{color:white;background:rgba(200,50,50,200);}")); + connect(m_closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested); +} + +// ── Action button factory ───────────────────────────────────────────────────── + +static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p) +{ + auto *b = new QPushButton(label, p); + b->setToolTip(tip); + b->setFixedHeight(26); + b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + b->setStyleSheet(QStringLiteral("QPushButton{" + " background:#192030;color:#b8c8de;border:1px solid #263040;" + " border-radius:4px;font-size:11px;padding:0 4px;" + "}" + "QPushButton:hover{background:#223050;color:white;}" + "QPushButton:pressed{background:#162030;}" + "QPushButton:disabled{color:#384858;border-color:#192030;}")); + return b; +} + +void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) +{ + // Clear previous contents + delete m_actionArea->layout(); + const auto old = m_actionArea->findChildren(QString{}, Qt::FindDirectChildrenOnly); + for (auto *w : old) { + w->deleteLater(); + } + + const QString name = QString::fromStdString(userInfo.name()); + const auto ownLevel = UserLevelFlags(m_ts->getUserInfo()->user_level()); + const bool isSelf = (name == QString::fromStdString(m_ts->getUserInfo()->name())); + const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator); + const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin); + const auto their = UserLevelFlags(userInfo.user_level()); + const bool isReg = their.testFlag(ServerInfo_User::IsRegistered); + + auto *grid = new QGridLayout(m_actionArea); + grid->setContentsMargins(8, 6, 8, 6); + grid->setSpacing(4); + + int row = 0, col = 0; + const int cols = 3; + auto add = [&](QPushButton *btn) { + grid->addWidget(btn, row, col); + if (++col == cols) { + col = 0; + ++row; + } + }; + + // ── Always visible ──────────────────────────────────────────────────────── + auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), m_actionArea); + chat->setEnabled(!isSelf && online); + connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); }); + add(chat); + + auto *prof = makeBtn(tr("Profile"), tr("View user profile"), m_actionArea); + connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); }); + add(prof); + + auto *games = makeBtn(tr("Games"), tr("Show this user's games"), m_actionArea); + games->setEnabled(!isSelf && online); + connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); }); + add(games); + + // ── Buddy / ignore (registered users only) ──────────────────────────────── + if (!isSelf && isReg) { + if (isBuddy) { + auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); }); + add(b); + } else { + auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); }); + add(b); + } + if (isIgnored) { + auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); }); + add(b); + } else { + auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); }); + add(b); + } + } + + // ── Moderator actions ───────────────────────────────────────────────────── + if (!isSelf && (isMod || isAdmin)) { + if (col != 0) { + ++row; + col = 0; + } // start mod section on a fresh row + + auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), m_actionArea); + auto *warn = makeBtn(tr("Warn"), tr("Warn user"), m_actionArea); + auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), m_actionArea); + auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), m_actionArea); + connect(ban, &QPushButton::clicked, this, [this, name] { emit banRequested(name); }); + connect(warn, &QPushButton::clicked, this, [this, name] { emit warnRequested(name); }); + connect(bLog, &QPushButton::clicked, this, [this, name] { emit banHistoryRequested(name); }); + connect(wLog, &QPushButton::clicked, this, [this, name] { emit warnHistoryRequested(name); }); + add(ban); + add(warn); + add(bLog); + add(wLog); + } + + // ── Admin actions ───────────────────────────────────────────────────────── + if (!isSelf && isAdmin) { + auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), m_actionArea); + connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); }); + add(notes); + + if (their.testFlag(ServerInfo_User::IsModerator)) { + auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); }); + add(b); + } else if (isReg) { + auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); }); + add(b); + } + if (their.testFlag(ServerInfo_User::IsJudge)) { + auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); }); + add(b); + } else if (isReg) { + auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), m_actionArea); + connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); }); + add(b); + } + } + + m_actionArea->adjustSize(); +} + +void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored) +{ + rebuildActionButtons(userInfo, online, isBuddy, isIgnored); + adjustSize(); +} + +void UserInfoPopup::onGamesContextMenu(const QPoint &pos) +{ + const QModelIndex idx = m_gamesView->indexAt(pos); + if (!idx.isValid()) { + return; + } + + const QVariant var = idx.data(PopupRoles::GameData); + if (!var.isValid()) { + return; + } + const ServerInfo_Game game = var.value(); + + QMenu menu(this); + menu.setStyleSheet( + QStringLiteral("QMenu{background:#12182a;color:#c8d8ec;border:1px solid #1e2838;border-radius:4px;}" + "QMenu::item:selected{background:#223050;}")); + + const bool canJoin = !game.started() && game.player_count() < game.max_players(); + QAction *join = menu.addAction(tr("Join game")); + join->setEnabled(canJoin); + + QAction *spec = nullptr; + if (game.spectators_allowed()) { + spec = menu.addAction(tr("Spectate")); + } + + const QAction *chosen = menu.exec(m_gamesView->viewport()->mapToGlobal(pos)); + if (!chosen) { + return; + } + + if (chosen == join) { + emit joinGameRequested(game.game_id(), game.room_id(), false); + } else if (spec && chosen == spec) { + emit joinGameRequested(game.game_id(), game.room_id(), true); + } +} + +// ── showForUser ─────────────────────────────────────────────────────────────── + +void UserInfoPopup::showForUser(const QString &userName, + const ServerInfo_User &userInfo, + bool online, + bool isBuddy, + bool isIgnored) +{ + m_currentUser = userName; + m_currentUserInfo = userInfo; + m_currentOnline = online; + + // Header + const QPixmap avatar = m_avatarCache ? m_avatarCache->value(userName) : QPixmap{}; + const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName)) + ? m_cardArtParamsMap->value(userName) + : CardArtParams{}; + const QString artKey = userName + u'|' + params.cardName; + const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; + m_header->setUserData(userInfo, online, avatar, cardArt, params); + + // Actions + rebuildActionButtons(userInfo, online, isBuddy, isIgnored); + + // Games list reset + m_gamesModel->clear(); + m_gamesView->hide(); + m_gamesStatus->setText(tr("Loading games…")); + m_gamesStatus->show(); + + // Close button — top-right corner, above everything + m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6); + m_closeBtn->raise(); + + adjustSize(); + fetchGames(); +} + +// ── Games fetch ─────────────────────────────────────────────────────────────── + +void UserInfoPopup::fetchGames() +{ + if (!m_client || m_currentUser.isEmpty()) { + return; + } + + Command_GetGamesOfUser cmd; + cmd.set_user_name(m_currentUser.toStdString()); + + const QString snapshot = m_currentUser; + PendingCommand *pend = m_client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, + [this, snapshot](const Response &r) { onGamesReceived(r, snapshot); }); + m_client->sendCommand(pend); +} + +void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) +{ + if (forUser != m_currentUser) { + return; // stale response — different user showing now + } + + m_gamesModel->clear(); + + if (r.response_code() != Response::RespOk) { + m_gamesStatus->setText(tr("Could not load games.")); + m_gamesStatus->show(); + m_gamesView->hide(); + return; + } + + const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext); + if (resp.game_list_size() == 0) { + m_gamesStatus->setText(tr("No active games.")); + m_gamesStatus->show(); + m_gamesView->hide(); + return; + } + + for (int i = 0; i < resp.game_list_size(); ++i) { + auto *item = new QStandardItem; + item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData); + item->setEditable(false); + m_gamesModel->appendRow(item); + } + + m_gamesStatus->hide(); + m_gamesView->show(); + + // Fit exactly to the number of visible rows, scroll when more than 5 + constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint + constexpr int maxRows = 5; + const int count = m_gamesModel->rowCount(); + const int visible = qMin(count, maxRows); + m_gamesView->setFixedHeight(visible * rowH + 2); + m_gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); + + adjustSize(); +} + +void UserInfoPopup::refreshGames() +{ + m_gamesModel->clear(); + m_gamesView->hide(); + m_gamesStatus->setText(tr("Loading games…")); + m_gamesStatus->show(); + fetchGames(); +} + +// ── Mouse events ────────────────────────────────────────────────────────────── + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void UserInfoPopup::enterEvent(QEnterEvent *e) +{ + QFrame::enterEvent(e); + emit mouseEnteredPopup(); +} +#else +void UserInfoPopup::enterEvent(QEvent *e) +{ + QFrame::enterEvent(e); + emit mouseEnteredPopup(); +} +#endif +void UserInfoPopup::leaveEvent(QEvent *e) +{ + QFrame::leaveEvent(e); + emit mouseLeftPopup(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h new file mode 100644 index 000000000..0e03147c4 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -0,0 +1,181 @@ +#ifndef COCKATRICE_USER_INFO_POPUP_H +#define COCKATRICE_USER_INFO_POPUP_H + +#include "../../interface/widgets/server/game_type_map.h" +#include "user_list_painter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class AbstractClient; +class QLabel; +class QPushButton; +class TabSupervisor; + +// ── Roles ───────────────────────────────────────────────────────────────────── + +namespace PopupRoles +{ +constexpr int GameData = Qt::UserRole + 10; +} + +// ── Header widget ───────────────────────────────────────────────────────────── + +/** + * @class UserInfoHeaderWidget + * @brief Paints the enlarged banner card art + circular avatar section at the + * top of the UserInfoPopup. + * + * Layout mirrors UserListPainter but at a larger scale: the card art fills the + * full width as a semi-transparent background, a bottom gradient ensures the + * avatar and username text remain legible, and the status ring colour matches + * the UserListPainter convention. + */ +class UserInfoHeaderWidget : public QWidget +{ + Q_OBJECT + + static constexpr int HeaderHeight = 130; + static constexpr int AvatarSize = 68; + static constexpr int AvatarPawnSize = 46; + static constexpr int LeftPad = 14; + static constexpr int AvatarToTextGap = 10; + +public: + explicit UserInfoHeaderWidget(QWidget *parent = nullptr); + + void setUserData(const ServerInfo_User &user, + bool online, + const QPixmap &avatar, + const QPixmap &cardArt, + const CardArtParams ¶ms); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + ServerInfo_User m_user; + bool m_online = false; + QPixmap m_avatar; + QPixmap m_cardArt; + CardArtParams m_params; +}; + +// ── Main popup ──────────────────────────────────────────────────────────────── + +/** + * @class UserInfoPopup + * @brief Floating panel showing an enlarged user card, quick action buttons, + * and a live scrollable games list. + * + * Lifecycle (mirrors DeckEditorDeckDockWidget): + * - showForUser() — populate, position externally, call show() + * - mouseEnteredPopup / mouseLeftPopup — caller manages hide timer + * - closeRequested() — emitted by the internal close button + * + * The popup is a Qt::Tool frameless child so windowOpacity animations and + * move() in screen coordinates work identically to CardInfoPictureEnlargedWidget. + * + * Action signals map 1-to-1 to UserContextMenu::exec*() methods so all action + * logic stays in one place. + */ +class UserInfoPopup : public QFrame +{ + Q_OBJECT + + static constexpr int PopupWidth = 316; + +public: + explicit UserInfoPopup(TabSupervisor *tabSupervisor, + AbstractClient *client, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap, + QWidget *parent); + + /** + * Populate the popup for @p userName and kick off a game list fetch. + * Call show() / move() externally after this. + */ + void + showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); + void fetchGames(); + + [[nodiscard]] QString currentUser() const + { + return m_currentUser; + } + + /** Called when buddy/ignore status changes externally while popup is open. */ + void updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); + +signals: + void mouseEnteredPopup(); + void mouseLeftPopup(); + void closeRequested(); + + /** Emitted when the user requests joining or spectating a game in the list. */ + void joinGameRequested(int gameId, int roomId, bool asSpectator); + + // ── Action signals — connect to UserContextMenu::exec*() ────────────────── + void chatRequested(const QString &userName); + void detailsRequested(const QString &userName); + void showGamesRequested(const QString &userName); + void addBuddyRequested(const QString &userName); + void removeBuddyRequested(const QString &userName); + void addIgnoreRequested(const QString &userName); + void removeIgnoreRequested(const QString &userName); + void banRequested(const QString &userName); + void warnRequested(const QString &userName); + void banHistoryRequested(const QString &userName); + void warnHistoryRequested(const QString &userName); + void adminNotesRequested(const QString &userName); + void promoteToModRequested(const QString &userName); + void demoteFromModRequested(const QString &userName); + void promoteToJudgeRequested(const QString &userName); + void demoteFromJudgeRequested(const QString &userName); + +protected: +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *e) override; +#else + void enterEvent(QEvent *e) override; +#endif + void leaveEvent(QEvent *e) override; + +private slots: + void refreshGames(); + void onGamesReceived(const Response &r, const QString &forUser); + void onGamesContextMenu(const QPoint &pos); + +private: + void buildUi(); + void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored); + + TabSupervisor *m_ts; + AbstractClient *m_client; + const QMap *m_avatarCache; + const QMap *m_cardArtCache; + const QMap *m_cardArtParamsMap; + + QString m_currentUser; + ServerInfo_User m_currentUserInfo; + bool m_currentOnline = false; + + UserInfoHeaderWidget *m_header; + QWidget *m_actionArea; ///< rebuilt per user + QListView *m_gamesView; + QStandardItemModel *m_gamesModel; + QLabel *m_gamesStatus; + QPushButton *m_closeBtn; + QPushButton *m_refreshBtn; +}; + +#endif // COCKATRICE_USER_INFO_POPUP_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_manager.cpp b/cockatrice/src/interface/widgets/server/user/user_list_manager.cpp index 4bc2c84d6..216420006 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_manager.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_manager.cpp @@ -42,6 +42,9 @@ void UserListManager::handleDisconnect() delete ownUserInfo; ownUserInfo = nullptr; + + // Full rebuild — all lists are gone + emit listReset(); } void UserListManager::setOwnUserInfo(const ServerInfo_User &userInfo) @@ -63,74 +66,77 @@ void UserListManager::processListUsersResponse(const Response &response) const int userListSize = resp.user_list_size(); for (int i = 0; i < userListSize; ++i) { const ServerInfo_User &info = resp.user_list(i); - const QString &userName = QString::fromStdString(info.name()); - onlineUsers.insert(userName, info); + onlineUsers.insert(QString::fromStdString(info.name()), info); } + + // Bulk load complete — widgets rebuild once from the now-populated map + emit listReset(); } void UserListManager::processUserJoinedEvent(const Event_UserJoined &event) { const auto &info = event.user_info(); - const QString &userName = QString::fromStdString(info.name()); - onlineUsers.insert(userName, info); + const QString name = QString::fromStdString(info.name()); + onlineUsers.insert(name, info); + + emit userJoinedOnline(info); } void UserListManager::processUserLeftEvent(const Event_UserLeft &event) { - const auto &userName = QString::fromStdString(event.name()); - onlineUsers.remove(userName); + const QString name = QString::fromStdString(event.name()); + onlineUsers.remove(name); + + emit userLeftOnline(name); } void UserListManager::buddyListReceived(const QList &_buddyList) { for (const auto &user : _buddyList) { - const auto &userName = QString::fromStdString(user.name()); - buddyUsers.insert(userName, user); + buddyUsers.insert(QString::fromStdString(user.name()), user); } + + // Bulk load — one reset covers all newly added entries + emit listReset(); } void UserListManager::ignoreListReceived(const QList &_ignoreList) { for (const auto &user : _ignoreList) { - const auto &userName = QString::fromStdString(user.name()); - ignoredUsers.insert(userName, user); + ignoredUsers.insert(QString::fromStdString(user.name()), user); } + + // Bulk load — one reset covers all newly added entries + emit listReset(); } void UserListManager::processAddToListEvent(const Event_AddToList &event) { const auto &user = event.user_info(); - const auto &userName = QString::fromStdString(user.name()); + const QString userName = QString::fromStdString(user.name()); + const QString listType = QString::fromStdString(event.list_name()); - const auto &userListType = QString::fromStdString(event.list_name()); - - QMap *userMap; - if (userListType == "buddy") { - userMap = &buddyUsers; - } else if (userListType == "ignore") { - userMap = &ignoredUsers; - } else { - return; + if (listType == "buddy") { + buddyUsers.insert(userName, user); + emit addedToBuddyList(user); + } else if (listType == "ignore") { + ignoredUsers.insert(userName, user); + emit addedToIgnoreList(user); } - - userMap->insert(userName, user); } void UserListManager::processRemoveFromListEvent(const Event_RemoveFromList &event) { - const auto &userListType = QString::fromStdString(event.list_name()); - const auto &userName = QString::fromStdString(event.user_name()); + const QString listType = QString::fromStdString(event.list_name()); + const QString userName = QString::fromStdString(event.user_name()); - QMap *userMap; - if (userListType == "buddy") { - userMap = &buddyUsers; - } else if (userListType == "ignore") { - userMap = &ignoredUsers; - } else { - return; + if (listType == "buddy") { + buddyUsers.remove(userName); + emit removedFromBuddyList(userName); + } else if (listType == "ignore") { + ignoredUsers.remove(userName); + emit removedFromIgnoreList(userName); } - - userMap->remove(userName); } bool UserListManager::isOwnUserRegistered() const @@ -155,16 +161,9 @@ bool UserListManager::isUserIgnored(const QString &userName) const const ServerInfo_User *UserListManager::getOnlineUser(const QString &userName) const { - const QString &userNameToMatchLower = userName.toLower(); - - const auto it = - std::find_if(onlineUsers.begin(), onlineUsers.end(), [&userNameToMatchLower](const ServerInfo_User &user) { - return userNameToMatchLower == QString::fromStdString(user.name()).toLower(); - }); - - if (it != onlineUsers.end()) { - return &*it; - } - - return nullptr; + const QString lower = userName.toLower(); + const auto it = std::find_if(onlineUsers.begin(), onlineUsers.end(), [&lower](const ServerInfo_User &user) { + return lower == QString::fromStdString(user.name()).toLower(); + }); + return it != onlineUsers.end() ? &*it : nullptr; } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_manager.h b/cockatrice/src/interface/widgets/server/user/user_list_manager.h index f09284bd0..6238f0799 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_manager.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_manager.h @@ -47,15 +47,17 @@ public: explicit UserListManager(AbstractClient *_client, QObject *parent = nullptr); ~UserListManager() override; - [[nodiscard]] QMap getAllUsersList() const + [[nodiscard]] const QMap &getAllUsersList() const { return onlineUsers; } - [[nodiscard]] QMap getBuddyList() const + + [[nodiscard]] const QMap &getBuddyList() const { return buddyUsers; } - [[nodiscard]] QMap getIgnoreList() const + + [[nodiscard]] const QMap &getIgnoreList() const { return ignoredUsers; } @@ -71,8 +73,26 @@ public slots: void handleDisconnect(); signals: - void userLeft(const QString &userName); - void userJoined(const ServerInfo_User &userInfo); + /** + * The entire list needs to be rebuilt from scratch. + * Fired on disconnect, reconnect, and initial bulk loads + * (Command_ListUsers response, initial buddy/ignore lists). + */ + void listReset(); + + // ── Online user presence ────────────────────────────────────────────────── + /** A user came online (or joined the room). Full ServerInfo_User available. */ + void userJoinedOnline(const ServerInfo_User &user); + /** A user went offline (or left the room). */ + void userLeftOnline(const QString &userName); + + // ── Buddy list mutations (individual, post-login) ───────────────────────── + void addedToBuddyList(const ServerInfo_User &user); + void removedFromBuddyList(const QString &userName); + + // ── Ignore list mutations (individual, post-login) ──────────────────────── + void addedToIgnoreList(const ServerInfo_User &user); + void removedFromIgnoreList(const QString &userName); }; #endif // COCKATRICE_USER_LIST_MANAGER_H diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp new file mode 100644 index 000000000..b5541b692 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -0,0 +1,342 @@ +#include "user_list_painter.h" + +#include "../../interface/pixel_map_generator.h" + +#include +#include +#include +#include +#include +#include + +static constexpr int RowHeight = 72; +static constexpr int AvatarSize = 36; +static constexpr int LeftPadding = 14; +static constexpr int TextSpacing = 10; + +QSize UserListPainter::sizeHint() +{ + return QSize(0, RowHeight); +} + +QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online) +{ + QColor accentColor; + + if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { + accentColor = QColor(245, 158, 11); + } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { + accentColor = QColor(59, 130, 246); + } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { + accentColor = QColor(168, 85, 247); + } else { + accentColor = QColor(100, 116, 139); + } + + if (!online) { + accentColor = accentColor.darker(160); + } + + return accentColor; +} + +int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRect &rect) +{ + int scrollBarWidth = 0; + + if (const auto *scrollArea = qobject_cast(option.widget)) { + const QScrollBar *sb = scrollArea->verticalScrollBar(); + if (sb && sb->isVisible()) { + scrollBarWidth = sb->width(); + } + } + + const int viewportRight = option.widget ? option.widget->width() - scrollBarWidth : rect.right(); + + return qMin(rect.right(), viewportRight - 4); +} + +void UserListPainter::drawBackground(QPainter *painter, + const QRectF &cardRect, + const QColor &accentColor, + bool selected) +{ + QLinearGradient bg(cardRect.topLeft(), cardRect.topRight()); + bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320)); + bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30)); + + painter->setPen(Qt::NoPen); + painter->setBrush(bg); + painter->drawRoundedRect(cardRect, 6, 6); + + painter->setBrush(accentColor); + painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); +} + +static QString makeKey(const QString &user, const QString &card) +{ + return user + u'|' + card; +} + +void UserListPainter::drawCardArt(QPainter *painter, + const QRect &rect, + int cardRight, + const QString &userName, + const QMap *cardArtCache, + const CardArtParams ¶ms, + const QPixmap *overridePixmap = nullptr) +{ + QPixmap art; + + if (overridePixmap && !overridePixmap->isNull()) { + art = *overridePixmap; + } else { + if (!cardArtCache) { + return; + } + + const QString key = makeKey(userName, params.cardName); + + if (!cardArtCache->contains(key)) { + return; + } + + art = cardArtCache->value(key); + } + + if (art.isNull()) { + return; + } + + const int cardH = rect.height() - 4; + const int totalW = cardRight - rect.left(); + const int marginL = qRound(totalW * params.marginPctL); + const int marginR = qRound(totalW * params.marginPctR); + const int drawW = totalW - marginL - marginR; + + const double basescale = qMax(double(drawW) / art.width(), double(cardH) / art.height()); + const double scale = basescale * params.zoom; + + const int scaledW = qRound(art.width() * scale); + const int scaledH = qRound(art.height() * scale); + + const QPixmap scaled = art.scaled(scaledW, scaledH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + + const int srcX = (scaledW - drawW) / 2; + const int srcY = qRound((scaledH - cardH) * params.verticalOffset); + + // Clamp srcY so we never copy outside the pixmap bounds + const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH)); + + QImage img = + scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + + { + QPainter mask(&img); + mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); + + QLinearGradient grad(0, 0, img.width(), 0); + grad.setColorAt(0.00, Qt::transparent); + grad.setColorAt(0.22, Qt::white); + grad.setColorAt(0.78, Qt::white); + grad.setColorAt(1.00, Qt::transparent); + + mask.fillRect(img.rect(), grad); + } + + painter->setOpacity(0.55); + painter->drawImage(rect.left() + marginL, rect.top() + 2, img); + painter->setOpacity(1.0); +} + +QRect UserListPainter::getAvatarRect(const QRect &rect) +{ + const int avatarX = rect.left() + LeftPadding; + const int avatarY = rect.top() + (rect.height() - AvatarSize) / 2; + return QRect(avatarX, avatarY, AvatarSize, AvatarSize); +} + +void UserListPainter::drawAvatar(QPainter *painter, + const QRect &avatarRect, + const QString &userName, + const QColor &accentColor, + const UserLevelFlags &userLevel, + const ServerInfo_User &userInfo, + const QString &privLevel, + const QMap *avatarCache) +{ + QPainterPath clipPath; + clipPath.addEllipse(avatarRect); + + painter->save(); + painter->setClipPath(clipPath); + + bool drewAvatar = false; + + if (avatarCache && avatarCache->contains(userName)) { + const QPixmap &avatar = avatarCache->value(userName); + if (!avatar.isNull()) { + painter->drawPixmap( + avatarRect, avatar.scaled(avatarRect.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + drewAvatar = true; + } + } + + if (!drewAvatar) { + painter->setBrush(accentColor.darker(200)); + painter->setPen(Qt::NoPen); + painter->drawEllipse(avatarRect); + + const QPixmap pawn = + UserLevelPixmapGenerator::generatePixmap(24, userLevel, userInfo.pawn_colors(), false, privLevel); + + painter->drawPixmap(avatarRect.center().x() - 12, avatarRect.center().y() - 12, pawn); + } + + painter->restore(); +} + +void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online) +{ + const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95); + + painter->setPen(QPen(statusColor, 2)); + painter->setBrush(Qt::NoBrush); + painter->drawEllipse(avatarRect.adjusted(-1, -1, 1, 1)); +} + +void UserListPainter::drawUserName(QPainter *painter, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + int textX, + const QString &userName, + bool online, + bool selected) +{ + QFont nameFont = option.font; + nameFont.setBold(true); + painter->setFont(nameFont); + + const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20); + const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10); + + painter->setPen(QColor(0, 0, 0, 200)); + painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName); + + painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115)); + painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName); +} + +void UserListPainter::drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo) +{ + const QPixmap flag = CountryPixmapGenerator::generatePixmap(13, QString::fromStdString(userInfo.country())); + if (!flag.isNull()) { + painter->drawPixmap(textX, rect.top() + 46, flag); + } +} + +QList UserListPainter::buildBadges(const UserLevelFlags &userLevel, const QString &privLevel) +{ + QList badges; + + if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { + badges << Badge{"ADMIN", QColor(245, 158, 11)}; + } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { + badges << Badge{"MOD", QColor(59, 130, 246)}; + } else if (userLevel.testFlag(ServerInfo_User::IsJudge)) { + badges << Badge{"JUDGE", QColor(168, 85, 247)}; + } + + if (privLevel == "VIP") { + badges << Badge{"VIP", QColor(20, 184, 166)}; + } else if (privLevel == "DONATOR") { + badges << Badge{"DONATOR", QColor(249, 115, 22)}; + } + + return badges; +} + +void UserListPainter::drawBadges(QPainter *painter, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + const QList &badges, + bool online) +{ + if (badges.isEmpty()) { + return; + } + + QFont badgeFont = option.font; + badgeFont.setPointSizeF(badgeFont.pointSizeF() * 0.68); + badgeFont.setBold(true); + painter->setFont(badgeFont); + + QFontMetrics fm(badgeFont); + + int totalBadgeW = 0; + for (const Badge &b : badges) { + totalBadgeW += fm.horizontalAdvance(b.text) + 8 + 4; + } + totalBadgeW -= 4; + + int bx = cardRight - 6 - totalBadgeW; + + for (const Badge &b : badges) { + const QColor col = online ? b.color : b.color.darker(180); + const int bw = fm.horizontalAdvance(b.text) + 8; + const QRect br(bx, rect.top() + 44, bw, 13); + + painter->setPen(Qt::NoPen); + painter->setBrush(col.darker(online ? 160 : 220)); + painter->drawRoundedRect(br, 3, 3); + + painter->setPen(col.lighter(online ? 160 : 100)); + painter->drawText(br, Qt::AlignCenter, b.text); + + bx += bw + 4; + } +} + +void UserListPainter::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index, + const ServerInfo_User &userInfo, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap) +{ + painter->save(); + painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing); + + const QRect rect = option.rect; + const bool online = index.data(Qt::UserRole + 1).toBool(); + const bool selected = option.state & QStyle::State_Selected; + const UserLevelFlags userLevel(userInfo.user_level()); + const QString userName = QString::fromStdString(userInfo.name()); + const QString privLevel = QString::fromStdString(userInfo.privlevel()); + const QColor accentColor = getAccentColor(userLevel, online); + const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2); + const int cardRight = getCardRight(option, rect); + + const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(userName)) + ? cardArtParamsMap->value(userName) + : CardArtParams{}; + + drawBackground(painter, cardRect, accentColor, selected); + drawCardArt(painter, rect, cardRight, userName, cardArtCache, params); + + const QRect avatarRect = getAvatarRect(rect); + drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache); + drawStatusRing(painter, avatarRect, online); + + const int textX = avatarRect.right() + TextSpacing; + drawUserName(painter, option, rect, cardRight, textX, userName, online, selected); + drawCountryFlag(painter, rect, textX, userInfo); + + const QList badges = buildBadges(userLevel, privLevel); + drawBadges(painter, option, rect, cardRight, badges, online); + + painter->restore(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.h b/cockatrice/src/interface/widgets/server/user/user_list_painter.h new file mode 100644 index 000000000..95486b75e --- /dev/null +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.h @@ -0,0 +1,86 @@ +#ifndef COCKATRICE_USER_LIST_PAINTER_H +#define COCKATRICE_USER_LIST_PAINTER_H + +#include "user_level.h" + +#include +#include +#include +#include +#include +#include + +class QPainter; +class QModelIndex; +class QStyleOptionViewItem; +class ServerInfo_User; + +struct CardArtParams +{ + QString cardName = ""; + double marginPctL = 0.33; + double marginPctR = 0.02; + double verticalOffset = 0.35; + double zoom = 1.0; +}; + +class UserListPainter +{ +public: + static void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index, + const ServerInfo_User &userInfo, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap); + + static QSize sizeHint(); + + static void drawCardArt(QPainter *painter, + const QRect &rect, + int cardRight, + const QString &userName, + const QMap *cardArtCache, + const CardArtParams ¶ms, + const QPixmap *overridePixmap); + +private: + struct Badge + { + QString text; + QColor color; + }; + + static QColor getAccentColor(const UserLevelFlags &userLevel, bool online); + static int getCardRight(const QStyleOptionViewItem &option, const QRect &rect); + static void drawBackground(QPainter *painter, const QRectF &cardRect, const QColor &accentColor, bool selected); + static QRect getAvatarRect(const QRect &rect); + static void drawAvatar(QPainter *painter, + const QRect &avatarRect, + const QString &userName, + const QColor &accentColor, + const UserLevelFlags &userLevel, + const ServerInfo_User &userInfo, + const QString &privLevel, + const QMap *avatarCache); + static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online); + static void drawUserName(QPainter *painter, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + int textX, + const QString &userName, + bool online, + bool selected); + static void drawCountryFlag(QPainter *painter, const QRect &rect, int textX, const ServerInfo_User &userInfo); + static QList buildBadges(const UserLevelFlags &userLevel, const QString &privLevel); + static void drawBadges(QPainter *painter, + const QStyleOptionViewItem &option, + const QRect &rect, + int cardRight, + const QList &badges, + bool online); +}; + +#endif // COCKATRICE_USER_LIST_PAINTER_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 11c9b60eb..ed06ea941 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -1,10 +1,13 @@ #include "user_list_widget.h" +#include "../../../../client/settings/cache_settings.h" +#include "../../../card_picture_loader/card_picture_loader.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/widgets/tabs/tab_account.h" #include "../../interface/widgets/tabs/tab_supervisor.h" #include "../game_selector.h" #include "user_context_menu.h" +#include "user_list_painter.h" #include #include @@ -15,13 +18,18 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include #include +#include +#include #include BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) @@ -308,7 +316,18 @@ QString AdminNotesDialog::getNotes() const return notes->toPlainText(); } -UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent) +namespace UserListRoles +{ +constexpr int Online = Qt::UserRole + 1; +constexpr int UserInfo = Qt::UserRole + 2; +} // namespace UserListRoles + +UserListItemDelegate::UserListItemDelegate(QObject *const parent, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap) + : QStyledItemDelegate(parent), avatarCache(avatarCache), cardArtCache(cardArtCache), + cardArtParamsMap(cardArtParamsMap) { } @@ -331,6 +350,32 @@ bool UserListItemDelegate::editorEvent(QEvent *event, return QStyledItemDelegate::editorEvent(event, model, option, index); } +QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!SettingsCache::instance().getStyleUserList()) { + return QStyledItemDelegate::sizeHint(option, index); + } + return UserListPainter::sizeHint(); +} + +void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!SettingsCache::instance().getStyleUserList()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + const QVariant var = index.data(UserListRoles::UserInfo); + + if (!var.isValid()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + UserListPainter::paint(painter, option, index, var.value(), avatarCache, cardArtCache, + cardArtParamsMap); +} + UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type) { setUserInfo(_userInfo); @@ -347,11 +392,12 @@ void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo) setData(2, Qt::UserRole, QString::fromStdString(userInfo.name())); setData(2, Qt::DisplayRole, QString::fromStdString(userInfo.name())); setData(3, Qt::InitialSortOrderRole, QString::fromStdString(userInfo.privlevel())); + setData(0, UserListRoles::UserInfo, QVariant::fromValue(userInfo)); } void UserListTWI::setOnline(bool online) { - setData(0, Qt::UserRole + 1, online); + setData(0, UserListRoles::Online, online); setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray)); } @@ -370,8 +416,8 @@ void UserListTWI::setOnline(bool online) bool UserListTWI::operator<(const QTreeWidgetItem &other) const { // Sort by online/offline - if (data(0, Qt::UserRole + 1) != other.data(0, Qt::UserRole + 1)) { - return data(0, Qt::UserRole + 1).toBool(); + if (data(0, UserListRoles::Online) != other.data(0, UserListRoles::Online)) { + return data(0, UserListRoles::Online).toBool(); } const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt()); @@ -418,20 +464,100 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, QWidget *parent) : QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0) { - itemDelegate = new UserListItemDelegate(this); + avatarProvider = new UserAvatarProvider(client, this); + cardArtProvider = new UserCardArtProvider(this); + + itemDelegate = + new UserListItemDelegate(this, &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap); + userContextMenu = new UserContextMenu(tabSupervisor, this); connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &UserListWidget::openMessageDialog); userTree = new QTreeWidget; - userTree->setColumnCount(3); - userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + userTree->setColumnCount(4); // 0=display, 1=flag(hidden), 2=name(hidden), 3=privlevel(hidden) + userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); userTree->header()->setMinimumSectionSize(0); userTree->setHeaderHidden(true); userTree->setRootIsDecorated(false); userTree->setIconSize(QSize(20, 18)); userTree->setItemDelegate(itemDelegate); userTree->setAlternatingRowColors(true); + userTree->hideColumn(1); + userTree->hideColumn(2); + userTree->hideColumn(3); connect(userTree, &QTreeWidget::itemActivated, this, &UserListWidget::userClicked); + userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + userTree->header()->setStretchLastSection(true); + + // ── Hover popup ─────────────────────────────────────────────────────────── + m_userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(), + &cardArtProvider->cache(), &cardArtParamsMap, + window()); // parented to main window so it floats above siblings + + m_userInfoPopup->hide(); + m_userInfoPopup->setWindowOpacity(0.0); + m_userInfoPopup->installEventFilter(this); + + connectPopupSignals(); + + m_showPopupTimer = new QTimer(this); + m_showPopupTimer->setSingleShot(true); + m_showPopupTimer->setInterval(280); + connect(m_showPopupTimer, &QTimer::timeout, this, [this] { + if (!m_hoveredUser.isEmpty()) { + showPopupForUser(m_hoveredUser); + } + }); + + m_hidePopupTimer = new QTimer(this); + m_hidePopupTimer->setSingleShot(true); + m_hidePopupTimer->setInterval(160); + connect(m_hidePopupTimer, &QTimer::timeout, this, [this] { + if (!m_popupPinned && !m_userInfoPopup->underMouse() && !userTree->underMouse()) { + hidePopup(); + } + }); + + userTree->setMouseTracking(true); + userTree->viewport()->setMouseTracking(true); + userTree->viewport()->installEventFilter(this); + + // Pin on item click + connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { + if (!SettingsCache::instance().getStyleUserList()) { + return; + } + const QString name = static_cast(item)->getUserInfo().name().c_str(); + m_popupPinned = false; // reset so showPopupForUser can update + showPopupForUser(name); + m_popupPinned = true; // pin after showing + }); + + connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this](const QItemSelection &sel, const QItemSelection &) { + // if (m_rebuildingTree) return; + if (sel.isEmpty() && m_popupPinned) { + m_popupPinned = false; + hidePopup(); + } + }); + + // Hide popup when list scrolls (reference row has moved) + connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { + m_showPopupTimer->stop(); + hidePopup(true); + }); + + // Forward join requests from popup upward + connect(m_userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); + + connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, + [this](const QString &) { userTree->viewport()->update(); }); + connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, + [this](const QString &) { userTree->viewport()->update(); }); + + connect(&SettingsCache::instance(), &SettingsCache::styleUserListChanged, this, &UserListWidget::applyDisplayMode); + applyDisplayMode(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(userTree); @@ -441,6 +567,280 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, retranslateUi(); } +void UserListWidget::bind(UserListManager *mgr) +{ + manager = mgr; + + // ── Full rebuild: disconnect / reconnect / bulk initial load ────────────── + connect(manager, &UserListManager::listReset, this, &UserListWidget::rebuild); + + // ── Online users list (AllUsersList / RoomList) ─────────────────────────── + if (type == AllUsersList || type == RoomList) { + connect(manager, &UserListManager::userJoinedOnline, this, + [this](const ServerInfo_User &user) { processUserInfo(user, true); }); + connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { deleteUser(name); }); + } + + // ── Buddy list ──────────────────────────────────────────────────────────── + if (type == BuddyList) { + connect(manager, &UserListManager::addedToBuddyList, this, [this](const ServerInfo_User &user) { + const QString name = QString::fromStdString(user.name()); + processUserInfo(user, manager->getOnlineUser(name) != nullptr); + }); + connect(manager, &UserListManager::removedFromBuddyList, this, + [this](const QString &name) { deleteUser(name); }); + // Track online presence changes for buddies already in the tree + connect(manager, &UserListManager::userJoinedOnline, this, [this](const ServerInfo_User &user) { + const QString name = QString::fromStdString(user.name()); + if (users.contains(name)) { + users[name]->setUserInfo(user); + setUserOnline(name, true); + } + }); + connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { + if (users.contains(name)) { + setUserOnline(name, false); + } + }); + } + + // ── Ignore list ─────────────────────────────────────────────────────────── + if (type == IgnoreList) { + connect(manager, &UserListManager::addedToIgnoreList, this, [this](const ServerInfo_User &user) { + const QString name = QString::fromStdString(user.name()); + processUserInfo(user, manager->getOnlineUser(name) != nullptr); + }); + connect(manager, &UserListManager::removedFromIgnoreList, this, + [this](const QString &name) { deleteUser(name); }); + } + + // ── Popup button refresh ────────────────────────────────────────────────── + // Any buddy/ignore mutation while the popup is open refreshes its buttons + auto refreshIfPopupOpen = [this](const QString &name) { + if (m_userInfoPopup && m_userInfoPopup->isVisible() && m_userInfoPopup->currentUser() == name) { + refreshPopupButtons(name); + } + }; + auto refreshCurrentPopup = [refreshIfPopupOpen](const ServerInfo_User &u) { + refreshIfPopupOpen(QString::fromStdString(u.name())); + }; + + connect(manager, &UserListManager::addedToBuddyList, this, refreshCurrentPopup); + connect(manager, &UserListManager::removedFromBuddyList, this, refreshIfPopupOpen); + connect(manager, &UserListManager::addedToIgnoreList, this, refreshCurrentPopup); + connect(manager, &UserListManager::removedFromIgnoreList, this, refreshIfPopupOpen); + connect(manager, &UserListManager::userJoinedOnline, this, refreshCurrentPopup); + connect(manager, &UserListManager::userLeftOnline, this, refreshIfPopupOpen); + + rebuild(); +} + +void UserListWidget::refreshPopupButtons(const QString &userName) +{ + UserListTWI *item = users.value(userName); + if (!item) { + return; + } + + const UserListProxy *proxy = tabSupervisor->getUserListManager(); + const bool online = item->data(0, UserListRoles::Online).toBool(); + const bool isBuddy = proxy->isUserBuddy(userName); + const bool isIgn = proxy->isUserIgnored(userName); + + m_userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn); + positionPopup(userName); // height may have changed — reposition +} + +void UserListWidget::hideEvent(QHideEvent *e) +{ + QGroupBox::hideEvent(e); + m_showPopupTimer->stop(); + m_hidePopupTimer->stop(); + hidePopup(true); +} + +void UserListWidget::applyDisplayMode() +{ + const bool styled = SettingsCache::instance().getStyleUserList(); + + if (styled) { + userTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + userTree->hideColumn(1); + userTree->hideColumn(2); + userTree->hideColumn(3); + } else { + userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + userTree->showColumn(1); + userTree->showColumn(2); + userTree->hideColumn(3); + } + + userTree->viewport()->update(); +} + +void UserListWidget::connectPopupSignals() +{ + connect(m_userInfoPopup, &UserInfoPopup::closeRequested, this, [this] { + m_popupPinned = false; + hidePopup(true); + }); + connect(m_userInfoPopup, &UserInfoPopup::mouseEnteredPopup, m_hidePopupTimer, &QTimer::stop); + connect(m_userInfoPopup, &UserInfoPopup::mouseLeftPopup, this, [this] { + if (!m_popupPinned) { + m_hidePopupTimer->start(); + } + }); + + // Wire all action signals to UserContextMenu::exec*() + connect(m_userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat); + connect(m_userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails); + connect(m_userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames); + connect(m_userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy); + connect(m_userInfoPopup, &UserInfoPopup::removeBuddyRequested, userContextMenu, + &UserContextMenu::execRemoveFromBuddy); + connect(m_userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore); + connect(m_userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu, + &UserContextMenu::execRemoveFromIgnore); + connect(m_userInfoPopup, &UserInfoPopup::banRequested, userContextMenu, &UserContextMenu::execBan); + connect(m_userInfoPopup, &UserInfoPopup::warnRequested, userContextMenu, &UserContextMenu::execWarn); + connect(m_userInfoPopup, &UserInfoPopup::banHistoryRequested, userContextMenu, &UserContextMenu::execBanHistory); + connect(m_userInfoPopup, &UserInfoPopup::warnHistoryRequested, userContextMenu, &UserContextMenu::execWarnHistory); + connect(m_userInfoPopup, &UserInfoPopup::adminNotesRequested, userContextMenu, &UserContextMenu::execAdminNotes); + connect(m_userInfoPopup, &UserInfoPopup::promoteToModRequested, this, + [this](const QString &n) { userContextMenu->execAdjustMod(n, true, false); }); + connect(m_userInfoPopup, &UserInfoPopup::demoteFromModRequested, this, + [this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); }); + connect(m_userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this, + [this](const QString &n) { userContextMenu->execAdjustMod(n, false, true); }); + connect(m_userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this, + [this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); }); +} + +bool UserListWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == userTree->viewport()) { + if (event->type() == QEvent::MouseMove) { + if (!SettingsCache::instance().getStyleUserList()) { + return QGroupBox::eventFilter(obj, event); + } + auto *me = static_cast(event); + auto *twi = static_cast(userTree->itemAt(me->pos())); + const QString hovName = twi ? QString::fromStdString(twi->getUserInfo().name()) : QString{}; + + if (hovName != m_hoveredUser) { + m_hoveredUser = hovName; + if (!hovName.isEmpty()) { + m_hidePopupTimer->stop(); + if (!m_popupPinned) { + m_showPopupTimer->start(); + } + } else { + m_showPopupTimer->stop(); + if (!m_popupPinned) { + m_hidePopupTimer->start(); + } + } + } + } else if (event->type() == QEvent::Leave) { + m_hoveredUser.clear(); + m_showPopupTimer->stop(); + if (!m_popupPinned) { + m_hidePopupTimer->start(); + } + } + } + + return QGroupBox::eventFilter(obj, event); +} + +void UserListWidget::showPopupForUser(const QString &userName) +{ + UserListTWI *item = users.value(userName); + if (!item) { + return; + } + + const ServerInfo_User &info = item->getUserInfo(); + const bool online = item->data(0, UserListRoles::Online).toBool(); + const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName); + const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName); + + m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); + + positionPopup(userName); + + m_userInfoPopup->show(); + m_userInfoPopup->raise(); + + // Fade in + m_userInfoPopup->setWindowOpacity(0.0); + auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); + fade->setDuration(120); + fade->setStartValue(0.0); + fade->setEndValue(1.0); + fade->start(QAbstractAnimation::DeleteWhenStopped); +} + +void UserListWidget::positionPopup(const QString &userName) +{ + UserListTWI *item = users.value(userName); + if (!item) { + return; + } + + QWidget *vp = userTree->viewport(); + const QRect itemR = userTree->visualItemRect(item); + const QPoint itemBR = vp->mapToGlobal(itemR.bottomRight()); + const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft()); + const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight()); + + // Force a fresh size calculation so popH is accurate + m_userInfoPopup->adjustSize(); + const int popW = m_userInfoPopup->width(); + const int popH = m_userInfoPopup->height(); + const int margin = 12; + + const QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); + + // ── X: left of the list if there's room, otherwise right ───────────────── + int x = (vpTL.x() >= popW + margin) ? vpTL.x() - popW - margin : vpTR.x() + margin; + x = qBound(screen.left() + margin, x, screen.right() - popW - margin); + + // ── Y: bottom of popup aligns with bottom of hovered row, grows upward ─── + int y = itemBR.y() - popH; + + // Clamp: never above the screen top + y = qMax(y, screen.top() + margin); + + // Clamp: never below the screen bottom (e.g. if the popup is taller + // than the space above the row, let it spill downward rather than clip) + y = qMin(y, screen.bottom() - popH - margin); + + m_userInfoPopup->move(x, y); +} + +void UserListWidget::hidePopup(bool immediate) +{ + m_showPopupTimer->stop(); + m_hidePopupTimer->stop(); + if (!m_userInfoPopup->isVisible()) { + return; + } + + if (immediate) { + m_userInfoPopup->hide(); + return; + } + + // Fade out + auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); + fade->setDuration(100); + fade->setStartValue(m_userInfoPopup->windowOpacity()); + fade->setEndValue(0.0); + connect(fade, &QPropertyAnimation::finished, m_userInfoPopup, &QWidget::hide); + fade->start(QAbstractAnimation::DeleteWhenStopped); +} + void UserListWidget::retranslateUi() { userContextMenu->retranslateUi(); @@ -461,9 +861,59 @@ void UserListWidget::retranslateUi() updateCount(); } +void UserListWidget::rebuild() +{ + userTree->clear(); + users.clear(); + cardArtParamsMap.clear(); + onlineCount = 0; + + if (!manager) { + return; + } + + const QMap *source = nullptr; + + switch (type) { + case AllUsersList: + case RoomList: + source = &manager->getAllUsersList(); + break; + case BuddyList: + source = &manager->getBuddyList(); + break; + case IgnoreList: + source = &manager->getIgnoreList(); + break; + } + + for (auto it = source->cbegin(); it != source->cend(); ++it) { + processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr); + } + + sortItems(); +} + void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) { const QString userName = QString::fromStdString(user.name()); + + // Always update params from the latest ServerInfo_User, whether the + // item is new or existing, so a live server-push refreshes the rendering. + if (user.has_card_art_params()) { + const auto &cap = user.card_art_params(); + CardArtParams params; + params.cardName = QString::fromStdString(cap.card_name()); + params.marginPctL = cap.margin_pct_l(); + params.marginPctR = cap.margin_pct_r(); + params.verticalOffset = cap.vertical_offset(); + params.zoom = cap.zoom(); + cardArtParamsMap.insert(userName, params); + cardArtProvider->requestCardArt(userName, params.cardName); + } else { + cardArtParamsMap.remove(userName); // clear stale params on removal + } + UserListTWI *item = users.value(userName); if (item) { item->setUserInfo(user); @@ -475,25 +925,28 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) ++onlineCount; } updateCount(); + avatarProvider->requestAvatar(userName); } item->setOnline(online); + sortItems(); + userTree->viewport()->update(); } bool UserListWidget::deleteUser(const QString &userName) { UserListTWI *twi = users.value(userName); - if (twi) { - users.remove(userName); - userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi)); - if (twi->data(0, Qt::UserRole + 1).toBool()) { - --onlineCount; - } - delete twi; - updateCount(); - return true; + if (!twi) { + return false; } - return false; + users.remove(userName); + userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi)); + if (twi->data(0, Qt::UserRole + 1).toBool()) { + --onlineCount; + } + delete twi; + updateCount(); + return true; } void UserListWidget::setUserOnline(const QString &userName, bool online) @@ -537,5 +990,5 @@ void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index void UserListWidget::sortItems() { - userTree->sortItems(1, Qt::AscendingOrder); + userTree->sortItems(0, Qt::AscendingOrder); } diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 5a8c00d10..d70cdfbbd 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -7,9 +7,17 @@ #ifndef USERLIST_H #define USERLIST_H +#include "../../cards/card_info_picture_art_crop_widget.h" +#include "user_avatar_provider.h" +#include "user_card_art_provider.h" +#include "user_info_popup.h" +#include "user_list_manager.h" +#include "user_list_painter.h" + #include #include #include +#include #include #include #include @@ -94,12 +102,21 @@ public: class UserListItemDelegate : public QStyledItemDelegate { + const QMap *avatarCache; + const QMap *cardArtCache; + const QMap *cardArtParamsMap; + public: - explicit UserListItemDelegate(QObject *const parent); + explicit UserListItemDelegate(QObject *const parent, + const QMap *avatarCache, + const QMap *cardArtCache, + const QMap *cardArtParamsMap); bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; class UserListTWI : public QTreeWidgetItem @@ -131,6 +148,22 @@ public: }; private: + UserListManager *manager = nullptr; + UserAvatarProvider *avatarProvider = nullptr; + UserCardArtProvider *cardArtProvider = nullptr; + QMap cardArtParamsMap; + // ── Hover popup ─────────────────────────────────────────────────────────── + UserInfoPopup *m_userInfoPopup = nullptr; + QTimer *m_showPopupTimer = nullptr; + QTimer *m_hidePopupTimer = nullptr; + QString m_hoveredUser; + bool m_popupPinned = false; + + void showPopupForUser(const QString &userName); + void hidePopup(bool immediate = false); + void positionPopup(const QString &userName); + void connectPopupSignals(); + QMap users; TabSupervisor *tabSupervisor; AbstractClient *client; @@ -141,6 +174,7 @@ private: int onlineCount; QString titleStr; void updateCount(); + void refreshPopupButtons(const QString &userName); private slots: void userClicked(QTreeWidgetItem *item, int column); signals: @@ -149,13 +183,18 @@ signals: void removeBuddy(const QString &userName); void addIgnore(const QString &userName); void removeIgnore(const QString &userName); + void joinGameRequested(int gameId, int roomId, bool asSpectator); public: UserListWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, QWidget *parent = nullptr); + void bind(UserListManager *mgr); + void applyDisplayMode(); + bool eventFilter(QObject *obj, QEvent *event) override; void retranslateUi(); + void rebuild(); void processUserInfo(const ServerInfo_User &user, bool online); bool deleteUser(const QString &userName); void setUserOnline(const QString &userName, bool online); @@ -165,6 +204,9 @@ public: } void showContextMenu(const QPoint &pos, const QModelIndex &index); void sortItems(); + +protected: + void hideEvent(QHideEvent *e) override; }; #endif diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index 2d32f3ce1..e00484ebf 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -111,6 +111,15 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); + styleUserListCheckBox.setChecked(settings.getStyleUserList()); + connect(&styleUserListCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings, &SettingsCache::setStyleUserList); + + auto stylingTabGrid = new QGridLayout; + stylingTabGrid->addWidget(&styleUserListCheckBox, 0, 0, 1, 2); + + stylingGroupBox = new QGroupBox; + stylingGroupBox->setLayout(stylingTabGrid); + // Menu settings showShortcutsCheckBox.setChecked(settings.getShowShortcuts()); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); @@ -284,6 +293,7 @@ AppearanceSettingsPage::AppearanceSettingsPage() auto *mainLayout = new QVBoxLayout; mainLayout->addWidget(themeGroupBox); mainLayout->addWidget(homeTabGroupBox); + mainLayout->addWidget(stylingGroupBox); mainLayout->addWidget(menuGroupBox); mainLayout->addWidget(printingsGroupBox); mainLayout->addWidget(cardsGroupBox); @@ -398,6 +408,9 @@ void AppearanceSettingsPage::retranslateUi() homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled")); homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right")); + stylingGroupBox->setTitle(tr("Styling settings")); + styleUserListCheckBox.setText(tr("Style user list")); + menuGroupBox->setTitle(tr("Menu settings")); showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus")); showGameSelectorFilterToolbarCheckBox.setText(tr("Show game filter toolbar above list in room tab")); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index 9ed27be4d..e223d70f8 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -37,6 +37,7 @@ private: QLabel homeTabBackgroundShuffleFrequencyLabel; QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; + QCheckBox styleUserListCheckBox; QLabel minPlayersForMultiColumnLayoutLabel; QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; @@ -58,6 +59,7 @@ private: QCheckBox invertVerticalCoordinateCheckBox; QGroupBox *themeGroupBox; QGroupBox *homeTabGroupBox; + QGroupBox *stylingGroupBox; QGroupBox *menuGroupBox; QGroupBox *printingsGroupBox; QGroupBox *cardsGroupBox; diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp new file mode 100644 index 000000000..6e8ab752a --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -0,0 +1,246 @@ +#include "tab_card_art_rules.h" + +#include "libcockatrice/card/database/card_database_manager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +CardArtRulesModel::CardArtRulesModel(AbstractClient *client, QObject *parent) + : QAbstractTableModel(parent), client(client) +{ +} + +int CardArtRulesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return static_cast(entries.size()); +} + +int CardArtRulesModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 3; +} + +QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + const auto &e = entries.at(index.row()); + + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: + return e.cardName; + case 1: + return e.mode; + case 2: + return e.reason; + } + } + + return {}; +} + +QVariant CardArtRulesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) { + return {}; + } + + switch (section) { + case 0: + return tr("Card"); + case 1: + return tr("Mode"); + case 2: + return tr("Reason"); + default: + return {}; + } +} + +void CardArtRulesModel::refresh() +{ + Command_ListCardArtRules cmd; + + PendingCommand *pend = client->prepareModeratorCommand(cmd); + + connect(pend, &PendingCommand::finished, this, &CardArtRulesModel::onRefreshFinished); + + client->sendCommand(pend); +} + +void CardArtRulesModel::clear() +{ + beginResetModel(); + entries.clear(); + endResetModel(); +} + +QString CardArtRulesModel::cardAt(int row) const +{ + if (row < 0 || row >= static_cast(entries.size())) { + return {}; + } + + return entries[row].cardName; +} + +void CardArtRulesModel::onRefreshFinished(const Response &r) +{ + if (r.response_code() != Response::RespOk) { + return; + } + + const auto &resp = r.GetExtension(Response_ListCardArtRules::ext); + + beginResetModel(); + entries.clear(); + + for (const auto &e : resp.entries()) { + entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.mode()), + QString::fromStdString(e.reason())}); + } + + endResetModel(); +} + +TabCardArtRules::TabCardArtRules(TabSupervisor *parent, AbstractClient *_client) : Tab(parent), client(_client) +{ + setupUi(); + refresh(); +} + +void TabCardArtRules::setupUi() +{ + auto *central = new QWidget(this); + + initSearchBar(); + + modeBox = new QComboBox; + reasonEdit = new QLineEdit; + + addBtn = new QPushButton; + removeBtn = new QPushButton; + refreshBtn = new QPushButton; + + modeBox->addItems({"ALLOW", "DENY"}); + + tableModel = new CardArtRulesModel(client, this); + + table = new QTableView; + table->setModel(tableModel); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setSelectionMode(QAbstractItemView::SingleSelection); + + auto *form = new QFormLayout; + form->addRow(tr("Card:"), searchEdit); + form->addRow(tr("Mode:"), modeBox); + form->addRow(tr("Reason:"), reasonEdit); + + auto *buttons = new QHBoxLayout; + buttons->addWidget(addBtn); + buttons->addWidget(removeBtn); + buttons->addWidget(refreshBtn); + + auto *layout = new QVBoxLayout; + layout->addLayout(form); + layout->addLayout(buttons); + layout->addWidget(table); + + central->setLayout(layout); + setCentralWidget(central); + + connect(addBtn, &QPushButton::clicked, this, &TabCardArtRules::addRule); + + connect(removeBtn, &QPushButton::clicked, this, &TabCardArtRules::removeSelected); + + connect(refreshBtn, &QPushButton::clicked, this, &TabCardArtRules::refresh); + + retranslateUi(); +} + +void TabCardArtRules::initSearchBar() +{ + searchEdit = new QLineEdit; + searchEdit->setPlaceholderText(tr("Type a card name...")); + + cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + cardDbDisplayModel = new CardDatabaseDisplayModel(this); + cardDbDisplayModel->setSourceModel(cardDbModel); + cardSearchModel = new CardSearchModel(cardDbDisplayModel, this); + + cardProxyModel = new CardCompleterProxyModel(this); + cardProxyModel->setSourceModel(cardSearchModel); + cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + searchCompleter = new QCompleter(cardProxyModel, this); + searchCompleter->setCompletionRole(Qt::DisplayRole); + searchCompleter->setCompletionMode(QCompleter::PopupCompletion); + searchCompleter->setCaseSensitivity(Qt::CaseInsensitive); + searchCompleter->setFilterMode(Qt::MatchContains); + searchCompleter->setMaxVisibleItems(15); + searchEdit->setCompleter(searchCompleter); + + connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults); + connect(searchEdit, &QLineEdit::textEdited, this, [this](const QString &text) { + const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + cardProxyModel->setFilterRegularExpression( + QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + searchCompleter->complete(); + } + }); + connect(searchCompleter, static_cast(&QCompleter::activated), this, + [this](const QString &name) { searchEdit->setText(name); }); +} + +void TabCardArtRules::retranslateUi() +{ + addBtn->setText(tr("Add rule")); + removeBtn->setText(tr("Remove rule")); + refreshBtn->setText(tr("Refresh")); +} + +void TabCardArtRules::refresh() +{ + tableModel->refresh(); +} + +void TabCardArtRules::addRule() +{ + Command_AddCardArtRule cmd; + cmd.set_card_name(searchEdit->text().toStdString()); + cmd.set_mode(modeBox->currentText().toStdString()); + cmd.set_reason(reasonEdit->text().toStdString()); + + client->sendCommand(client->prepareModeratorCommand(cmd)); + + refresh(); +} + +void TabCardArtRules::removeSelected() +{ + QModelIndex idx = table->currentIndex(); + if (!idx.isValid()) { + return; + } + + Command_RemoveCardArtRule cmd; + cmd.set_card_name(tableModel->cardAt(idx.row()).toStdString()); + + client->sendCommand(client->prepareModeratorCommand(cmd)); + + refresh(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h new file mode 100644 index 000000000..a47f1267d --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h @@ -0,0 +1,89 @@ +#ifndef COCKATRICE_DLG_CARD_ART_RULES_H +#define COCKATRICE_DLG_CARD_ART_RULES_H + +#include "card/card_search_model.h" +#include "tab_supervisor.h" + +#include +#include +#include +#include +#include + +class AbstractClient; + +class CardArtRulesModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + struct Entry + { + QString cardName; + QString mode; + QString reason; + }; + + explicit CardArtRulesModel(AbstractClient *client, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + void refresh(); + void clear(); + + QString cardAt(int row) const; + +private slots: + void onRefreshFinished(const Response &r); + +private: + AbstractClient *client; + std::vector entries; +}; + +class TabCardArtRules : public Tab +{ + Q_OBJECT + +public: + TabCardArtRules(TabSupervisor *parent, AbstractClient *client); + + QString getTabText() const override + { + return tr("Card Art Rules"); + } + void retranslateUi() override; + +private: + void setupUi(); + +private slots: + void addRule(); + void removeSelected(); + void refresh(); + +private: + AbstractClient *client; + + QLineEdit *searchEdit; + void initSearchBar(); + QCompleter *searchCompleter; + CardDatabaseModel *cardDbModel; + CardDatabaseDisplayModel *cardDbDisplayModel; + CardSearchModel *cardSearchModel; + CardCompleterProxyModel *cardProxyModel; + QComboBox *modeBox; + QLineEdit *reasonEdit; + + QPushButton *addBtn; + QPushButton *removeBtn; + QPushButton *refreshBtn; + + QTableView *table; + CardArtRulesModel *tableModel; +}; + +#endif // COCKATRICE_DLG_CARD_ART_RULES_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 424742e9b..c7495da5a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -49,10 +49,25 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, QMap tempMap; tempMap.insert(info.room_id(), gameTypes); gameSelector = new GameSelector(client, tabSupervisor, this, QMap(), tempMap, true, true); + + auto *tabs = new QTabWidget(this); + + friendsList = new UserListWidget(tabSupervisor, client, UserListWidget::BuddyList); + friendsList->bind(tabSupervisor->getUserListManager()); userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList); + userList->bind(tabSupervisor->getUserListManager()); + ignoreList = new UserListWidget(tabSupervisor, client, UserListWidget::IgnoreList); + ignoreList->bind(tabSupervisor->getUserListManager()); + + connect(friendsList, SIGNAL(openMessageDialog(const QString &, bool)), this, + SIGNAL(openMessageDialog(const QString &, bool))); connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this, SIGNAL(openMessageDialog(const QString &, bool))); + tabs->addTab(friendsList, tr("Friends")); + tabs->addTab(userList, tr("Online")); + tabs->addTab(ignoreList, tr("Ignored")); + chatView = new ChatView(tabSupervisor, nullptr, true, this); connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup); connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab); @@ -101,7 +116,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, auto *hbox = new QHBoxLayout; hbox->addWidget(splitter, 3); - hbox->addWidget(userList, 1); + hbox->addWidget(tabs, 1); aLeaveRoom = new QAction(this); connect(aLeaveRoom, &QAction::triggered, this, &TabRoom::closeRequest); @@ -112,10 +127,8 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, const int userListSize = info.user_list_size(); for (int i = 0; i < userListSize; ++i) { - userList->processUserInfo(info.user_list(i), true); autocompleteUserList.append("@" + QString::fromStdString(info.user_list(i).name())); } - userList->sortItems(); const int gameListSize = info.game_list_size(); for (int i = 0; i < gameListSize; ++i) { @@ -269,8 +282,6 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event) void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) { - userList->processUserInfo(event.user_info(), true); - userList->sortItems(); if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) { autocompleteUserList << "@" + QString::fromStdString(event.user_info().name()); sayEdit->setCompletionList(autocompleteUserList); @@ -279,7 +290,6 @@ void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event) { - userList->deleteUser(QString::fromStdString(event.name())); autocompleteUserList.removeOne("@" + QString::fromStdString(event.name())); sayEdit->setCompletionList(autocompleteUserList); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index eeb5a9e14..d669b6107 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -56,7 +56,9 @@ private: QMap gameTypes; GameSelector *gameSelector; + UserListWidget *friendsList; UserListWidget *userList; + UserListWidget *ignoreList; const UserListProxy *userListProxy; ChatView *chatView; QLabel *sayLabel; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index e7075f78f..52309d94b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -9,6 +9,7 @@ #include "api/edhrec/tab_edhrec_main.h" #include "tab_account.h" #include "tab_admin.h" +#include "tab_card_art_rules.h" #include "tab_deck_editor.h" #include "tab_deck_storage.h" #include "tab_game.h" @@ -179,6 +180,10 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * aTabAdmin->setCheckable(true); connect(aTabAdmin, &QAction::triggered, this, &TabSupervisor::actTabAdmin); + aTabCardArtRules = new QAction(this); + aTabCardArtRules->setCheckable(true); + connect(aTabCardArtRules, &QAction::triggered, this, &TabSupervisor::actTabCardArtRules); + aTabLog = new QAction(this); aTabLog->setCheckable(true); connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog); @@ -435,6 +440,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) tabsMenu->addSeparator(); tabsMenu->addAction(aTabAdmin); tabsMenu->addAction(aTabLog); + tabsMenu->addAction(aTabCardArtRules); if (SettingsCache::instance().getTabAdminOpen()) { openTabAdmin(); @@ -442,6 +448,7 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo) if (SettingsCache::instance().getTabLogOpen()) { openTabLog(); } + openTabCardArtRules(); } retranslateUi(); @@ -681,6 +688,30 @@ void TabSupervisor::openTabAdmin() aTabAdmin->setChecked(true); } +void TabSupervisor::actTabCardArtRules(bool checked) +{ + if (checked && !tabCardArtRules) { + openTabCardArtRules(); + setCurrentWidget(tabCardArtRules); + } else if (!checked && tabCardArtRules) { + tabCardArtRules->closeRequest(); + } +} + +void TabSupervisor::openTabCardArtRules() +{ + tabCardArtRules = new TabCardArtRules(this, client); + + myAddTab(tabCardArtRules, aTabCardArtRules); + + connect(tabCardArtRules, &QObject::destroyed, this, [this] { + tabCardArtRules = nullptr; + aTabCardArtRules->setChecked(false); + }); + + aTabCardArtRules->setChecked(true); +} + void TabSupervisor::actTabLog(bool checked) { SettingsCache::instance().setTabLogOpen(checked); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index e77fb4f7b..3eac144b7 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -24,6 +24,7 @@ #include #include +class TabCardArtRules; inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor"); class UserListManager; @@ -103,6 +104,7 @@ private: TabDeckStorage *tabDeckStorage; TabReplays *tabReplays; TabAdmin *tabAdmin; + TabCardArtRules *tabCardArtRules; TabLog *tabLog; QMap roomTabs; QMap gameTabs; @@ -112,7 +114,8 @@ private: bool isLocalGame; QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage, - *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabLog; + *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin, + *aTabCardArtRules, *aTabLog; int myAddTab(Tab *tab, QAction *manager = nullptr); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); @@ -145,7 +148,7 @@ public: return userInfo; } [[nodiscard]] AbstractClient *getClient() const; - [[nodiscard]] const UserListManager *getUserListManager() const + [[nodiscard]] UserListManager *getUserListManager() const { return userListManager; } @@ -197,6 +200,8 @@ private slots: void openTabDeckStorage(); void openTabReplays(); void openTabAdmin(); + void actTabCardArtRules(bool checked); + void openTabCardArtRules(); void openTabLog(); void updateCurrent(int index); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp index 3da9ddc73..7abccfca8 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp @@ -190,6 +190,25 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session, return authState; } +void Server::broadcastUserInfoUpdate(Server_ProtocolHandler *source) +{ + Event_UserJoined event; + event.mutable_user_info()->CopyFrom(source->copyUserInfo(false)); + + SessionEvent *se = Server_ProtocolHandler::prepareSessionEvent(event); + + clientsLock.lockForRead(); + for (auto &client : clients) { + if (client->getAcceptsUserListChanges()) { + client->sendProtocolItem(*se); + } + } + clientsLock.unlock(); + + sendIsl_SessionEvent(*se); + delete se; +} + void Server::addPersistentPlayer(const QString &userName, int roomId, int gameId, int playerId) { QWriteLocker locker(&persistentPlayersLock); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.h b/libcockatrice_network/libcockatrice/network/server/remote/server.h index ab57fac4e..2fca46593 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.h @@ -64,6 +64,7 @@ public: QString &clientid, QString &clientVersion, QString &connectionType); + void broadcastUserInfoUpdate(Server_ProtocolHandler *source); const QMap &getRooms() { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index b4c7b6ac8..20a4cb08d 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -122,6 +122,7 @@ set(PROTO_FILES response_activate.proto response_adjust_mod.proto response_ban_history.proto + response_card_art_rule_entry.proto response_deck_download.proto response_deck_list.proto response_deck_upload.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index 9d01b51d2..c10b9de22 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -11,6 +11,9 @@ message ModeratorCommand { FORCE_ACTIVATE_USER = 1007; GET_ADMIN_NOTES = 1008; UPDATE_ADMIN_NOTES = 1009; + ADD_CARD_ART_RULE = 1010; + REMOVE_CARD_ART_RULE = 1011; + LIST_CARD_ART_RULES = 1012; } extensions 100 to max; } @@ -106,3 +109,27 @@ message Command_UpdateAdminNotes { optional string user_name = 1; optional string notes = 2; } + +message Command_AddCardArtRule { + extend ModeratorCommand { + optional Command_AddCardArtRule ext = 1010; + } + + optional string card_name = 1; + optional string mode = 2; // "ALLOW" or "DENY" + optional string reason = 3; +} + +message Command_RemoveCardArtRule { + extend ModeratorCommand { + optional Command_RemoveCardArtRule ext = 1011; + } + + optional string card_name = 1; +} + +message Command_ListCardArtRules { + extend ModeratorCommand { + optional Command_ListCardArtRules ext = 1012; + } +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto index dece8ae17..e719f3e92 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto @@ -68,6 +68,7 @@ message Response { REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; REPLAY_GET_CODE = 1102; + CARD_ART_RULE_LIST = 1200; } required uint64 cmd_id = 1; optional ResponseCode response_code = 2; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto new file mode 100644 index 000000000..b13c79742 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_CardArtRuleEntry { + optional string card_name = 1; + optional string mode = 2; + optional string reason = 3; +} + +message Response_ListCardArtRules { + extend Response { + optional Response_ListCardArtRules ext = 1200; + } + repeated Response_CardArtRuleEntry entries = 1; +} \ No newline at end of file diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto index 10add611f..4bbbe46bb 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto @@ -1,4 +1,5 @@ syntax = "proto2"; + message ServerInfo_User { enum UserLevelFlag { IsNothing = 0; @@ -12,6 +13,13 @@ message ServerInfo_User { optional string left_side = 1; optional string right_side = 2; }; + message CardArtParams { + optional string card_name = 1; + optional double margin_pct_l = 2 [default = 0.33]; + optional double margin_pct_r = 3 [default = 0.02]; + optional double vertical_offset = 4 [default = 0.35]; + optional double zoom = 5 [default = 1.0]; + }; optional string name = 1; optional uint32 user_level = 2; @@ -28,4 +36,5 @@ message ServerInfo_User { optional string clientid = 13; optional string privlevel = 14; optional PawnColorsOverride pawn_colors = 15; -} + optional CardArtParams card_art_params = 16; +} \ No newline at end of file diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index cecf87370..ff5dbff32 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -27,6 +27,7 @@ message SessionCommand { FORGOT_PASSWORD_RESET = 1022; FORGOT_PASSWORD_CHALLENGE = 1023; REQUEST_PASSWORD_SALT = 1024; + SET_CARD_ART_PARAMS = 1025; REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; REPLAY_MODIFY_MATCH = 1102; @@ -205,3 +206,14 @@ message Command_RequestPasswordSalt { } required string user_name = 1; } + +message Command_SetCardArtParams { + extend SessionCommand { + optional Command_SetCardArtParams ext = 1025; + } + optional string card_name = 1; + optional double margin_pct_l = 2; + optional double margin_pct_r = 3; + optional double vertical_offset = 4; + optional double zoom = 5; +} diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index c0c7d8cc9..df29d6c9f 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -17,6 +17,7 @@ set(UTILITY_HEADERS libcockatrice/utility/passwordhasher.h libcockatrice/utility/trice_limits.h libcockatrice/utility/zone_names.h + libcockatrice/utility/days_years_between.h ) add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) diff --git a/libcockatrice_utility/libcockatrice/utility/days_years_between.h b/libcockatrice_utility/libcockatrice/utility/days_years_between.h index c0f5da23a..4b3b5bc0c 100644 --- a/libcockatrice_utility/libcockatrice/utility/days_years_between.h +++ b/libcockatrice_utility/libcockatrice/utility/days_years_between.h @@ -1,3 +1,6 @@ +#ifndef COCKATRICE_DAYS_YEARS_BETWEEN_H +#define COCKATRICE_DAYS_YEARS_BETWEEN_H + #include inline static QPair getDaysAndYearsBetween(const QDate &then, const QDate &now) @@ -6,3 +9,5 @@ inline static QPair getDaysAndYearsBetween(const QDate &then, const QD int days = then.addYears(years).daysTo(now); return {days, years}; } + +#endif // COCKATRICE_DAYS_YEARS_BETWEEN_H diff --git a/servatrice/migrations/servatrice_0034_to_0035.sql b/servatrice/migrations/servatrice_0034_to_0035.sql new file mode 100644 index 000000000..83502a949 --- /dev/null +++ b/servatrice/migrations/servatrice_0034_to_0035.sql @@ -0,0 +1,18 @@ +ALTER TABLE `cockatrice_users` ADD COLUMN `card_art_params` TEXT DEFAULT NULL, ALGORITHM=INSTANT; + +CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `card_name` varchar(255) NOT NULL, + `mode` enum('ALLOW','DENY') NOT NULL, + `reason` varchar(255) DEFAULT NULL, + `created_by` int(7) unsigned DEFAULT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_card_name` (`card_name`), + KEY `idx_mode` (`mode`), + FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) + ON DELETE SET NULL + ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; + +UPDATE cockatrice_schema_version SET version=35 WHERE version=34; diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index fa644dbc0..badd82d6d 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( PRIMARY KEY (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -INSERT INTO cockatrice_schema_version VALUES(34); +INSERT INTO cockatrice_schema_version VALUES(35); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( @@ -43,6 +43,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_users` ( `passwordLastChangedDate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `leftPawnColorOverride` varchar(255), `rightPawnColorOverride` varchar(255), + `card_art_params` TEXT DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`), KEY `token` (`token`), @@ -300,3 +301,18 @@ CREATE TABLE IF NOT EXISTS `cockatrice_audit` ( PRIMARY KEY (`id`), KEY `user_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `card_name` varchar(255) NOT NULL, + `mode` enum('ALLOW','DENY') NOT NULL, + `reason` varchar(255) DEFAULT NULL, + `created_by` int(7) unsigned DEFAULT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_card_name` (`card_name`), + KEY `idx_mode` (`mode`), + FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) + ON DELETE SET NULL + ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 73643825e..92ff80755 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -681,6 +683,30 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer if (!clientid.isEmpty()) { result.set_clientid(clientid.toStdString()); } + + const QString cardArtParamsJson = query->value(12).toString(); + if (!cardArtParamsJson.isEmpty()) { + const QJsonDocument doc = QJsonDocument::fromJson(cardArtParamsJson.toUtf8()); + if (doc.isObject()) { + const QJsonObject obj = doc.object(); + auto *cap = result.mutable_card_art_params(); + if (obj.contains("card_name")) { + cap->set_card_name(obj["card_name"].toString().toStdString()); + } + if (obj.contains("marginPctL")) { + cap->set_margin_pct_l(obj["marginPctL"].toDouble(0.33)); + } + if (obj.contains("marginPctR")) { + cap->set_margin_pct_r(obj["marginPctR"].toDouble(0.02)); + } + if (obj.contains("verticalOffset")) { + cap->set_vertical_offset(obj["verticalOffset"].toDouble(0.35)); + } + if (obj.contains("zoom")) { + cap->set_zoom(obj["zoom"].toDouble(1.0)); + } + } + } } return result; } @@ -698,7 +724,7 @@ ServerInfo_User Servatrice_DatabaseInterface::getUserData(const QString &name, b QSqlQuery *query = prepareQuery("select id, name, admin, country, privlevel, leftPawnColorOverride, " "rightPawnColorOverride, realname, avatar_bmp, registrationDate, " - "email, clientid from {prefix}_users where " + "email, clientid, card_art_params from {prefix}_users where " "name = :name and active = 1"); query->bindValue(":name", name); if (!execSqlQuery(query)) { diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 68080404c..1e3501ec7 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -10,7 +10,7 @@ #include #include -#define DATABASE_SCHEMA_VERSION 34 +#define DATABASE_SCHEMA_VERSION 35 class Servatrice; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index bc90a3ef1..f9d276941 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -59,8 +61,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -212,6 +216,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdAccountEdit(cmd.GetExtension(Command_AccountEdit::ext), rc); case SessionCommand::ACCOUNT_IMAGE: return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc); + case SessionCommand::SET_CARD_ART_PARAMS: + return cmdSetCardArtParams(cmd.GetExtension(Command_SetCardArtParams::ext), rc); case SessionCommand::ACCOUNT_PASSWORD: return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc); case SessionCommand::REQUEST_PASSWORD_SALT: @@ -247,6 +253,12 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCo return cmdGetAdminNotes(cmd.GetExtension(Command_GetAdminNotes::ext), rc); case ModeratorCommand::UPDATE_ADMIN_NOTES: return cmdUpdateAdminNotes(cmd.GetExtension(Command_UpdateAdminNotes::ext), rc); + case ModeratorCommand::ADD_CARD_ART_RULE: + return cmdAddCardArtRule(cmd.GetExtension((Command_AddCardArtRule::ext)), rc); + case ModeratorCommand::REMOVE_CARD_ART_RULE: + return cmdRemoveCardArtRule(cmd.GetExtension((Command_RemoveCardArtRule::ext)), rc); + case ModeratorCommand::LIST_CARD_ART_RULES: + return cmdListCardArtRules(cmd.GetExtension((Command_ListCardArtRules::ext)), rc); default: return Response::RespFunctionNotAllowed; } @@ -1565,6 +1577,161 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Comm return Response::RespOk; } +bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName) +{ + QSqlQuery *q = sqlInterface->prepareQuery("SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name"); + + q->bindValue(":name", cardName); + + if (!sqlInterface->execSqlQuery(q)) { + qWarning() << "Card art rule lookup failed; failing open for" << cardName; + return true; + } + + if (!q->next()) { + return true; // default allow + } + + return q->value(0).toString() != "DENY"; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const Command_SetCardArtParams &cmd, + ResponseContainer & /* rc */) +{ + if (authState != PasswordRight) { + return Response::RespFunctionNotAllowed; + } + + const QString cardName = QString::fromStdString(cmd.card_name()); + + if (cardName.length() > MAX_NAME_LENGTH) { + return Response::RespInvalidData; + } + + if (cardName.isEmpty()) { + // Removal path + QSqlQuery *q = sqlInterface->prepareQuery("UPDATE {prefix}_users SET card_art_params = NULL WHERE id = :id"); + q->bindValue(":id", userInfo->id()); + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + userInfo->clear_card_art_params(); + server->broadcastUserInfoUpdate(this); + return Response::RespOk; + } + + if (!isCardNameAllowed(cardName)) { + return Response::RespFunctionNotAllowed; + } + + // Clamp everything to sane ranges server-side so a malicious client + // can't store garbage that breaks other clients' rendering. + const double marginPctL = qBound(0.0, cmd.margin_pct_l(), 0.95); + const double marginPctR = qBound(0.0, cmd.margin_pct_r(), 0.95); + const double verticalOffset = qBound(0.0, cmd.vertical_offset(), 1.0); + const double zoom = qBound(0.1, cmd.zoom(), 4.0); + + QJsonObject obj; + obj["card_name"] = cardName; + obj["marginPctL"] = marginPctL; + obj["marginPctR"] = marginPctR; + obj["verticalOffset"] = verticalOffset; + obj["zoom"] = zoom; + const QString json = QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); + + QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set card_art_params=:params where id=:id"); + query->bindValue(":params", json); + query->bindValue(":id", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) { + return Response::RespInternalError; + } + + // Keep the in-memory userInfo in sync + auto *cap = userInfo->mutable_card_art_params(); + cap->set_card_name(cmd.card_name()); + cap->set_margin_pct_l(marginPctL); + cap->set_margin_pct_r(marginPctR); + cap->set_vertical_offset(verticalOffset); + cap->set_zoom(zoom); + + const QString name = QString::fromStdString(userInfo->name()); + server->broadcastUserInfoUpdate(this); + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdAddCardArtRule(const Command_AddCardArtRule &cmd, + ResponseContainer &) +{ + const QString cardName = QString::fromStdString(cmd.card_name()); + const QString mode = QString::fromStdString(cmd.mode()); + + if (mode != "ALLOW" && mode != "DENY") { + return Response::RespInvalidData; + } + if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH) { + return Response::RespInvalidData; + } + + QSqlQuery *q = sqlInterface->prepareQuery("INSERT INTO {prefix}_card_art_name_rules " + "(card_name, mode, reason, created_by) " + "VALUES (:name, :mode, :reason, :uid) " + "ON DUPLICATE KEY UPDATE mode=:mode2, reason=:reason2"); + + q->bindValue(":name", cardName); + q->bindValue(":mode", mode); + q->bindValue(":mode2", mode); + q->bindValue(":reason", QString::fromStdString(cmd.reason())); + q->bindValue(":reason2", QString::fromStdString(cmd.reason())); + q->bindValue(":uid", userInfo->id()); + + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, + ResponseContainer &) +{ + auto cardName = QString::fromStdString(cmd.card_name()); + if (cardName.length() > MAX_NAME_LENGTH) { + return Response::RespInvalidData; + } + QSqlQuery *q = sqlInterface->prepareQuery("DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name"); + + q->bindValue(":name", cardName); + + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const Command_ListCardArtRules &, + ResponseContainer &rc) +{ + QSqlQuery *q = sqlInterface->prepareQuery("SELECT card_name, mode, reason FROM {prefix}_card_art_name_rules"); + + if (!sqlInterface->execSqlQuery(q)) { + return Response::RespInternalError; + } + + auto *re = new Response_ListCardArtRules; + + while (q->next()) { + auto *entry = re->add_entries(); + entry->set_card_name(q->value(0).toString().toStdString()); + entry->set_mode(q->value(1).toString().toStdString()); + entry->set_reason(q->value(2).toString().toStdString()); + } + + rc.setResponseExtension(re); + return Response::RespOk; +} + Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer & /* rc */) { diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index e10aa0dde..c0732ccd9 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -129,6 +129,11 @@ private: Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); + bool isCardNameAllowed(const QString &cardName); + Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &); + Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &); + Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &); + Response::ResponseCode cmdListCardArtRules(const Command_ListCardArtRules &, ResponseContainer &rc); Response::ResponseCode cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer &rc); Response::ResponseCode cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, ResponseContainer &rc); Response::ResponseCode cmdForceActivateUser(const Command_ForceActivateUser &cmd, ResponseContainer &rc); From 6dc974a05d7bdff5c088044ee28b5fc3427a8c7c Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:23:55 -0400 Subject: [PATCH 41/50] [UserListDelegate] Consider providerId (#7018) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../server/user/user_card_art_provider.cpp | 13 +-- .../server/user/user_card_art_provider.h | 2 +- .../server/user/user_card_settings_dialog.cpp | 91 +++++++++++++++++-- .../server/user/user_card_settings_dialog.h | 7 ++ .../widgets/server/user/user_info_popup.cpp | 2 +- .../widgets/server/user/user_list_painter.cpp | 6 +- .../widgets/server/user/user_list_painter.h | 1 + .../widgets/server/user/user_list_widget.cpp | 3 +- .../widgets/tabs/tab_card_art_rules.cpp | 59 ++++++++++-- .../widgets/tabs/tab_card_art_rules.h | 4 + .../protocol/pb/moderator_commands.proto | 6 +- .../pb/response_card_art_rule_entry.proto | 5 +- .../protocol/pb/serverinfo_user.proto | 9 +- .../protocol/pb/session_commands.proto | 9 +- .../migrations/servatrice_0034_to_0035.sql | 3 +- servatrice/servatrice.sql | 3 +- .../src/servatrice_database_interface.cpp | 3 + servatrice/src/serversocketinterface.cpp | 36 +++++--- servatrice/src/serversocketinterface.h | 2 +- 19 files changed, 211 insertions(+), 53 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp index 70a56375e..67fb4f684 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -5,9 +5,9 @@ #include #include -static QString makeKey(const QString &user, const QString &card) +static QString makeKey(const QString &user, const QString &card, const QString &providerId) { - return user + u'|' + card; + return user + u'|' + card + u'|' + providerId; } UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent) @@ -31,13 +31,13 @@ const QMap &UserCardArtProvider::cache() const return cardArtCache; } -void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName) +void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName, const QString &providerId) { if (cardName.isEmpty()) { return; } - const QString key = makeKey(userName, cardName); + const QString key = makeKey(userName, cardName, providerId); if (cardArtCache.contains(key) || pending.contains(key)) { return; @@ -83,15 +83,16 @@ void UserCardArtProvider::processQueue() const QString key = queue.dequeue(); const QStringList parts = key.split(u'|'); - if (parts.size() != 2) { + if (parts.size() != 3) { pending.remove(key); continue; } const QString userName = parts.at(0); const QString cardName = parts.at(1); + const QString providerId = parts.at(2); - ExactCard card = CardDatabaseManager::query()->getCard({cardName}); + ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId}); if (!card) { pending.remove(key); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h index a3ab874b7..fb2f37812 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -14,7 +14,7 @@ class UserCardArtProvider : public QObject public: explicit UserCardArtProvider(QObject *parent = nullptr); - void requestCardArt(const QString &userName, const QString &cardName); + void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); const QMap &cache() const; static QPixmap cropCardArt(const QPixmap &fullRes); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 335ee097e..56c9600a0 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -70,7 +70,7 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *) QString(), // userName not needed for override path nullptr, // no cache params, - &sourcePixmap // 👈 direct pixmap + &sourcePixmap // direct pixmap ); // Avatar placeholder so the left-margin interaction is visible @@ -174,6 +174,13 @@ void UserCardArtSettingsDialog::setupUi() { initializeSearchBar(); + providerComboBox = new QComboBox; + connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() { + currentParams.cardProviderId = providerComboBox->currentData().toString(); + reloadPreview(); + onParamChanged(); + }); + marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01); marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01); verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01); @@ -181,6 +188,7 @@ void UserCardArtSettingsDialog::setupUi() auto *form = new QFormLayout; form->addRow(tr("Card name:"), searchBar); + form->addRow(tr("Card ProviderId:"), providerComboBox); form->addRow(tr("Left margin (%):"), marginLSpin); form->addRow(tr("Right margin (%):"), marginRSpin); form->addRow(tr("Vertical offset:"), verticalOffsetSpin); @@ -219,6 +227,32 @@ void UserCardArtSettingsDialog::setupUi() connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); } +void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName) +{ + providerComboBox->clear(); + + auto card = CardDatabaseManager::query()->getCard({cardName}); + + const auto &sets = card.getInfo().getSets(); + + for (const auto &printings : sets) { + for (const auto &p : printings) { + + QString setName = p.getSet()->getLongName(); + QString collector = p.getProperty("num"); + QString uuid = p.getUuid(); + + QString label = setName; + + if (!collector.isEmpty()) { + label += " #" + collector; + } + + providerComboBox->addItem(label, uuid); + } + } +} + void UserCardArtSettingsDialog::onCardNameChanged(const QString &name) { if (name.isEmpty()) { @@ -231,27 +265,68 @@ void UserCardArtSettingsDialog::onCardNameChanged(const QString &name) if (!card) { currentPixmap = QPixmap(); preview->setPixmap(currentPixmap); + providerComboBox->clear(); return; } currentParams.cardName = name; + populateProviderCombo(name); + + if (providerComboBox->count() == 0) { + // No printings found for this card; nothing to preview. + currentPixmap = QPixmap(); + preview->setPixmap(currentPixmap); + currentParams.cardProviderId.clear(); + return; + } + + currentParams.cardProviderId = providerComboBox->currentData().toString(); + reloadPreview(); +} + +void UserCardArtSettingsDialog::reloadPreview() +{ + if (currentParams.cardName.isEmpty()) { + return; + } + + ExactCard card = CardDatabaseManager::query()->getCard({currentParams.cardName, currentParams.cardProviderId}); + if (!card) { + return; + } + + // CardPictureLoader::getPixmap() is async on a cache miss: it enqueues a + // background download and returns a null pixmap immediately. When that + // download finishes, CardPictureLoader::imageLoaded() caches the result + // and calls card.emitPixmapUpdated(), which emits pixmapUpdated() on the + // underlying CardInfo (see exact_card.h). Listen for that, scoped to + // whichever CardInfo we just asked for, so the preview catches up once + // the image actually arrives instead of staying on the placeholder. + // + // Disconnect any previous listener first -- otherwise switching cards + // repeatedly stacks up connections to old CardInfo objects, each of + // which would still fire reloadPreview() (harmlessly, but wastefully) + // whenever ITS art finishes loading later. + disconnect(pixmapUpdatedConnection); + QPixmap fullRes; CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); if (fullRes.isNull()) { - connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this, card](const PrintingInfo &) { - disconnect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, nullptr); - QPixmap loaded; - CardPictureLoader::getPixmap(loaded, card, QSize(745, 1040)); - currentPixmap = UserCardArtProvider::cropCardArt(loaded); - preview->setPixmap(currentPixmap); - }); + // Not loaded yet -- wait for the signal instead of giving up. + // card.getCardPtr() is a CardInfoPtr (QSharedPointer); + // .data() gives the raw QObject* needed for connect(). + CardInfo *cardInfo = card.getCardPtr().data(); + if (cardInfo) { + pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); }); + } return; } currentPixmap = UserCardArtProvider::cropCardArt(fullRes); preview->setPixmap(currentPixmap); + preview->setParams(currentParams); } void UserCardArtSettingsDialog::onParamChanged() diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h index cac26c919..018043278 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h @@ -3,6 +3,7 @@ #include "user_list_painter.h" +#include #include #include @@ -43,10 +44,12 @@ public: private slots: void onCardNameChanged(const QString &name); + void reloadPreview(); void onParamChanged(); private: void setupUi(); + void populateProviderCombo(const QString &cardName); void initializeSearchBar(); QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step); @@ -57,6 +60,10 @@ private: CardSearchModel *searchModel; CardCompleterProxyModel *proxyModel; + QComboBox *providerComboBox; + + QMetaObject::Connection pixmapUpdatedConnection; + QDoubleSpinBox *marginLSpin; QDoubleSpinBox *marginRSpin; QDoubleSpinBox *verticalOffsetSpin; diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index fd62d5ddf..2b4dcb8ed 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -542,7 +542,7 @@ void UserInfoPopup::showForUser(const QString &userName, const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName)) ? m_cardArtParamsMap->value(userName) : CardArtParams{}; - const QString artKey = userName + u'|' + params.cardName; + const QString artKey = userName + u'|' + params.cardName + u'|' + params.cardProviderId; const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; m_header->setUserData(userInfo, online, avatar, cardArt, params); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp index b5541b692..8891ff268 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -73,9 +73,9 @@ void UserListPainter::drawBackground(QPainter *painter, painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); } -static QString makeKey(const QString &user, const QString &card) +static QString makeKey(const QString &user, const QString &card, const QString &providerId) { - return user + u'|' + card; + return user + u'|' + card + u'|' + providerId; } void UserListPainter::drawCardArt(QPainter *painter, @@ -95,7 +95,7 @@ void UserListPainter::drawCardArt(QPainter *painter, return; } - const QString key = makeKey(userName, params.cardName); + const QString key = makeKey(userName, params.cardName, params.cardProviderId); if (!cardArtCache->contains(key)) { return; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.h b/cockatrice/src/interface/widgets/server/user/user_list_painter.h index 95486b75e..28cab9675 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.h @@ -18,6 +18,7 @@ class ServerInfo_User; struct CardArtParams { QString cardName = ""; + QString cardProviderId = ""; double marginPctL = 0.33; double marginPctR = 0.02; double verticalOffset = 0.35; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index ed06ea941..a48d95cb5 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -904,12 +904,13 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) const auto &cap = user.card_art_params(); CardArtParams params; params.cardName = QString::fromStdString(cap.card_name()); + params.cardProviderId = QString::fromStdString(cap.card_provider_id()); params.marginPctL = cap.margin_pct_l(); params.marginPctR = cap.margin_pct_r(); params.verticalOffset = cap.vertical_offset(); params.zoom = cap.zoom(); cardArtParamsMap.insert(userName, params); - cardArtProvider->requestCardArt(userName, params.cardName); + cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId); } else { cardArtParamsMap.remove(userName); // clear stale params on removal } diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp index 6e8ab752a..3dc4de15f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -27,7 +27,7 @@ int CardArtRulesModel::rowCount(const QModelIndex &parent) const int CardArtRulesModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return 3; + return 4; } QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const @@ -43,8 +43,10 @@ QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const case 0: return e.cardName; case 1: - return e.mode; + return e.cardProviderId; case 2: + return e.mode; + case 3: return e.reason; } } @@ -62,8 +64,10 @@ QVariant CardArtRulesModel::headerData(int section, Qt::Orientation orientation, case 0: return tr("Card"); case 1: - return tr("Mode"); + return tr("ProviderId"); case 2: + return tr("Mode"); + case 3: return tr("Reason"); default: return {}; @@ -97,6 +101,15 @@ QString CardArtRulesModel::cardAt(int row) const return entries[row].cardName; } +const CardArtRulesModel::Entry *CardArtRulesModel::entryAt(int row) const +{ + if (row < 0 || row >= static_cast(entries.size())) { + return nullptr; + } + + return &entries[row]; +} + void CardArtRulesModel::onRefreshFinished(const Response &r) { if (r.response_code() != Response::RespOk) { @@ -109,8 +122,8 @@ void CardArtRulesModel::onRefreshFinished(const Response &r) entries.clear(); for (const auto &e : resp.entries()) { - entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.mode()), - QString::fromStdString(e.reason())}); + entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.card_provider_id()), + QString::fromStdString(e.mode()), QString::fromStdString(e.reason())}); } endResetModel(); @@ -128,6 +141,7 @@ void TabCardArtRules::setupUi() initSearchBar(); + providerComboBox = new QComboBox; modeBox = new QComboBox; reasonEdit = new QLineEdit; @@ -146,6 +160,7 @@ void TabCardArtRules::setupUi() auto *form = new QFormLayout; form->addRow(tr("Card:"), searchEdit); + form->addRow(tr("ProviderId:"), providerComboBox); form->addRow(tr("Mode:"), modeBox); form->addRow(tr("Reason:"), reasonEdit); @@ -204,6 +219,34 @@ void TabCardArtRules::initSearchBar() }); connect(searchCompleter, static_cast(&QCompleter::activated), this, [this](const QString &name) { searchEdit->setText(name); }); + connect(searchEdit, &QLineEdit::editingFinished, this, + [this]() { populateProviderCombo(searchEdit->text().trimmed()); }); +} + +void TabCardArtRules::populateProviderCombo(const QString &cardName) +{ + providerComboBox->clear(); + + auto card = CardDatabaseManager::query()->getCard({cardName}); + + const auto &sets = card.getInfo().getSets(); + + for (const auto &printings : sets) { + for (const auto &p : printings) { + + QString setName = p.getSet()->getLongName(); + QString collector = p.getProperty("num"); + QString uuid = p.getUuid(); + + QString label = setName; + + if (!collector.isEmpty()) { + label += " #" + collector; + } + + providerComboBox->addItem(label, uuid); + } + } } void TabCardArtRules::retranslateUi() @@ -222,6 +265,7 @@ void TabCardArtRules::addRule() { Command_AddCardArtRule cmd; cmd.set_card_name(searchEdit->text().toStdString()); + cmd.set_card_provider_id(providerComboBox->currentData().toString().toStdString()); cmd.set_mode(modeBox->currentText().toStdString()); cmd.set_reason(reasonEdit->text().toStdString()); @@ -238,7 +282,10 @@ void TabCardArtRules::removeSelected() } Command_RemoveCardArtRule cmd; - cmd.set_card_name(tableModel->cardAt(idx.row()).toStdString()); + const auto e = tableModel->entryAt(idx.row()); + + cmd.set_card_name(e->cardName.toStdString()); + cmd.set_card_provider_id(e->cardProviderId.toStdString()); client->sendCommand(client->prepareModeratorCommand(cmd)); diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h index a47f1267d..b9ea2ca83 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h @@ -20,6 +20,7 @@ public: struct Entry { QString cardName; + QString cardProviderId; QString mode; QString reason; }; @@ -35,6 +36,7 @@ public: void clear(); QString cardAt(int row) const; + const Entry *entryAt(int row) const; private slots: void onRefreshFinished(const Response &r); @@ -70,11 +72,13 @@ private: QLineEdit *searchEdit; void initSearchBar(); + void populateProviderCombo(const QString &cardName); QCompleter *searchCompleter; CardDatabaseModel *cardDbModel; CardDatabaseDisplayModel *cardDbDisplayModel; CardSearchModel *cardSearchModel; CardCompleterProxyModel *cardProxyModel; + QComboBox *providerComboBox; QComboBox *modeBox; QLineEdit *reasonEdit; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index c10b9de22..ca46e4dd7 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -116,8 +116,9 @@ message Command_AddCardArtRule { } optional string card_name = 1; - optional string mode = 2; // "ALLOW" or "DENY" - optional string reason = 3; + optional string card_provider_id = 2; + optional string mode = 3; // "ALLOW" or "DENY" + optional string reason = 4; } message Command_RemoveCardArtRule { @@ -126,6 +127,7 @@ message Command_RemoveCardArtRule { } optional string card_name = 1; + optional string card_provider_id = 2; } message Command_ListCardArtRules { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto index b13c79742..25b76e09f 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto @@ -3,8 +3,9 @@ import "response.proto"; message Response_CardArtRuleEntry { optional string card_name = 1; - optional string mode = 2; - optional string reason = 3; + optional string card_provider_id = 2; + optional string mode = 3; + optional string reason = 4; } message Response_ListCardArtRules { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto index 4bbbe46bb..98cc3ce6a 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto @@ -15,10 +15,11 @@ message ServerInfo_User { }; message CardArtParams { optional string card_name = 1; - optional double margin_pct_l = 2 [default = 0.33]; - optional double margin_pct_r = 3 [default = 0.02]; - optional double vertical_offset = 4 [default = 0.35]; - optional double zoom = 5 [default = 1.0]; + optional string card_provider_id = 2; + optional double margin_pct_l = 3 [default = 0.33]; + optional double margin_pct_r = 4 [default = 0.02]; + optional double vertical_offset = 5 [default = 0.35]; + optional double zoom = 6 [default = 1.0]; }; optional string name = 1; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index ff5dbff32..9d207c711 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -212,8 +212,9 @@ message Command_SetCardArtParams { optional Command_SetCardArtParams ext = 1025; } optional string card_name = 1; - optional double margin_pct_l = 2; - optional double margin_pct_r = 3; - optional double vertical_offset = 4; - optional double zoom = 5; + optional string card_provider_id = 2; + optional double margin_pct_l = 3; + optional double margin_pct_r = 4; + optional double vertical_offset = 5; + optional double zoom = 6; } diff --git a/servatrice/migrations/servatrice_0034_to_0035.sql b/servatrice/migrations/servatrice_0034_to_0035.sql index 83502a949..acaad9c8b 100644 --- a/servatrice/migrations/servatrice_0034_to_0035.sql +++ b/servatrice/migrations/servatrice_0034_to_0035.sql @@ -3,12 +3,13 @@ ALTER TABLE `cockatrice_users` ADD COLUMN `card_art_params` TEXT DEFAULT NULL, A CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `card_name` varchar(255) NOT NULL, + `card_provider_id` varchar(255) NOT NULL, `mode` enum('ALLOW','DENY') NOT NULL, `reason` varchar(255) DEFAULT NULL, `created_by` int(7) unsigned DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `uniq_card_name` (`card_name`), + UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`), KEY `idx_mode` (`mode`), FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index badd82d6d..7f530063c 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -305,12 +305,13 @@ CREATE TABLE IF NOT EXISTS `cockatrice_audit` ( CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `card_name` varchar(255) NOT NULL, + `card_provider_id` varchar(255) NOT NULL, `mode` enum('ALLOW','DENY') NOT NULL, `reason` varchar(255) DEFAULT NULL, `created_by` int(7) unsigned DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `uniq_card_name` (`card_name`), + UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`), KEY `idx_mode` (`mode`), FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 92ff80755..d5e1f13ef 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -693,6 +693,9 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer if (obj.contains("card_name")) { cap->set_card_name(obj["card_name"].toString().toStdString()); } + if (obj.contains("card_provider_id")) { + cap->set_card_provider_id(obj["card_provider_id"].toString().toStdString()); + } if (obj.contains("marginPctL")) { cap->set_margin_pct_l(obj["marginPctL"].toDouble(0.33)); } diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index f9d276941..55f779468 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1577,11 +1577,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Comm return Response::RespOk; } -bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName) +bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName, const QString &cardProviderId) { - QSqlQuery *q = sqlInterface->prepareQuery("SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name"); + QSqlQuery *q = sqlInterface->prepareQuery( + "SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name AND card_provider_id = :provider"); q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); if (!sqlInterface->execSqlQuery(q)) { qWarning() << "Card art rule lookup failed; failing open for" << cardName; @@ -1603,8 +1605,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const } const QString cardName = QString::fromStdString(cmd.card_name()); + const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); - if (cardName.length() > MAX_NAME_LENGTH) { + if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } @@ -1620,7 +1623,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const return Response::RespOk; } - if (!isCardNameAllowed(cardName)) { + if (!isCardNameAllowed(cardName, cardProviderId)) { return Response::RespFunctionNotAllowed; } @@ -1633,6 +1636,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const QJsonObject obj; obj["card_name"] = cardName; + obj["card_provider_id"] = cardProviderId; obj["marginPctL"] = marginPctL; obj["marginPctR"] = marginPctR; obj["verticalOffset"] = verticalOffset; @@ -1649,6 +1653,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const // Keep the in-memory userInfo in sync auto *cap = userInfo->mutable_card_art_params(); cap->set_card_name(cmd.card_name()); + cap->set_card_provider_id(cmd.card_provider_id()); cap->set_margin_pct_l(marginPctL); cap->set_margin_pct_r(marginPctR); cap->set_vertical_offset(verticalOffset); @@ -1664,21 +1669,23 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAddCardArtRule(const Co ResponseContainer &) { const QString cardName = QString::fromStdString(cmd.card_name()); + const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); const QString mode = QString::fromStdString(cmd.mode()); if (mode != "ALLOW" && mode != "DENY") { return Response::RespInvalidData; } - if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH) { + if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } QSqlQuery *q = sqlInterface->prepareQuery("INSERT INTO {prefix}_card_art_name_rules " - "(card_name, mode, reason, created_by) " - "VALUES (:name, :mode, :reason, :uid) " + "(card_name, card_provider_id, mode, reason, created_by) " + "VALUES (:name, :provider, :mode, :reason, :uid) " "ON DUPLICATE KEY UPDATE mode=:mode2, reason=:reason2"); q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); q->bindValue(":mode", mode); q->bindValue(":mode2", mode); q->bindValue(":reason", QString::fromStdString(cmd.reason())); @@ -1696,12 +1703,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const ResponseContainer &) { auto cardName = QString::fromStdString(cmd.card_name()); - if (cardName.length() > MAX_NAME_LENGTH) { + auto cardProviderId = QString::fromStdString(cmd.card_provider_id()); + if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } - QSqlQuery *q = sqlInterface->prepareQuery("DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name"); + QSqlQuery *q = sqlInterface->prepareQuery( + "DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name AND card_provider_id=:provider"); q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; @@ -1713,7 +1723,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const Command_ListCardArtRules &, ResponseContainer &rc) { - QSqlQuery *q = sqlInterface->prepareQuery("SELECT card_name, mode, reason FROM {prefix}_card_art_name_rules"); + QSqlQuery *q = sqlInterface->prepareQuery( + "SELECT card_name, card_provider_id, mode, reason FROM {prefix}_card_art_name_rules"); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; @@ -1724,8 +1735,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const while (q->next()) { auto *entry = re->add_entries(); entry->set_card_name(q->value(0).toString().toStdString()); - entry->set_mode(q->value(1).toString().toStdString()); - entry->set_reason(q->value(2).toString().toStdString()); + entry->set_card_provider_id(q->value(1).toString().toStdString()); + entry->set_mode(q->value(2).toString().toStdString()); + entry->set_reason(q->value(3).toString().toStdString()); } rc.setResponseExtension(re); diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index c0732ccd9..0d66ae78f 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -129,7 +129,7 @@ private: Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); - bool isCardNameAllowed(const QString &cardName); + bool isCardNameAllowed(const QString &cardName, const QString &cardProviderId); Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &); Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &); Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &); From 4cbc00b9c43127c03d60f605549060c50ccb508a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:25:30 +0200 Subject: [PATCH 42/50] Bump actions/cache from 5 to 6 (#7019) --- .github/workflows/desktop-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 74f905351..a6b9d5340 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -162,7 +162,7 @@ jobs: - name: "Restore compiler cache (ccache)" id: ccache_restore - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: @@ -215,7 +215,7 @@ jobs: - name: "Save updated compiler cache (ccache)" if: github.ref == 'refs/heads/master' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: key: ${{ steps.ccache_restore.outputs.cache-primary-key }} path: ${{ env.CACHE }} @@ -365,7 +365,7 @@ jobs: - name: "[macOS] Restore compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 id: ccache_restore - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: @@ -387,7 +387,7 @@ jobs: - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" if: matrix.os == 'macOS' id: restore_qt - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} path: ${{ github.workspace }}/Qt @@ -410,7 +410,7 @@ jobs: - name: "[macOS] Cache thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} path: ${{ github.workspace }}/Qt @@ -473,7 +473,7 @@ jobs: - name: "[macOS] Save updated compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: key: ${{ steps.ccache_restore.outputs.cache-primary-key }} path: ${{ env.CCACHE_DIR }} From ad4922537ddef6f1f06007b2978dae7e12a7af46 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:44:24 -0400 Subject: [PATCH 43/50] [UserListDelegate] Supply providerid in cmd, position popup correctly (#7020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [UserListDelegate] Transmit providerId in cmd when setting user banner card * [UserListDelegate] Position popup correctly * Lint. --------- Co-authored-by: Lukas Brübach --- .../widgets/server/user/user_info_box.cpp | 1 + .../widgets/server/user/user_list_widget.cpp | 48 ++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index e6cf38787..416cd42e3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -335,6 +335,7 @@ void UserInfoBox::actBannerCard() Command_SetCardArtParams cmd; cmd.set_card_name(p.cardName.toStdString()); if (!p.cardName.isEmpty()) { + cmd.set_card_provider_id(p.cardProviderId.toStdString()); cmd.set_margin_pct_l(p.marginPctL); cmd.set_margin_pct_r(p.marginPctR); cmd.set_vertical_offset(p.verticalOffset); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index a48d95cb5..c094f8a6b 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -767,13 +767,17 @@ void UserListWidget::showPopupForUser(const QString &userName) m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); - positionPopup(userName); - + // Realize the native window at opacity 0 before positioning so that: + // 1) move() applies to an existing native handle (not overridden by Qt's + // default centering logic on first show) + // 2) adjustSize() inside positionPopup() can measure the final laid-out + // geometry correctly + m_userInfoPopup->setWindowOpacity(0.0); m_userInfoPopup->show(); m_userInfoPopup->raise(); - // Fade in - m_userInfoPopup->setWindowOpacity(0.0); + positionPopup(userName); // geometry is now accurate; move() sticks + auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); fade->setDuration(120); fade->setStartValue(0.0); @@ -790,11 +794,10 @@ void UserListWidget::positionPopup(const QString &userName) QWidget *vp = userTree->viewport(); const QRect itemR = userTree->visualItemRect(item); - const QPoint itemBR = vp->mapToGlobal(itemR.bottomRight()); + const QPoint itemTL = vp->mapToGlobal(itemR.topLeft()); const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft()); const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight()); - // Force a fresh size calculation so popH is accurate m_userInfoPopup->adjustSize(); const int popW = m_userInfoPopup->width(); const int popH = m_userInfoPopup->height(); @@ -802,19 +805,32 @@ void UserListWidget::positionPopup(const QString &userName) const QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); - // ── X: left of the list if there's room, otherwise right ───────────────── - int x = (vpTL.x() >= popW + margin) ? vpTL.x() - popW - margin : vpTR.x() + margin; + // ── X: prefer the side with more space ─────────────────────────────────── + const int spaceLeft = vpTL.x() - screen.left() - margin; + const int spaceRight = screen.right() - vpTR.x() - margin; + int x; + if (spaceLeft >= spaceRight) { + x = (spaceLeft >= popW) ? (vpTL.x() - margin - popW) : (vpTR.x() + margin); + } else { + x = (spaceRight >= popW) ? (vpTR.x() + margin) : (vpTL.x() - margin - popW); + } x = qBound(screen.left() + margin, x, screen.right() - popW - margin); - // ── Y: bottom of popup aligns with bottom of hovered row, grows upward ─── - int y = itemBR.y() - popH; + // ── Y: grow down if there's room, otherwise grow up ─────────────────────── + const int itemTopY = itemTL.y(); + const int spaceBelow = screen.bottom() - itemTopY - margin; + const int spaceAbove = itemTopY - screen.top() - margin; - // Clamp: never above the screen top - y = qMax(y, screen.top() + margin); - - // Clamp: never below the screen bottom (e.g. if the popup is taller - // than the space above the row, let it spill downward rather than clip) - y = qMin(y, screen.bottom() - popH - margin); + int y; + if (spaceBelow >= popH) { + y = itemTopY; // top edges align, popup grows downward + } else if (spaceAbove >= popH) { + y = itemTopY - popH; // bottom of popup meets top of item, grows upward + } else { + // Neither side fits cleanly — pick the roomier side and let clamp handle the rest + y = (spaceBelow >= spaceAbove) ? itemTopY : (itemTopY - popH); + } + y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin); m_userInfoPopup->move(x, y); } From 055ba9a16f9192bd23fc01fe571def0b2581379c Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Sat, 27 Jun 2026 18:53:21 -0400 Subject: [PATCH 44/50] Add subtype breakdown counter for card selection (#6923) * Add subtype breakdown counter for card selection Display a categorized count of creature subtypes (and other card type subtypes) when multiple cards are selected. The breakdown appears above the total selection counter in the bottom-right corner. Subtypes are grouped by main card type and sorted by frequency, with the most common subtypes positioned adjacent to the total count for quick reference. The feature can be toggled via a new checkbox in Settings > User Interface. * Alignment fix * Computation logic moved to helper funtction in separate file * Rename SubtypeCounter to SubtypeTally * Fix subtype tally alignment by using grid layout instead of character padding * Rename count to tally in the subtype breakdown feature * partial rename * list position fixed * Clean up code and documentation * Rename subtypeCountLabelStyle to subtypeTallyLabelStyle and fix include ordering * Fix include path for selection_subtype_tally.h after file relocation * fixed count to tally rename inconsistencies --- cockatrice/CMakeLists.txt | 1 + .../src/client/settings/cache_settings.cpp | 7 + .../src/client/settings/cache_settings.h | 6 + .../src/game/selection_subtype_tally.cpp | 64 ++++++++ cockatrice/src/game/selection_subtype_tally.h | 36 +++++ cockatrice/src/game_graphics/game_view.cpp | 142 +++++++++++++++--- cockatrice/src/game_graphics/game_view.h | 9 ++ cockatrice/src/interface/theme_manager.cpp | 3 + .../user_interface_settings_page.cpp | 14 +- .../user_interface_settings_page.h | 1 + .../libcockatrice/utility/qt_utils.h | 1 + 11 files changed, 258 insertions(+), 26 deletions(-) create mode 100644 cockatrice/src/game/selection_subtype_tally.cpp create mode 100644 cockatrice/src/game/selection_subtype_tally.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 18679664b..166b807d9 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -83,6 +83,7 @@ set(cockatrice_SOURCES src/game/game_state.cpp src/game_graphics/game_view.cpp src/game_graphics/hand_counter.cpp + src/game/selection_subtype_tally.cpp src/game_graphics/log/message_log_widget.cpp src/game/phase.cpp src/game_graphics/phases_toolbar.cpp diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 28e5eb187..b6bc8a47d 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -313,6 +313,7 @@ SettingsCache::SettingsCache() showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool(); showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool(); + showSubtypeSelectionTally = settings->value("interface/showsubtypeselectiontally", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); @@ -1395,6 +1396,12 @@ void SettingsCache::setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSele settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount); } +void SettingsCache::setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally) +{ + showSubtypeSelectionTally = static_cast(_showSubtypeSelectionTally); + settings->setValue("interface/showsubtypeselectiontally", showSubtypeSelectionTally); +} + void SettingsCache::loadPaths() { QString dataPath = getDataPath(); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 5a5e0c546..29af89587 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -355,6 +355,7 @@ private: bool showStatusBar; bool showDragSelectionCount; bool showTotalSelectionCount; + bool showSubtypeSelectionTally; public: SettingsCache(); @@ -478,6 +479,10 @@ public: { return showTotalSelectionCount; } + [[nodiscard]] bool getShowSubtypeSelectionTally() const + { + return showSubtypeSelectionTally; + } [[nodiscard]] bool getNotificationsEnabled() const { return notificationsEnabled; @@ -1176,5 +1181,6 @@ public slots: void setRoundCardCorners(bool _roundCardCorners); void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount); void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount); + void setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally); }; #endif diff --git a/cockatrice/src/game/selection_subtype_tally.cpp b/cockatrice/src/game/selection_subtype_tally.cpp new file mode 100644 index 000000000..e9f87fab9 --- /dev/null +++ b/cockatrice/src/game/selection_subtype_tally.cpp @@ -0,0 +1,64 @@ +#include "selection_subtype_tally.h" + +#include "../game_graphics/board/card_item.h" + +#include +#include + +namespace +{ + +/** @brief Extracts subtypes from a single card face's type line. */ +QStringList extractSubtypesFromFace(const QString &faceType) +{ + // Card type format: "Creature — Goblin Warrior" or "Legendary Enchantment — Saga" + QStringList parts = faceType.split(QStringLiteral(" — ")); + if (parts.size() > 1) { + return parts[1].split(QStringLiteral(" "), Qt::SkipEmptyParts); + } + return {}; +} + +} // anonymous namespace + +namespace SelectionSubtypeTally +{ + +QList countSubtypes(const QList &cards) +{ + QMap subtypeCounts; + + for (CardItem *card : cards) { + if (card->getFaceDown() || card->getCard().isEmpty()) { + continue; + } + + QString cardType = card->getCardInfo().getCardType(); + // Handle double-faced cards: "Creature — Human // Creature — Werewolf" + QStringList cardFaces = cardType.split(QStringLiteral(" // ")); + + for (const QString &face : cardFaces) { + QStringList subtypes = extractSubtypesFromFace(face); + for (const QString &subtype : subtypes) { + subtypeCounts[subtype]++; + } + } + } + + QList entries; + for (auto it = subtypeCounts.constBegin(); it != subtypeCounts.constEnd(); ++it) { + entries.append({it.key(), it.value()}); + } + + // Sort by count ascending, then alphabetically (lowest counts at bottom of display) + std::sort(entries.begin(), entries.end(), [](const SubtypeEntry &a, const SubtypeEntry &b) { + if (a.count != b.count) { + return a.count < b.count; + } + return a.name < b.name; + }); + + return entries; +} + +} // namespace SelectionSubtypeTally diff --git a/cockatrice/src/game/selection_subtype_tally.h b/cockatrice/src/game/selection_subtype_tally.h new file mode 100644 index 000000000..9038653f6 --- /dev/null +++ b/cockatrice/src/game/selection_subtype_tally.h @@ -0,0 +1,36 @@ +#ifndef SELECTION_SUBTYPE_TALLY_H +#define SELECTION_SUBTYPE_TALLY_H + +#include +#include + +class CardItem; + +/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */ +struct SubtypeEntry +{ + QString name; ///< The subtype name + int count; ///< Number of selected cards with this subtype + + bool operator==(const SubtypeEntry &other) const + { + return name == other.name && count == other.count; + } +}; + +/** + * @brief Extracts and tallies subtypes from selected cards. + */ +namespace SelectionSubtypeTally +{ +/** + * @brief Parses card type lines and counts each subtype occurrence. + * + * Skips face-down cards and cards without type info. + * @param cards The list of selected card items to analyze. + * @return Entries sorted by count ascending, then alphabetically. + */ +QList countSubtypes(const QList &cards); +} // namespace SelectionSubtypeTally + +#endif diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index 41befd9a4..c2d9b2b3b 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -1,12 +1,16 @@ #include "game_view.h" #include "../client/settings/cache_settings.h" +#include "../game/selection_subtype_tally.h" #include "game_scene.h" #include +#include #include +#include #include #include +#include // QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. // This subclass disables that behavior so dragCountLabel can appear above it. @@ -55,31 +59,40 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par refreshShortcuts(); rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); - const QString countLabelStyle = "color: white; " - "font-size: 14px; " - "font-weight: bold; " - "background-color: rgba(0, 0, 0, 160); " - "border-radius: 3px; " - "padding: 1px 2px;"; + const QString baseProperties = "color: white; " + "font-family: monospace; " + "background-color: rgba(0, 0, 0, 160); " + "border-radius: 3px; " + "padding: 1px 2px; " + "white-space: pre;"; + + const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;"; + const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;"; + const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;"; dragCountLabel = new QLabel(this); - dragCountLabel->setStyleSheet(countLabelStyle); + dragCountLabel->setStyleSheet(dragCountLabelStyle); dragCountLabel->hide(); dragCountLabel->raise(); totalCountLabel = new QLabel(this); - totalCountLabel->setStyleSheet(countLabelStyle); + totalCountLabel->setStyleSheet(totalCountLabelStyle); totalCountLabel->hide(); + + subtypeTallyContainer = new QWidget(this); + subtypeTallyContainer->setStyleSheet(subtypeTallyLabelStyle); + subtypeTallyLayout = new QGridLayout(subtypeTallyContainer); + subtypeTallyLayout->setContentsMargins(2, 2, 2, 2); + subtypeTallyLayout->setSpacing(2); + subtypeTallyContainer->hide(); } void GameView::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); - GameScene *s = dynamic_cast(scene()); - if (s) { - s->processViewSizeChange(event->size()); - } + GameScene *s = static_cast(scene()); + s->processViewSizeChange(event->size()); updateSceneRect(scene()->sceneRect()); updateTotalSelectionCount(event->size()); @@ -164,29 +177,114 @@ void GameView::refreshShortcuts() SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); } +void GameView::clearSubtypeLabels() +{ + QtUtils::clearLayoutRec(subtypeTallyLayout); +} + +QSize GameView::rebuildSubtypeLabels(const QList &entries) +{ + clearSubtypeLabels(); + + const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;"); + const QString countStyle = + QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;"); + + int totalHeight = 0; + int maxNameWidth = 0; + int maxCountWidth = 0; + + int row = 0; + for (const SubtypeEntry &entry : entries) { + auto *nameLabel = new QLabel(entry.name, subtypeTallyContainer); + nameLabel->setStyleSheet(nameStyle); + nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + subtypeTallyLayout->addWidget(nameLabel, row, 0); + + auto *countLabel = new QLabel(QString::number(entry.count), subtypeTallyContainer); + countLabel->setStyleSheet(countStyle); + countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + subtypeTallyLayout->addWidget(countLabel, row, 1); + + QSize nameSize = nameLabel->sizeHint(); + QSize countSize = countLabel->sizeHint(); + maxNameWidth = qMax(maxNameWidth, nameSize.width()); + maxCountWidth = qMax(maxCountWidth, countSize.width()); + totalHeight += qMax(nameSize.height(), countSize.height()); + + ++row; + } + + int spacing = subtypeTallyLayout->spacing(); + int margins = subtypeTallyLayout->contentsMargins().left() + subtypeTallyLayout->contentsMargins().right(); + int verticalMargins = subtypeTallyLayout->contentsMargins().top() + subtypeTallyLayout->contentsMargins().bottom(); + + int width = maxNameWidth + spacing + maxCountWidth + margins; + int height = totalHeight + (row - 1) * spacing + verticalMargins; + + return QSize(width, height); +} + void GameView::updateTotalSelectionCount(const QSize &viewSize) { - if (!SettingsCache::instance().getShowTotalSelectionCount()) { - totalCountLabel->hide(); - return; - } + constexpr int kMarginInPixels = 10; + constexpr int kSpacingBetweenLabels = 4; + + int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); + int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); int count = scene()->selectedItems().count(); - if (count > 1) { + if (!SettingsCache::instance().getShowTotalSelectionCount() || count <= 1) { + totalCountLabel->hide(); + } else { totalCountLabel->setText(QString::number(count)); totalCountLabel->adjustSize(); - constexpr int kMarginInPixels = 10; - int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); - int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); int x = availableWidth - totalCountLabel->width() - kMarginInPixels; int y = availableHeight - totalCountLabel->height() - kMarginInPixels; totalCountLabel->move(x, y); totalCountLabel->show(); - } else { - totalCountLabel->hide(); } + + if (!SettingsCache::instance().getShowSubtypeSelectionTally() || count <= 1) { + subtypeTallyContainer->hide(); + cachedSubtypeEntries.clear(); + return; + } + + GameScene *gameScene = static_cast(scene()); + QList entries = SelectionSubtypeTally::countSubtypes(gameScene->selectedCards()); + + if (entries.isEmpty()) { + subtypeTallyContainer->hide(); + cachedSubtypeEntries.clear(); + return; + } + + // Only rebuild labels if entries changed + QSize containerSize; + if (entries != cachedSubtypeEntries) { + cachedSubtypeEntries = entries; + containerSize = rebuildSubtypeLabels(entries); + subtypeTallyContainer->resize(containerSize); + } else { + containerSize = subtypeTallyContainer->size(); + } + + int x = availableWidth - containerSize.width() - kMarginInPixels; + int y; + + if (totalCountLabel->isVisible()) { + y = totalCountLabel->y() - containerSize.height() - kSpacingBetweenLabels; + } else { + y = availableHeight - containerSize.height() - kMarginInPixels; + } + + y = qMax(kMarginInPixels, y); + + subtypeTallyContainer->move(x, y); + subtypeTallyContainer->show(); } /** diff --git a/cockatrice/src/game_graphics/game_view.h b/cockatrice/src/game_graphics/game_view.h index 80e8e96b5..4047c87ab 100644 --- a/cockatrice/src/game_graphics/game_view.h +++ b/cockatrice/src/game_graphics/game_view.h @@ -7,9 +7,12 @@ #ifndef GAMEVIEW_H #define GAMEVIEW_H +#include "../game/selection_subtype_tally.h" + #include class GameScene; +class QGridLayout; class QLabel; class QRubberBand; @@ -21,7 +24,13 @@ private: QRubberBand *rubberBand; QLabel *dragCountLabel; QLabel *totalCountLabel; + QWidget *subtypeTallyContainer; + QGridLayout *subtypeTallyLayout; QPointF selectionOrigin; + QList cachedSubtypeEntries; ///< Cached entries to avoid redundant rebuilds + + QSize rebuildSubtypeLabels(const QList &entries); + void clearSubtypeLabels(); protected: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 086845fe6..4ba35a00e 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -271,6 +271,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, const PaletteConfig &palCfg, const QString &activeScheme) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 0)) + Q_UNUSED(activeScheme) +#endif QString styleName = themeCfg.styleName; if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (themeName == FUSION_THEME_NAME) { diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 6039e3758..44b30d29c 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -68,6 +68,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setShowTotalSelectionCount); + showSubtypeSelectionTallyCheckBox.setChecked(SettingsCache::instance().getShowSubtypeSelectionTally()); + connect(&showSubtypeSelectionTallyCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setShowSubtypeSelectionTally); + useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus()); connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); @@ -86,8 +90,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGrid->addWidget(&annotateTokensCheckBox, 6, 0); generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0); generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0); - generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0); - generalGrid->addWidget(&keepGameChatFocusCheckBox, 10, 0); + generalGrid->addWidget(&showSubtypeSelectionTallyCheckBox, 9, 0); + generalGrid->addWidget(&useTearOffMenusCheckBox, 10, 0); + generalGrid->addWidget(&keepGameChatFocusCheckBox, 11, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -209,8 +214,9 @@ void UserInterfaceSettingsPage::retranslateUi() closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed")); focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened")); annotateTokensCheckBox.setText(tr("Annotate card text on tokens")); - showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection")); - showTotalSelectionCountCheckBox.setText(tr("Show total selection counter")); + showDragSelectionCountCheckBox.setText(tr("Show selection count during drag selection")); + showTotalSelectionCountCheckBox.setText(tr("Show total selection count")); + showSubtypeSelectionTallyCheckBox.setText(tr("Show subtype breakdown in selection tally")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); keepGameChatFocusCheckBox.setText( tr("Keep game chat focused when clicking in game (Note: disables card view search bar)")); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index e10ed2a06..06f0e6b83 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -29,6 +29,7 @@ private: QCheckBox annotateTokensCheckBox; QCheckBox showDragSelectionCountCheckBox; QCheckBox showTotalSelectionCountCheckBox; + QCheckBox showSubtypeSelectionTallyCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox keepGameChatFocusCheckBox; QCheckBox tapAnimationCheckBox; diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h index 334e56027..8e5212031 100644 --- a/libcockatrice_utility/libcockatrice/utility/qt_utils.h +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -1,5 +1,6 @@ #ifndef COCKATRICE_QT_UTILS_H #define COCKATRICE_QT_UTILS_H +#include #include namespace QtUtils From fcac7493adbb138366e016fd13ec2543367f4ba0 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:03:07 -0700 Subject: [PATCH 45/50] [Card] Add facedown property to CardRelation (#6997) * [Card] Add facedown property to CardRelation * trailing newline * fix comments * update schema --- cockatrice/src/game/player/player_actions.cpp | 17 +++++++++++------ cockatrice/src/game/player/player_actions.h | 3 ++- doc/carddatabase_v4/cards.xsd | 1 + .../card/database/card_database.cpp | 3 ++- .../card/database/parser/cockatrice_xml_4.cpp | 11 ++++++++++- .../card/relation/card_relation.cpp | 8 +++++--- .../libcockatrice/card/relation/card_relation.h | 15 ++++++++++++++- 7 files changed, 45 insertions(+), 13 deletions(-) diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index fffd23ccf..c9e8628f4 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -1018,8 +1018,9 @@ void PlayerActions::actCreateAllRelatedCards() if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { dbName = cardRelationAll->getName(); bool persistent = cardRelationAll->getIsPersistent(); + bool faceDown = cardRelationAll->getIsFaceDown(); for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } ++tokensTypesCreated; if (tokensTypesCreated == 1) { @@ -1034,8 +1035,9 @@ void PlayerActions::actCreateAllRelatedCards() if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { dbName = cardRelationNotExcluded->getName(); bool persistent = cardRelationNotExcluded->getIsPersistent(); + bool faceDown = cardRelationNotExcluded->getIsFaceDown(); for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } ++tokensTypesCreated; if (tokensTypesCreated == 1) { @@ -1073,6 +1075,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, const QString dbName = cardRelation->getName(); const bool persistent = cardRelation->getIsPersistent(); + const bool faceDown = cardRelation->getIsFaceDown(); // Variable relations always use DoesNotAttach, regardless of the count the user // entered. @@ -1081,7 +1084,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, return false; } for (int i = 0; i < variableCount; ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } return true; } @@ -1090,7 +1093,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, if (count > 1) { for (int i = 0; i < count; ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } return true; } @@ -1110,7 +1113,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, playCardToTable(sourceCard, false); } - createCard(sourceCard, dbName, attachType, persistent); + createCard(sourceCard, dbName, attachType, persistent, faceDown); return true; } @@ -1137,7 +1140,8 @@ void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardR void PlayerActions::createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attachType, - bool persistent) + bool persistent, + bool faceDown) { CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName); @@ -1172,6 +1176,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, cmd.set_destroy_on_zone_change(!persistent); cmd.set_x(gridPoint.x()); cmd.set_y(gridPoint.y()); + cmd.set_face_down(faceDown); ExactCard relatedCard = CardDatabaseManager::query()->getCardFromSameSet(cardInfo->getName(), sourceCard->getCard().getPrinting()); diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 3f1960892..fa4d54110 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -240,7 +240,8 @@ private: void createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attach = CardRelationType::DoesNotAttach, - bool persistent = false); + bool persistent = false, + bool faceDown = false); void playSelectedCards(QList selectedCards, bool faceDown = false); diff --git a/doc/carddatabase_v4/cards.xsd b/doc/carddatabase_v4/cards.xsd index 92d30b94f..59ca3e560 100644 --- a/doc/carddatabase_v4/cards.xsd +++ b/doc/carddatabase_v4/cards.xsd @@ -6,6 +6,7 @@ + diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp index edad46174..261b36690 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -85,7 +85,8 @@ void CardDatabase::refreshCachedReverseRelatedCards() for (auto *rel : card->getReverseRelatedCards()) { if (auto target = cards.value(rel->getName())) { auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(), - rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent()); + rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(), + rel->getIsFaceDown()); target->addReverseRelatedCards2Me(newRel); } } diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 96a5ac104..c242425ab 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -329,6 +329,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) bool exclude = false; bool variable = false; bool persistent = false; + bool facedown = false; int count = 1; QXmlStreamAttributes attrs = xml.attributes(); QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements); @@ -360,7 +361,12 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) persistent = true; } - auto *relation = new CardRelation(cardName, attachType, exclude, variable, count, persistent); + if (attrs.hasAttribute("facedown")) { + facedown = true; + } + + auto *relation = + new CardRelation(cardName, attachType, exclude, variable, count, persistent, facedown); if (xmlName == "reverse-related") { reverseRelatedCards << relation; } else { @@ -510,6 +516,9 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in if (i->getIsPersistent()) { xml.writeAttribute("persistent", "persistent"); } + if (i->getIsFaceDown()) { + xml.writeAttribute("facedown", "facedown"); + } if (i->getIsVariable()) { if (1 == i->getDefaultCount()) { xml.writeAttribute("count", "x"); diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp index 90e59e439..8903c892d 100644 --- a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp @@ -7,8 +7,10 @@ CardRelation::CardRelation(const QString &_name, bool _isCreateAllExclusion, bool _isVariableCount, int _defaultCount, - bool _isPersistent) + bool _isPersistent, + bool _isFaceDown) : name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion), - isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent) + isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent), + isFaceDown(_isFaceDown) { -} \ No newline at end of file +} diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.h b/libcockatrice_card/libcockatrice/card/relation/card_relation.h index 9ff704097..a1864f5b2 100644 --- a/libcockatrice_card/libcockatrice/card/relation/card_relation.h +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.h @@ -31,6 +31,7 @@ private: bool isVariableCount; ///< True if the number of creations is variable. int defaultCount; ///< Default number of cards created or involved. bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change. + bool isFaceDown; ///< True if this relation creates the tokens facedown public: /** @@ -42,13 +43,15 @@ public: * @param _isVariableCount Whether the count is variable. * @param _defaultCount Default number for creations or transformations. * @param _isPersistent Whether the relation persists across zone changes. + * @param _isFaceDown Whether the relation creates the token face down */ explicit CardRelation(const QString &_name = QString(), CardRelationType _attachType = CardRelationType::DoesNotAttach, bool _isCreateAllExclusion = false, bool _isVariableCount = false, int _defaultCount = 1, - bool _isPersistent = false); + bool _isPersistent = false, + bool _isFaceDown = false); /** * @brief Returns the name of the related card. @@ -151,6 +154,16 @@ public: { return isPersistent; } + + /** + * @brief Returns whether the relation creates the token facedown. + * + * @return True if facedown, false otherwise. + */ + [[nodiscard]] bool getIsFaceDown() const + { + return isFaceDown; + } }; #endif // COCKATRICE_CARD_RELATION_H From 05ae6f47a6b283a44a3b606a10523e0933610b48 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Sun, 28 Jun 2026 19:10:57 -0400 Subject: [PATCH 46/50] Unify counter clamp arithmetic into shared addClamped() helper (#7009) * Unify counter clamp arithmetic into shared addClamped() helper - Add addClamped() in new header clamped_arithmetic.h; uses a 64-bit intermediate so the addition cannot overflow int. - Use it in Server_Card::incrementCounter() (clamps [0, MAX_COUNTERS_ON_CARD]) and Server_Counter::incrementCount() (clamps [INT_MIN, INT_MAX]), removing the duplicated overflow-safe logic and its keep-in-sync TODO. - Inline incrementCount() into server_counter.h; server_counter.cpp now holds only the constructor and getInfo(). - Clarify the card-counter bounds comment in trice_limits.h. * Rename MAX_COUNTERS_ON_CARD to MAX_COUNTER_VALUE The constant caps the counter's value, not how many counters can be on the card * Add direct unit tests for addClamped() helper * Harden offsetCardCounter() against signed-int overflow Replace the raw oldValue + offset sum with addClamped(), clamping to [0, MAX_COUNTER_VALUE] without overflow. * Comment update * Remove class names from addClamped() docstring --- cockatrice/src/game/player/player_actions.cpp | 18 +++++--- .../server/remote/game/server_card.cpp | 11 +++-- .../network/server/remote/game/server_card.h | 4 +- .../server/remote/game/server_counter.cpp | 12 ----- .../server/remote/game/server_counter.h | 9 +++- libcockatrice_utility/CMakeLists.txt | 1 + .../utility/clamped_arithmetic.h | 22 ++++++++++ .../libcockatrice/utility/trice_limits.h | 20 ++++++--- tests/CMakeLists.txt | 6 +++ tests/clamped_arithmetic_test.cpp | 44 +++++++++++++++++++ tests/server_card_counter_test.cpp | 16 +++---- 11 files changed, 122 insertions(+), 41 deletions(-) create mode 100644 libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h create mode 100644 tests/clamped_arithmetic_test.cpp diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index c9e8628f4..1c469a42a 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1530,12 +1531,15 @@ void PlayerActions::offsetCardCounter(QList selectedCards, int count QList commandList; for (auto card : selectedCards) { int oldValue = card->getCounters().value(counterId, 0); - int newValue = oldValue + offset; - // Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD]. - // Compare clamped value to allow recovery from invalid states. - int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD); - if (clampedValue != oldValue) { + // Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE]; + // a result differing from oldValue also corrects an out-of-range cached value. + // Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter). + // This client-side clamp is a defense-in-depth UX check, consistent with + // actSetCardCounter and actIncrementAllCardCounters; the server remains the + // authoritative enforcer of the bounds. + int newValue = addClamped(oldValue, offset, 0, MAX_COUNTER_VALUE); + if (newValue != oldValue) { auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); @@ -1568,7 +1572,7 @@ void PlayerActions::actSetCardCounter(QList selectedCards, int count Expression exp(oldValue); double parsed = exp.parse(counterValue); // Clamp in double precision first to avoid UB, then cast - int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTERS_ON_CARD))); + int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTER_VALUE))); auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); @@ -1598,7 +1602,7 @@ void PlayerActions::actIncrementAllCardCounters(QList cardsToUpdate) counterIterator.next(); int counterId = counterIterator.key(); int currentValue = counterIterator.value(); - if (currentValue >= MAX_COUNTERS_ON_CARD) { + if (currentValue >= MAX_COUNTER_VALUE) { continue; } diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp index b858314c0..4ff0cfb5b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -114,8 +115,8 @@ QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) { - // Clamp to valid card counter range [0, MAX_COUNTERS_ON_CARD] - value = qBound(0, value, MAX_COUNTERS_ON_CARD); + // Clamp to valid card counter range [0, MAX_COUNTER_VALUE] + value = qBound(0, value, MAX_COUNTER_VALUE); const int oldValue = counters.value(_id, 0); if (value == oldValue) { @@ -139,10 +140,8 @@ bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) bool Server_Card::incrementCounter(int counterId, int delta, Event_SetCardCounter *event) { const int oldValue = counters.value(counterId, 0); - const auto result = static_cast(oldValue) + static_cast(delta); - // Clamp to [0, MAX_COUNTERS_ON_CARD] for card counters - const int newValue = - static_cast(qBound(static_cast(0), result, static_cast(MAX_COUNTERS_ON_CARD))); + // Clamp to [0, MAX_COUNTER_VALUE] for card counters + const int newValue = addClamped(oldValue, delta, 0, MAX_COUNTER_VALUE); if (newValue == oldValue) { return false; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h index 3d7e649b9..a2698ad61 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h @@ -156,7 +156,7 @@ public: /** * @brief Sets a card counter to an exact value with clamping. * @param _id The counter ID. - * @param value The desired value (clamped to [0, MAX_COUNTERS_ON_CARD]; 0 removes the counter). + * @param value The desired value (clamped to [0, MAX_COUNTER_VALUE]; 0 removes the counter). * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. */ @@ -168,7 +168,7 @@ public: * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. * @note If counter does not exist, starts from 0. Counter is removed if result is 0. - * @note Clamps result to [0, MAX_COUNTERS_ON_CARD]. + * @note Clamps result to [0, MAX_COUNTER_VALUE]. */ [[nodiscard]] bool incrementCounter(int counterId, int delta, Event_SetCardCounter *event = nullptr); void setTapped(bool _tapped) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp index e65205cbb..b18e11c2b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp @@ -1,24 +1,12 @@ #include "server_counter.h" #include -#include Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) { } -//! \todo Extract overflow-safe arithmetic into shared helper. -//! Duplicated in Server_Card::incrementCounter() - keep in sync if modified. -bool Server_Counter::incrementCount(int delta) -{ - const int oldCount = count; - const auto result = static_cast(count) + static_cast(delta); - count = static_cast(qBound(static_cast(std::numeric_limits::min()), result, - static_cast(std::numeric_limits::max()))); - return count != oldCount; -} - void Server_Counter::getInfo(ServerInfo_Counter *info) { info->set_id(id); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h index 8226e663f..ca093b7cf 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h @@ -22,6 +22,8 @@ #include #include +#include +#include class ServerInfo_Counter; @@ -92,7 +94,12 @@ public: * @return true if the value changed, false otherwise. * @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow. */ - [[nodiscard]] bool incrementCount(int delta); + [[nodiscard]] bool incrementCount(int delta) + { + const int oldCount = count; + count = addClamped(count, delta, std::numeric_limits::min(), std::numeric_limits::max()); + return count != oldCount; + } /** * @brief Populates info with this counter's current state for network serialization. diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index df29d6c9f..13f13bb56 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -16,6 +16,7 @@ set(UTILITY_HEADERS libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h libcockatrice/utility/trice_limits.h + libcockatrice/utility/clamped_arithmetic.h libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h ) diff --git a/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h b/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h new file mode 100644 index 000000000..1afac758c --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h @@ -0,0 +1,22 @@ +#ifndef CLAMPED_ARITHMETIC_H +#define CLAMPED_ARITHMETIC_H + +#include +#include + +/** + * @brief Overflow-safe clamped addition: returns value + delta bounded to [minValue, maxValue]. + * + * Uses a 64-bit intermediate so the addition cannot overflow int. Shared by the bounded + * counter arithmetic in both the client and the server. + * + * @note Requires minValue <= maxValue. Bounds come from trusted compile-time call sites; + * qBound() asserts this internally in debug builds. + */ +inline int addClamped(int value, int delta, int minValue, int maxValue) +{ + const auto result = static_cast(value) + static_cast(delta); + return static_cast(qBound(static_cast(minValue), result, static_cast(maxValue))); +} + +#endif // CLAMPED_ARITHMETIC_H diff --git a/libcockatrice_utility/libcockatrice/utility/trice_limits.h b/libcockatrice_utility/libcockatrice/utility/trice_limits.h index 833ce1b98..23227eb3c 100644 --- a/libcockatrice_utility/libcockatrice/utility/trice_limits.h +++ b/libcockatrice_utility/libcockatrice/utility/trice_limits.h @@ -1,6 +1,9 @@ #ifndef TRICE_LIMITS_H #define TRICE_LIMITS_H +//! \todo Split trice_limits.h into focused single-purpose headers: string_limits.h, +//! dice_limits.h, counter_limits.h. + #include // max size for short strings, like names and things that are generally a single phrase @@ -15,11 +18,18 @@ constexpr uint MAXIMUM_DIE_SIDES = 1000000; constexpr uint MINIMUM_DICE_TO_ROLL = 1; constexpr uint MAXIMUM_DICE_TO_ROLL = 100; -// Card counter value bounds [0, MAX_COUNTERS_ON_CARD]. -// Counters on cards (e.g., +1/+1 counters, charge counters) are non-negative physical game objects. -// The max of 999 is a display constraint (3-digit rendering) and reasonable gameplay limit. -// Server enforces these bounds; client may also check for UX optimization. -constexpr int MAX_COUNTERS_ON_CARD = 999; +/** + * @brief Upper bound for a bounded counter's value: [0, MAX_COUNTER_VALUE]. + * + * Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters + * something holds. Applies to counters that are constrained to a non-negative display range, + * such as card counters and commander tax. Unbounded counters (e.g. a player's life total) + * do not use this limit and may go negative, saturating only at the int range. + * + * The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit. + * The server enforces these bounds; the client may also check them for UX optimization. + */ +constexpr int MAX_COUNTER_VALUE = 999; // optimized functions to get qstrings that are at most that long static inline QString nameFromStdString(const std::string &_string) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 04ac7fcee..a179a3603 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ enable_testing() add_test(NAME dummy_test COMMAND dummy_test) add_test(NAME expression_test COMMAND expression_test) +add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test) add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) add_test(NAME server_card_counter_test COMMAND server_card_counter_test) @@ -16,6 +17,7 @@ set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) add_executable(dummy_test dummy_test.cpp) add_executable(expression_test expression_test.cpp) +add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp) add_executable(test_age_formatting test_age_formatting.cpp) add_executable(password_hash_test password_hash_test.cpp) add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) @@ -49,6 +51,7 @@ if(NOT GTEST_FOUND) set(GTEST_BOTH_LIBRARIES gtest) add_dependencies(dummy_test gtest) add_dependencies(expression_test gtest) + add_dependencies(clamped_arithmetic_test gtest) add_dependencies(test_age_formatting gtest) add_dependencies(password_hash_test gtest) add_dependencies(deck_hash_performance_test gtest) @@ -59,6 +62,9 @@ endif() include_directories(${GTEST_INCLUDE_DIRS}) target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES}) target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries( + clamped_arithmetic_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) target_link_libraries( test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) diff --git a/tests/clamped_arithmetic_test.cpp b/tests/clamped_arithmetic_test.cpp new file mode 100644 index 000000000..2471d5870 --- /dev/null +++ b/tests/clamped_arithmetic_test.cpp @@ -0,0 +1,44 @@ +/** @file clamped_arithmetic_test.cpp + * @brief Tests for shared helpers in clamped_arithmetic.h. + * @ingroup Tests + */ + +#include +#include +#include + +TEST(AddClamped, AddsWithinBounds) +{ + EXPECT_EQ(addClamped(5, 3, 0, 100), 8); + EXPECT_EQ(addClamped(10, -3, 0, 100), 7); +} + +TEST(AddClamped, ClampsToUpperAndLowerBound) +{ + EXPECT_EQ(addClamped(99, 5, 0, 100), 100); // saturates at max + EXPECT_EQ(addClamped(2, -10, 0, 100), 0); // saturates at min + EXPECT_EQ(addClamped(999, 1, 0, 999), 999); // crossing the counter cap holds at the bound +} + +TEST(AddClamped, IntOverflowDoesNotWrap) +{ + // The 64-bit intermediate must prevent signed-int overflow UB. + constexpr int intMax = std::numeric_limits::max(); + constexpr int intMin = std::numeric_limits::min(); + EXPECT_EQ(addClamped(intMax, 1, intMin, intMax), intMax); + EXPECT_EQ(addClamped(intMax, intMax, intMin, intMax), intMax); +} + +TEST(AddClamped, IntUnderflowDoesNotWrap) +{ + constexpr int intMax = std::numeric_limits::max(); + constexpr int intMin = std::numeric_limits::min(); + EXPECT_EQ(addClamped(intMin, -1, intMin, intMax), intMin); + EXPECT_EQ(addClamped(intMin, intMin, intMin, intMax), intMin); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/server_card_counter_test.cpp b/tests/server_card_counter_test.cpp index ff906b906..b6aacc31b 100644 --- a/tests/server_card_counter_test.cpp +++ b/tests/server_card_counter_test.cpp @@ -28,9 +28,9 @@ TEST(ServerCardCounter, IncrementExistingCounter) TEST(ServerCardCounter, IncrementOverflowProtection) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); EXPECT_FALSE(card.incrementCounter(1, 1)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, DecrementUnderflowProtection) @@ -113,13 +113,13 @@ TEST(ServerCardCounter, IncrementCounterPopulatesEvent) TEST(ServerCardCounter, IncrementCounterEventReflectsClampedValue) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); Event_SetCardCounter event; EXPECT_TRUE(card.incrementCounter(1, 10, &event)); EXPECT_EQ(event.counter_id(), 1); - EXPECT_EQ(event.counter_value(), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(event.counter_value(), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) @@ -133,7 +133,7 @@ TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) TEST(ServerCardCounter, IncrementCounterEventNotPopulatedWhenUnchanged) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); Event_SetCardCounter event; event.set_counter_id(999); @@ -156,7 +156,7 @@ TEST(ServerCardCounter, SetCounterClampsAboveMaxToMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); EXPECT_TRUE(card.setCounter(1, 1500)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) @@ -171,9 +171,9 @@ TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) TEST(ServerCardCounter, IncrementDoesNotExceedMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); EXPECT_TRUE(card.incrementCounter(1, 10)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } int main(int argc, char **argv) From 4a384f2a754ce0384ccaa1711d4af910b1286287 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Mon, 29 Jun 2026 11:03:30 -0400 Subject: [PATCH 47/50] fix theme manager return type warning (#7026) --- cockatrice/src/interface/theme_manager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 4ba35a00e..7dc757062 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -399,6 +399,7 @@ static QString roleBgName(ThemeManager::Role role) default: Q_ASSERT(false); + return {}; } } From 18b23b19a7b3b757f6204d5e5cfdd1c06db92326 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Mon, 29 Jun 2026 17:37:52 -0400 Subject: [PATCH 48/50] Split trice_limits.h into dedicated headers (#7025) * Split trice_limits.h into dedicated headers * Updated docstrings --- cockatrice/src/game/player/player_actions.cpp | 2 +- .../src/game_graphics/board/card_item.h | 1 - .../deckview/deck_view_container.cpp | 2 +- .../dialogs/dlg_create_token.cpp | 2 +- .../game_graphics/dialogs/dlg_roll_dice.cpp | 2 +- .../game_graphics/player/player_dialogs.cpp | 1 + .../deck_editor_deck_dock_widget.cpp | 2 +- .../interface/widgets/dialogs/dlg_connect.cpp | 2 +- .../widgets/dialogs/dlg_create_game.cpp | 2 +- .../widgets/dialogs/dlg_edit_avatar.cpp | 2 +- .../widgets/dialogs/dlg_edit_password.cpp | 2 +- .../widgets/dialogs/dlg_edit_tokens.cpp | 2 +- .../widgets/dialogs/dlg_edit_user.cpp | 2 +- .../dialogs/dlg_forgot_password_challenge.cpp | 2 +- .../dialogs/dlg_forgot_password_request.cpp | 2 +- .../dialogs/dlg_forgot_password_reset.cpp | 2 +- .../widgets/dialogs/dlg_register.cpp | 2 +- .../widgets/server/user/user_list_widget.cpp | 2 +- .../settings_page/messages_settings_page.cpp | 1 + .../widgets/tabs/abstract_tab_deck_editor.cpp | 2 +- .../interface/widgets/tabs/tab_account.cpp | 2 +- .../src/interface/widgets/tabs/tab_admin.cpp | 2 +- .../widgets/tabs/tab_deck_editor.cpp | 1 - .../widgets/tabs/tab_deck_storage.cpp | 1 + .../src/interface/widgets/tabs/tab_game.cpp | 2 +- .../src/interface/widgets/tabs/tab_logs.cpp | 2 +- .../interface/widgets/tabs/tab_message.cpp | 2 +- .../src/interface/widgets/tabs/tab_room.cpp | 2 +- .../widgets/utility/get_text_with_max.h | 2 +- .../game/server_abstract_participant.cpp | 2 +- .../remote/game/server_abstract_player.cpp | 3 +- .../server/remote/game/server_card.cpp | 2 +- .../server/remote/game/server_player.cpp | 2 +- .../server/remote/server_protocolhandler.cpp | 2 +- .../network/server/remote/server_room.cpp | 2 +- .../protocol/debug_pb_message.cpp | 2 +- libcockatrice_utility/CMakeLists.txt | 4 +- .../libcockatrice/utility/counter_limits.h | 17 +++++++ .../libcockatrice/utility/dice_limits.h | 15 ++++++ .../libcockatrice/utility/string_limits.h | 31 ++++++++++++ .../libcockatrice/utility/trice_limits.h | 48 ------------------- servatrice/src/serversocketinterface.cpp | 2 +- tests/server_card_counter_test.cpp | 2 +- 43 files changed, 103 insertions(+), 84 deletions(-) create mode 100644 libcockatrice_utility/libcockatrice/utility/counter_limits.h create mode 100644 libcockatrice_utility/libcockatrice/utility/dice_limits.h create mode 100644 libcockatrice_utility/libcockatrice/utility/string_limits.h delete mode 100644 libcockatrice_utility/libcockatrice/utility/trice_limits.h diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 1c469a42a..df9eb8a1d 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -28,8 +28,8 @@ #include #include #include +#include #include -#include #include // milliseconds in between triggers of the move top cards until action diff --git a/cockatrice/src/game_graphics/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h index 8efcd085d..37f3bab50 100644 --- a/cockatrice/src/game_graphics/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -12,7 +12,6 @@ #include "abstract_card_item.h" #include -#include class CardDatabase; class CardDragItem; diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index 21284c517..d476a5012 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false) { diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index 11c24b72e..1a9dc59b2 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent) : QDialog(parent), predefinedTokens(_predefinedTokens) diff --git a/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp index dfb3d0bc5..4f5d5b861 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/game_graphics/player/player_dialogs.cpp b/cockatrice/src/game_graphics/player/player_dialogs.cpp index 3c26ae1fe..95c225812 100644 --- a/cockatrice/src/game_graphics/player/player_dialogs.cpp +++ b/cockatrice/src/game_graphics/player/player_dialogs.cpp @@ -8,6 +8,7 @@ #include #include +#include PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions) : QObject(_player), player(_player), playerActions(_playerActions) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index f751fa225..f61a01168 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index fdbc90542..2a7fc6c06 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index 30364f242..f892801f1 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include void DlgCreateGame::sharedCtor() { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp index db5f21701..b5710b208 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image() { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp index cdd4433a7..63181b44f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp index 381aa2b11..f249976c2 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp index 7015f9d47..85b95f335 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include DlgEditUser::DlgEditUser(QWidget *parent, QString email, QString country, QString realName) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp index 24e9030e0..14c3cda47 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp index c33a41bed..fc934d082 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp index d2eb081d1..d30887988 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp index 0f7c17b18..942782403 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index c094f8a6b..32f46a79f 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -30,7 +30,7 @@ #include #include #include -#include +#include BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index 1e6f99245..831cb442c 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -6,6 +6,7 @@ #include #include #include +#include MessagesSettingsPage::MessagesSettingsPage() { diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index a8cc4cee6..4bb8ffca4 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -43,7 +43,7 @@ #include #include #include -#include +#include /** * @brief Constructs the AbstractTabDeckEditor. diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp index e61732f90..2cc8165e8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo) : Tab(_tabSupervisor), client(_client) diff --git a/cockatrice/src/interface/widgets/tabs/tab_admin.cpp b/cockatrice/src/interface/widgets/tabs/tab_admin.cpp index 533e1cc83..16f17e03e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_admin.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_admin.cpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 4e7cbfecf..3878e12e0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -21,7 +21,6 @@ #include #include #include -#include /** * @brief Constructs a new TabDeckEditor object. diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index bdf7901f1..8e2b114d9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -28,6 +28,7 @@ #include #include #include +#include TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index a81161e83..600647171 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -44,7 +44,7 @@ #include #include #include -#include +#include TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp index 9a030e7d9..e3678a903 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index d77cb0391..2cacab731 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_message.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabMessage::TabMessage(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index c7495da5a..ec05e9ff6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include TabRoom::TabRoom(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/utility/get_text_with_max.h b/cockatrice/src/interface/widgets/utility/get_text_with_max.h index 923d6f427..424bb0c6a 100644 --- a/cockatrice/src/interface/widgets/utility/get_text_with_max.h +++ b/cockatrice/src/interface/widgets/utility/get_text_with_max.h @@ -9,7 +9,7 @@ #include #include -#include +#include QString getTextWithMax(QWidget *parent, const QString &title, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index 493b8e966..5c0fdf944 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -48,7 +48,7 @@ #include #include #include -#include +#include Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, int _playerId, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index 157fa6441..44e317bf7 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -47,7 +47,8 @@ #include #include #include -#include +#include +#include #include #include #include diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp index 4ff0cfb5b..8c7feadba 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index 56e3f9f8e..d502fc7d6 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -47,7 +47,7 @@ #include #include #include -#include +#include #include Server_Player::Server_Player(Server_Game *_game, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 27ebaf228..c441da781 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -26,7 +26,7 @@ #include #include #include -#include +#include Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, Server_DatabaseInterface *_databaseInterface, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 1bd928e09..1f29e62fb 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include Server_Room::Server_Room(int _id, int _chatHistorySize, diff --git a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp index c419a68d4..4d4889323 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include // FastFieldValuePrinter is added in protobuf 3.4, going out of our way to add the old FieldValuePrinter is not worth it #if GOOGLE_PROTOBUF_VERSION > 3004000 diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 13f13bb56..2d34cad31 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -15,7 +15,9 @@ set(UTILITY_HEADERS libcockatrice/utility/levenshtein.h libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h - libcockatrice/utility/trice_limits.h + libcockatrice/utility/string_limits.h + libcockatrice/utility/dice_limits.h + libcockatrice/utility/counter_limits.h libcockatrice/utility/clamped_arithmetic.h libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h diff --git a/libcockatrice_utility/libcockatrice/utility/counter_limits.h b/libcockatrice_utility/libcockatrice/utility/counter_limits.h new file mode 100644 index 000000000..1343dbb3f --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/counter_limits.h @@ -0,0 +1,17 @@ +#ifndef COUNTER_LIMITS_H +#define COUNTER_LIMITS_H + +/** + * @brief Upper bound for a bounded counter's value: [0, MAX_COUNTER_VALUE]. + * + * Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters + * something holds. Applies to counters that are constrained to a non-negative display range, + * such as card counters and commander tax. Unbounded counters (e.g. a player's life total) + * do not use this limit and may go negative, saturating only at the int range. + * + * The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit. + * The server enforces these bounds; the client may also check them for UX optimization. + */ +constexpr int MAX_COUNTER_VALUE = 999; + +#endif // COUNTER_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/dice_limits.h b/libcockatrice_utility/libcockatrice/utility/dice_limits.h new file mode 100644 index 000000000..e1407c57e --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/dice_limits.h @@ -0,0 +1,15 @@ +#ifndef DICE_LIMITS_H +#define DICE_LIMITS_H + +#include // for uint + +/** @brief Fewest sides a rollable die may have. */ +constexpr uint MINIMUM_DIE_SIDES = 2; +/** @brief Most sides a rollable die may have. */ +constexpr uint MAXIMUM_DIE_SIDES = 1000000; +/** @brief Fewest dice that may be rolled at once. */ +constexpr uint MINIMUM_DICE_TO_ROLL = 1; +/** @brief Most dice that may be rolled at once. */ +constexpr uint MAXIMUM_DICE_TO_ROLL = 100; + +#endif // DICE_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/string_limits.h b/libcockatrice_utility/libcockatrice/utility/string_limits.h new file mode 100644 index 000000000..cca804bf0 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/string_limits.h @@ -0,0 +1,31 @@ +#ifndef STRING_LIMITS_H +#define STRING_LIMITS_H + +#include +#include +#include + +/** @brief Max size for short strings, like names and things that are generally a single phrase. */ +constexpr int MAX_NAME_LENGTH = 0xff; +/** @brief Max size for chat messages and text contents. */ +constexpr int MAX_TEXT_LENGTH = 0xfff; +/** @brief Max size for deck files and pictures (about 2 megabytes). */ +constexpr int MAX_FILE_LENGTH = 0x1fffff; + +/** @brief Returns a QString from a std::string, truncated to at most MAX_NAME_LENGTH bytes. */ +inline QString nameFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH)); +} +/** @brief Returns a QString from a std::string, truncated to at most MAX_TEXT_LENGTH bytes. */ +inline QString textFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH)); +} +/** @brief Returns a QString from a std::string, truncated to at most MAX_FILE_LENGTH bytes. */ +inline QString fileFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH)); +} + +#endif // STRING_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/trice_limits.h b/libcockatrice_utility/libcockatrice/utility/trice_limits.h deleted file mode 100644 index 23227eb3c..000000000 --- a/libcockatrice_utility/libcockatrice/utility/trice_limits.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef TRICE_LIMITS_H -#define TRICE_LIMITS_H - -//! \todo Split trice_limits.h into focused single-purpose headers: string_limits.h, -//! dice_limits.h, counter_limits.h. - -#include - -// max size for short strings, like names and things that are generally a single phrase -constexpr int MAX_NAME_LENGTH = 0xff; -// max size for chat messages and text contents -constexpr int MAX_TEXT_LENGTH = 0xfff; -// max size for deck files and pictures -constexpr int MAX_FILE_LENGTH = 0x1fffff; // about 2 megabytes - -constexpr uint MINIMUM_DIE_SIDES = 2; -constexpr uint MAXIMUM_DIE_SIDES = 1000000; -constexpr uint MINIMUM_DICE_TO_ROLL = 1; -constexpr uint MAXIMUM_DICE_TO_ROLL = 100; - -/** - * @brief Upper bound for a bounded counter's value: [0, MAX_COUNTER_VALUE]. - * - * Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters - * something holds. Applies to counters that are constrained to a non-negative display range, - * such as card counters and commander tax. Unbounded counters (e.g. a player's life total) - * do not use this limit and may go negative, saturating only at the int range. - * - * The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit. - * The server enforces these bounds; the client may also check them for UX optimization. - */ -constexpr int MAX_COUNTER_VALUE = 999; - -// optimized functions to get qstrings that are at most that long -static inline QString nameFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH)); -} -static inline QString textFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH)); -} -static inline QString fileFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH)); -} - -#endif // TRICE_LIMITS_H diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 55f779468..6ceebfca9 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -83,7 +83,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/tests/server_card_counter_test.cpp b/tests/server_card_counter_test.cpp index b6aacc31b..c8bc43f8f 100644 --- a/tests/server_card_counter_test.cpp +++ b/tests/server_card_counter_test.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include TEST(ServerCardCounter, IncrementNewCounter) From 9b0348240dba15cf8ac7929ae637660b7c8e6451 Mon Sep 17 00:00:00 2001 From: Skagra42 <72851315+Skagra42@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:46:37 -0600 Subject: [PATCH 49/50] Search by date. (#7027) --- cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp | 2 +- .../models/database/card_set/card_sets_model.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index c693fb02e..b17a9306f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp @@ -62,7 +62,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) // search field searchField = new LineEditUnfocusable; searchField->setObjectName("searchEdit"); - searchField->setPlaceholderText(tr("Search by set name, code, or type")); + searchField->setPlaceholderText(tr("Search by set name, code, type, or release date")); searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition); searchField->setClearButtonEnabled(true); setFocusProxy(searchField); diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp index 5e0cc31d8..8fd5311f2 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp @@ -303,12 +303,14 @@ bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &source auto typeIndex = sourceModel()->index(sourceRow, SetsModel::SetTypeCol, sourceParent); auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent); auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent); + auto dateIndex = sourceModel()->index(sourceRow, SetsModel::ReleaseDateCol, sourceParent); const auto filter = filterRegularExpression(); return (sourceModel()->data(typeIndex).toString().contains(filter) || sourceModel()->data(nameIndex).toString().contains(filter) || - sourceModel()->data(shortNameIndex).toString().contains(filter)); + sourceModel()->data(shortNameIndex).toString().contains(filter) || + sourceModel()->data(dateIndex).toString().contains(filter)); } bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const From baddbfae14f356e977bff5f6df5e38355f41624b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:25:10 +0200 Subject: [PATCH 50/50] Update translation source strings (#7028) Co-authored-by: github-actions --- cockatrice/cockatrice_en@source.ts | 5064 ++++++++++++++++------------ oracle/oracle_en@source.ts | 4 +- 2 files changed, 2949 insertions(+), 2119 deletions(-) diff --git a/cockatrice/cockatrice_en@source.ts b/cockatrice/cockatrice_en@source.ts index 810b345df..e06972ddc 100644 --- a/cockatrice/cockatrice_en@source.ts +++ b/cockatrice/cockatrice_en@source.ts @@ -4,7 +4,7 @@ AbstractCounter - + &Set counter... @@ -12,12 +12,12 @@ AbstractCounterDialog - + Set counter - + New value for counter '%1': @@ -38,60 +38,60 @@ AbstractTabDeckEditor - + Open in new tab - + Are you sure? - + The decklist has been modified. Do you want to save the changes? - - - - - - + + + + + + Error - + Could not open deck at %1 - + Could not save remote deck - - + + The deck could not be saved. Please check that the directory is writable and try again. - + Save deck - + The deck could not be saved. - + There are no cards in your deck to be exported @@ -107,12 +107,12 @@ Please check that the directory is writable and try again. AdminNotesDialog - + Update Notes - + Admin Notes for %1 @@ -129,172 +129,227 @@ Please check that the directory is writable and try again. Sideboard
+ + + Tokens + + AppearanceSettingsPage - + seconds - + Error - + Could not create themes directory at '%1'. - + Theme settings - + Current theme: - + Open themes folder - + Home tab background source: - + Home tab background shuffle frequency: - + Disabled - - Display card name of background in bottom right: - - - - + Menu settings - + Show keyboard shortcuts in right-click menus - + Show game filter toolbar above list in room tab - + Card rendering - + Display card names on cards having a picture - + Auto-Rotate cards with sideways layout - + Override all card art with personal set preference (Pre-ProviderID change behavior) - + + Light + + + + + Dark + + + + + System + + + + + Active theme palette: + + + + + Edit theme palette + + + + + Home tab settings + + + + + Display card name of background in bottom right + + + + + Styling settings + + + + + Style user list + + + + + Card printings + + + + Bump sets that the deck contains cards from to the top in the printing selector - + Scale cards on mouse over - + Use rounded card corners - + + Card layout + + + + Minimum overlap percentage of cards on the stack and in vertical hand - + Maximum initial height for card view window: - - + + rows - + Maximum expanded height for card view window: - + Card counters - + Counter %1 - + Hand layout - + Display hand horizontally (wastes space) - + Enable left justification - + Table grid layout - + Invert vertical coordinate - + Minimum player count for multi-column layout: - + Maximum font size for information displayed on cards: @@ -302,12 +357,12 @@ Please check that the directory is writable and try again. ArchidektApiResponseDeckDisplayWidget - + Back to results - + Open Deck in Deck Editor @@ -333,111 +388,111 @@ Please check that the directory is writable and try again. BanDialog - + ban &user name - + ban &IP address - + ban client I&D - + Ban type - + &permanent ban - + &temporary ban - + &Days: - + &Hours: - + &Minutes: - + Duration of the ban - + Please enter the reason for the ban. This is only saved for moderators and cannot be seen by the banned person. - + Please enter the reason for the ban that will be visible to the banned person. - + Redact all messages from this user in all rooms - + &OK - + &Cancel - + Ban user from server - - - - + + + + Error - + You have to select a name-based, IP-based, clientId based, or some combination of the three to place a ban. - + You must have a value in the name ban when selecting the name ban checkbox. - + You must have a value in the ip ban when selecting the ip ban checkbox. - + You must have a value in the clientid ban when selecting the clientid ban checkbox. @@ -445,59 +500,123 @@ This is only saved for moderators and cannot be seen by the banned person. BetaReleaseChannel - + Beta - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the file update server. + + CardArtPreviewWidget + + + No card selected + + + + + CardArtRulesModel + + + Card + + + + + ProviderId + + + + + Mode + + + + + Reason + + + CardDatabaseModel - + Name - + Sets - + Mana cost - + Card type - + P/T - + Color(s) + + CardDatabaseView + + + Add to Deck + + + + + Add to Sideboard + + + + + Select Printing + + + + + Show on EDHRec (Commander) + + + + + Show on EDHRec (Card) + + + + + Show Related cards + + + CardFilter @@ -626,22 +745,22 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoPictureWidget - + View related cards - + Add card to deck - + Mainboard - + Sideboard @@ -664,12 +783,12 @@ This is only saved for moderators and cannot be seen by the banned person.
- + Related cards: - + Unknown card: @@ -677,128 +796,146 @@ This is only saved for moderators and cannot be seen by the banned person. CardMenu - + Re&veal to... - + &All players - + View related cards - + Token: - + All tokens - + &Select All - + S&elect Row - + S&elect Column - + &Play - + &Hide - + Play &Face Down - + &Tap / Untap Turn sideways or back again - + Skip &untapping - + T&urn Over Turn face up/face down - + &Peek at card face - + &Clone - + Attac&h to card... - + Unattac&h - + &Draw arrow... - + &Set annotation... - + + Reduce life by power + + + + Ca&rd counters - + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... + + CardPictureLoaderCacheMethod + + + Network Cache + + + + + Filesystem + + + CardSizeWidget @@ -810,133 +947,133 @@ This is only saved for moderators and cannot be seen by the banned person. CardZoneLogic - + their hand nominative - + %1's hand nominative - + their library look at zone - + %1's library look at zone - + of their library top cards of zone, - + of %1's library top cards of zone - + their library reveal zone - + %1's library reveal zone - + their library shuffle - + %1's library shuffle - + their library nominative - + %1's library nominative - + their graveyard nominative - + %1's graveyard nominative - + their exile nominative - + %1's exile nominative - + their sideboard look at zone - + %1's sideboard look at zone - + their sideboard nominative - + %1's sideboard nominative - + their custom zone '%1' nominative - + %1's custom zone '%2' nominative @@ -958,16 +1095,424 @@ This is only saved for moderators and cannot be seen by the banned person. + + ColorButton + + + Click to pick a color + + + + + Pick colour + + + + + ConnectionController + + + + The server has reached its maximum user capacity, please check back later. + + + + + There are too many concurrent connections from your address. + + + + + Banned by moderator + + + + + Expected end time: %1 + + + + + This ban lasts indefinitely. + + + + + Scheduled server shutdown. + + + + + + Invalid username. + + + + + You have been logged out due to logging in at another location. + + + + + Connection closed + + + + + The server has terminated your connection. +Reason: %1 + + + + + The server is going to be restarted in %n minute(s). +All running games will be lost. +Reason for shutdown: %1 + + + + + + + + Scheduled server shutdown + + + + + Failed Login + + + + + Your client seems to be missing features this server requires for connection. + + + + + To update your client, go to 'Help -> Check for Client Updates'. + + + + + + + + + + + + + + + + + + + + + + Error + + + + + Incorrect username or password. Please check your authentication information and try again. + + + + + There is already an active session using this user name. +Please close that session first and re-login. + + + + + + You are banned until %1. + + + + + + You are banned indefinitely. + + + + + This server requires user registration. Do you want to register now? + + + + + This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client. +Please close and reopen your client to try again. + + + + + An internal error has occurred, please close and reopen Cockatrice before trying again. +If the error persists, ensure you are running the latest version of the software and if needed contact the software developers. + + + + + Account activation + + + + + Your account has not been activated yet. +You need to provide the activation token received in the activation email. + + + + + Server Full + + + + + Unknown login error: %1 + + + + + + +This usually means that your client version is out of date, and the server sent a reply your client doesn't understand. + + + + + + + + + + Registration denied + + + + + Registration is currently disabled on this server + + + + + There is already an existing account with the same user name. + + + + + It's mandatory to specify a valid email address when registering. + + + + + It appears you are attempting to register a new account on this server yet you already have an account registered with the email provided. This server restricts the number of accounts a user can register per address. Please contact the server operator for further assistance or to obtain your credential information. + + + + + Password too short. + + + + + Registration failed for a technical problem on the server. + + + + + The connection to the server has been lost. + + + + + Unknown registration error: %1 + + + + + Account activation failed + + + + + Socket error: %1 + + + + + Server timeout + + + + + You are trying to connect to an obsolete server. Please downgrade your Cockatrice version or connect to a suitable server. +Local version is %1, remote version is %2. + + + + + Your Cockatrice client is obsolete. Please update your Cockatrice version. +Local version is %1, remote version is %2. + + + + + + Success + + + + + Registration accepted. +Will now login. + + + + + Account activation accepted. +Will now login. + + + + + Information + + + + + This server supports additional features that your client doesn't have. +This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. + +To update your client, go to Help -> Check for Updates. + + + + + + + Reset Password + + + + + Your password has been reset successfully, you can now log in using the new credentials. + + + + + Failed to reset user account password, please contact the server operator to reset your password. + + + + + Activation request received, please check your email for an activation token. + + + + + Connecting to %1... + + + + + Registering to %1 as %2... + + + + + Disconnected + + + + + Connected, logging in at %1 + + + + + Requesting forgotten password to %1 as %2... + + + + + Your username must respect these rules: + + + + + is %1 - %2 characters long + + + + + can %1 contain lowercase characters + + + + + + + + NOT + + + + + can %1 contain uppercase characters + + + + + can %1 contain numeric characters + + + + + can contain the following punctuation: %1 + + + + + first character can %1 be a punctuation mark + + + + + no unacceptable language as specified by these server rules: + note that the following lines will not be translated + + + + + can not contain any of the following words: %1 + + + + + can not match any of the following expressions: %1 + + + + + You may only use A-Z, a-z, 0-9, _, ., and - in your username. + + + CustomZoneMenu - + C&ustom Zones - - + + View custom zone '%1' @@ -975,30 +1520,35 @@ This is only saved for moderators and cannot be seen by the banned person. DeckAnalyticsWidget - + Add Panel - + Remove Panel - + Save Layout - + Load Layout + + + Include Sideboard + + DeckEditorCardDatabaseDockWidget - + Card Database @@ -1014,47 +1564,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorDatabaseDisplayWidget - + Search by card name (or search expressions) - - Add to Deck - - - - - Add to Sideboard - - - - - Select Printing - - - - - Show on EDHRec (Commander) - - - - - Show on EDHRec (Card) - - - - - Show Related cards - - - - + Add card to &maindeck - + Add card to &sideboard @@ -1087,72 +1607,72 @@ This is only saved for moderators and cannot be seen by the banned person. - + Select Printing - + Deck - + Deck &name: - + Banner Card/Tags Visibility Settings - + Show banner card selection menu - + Show tags selection menu - + &Comments: - + Group by: - + Format: - + Hash: - + &Increment number - + &Decrement number - + &Remove row - + Swap card to/from sideboard @@ -1160,17 +1680,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorFilterDockWidget - + Filters - + &Clear all filters - + Delete selected @@ -1301,169 +1821,113 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorSettingsPage - - + + Update Spoilers - - + Success - + Download URLs have been reset. - - Downloaded card pictures have been reset. - - - - - Error - - - - - One or more downloaded card pictures could not be cleared. - - - - + Add URL - - + + URL: - - + + Edit URL - - Network Cache Size: - - - - - Redirect Cache TTL: - - - - - How long cached redirects for urls are valid for. - - - - - Picture Cache Size: - - - - + Add New URL - + Remove URL - - Day(s) - - - - + Updating... - + Choose path - + URL Download Priority - + Spoilers - + Download Spoilers Automatically - + Spoiler Location: - + Last Change - + Spoilers download automatically on launch - + Press the button to manually update without relaunching - + Do not close settings until manual update is complete - + Download card pictures on the fly - + How to add a custom URL - - Delete Downloaded Images - - - - + Reset Download URLs - - - On-disk cache for downloaded pictures - - - - - In-memory cache for pictures not currently on screen - - DeckListHistoryManagerWidget @@ -1529,12 +1993,12 @@ This is only saved for moderators and cannot be seen by the banned person. DeckLoader - + Common deck formats (%1) - + All files (*.*) @@ -1756,27 +2220,27 @@ This is only saved for moderators and cannot be seen by the banned person. - + Moved to %1 1 × "%2" (%3) - + Removed "%1" (all copies) - + %1 1 × "%2" (%3) - + Added - + Removed @@ -1798,74 +2262,74 @@ This is only saved for moderators and cannot be seen by the banned person. DeckViewContainer - + Load deck... - + Load remote deck... - + Load from clipboard... - + Load from website... - + Unload deck - + Ready to start - + Force start - + Sideboard unlocked - + Sideboard locked - - + + Error - + The selected file could not be loaded. - + Deck is greater than maximum file size. - + Are you sure you want to force start? This will kick all non-ready players from the game. - + Cockatrice @@ -1964,7 +2428,7 @@ This will kick all non-ready players from the game. - + Webpage @@ -2004,37 +2468,37 @@ This will kick all non-ready players from the game. - + Server URL - + Communication Port - + Unique Server Name - + Connection Warning - + You need to name your new connection profile. - + Connect Warning - + The player name can't be empty. @@ -2147,17 +2611,17 @@ This will kick all non-ready players from the game. - + Game information - + Error - + Server error. @@ -2165,97 +2629,97 @@ This will kick all non-ready players from the game. DlgCreateToken - + &Name: - + Token - + C&olor: - + white - + blue - + black - + red - + green - + multicolor - + colorless - + &P/T: - + &Annotation: - + &Destroy token when it leaves the table - + Create face-down (Only hides name) - + Token data - + Show &all tokens - + Show tokens from this &deck - + Choose token from list - + Create token @@ -2289,7 +2753,7 @@ This will kick all non-ready players from the game. - + @@ -2304,12 +2768,12 @@ This will kick all non-ready players from the game. - + Duplicate Tag - + This tag already exists. @@ -2495,12 +2959,12 @@ To remove your current avatar, confirm without choosing a new image. - + Error - + The chosen name conflicts with an existing card or token. Make sure to enable the 'Token' set in the "Manage sets" dialog to display them correctly. @@ -2524,12 +2988,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia - + Real name: - + Edit user profile @@ -2567,113 +3031,113 @@ Make sure to enable the 'Token' set in the "Manage sets" dia - + Hide 'buddies only' games - + Hide full games - + Hide games that have started - + Hide password protected games - + Hide 'ignored user' games - + Hide games not created by buddies Hide games not created by buddy - + Hide games with forced open decklists - + &Newer than: - + Game &description: - + &Creator name: - + General - + &Game types - + at &least: - + at &most: - + Maximum player count - + Restrictions - + Show games only if &spectators can watch - + Show spectator password p&rotected games - + Show only if spectators can ch&at - + Show only if spectators can see &hands - + Spectators - + Filter games @@ -2976,37 +3440,37 @@ https://tappedout.net/mtg-decks/your-deck-name/ DlgMoveTopCardsUntil - + Card name (or search expressions): - + Number of hits: - + Auto play hits - + Put top cards on stack until... - + No cards matching the search expression exists in the card database. Proceed anyways? - + Cockatrice - + Invalid filter @@ -3065,40 +3529,40 @@ Your email will be used to verify your account. - + Real name: - + Register to server - - - - + + + + Registration Warning - + Your password is too short. - + Your passwords do not match, please try again. - + Your email addresses do not match, please try again. - + The player name can't be empty. @@ -3106,17 +3570,17 @@ Your email will be used to verify your account. DlgRollDice - + Number of sides: - + Number of dice: - + Roll Dice @@ -3167,12 +3631,12 @@ Your email will be used to verify your account. DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -3183,7 +3647,7 @@ Would you like to change your database location setting? - + Your card database version is too old. This can cause problems loading card information or images @@ -3194,7 +3658,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -3203,21 +3667,21 @@ Would you like to change your database location setting? - + File Error loading your card database. Would you like to change your database location setting? - + Your card database was loaded but contains no cards. Would you like to change your database location setting? - + Unknown card database load status Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues @@ -3226,59 +3690,64 @@ Would you like to change your database location setting? - - - + + + Error - + The path to your deck directory is invalid. Would you like to go back and set the correct path? - + The path to your card pictures directory is invalid. Would you like to go back and set the correct path? - + Settings - + General - + Appearance - + User Interface - + + Storage + + + + Card Sources - + Chat - + Sound - + Shortcuts @@ -3370,9 +3839,9 @@ You can always change this behavior in the 'General' settings tab. - - - + + + Error @@ -3424,31 +3893,31 @@ Please visit the download page to update manually. - + Update Available - + A new version of Cockatrice is available! - + New version - + Released - + Changelog @@ -3458,50 +3927,50 @@ Please visit the download page to update manually. - + Unfortunately, the automatic updater failed to find a compatible download. You may have to manually download the new version. - + Please check the <a href="%1">releases page</a> on our Github and download the build for your system. - - - + + + Update Error - + An error occurred while checking for updates: - + An error occurred while downloading an update: - + Installing... - + Cockatrice is unable to open the installer. - + Try to update manually by closing Cockatrice and running the installer. - + Download location @@ -3591,67 +4060,67 @@ You may have to manually download the new version. DrawProbabilityWidget - + Draw Probability - + Probability of drawing - + Card Name - + Type - + Subtype - + Mana Value - + At least - + Exactly - + card(s) having drawn at least - + cards - + Category - + Qty - + Odds (%) @@ -3765,7 +4234,7 @@ You may have to manually download the new version. FilterBuilder - + Type your filter here @@ -3796,22 +4265,22 @@ You may have to manually download the new version. GameEventHandler - + kicked by game host or moderator - + player left the game - + player disconnected from server - + reason unknown @@ -3819,140 +4288,140 @@ You may have to manually download the new version. GameSelector - - - - - - - - - + + + + + + + + + Error - + Please join the appropriate room first. - + Wrong password. - + Spectators are not allowed in this game. - + The game is already full. - + The game does not exist any more. - + This game is only open to registered users. - + This game is only open to its creator's buddies. - + You are being ignored by the creator of this game. - + Join Game - + Spectate Game - + Game Information - + Join Game as Judge - + Spectate Game as Judge - + Join game - + Password: - + Please join the respective room first. - + &Filter games - + C&lear filter - + C&reate - + &Join - + Join as judge - + J&oin as spectator - + Join as judge spectator - + Games shown: %1 / %2 - + Games @@ -3960,32 +4429,32 @@ You may have to manually download the new version. GameSelectorQuickFilterToolBar - + All types - + Filter by game name... - + Filter by game type/format - + Hide games not created by buddies - + Hide full games - + Hide started games @@ -3993,12 +4462,12 @@ You may have to manually download the new version. GamesModel - + >1 day - + %1%2 hr short age in hours @@ -4007,12 +4476,12 @@ You may have to manually download the new version. - + new - + %1%2 min short age in minutes @@ -4021,83 +4490,83 @@ You may have to manually download the new version. - + password - + buddies only - + reg. users only - + open decklists - + can chat - + see hands - + can see hands - + not allowed - + Room - + Age - + Description - + Creator - + Type - + Restrictions - + Players - + Spectators @@ -4105,143 +4574,158 @@ You may have to manually download the new version. GeneralSettingsPage - + Reset all paths - + All paths have been reset - - - - - - - + + + + + + + Choose path - - Personal settings + + Language settings - + Language: - + + Version settings + + + + + Card database + + + + + Startup settings + + + + Paths (editing disabled in portable mode) - + Paths - + How to help with translations - + Decks directory: - + Filters directory: - + Replays directory: - + Pictures directory: - + Card database: - + Custom database directory: - + Token database: - + Update channel - + Check for client updates on startup - + Check for card database updates on startup - + Don't check - + Prompt for update - + Always update in the background - + Check for card database updates every - + days - + Notify if a feature supported by the server is missing in my client - + Automatically run Oracle when running a new version of Cockatrice - + Show tips on startup - + Last update check on %1 (%2 days ago) @@ -4249,47 +4733,47 @@ You may have to manually download the new version. GraveyardMenu - + &Graveyard - + &View graveyard - + &Move graveyard to... - + &Top of library - + &Bottom of library - + &All players - + &Hand - + &Exile - + Reveal random card to... @@ -4297,88 +4781,88 @@ You may have to manually download the new version. HandMenu - + &Hand - + &View hand - + Sort hand by... - + Name - + Type - + Mana Value - + Take &mulligan (Choose hand size) - + Take mulligan (Same hand size) - + Take mulligan (Hand size - 1) - + &Move hand to... - + &Top of library - + &Bottom of library - + &Graveyard - + &Exile - + &Reveal hand to... - - + + All players - + Reveal r&andom card to... @@ -4386,52 +4870,52 @@ You may have to manually download the new version. HomeWidget - + Create New Deck - + Browse Decks - + Browse Card Database - + Browse EDHRec - + Browse Archidekt - + View Replays - + Quit - + Connecting... - + Connect - + Play @@ -4439,213 +4923,213 @@ You may have to manually download the new version. LibraryMenu - + &Library - + &View library - + View &top cards of library... - + View bottom cards of library... - + Reveal &library to... - + Lend library to... - + Reveal &top cards to... - + &Top of library... - + &Bottom of library... - + &Always reveal top card - + &Always look at top card - + &Open deck in deck editor - + &Draw card - + D&raw cards... - + &Undo last draw - + Shuffle - + &Play top card - + Play top card &face down - + Put top card on &bottom - + Move top card to grave&yard - + Move top card to e&xile - + Move top cards to &graveyard... - + Move top cards to graveyard face down... - + Move top cards to &exile... - + Move top cards to exile face down... - + Put top cards on stack &until... - + Shuffle top cards... - + &Draw bottom card - + D&raw bottom cards... - + &Play bottom card - + Play bottom card &face down - + Move bottom card to grave&yard - + Move bottom card to e&xile - + Move bottom cards to &graveyard... - + Move bottom cards to graveyard face down... - + Move bottom cards to &exile... - + Move bottom cards to exile face down... - + Put bottom card on &top - + Shuffle bottom cards... - - + + &All players - + Reveal top cards of library - + Number of cards: (max. %1) @@ -4653,655 +5137,300 @@ You may have to manually download the new version. MainWindow - - - The server has reached its maximum user capacity, please check back later. - - - - - There are too many concurrent connections from your address. - - - - - Banned by moderator - - - - - Expected end time: %1 - - - - - This ban lasts indefinitely. - - - - - Scheduled server shutdown. - - - - - - Invalid username. - - - - - You have been logged out due to logging in at another location. - - - - - Connection closed - - - - - The server has terminated your connection. -Reason: %1 - - - - - The server is going to be restarted in %n minute(s). -All running games will be lost. -Reason for shutdown: %1 - - - - - - - - Scheduled server shutdown - - - - - - Success - - - - - Registration accepted. -Will now login. - - - - - Account activation accepted. -Will now login. - - - - - + + Player %1 - + Load replay - + About Cockatrice - + Version - + Cockatrice Webpage - + Project Manager: - + Past Project Managers: - + Developers: - + Our Developers - + Help Develop! - + Translators: - + Our Translators - + Help Translate! - + Support: - + Report an Issue - + Troubleshooting - + F.A.Q. - - - - - - - - - - - - - - - - - - - - + + Error - - Server timeout - - - - - Failed Login - - - - - Your client seems to be missing features this server requires for connection. - - - - - To update your client, go to 'Help -> Check for Client Updates'. - - - - - Incorrect username or password. Please check your authentication information and try again. - - - - - There is already an active session using this user name. -Please close that session first and re-login. - - - - - - You are banned until %1. - - - - - - You are banned indefinitely. - - - - - This server requires user registration. Do you want to register now? - - - - - This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client. -Please close and reopen your client to try again. - - - - - An internal error has occurred, please close and reopen Cockatrice before trying again. -If the error persists, ensure you are running the latest version of the software and if needed contact the software developers. - - - - - Account activation - - - - - Your account has not been activated yet. -You need to provide the activation token received in the activation email. - - - - - Server Full - - - - - Unknown login error: %1 - - - - - - -This usually means that your client version is out of date, and the server sent a reply your client doesn't understand. - - - - - Your username must respect these rules: - - - - - is %1 - %2 characters long - - - - - can %1 contain lowercase characters - - - - - - - - NOT - - - - - can %1 contain uppercase characters - - - - - can %1 contain numeric characters - - - - - can contain the following punctuation: %1 - - - - - first character can %1 be a punctuation mark - - - - - no unacceptable language as specified by these server rules: - note that the following lines will not be translated - - - - - can not contain any of the following words: %1 - - - - - can not match any of the following expressions: %1 - - - - - You may only use A-Z, a-z, 0-9, _, ., and - in your username. - - - - - - - - - - Registration denied - - - - - Registration is currently disabled on this server - - - - - There is already an existing account with the same user name. - - - - - It's mandatory to specify a valid email address when registering. - - - - - It appears you are attempting to register a new account on this server yet you already have an account registered with the email provided. This server restricts the number of accounts a user can register per address. Please contact the server operator for further assistance or to obtain your credential information. - - - - - Password too short. - - - - - Registration failed for a technical problem on the server. - - - - - The connection to the server has been lost. - - - - - Unknown registration error: %1 - - - - - Account activation failed - - - - - Socket error: %1 - - - - - You are trying to connect to an obsolete server. Please downgrade your Cockatrice version or connect to a suitable server. -Local version is %1, remote version is %2. - - - - - Your Cockatrice client is obsolete. Please update your Cockatrice version. -Local version is %1, remote version is %2. - - - - - Connecting to %1... - - - - - Registering to %1 as %2... - - - - - Disconnected - - - - - Connected, logging in at %1 - - - - - - - Requesting forgotten password to %1 as %2... - - - - + &Connect... - + &Disconnect - + Start &local game... - + &Watch replay... - + &Full screen - + &Register to server... - + &Restore password... - + &Settings... - + &Exit - + A&ctions - + &Cockatrice - + C&ard Database - + &Manage sets... - + Edit custom &tokens... - + Open custom image folder - + Open custom sets folder - + Add custom sets/cards - + Reload card database - + Tabs - + &Help - + &About Cockatrice - + &Tip of the Day - + Check for Client Updates - + Check for Card Updates... - + Check for Card Updates (Automatic) - + Show Status Bar - + View &Debug Log - + Open Settings Folder - + Show/Hide - + New Version - + Congratulations on updating to Cockatrice %1! Oracle will now launch to update your card database. - + Cockatrice installed - + Congratulations on installing Cockatrice %1! Oracle will now launch to install the initial card database. - + Card database - + Cockatrice is unable to load the card database. Do you want to update your card database now? If unsure or first time user, choose "Yes" - - + + Yes - - + + No - + Open settings - + New sets found - + %n new set(s) found in the card database Set code(s): %1 Do you want to enable it/them? @@ -5311,179 +5440,153 @@ Do you want to enable it/them? - + + Yes, always enable + + + + View sets - + Welcome - + Hi! It seems like you're running this version of Cockatrice for the first time. All the sets in the card database have been enabled. Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog. - - + Information - + A card database update is already running. - + Unable to run the card database updater: - + Card database update running. - + Failed to start. The file might be missing, or permissions might be incorrect. - + The process crashed some time after starting successfully. - + Timed out. The process took too long to respond. The last waitFor...() function timed out. - + An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel. - + An error occurred when attempting to read from the process. For example, the process may not be running. - + Unknown error occurred. - + The card database updater exited with an error: %1 - - This server supports additional features that your client doesn't have. -This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. - -To update your client, go to Help -> Check for Updates. - - - - - - - - + + + + + Load sets/cards - + Selected file cannot be found. - + You can only import XML databases at this time. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. - - - - - Reset Password - - - - - Your password has been reset successfully, you can now log in using the new credentials. - - - - - Failed to reset user account password, please contact the server operator to reset your password. - - - - - Activation request received, please check your email for an activation token. - - ManaBaseConfigDialog - + Mana Base Configuration - + Display type: - + pie - + bar - + combinedBar - + Filter Colors (optional): - + OK - + Cancel @@ -5555,27 +5658,27 @@ Cockatrice will now reload the card database. ManaDevotionConfigDialog - + Display type: - + pie - + bar - + combinedBar - + Filter Colors (optional): @@ -5645,297 +5748,297 @@ Cockatrice will now reload the card database. MessageLogWidget - + from play - + from their graveyard - + from exile - + from their hand - + the top card of %1's library - + the top card of their library - + from the top of %1's library - + from the top of their library - + the bottom card of %1's library - + the bottom card of their library - + from the bottom of %1's library - + from the bottom of their library - + from %1's library - + from their library - + from sideboard - + from the stack - + from custom zone '%1' - + %1 is now keeping the top card %2 revealed. - + %1 is not revealing the top card %2 any longer. - + %1 can now look at top card %2 at any time. - + %1 no longer can look at top card %2 at any time. - + %1 attaches %2 to %3's %4. - + %1 has conceded the game. - + %1 has unconceded the game. - + %1 has restored connection to the game. - + %1 has lost connection to the game. - + %1 points from their %2 to themselves. - + %1 points from their %2 to %3. - + %1 points from %2's %3 to themselves. - + %1 points from %2's %3 to %4. - + %1 points from their %2 to their %3. - + %1 points from their %2 to %3's %4. - + %1 points from %2's %3 to their own %4. - + %1 points from %2's %3 to %4's %5. - + %1 creates a face down token. - + %1 creates token: %2%3. - + %1 has loaded a deck (%2). - + %1 has loaded a deck with %2 sideboard cards (%3). - + %1 destroys %2. - + a card - + %1 gives %2 control over %3. - + %1 puts %2 into play%3 face down. - + %1 puts %2 into play%3. - + %1 puts %2%3 into their graveyard face down. - + %1 puts %2%3 into their graveyard. - + %1 exiles %2%3 face down. - + %1 exiles %2%3. - + %1 moves %2%3 to their hand. - + %1 puts %2%3 into their library. - + %1 puts %2%3 onto the bottom of their library. - + %1 puts %2%3 on top of their library. - + %1 puts %2%3 into their library %4 cards from the top. - + %1 moves %2%3 to sideboard. - + %1 plays %2%3 face down. - + %1 plays %2%3. - + %1 moves %2%3 to custom zone '%4' face down. - + %1 moves %2%3 to custom zone '%4'. - + %1 tries to draw from an empty library - + %1 draws %2 card(s). @@ -5943,12 +6046,12 @@ Cockatrice will now reload the card database. - + %1 is looking at %2. - + %1 is looking at the %4 %3 card(s) %2. top card for singular, top %3 cards for plural @@ -5957,72 +6060,72 @@ Cockatrice will now reload the card database. - + bottom - + top - + %1 turns %2 face-down. - + %1 turns %2 face-up. - + The game has been closed. - + The game has started. - + You are flooding the game. Please wait a couple of seconds. - + %1 has joined the game. - + %1 is now watching the game. - + You have been kicked out of the game. - + %1 has left the game (%2). - + %1 is not watching the game any more (%2). - + %1 is not ready to start the game any more. - + %1 shuffles their deck and draws a new hand of %2 card(s). @@ -6030,28 +6133,28 @@ Cockatrice will now reload the card database. - + %1 shuffles their deck and draws a new hand. - + You are watching a replay of game #%1. - + %1 is ready to start the game. - + cards an unknown amount of cards - + %1 card(s) a card for singular, %1 cards for plural @@ -6060,213 +6163,218 @@ Cockatrice will now reload the card database. - + %1 lends %2 to %3. - + %1 reveals %2 to %3. - + %1 reveals %2. - + %1 randomly reveals %2%3 to %4. - + %1 randomly reveals %2%3. - + %1 peeks at face down card #%2. - + %1 peeks at face down card #%2: %3. - + %1 reveals %2%3 to %4. - + %1 reveals %2%3. - + %1 reversed turn order, now it's %2. - + reversed - + normal - + Heads - + Tails - + %1 flipped a coin. It landed as %2. - + %1 rolls a %2 with a %3-sided die. - + %1 flips %2 coins. There are %3 heads and %4 tails. - + %1 rolls a %2-sided dice %3 times: %4. - + %1's turn. - + %1 sets annotation of %2 to %3. - - %1 places %2 "%3" counter(s) on %4 (now %5). + + %1 places %2 %3%4 counter(s) on %5 (now %6). - - %1 removes %2 "%3" counter(s) from %4 (now %5). + + %1 removes %2 %3%4 counter(s) from %5 (now %6). - + + %1 failed to undo their last draw. + + + + %1 sets counter %2 to %3 (%4%5). - + %1 sets %2 to not untap normally. - + %1 sets %2 to untap normally. - + %1 removes the PT of %2. - + %1 changes the PT of %2 from nothing to %4. - + %1 changes the PT of %2 from %3 to %4. - + %1 has locked their sideboard. - + %1 has unlocked their sideboard. - + %1 taps their permanents. - + %1 untaps their permanents. - + %1 taps %2. - + %1 untaps %2. - + %1 shuffles %2. - + %1 shuffles the bottom %3 cards of %2. - + %1 shuffles the top %3 cards of %2. - + %1 shuffles cards %3 - %4 of %2. - + %1 unattaches %2. - + %1 undoes their last draw. - + %1 undoes their last draw (%2). @@ -6274,110 +6382,115 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Word1 Word2 Word3 - + Add New Message - + Edit Message - + Remove Message - + Add message - - + + Message: - + Edit message - + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + How to use in-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered users - - + + Ignore private messages sent by non-buddy users + + + + + Invert text color - + Enable desktop notifications for private messages - + Enable desktop notification for mentions - + Enable room message history on join - - + + (Color is hexadecimal) - + Separate words with a space, alphanumeric characters only @@ -6385,42 +6498,42 @@ Cockatrice will now reload the card database. MoveMenu - + Move to - + &Top of library in random order - + X cards from the top of library... - + &Bottom of library in random order - + T&able - + &Hand - + &Graveyard - + &Exile @@ -6433,47 +6546,47 @@ Cockatrice will now reload the card database. - + Mana Value - + Color(s) - + Loyalty - + Main Card Type - + Mana Cost - + P/T - + Side - + Layout - + Color Identity @@ -6496,6 +6609,152 @@ Cockatrice will now reload the card database. + + PaletteEditorDialog + + + + Reset + + + + + + Apply + + + + + + Save && Apply + + + + + ▼ Edit Palette + + + + + + ▶ Edit Palette + + + + + Palette Editor — %1 + + + + + <b>Palette Editor</b> &nbsp;·&nbsp; %1 + + + + + This theme ships no default palette files + + + + + Replace current colours with the theme author's defaults + + + + + Switch between the light and dark palette files + + + + + Editing: + + + + + Show or hide the per-role colour grid for manual tweaks + + + + + ↺ Revert to theme default + + + + + Discard unsaved edits and restore the last saved palette + + + + + Preview this palette without saving to disk + + + + + Write palette-%1.toml and reload the theme + + + + + Cannot save: this theme has no directory on disk + + + + + Save failed + + + + + Could not write %1 to: +%2 + + + + + No default found + + + + + No default palette file found for the "%1" scheme. + + + + + PaletteGridWidget + + + Active + + + + + Disabled + + + + + Inactive + + + + + Normal interactive state + + + + + Widget is disabled / not interactive + + + + + Window is in background / unfocused + + + Phase @@ -6562,57 +6821,57 @@ Cockatrice will now reload the card database. PhasesToolbar - + Untap step - + Upkeep step - + Draw step - + First main phase - + Beginning of combat step - + Declare attackers step - + Declare blockers step - + Combat damage step - + End of combat step - + Second main phase - + End of turn step @@ -6620,7 +6879,7 @@ Cockatrice will now reload the card database. PictureLoader - + en code for scryfall's language property, not available for all languages @@ -6629,151 +6888,142 @@ Cockatrice will now reload the card database. PlayerActions - - View top cards of library - - - - - - - - - - - - - Number of cards: (max. %1) - - - - - View bottom cards of library - - - - - Shuffle top cards of library - - - - - Shuffle bottom cards of library - - - - - Draw hand - - - - - 0 and lower are in comparison to current hand size - - - - - Draw cards - - - - - - - + + + + grave - - - - + + + + exile + + + PlayerDialogs - + + View top cards of library + + + + + + + + + + + + + Number of cards: (max. %1) + + + + + View bottom cards of library + + + + + Shuffle top cards of library + + + + + Shuffle bottom cards of library + + + + + Draw hand + + + + + 0 and lower are in comparison to current hand size + + + + + Draw cards + + + + Move top cards to %1 - + Move bottom cards to %1 - + Draw bottom cards - - - C&reate another %1 token - - - - + Create tokens - - + Number: - + Place card X cards from top of library - + Which position should this card be placed: - + (max. %1) - + Change power/toughness - + Change stats to: - + Set annotation - + Please enter the new annotation: - - - Set counters - - PlayerMenu - + Player "%1" - + &Counters @@ -6803,7 +7053,7 @@ This setting means you'll only see the default printing for each card, inst - + Printing Selector @@ -6811,22 +7061,22 @@ This setting means you'll only see the default printing for each card, inst PrintingSelectorCardOverlayWidget - + Preference - + Pin Printing - + Unpin Printing - + Show Related cards @@ -6897,57 +7147,57 @@ This setting means you'll only see the default printing for each card, inst PtMenu - + Power / toughness - + &Increase power - + &Decrease power - + I&ncrease toughness - + D&ecrease toughness - + In&crease power and toughness - + Dec&rease power and toughness - + Increase power and decrease toughness - + Decrease power and increase toughness - + Set &power and toughness... - + Reset p&ower and toughness @@ -6955,37 +7205,37 @@ This setting means you'll only see the default printing for each card, inst QMenuBar - + Services - + Hide %1 - + Hide Others - + Show All - + Preferences... - + Quit %1 - + About %1 @@ -6993,17 +7243,17 @@ This setting means you'll only see the default printing for each card, inst QObject - + Cockatrice card database (*.xml) - + All files (*.*) - + Cockatrice replays (*.cor) @@ -7063,110 +7313,177 @@ Are you sure you would like to disable this feature? QPlatformTheme - + OK - + Save - + Save All - + Open - + &Yes - + Yes to &All - + &No - + N&o to All - + Abort - + Retry - + Ignore - + Close - + Cancel - + Discard - + Help - + Apply - + Reset - + Restore Defaults + + QuickSetupPanel + + + %1% + + + + + <b>Quick Setup</b> + + + + + Generate all palette roles automatically from a single accent colour + + + + + Accent: + + + + + Primary hue. Used directly for highlights and links. +At high intensity it also tints buttons and backgrounds. + + + + + Intensity: + + + + + Subtle + + + + + Full colour + + + + + 0–30 Subtle tint — only highlights and links change hue +30–70 Accented — buttons, tooltips, and borders join in +70–100 Full colour — backgrounds, everything + + + + + 70% + + + + + Generate ↓ + + + + + Derive all palette roles from the accent colour above. +Fine-tune individual colours in the grid afterwards. + + + RemoteDeckList_TreeModel - + Name - + ID - + Upload time @@ -7174,32 +7491,32 @@ Are you sure you would like to disable this feature? RemoteReplayList_TreeModel - + ID - + Name - + Players - + Keep - + Time started - + Duration (sec) @@ -7207,37 +7524,37 @@ Are you sure you would like to disable this feature? RfgMenu - + &Exile - + &View exile - + &Move exile to... - + &Top of library - + &Bottom of library - + &Hand - + &Graveyard @@ -7283,7 +7600,7 @@ Are you sure you would like to disable this feature? SayMenu - + S&ay @@ -7324,27 +7641,27 @@ Are you sure you would like to disable this feature? SetsModel - + Enabled - + Set type - + Set code - + Long name - + Release date @@ -7352,53 +7669,53 @@ Are you sure you would like to disable this feature? ShortcutSettingsPage - - + + Restore all default shortcuts - + Do you really want to restore all default shortcuts? - + Clear all default shortcuts - + Do you really want to clear all shortcuts? - + Section: - + Action: - + Shortcut: - + How to set custom shortcuts - + Clear all shortcuts - + Search by shortcut name @@ -7452,12 +7769,12 @@ Please check your shortcut settings! SideboardMenu - + &Sideboard - + &View sideboard @@ -7465,27 +7782,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -7557,16 +7874,146 @@ Please check your shortcut settings! - + No reply received from the tag update server. - + Invalid reply received from the tag update server. + + StorageSettingsPage + + + + + Success + + + + + Cached card pictures have been reset. + + + + + Downloaded card pictures have been reset. + + + + + Error + + + + + One or more downloaded card pictures could not be cleared. + + + + + In-memory (currently loaded) card pictures have been reset. + + + + + Card Picture Loader Caching Method: + + + + + The network cache is the preferred way of storing images. Downloaded images are stored here until the size of the cache exceeds the configured size. Cockatrice automatically monitors this cache and deletes the least recently seen card images to ensure the cache does not exceed the configured size. + + + + + Writing card images directly to a folder on your hard drive is another way of storing images. This does not change how Cockatrice accesses or downloads images. Cockatrice will NOT automatically monitor and clear this folder, so if you enable this option, it is up to you to ensure sufficient available space. It should also be noted that if a provider outage causes you to download the wrong picture (i.e. wrong printing) you will be stuck with it until you manually delete the file, as opposed to using the network cache, which automatically rotates and thus correct errors after a while. + + + + + This is the in-memory picture cache used by the application at runtime. It determines how much memory (RAM) Cockatrice can use before it has to fetch card images from the hard disk again. Increasing this will allow more card images to be displayed at once but shouldn't be necessary. Clearing this will make Cockatrice reload all images from the network cache or the disk. + + + + + Delete Cached Images + + + + + Delete Saved Images + + + + + Clear In-Memory Images + + + + + Card Picture Loader Cache Method + + + + + Network Cache + + + + + Filesystem + + + + + In-Memory Picture Cache + + + + + Network Cache Size: + + + + + On-disk cache for downloaded pictures + + + + + Redirect Cache TTL: + + + + + How long cached redirects for urls are valid for. + + + + + Picture Cache Size: + + + + + In-memory cache for pictures not currently on screen + + + + + Naming scheme: + + + + + Day(s) + + + TabAccount @@ -7702,117 +8149,117 @@ Please check your shortcut settings! TabArchidekt - - + + Desc. - - + + AND - - + + Require ALL selected colors - - + + Deck name... - - + + Owner... - - + + Packages - - + + Advanced Filters - + Bracket: - - + + Any - - + + Contains card... - - + + Commander... - - + + Tag... - - + + Deck Size - + Cards: - - + + Asc. - + Sort by: - + Filter by: - + Display Settings - - + + Search - - + + Formats @@ -7822,6 +8269,54 @@ Please check your shortcut settings! + + TabCardArtRules + + + Card: + + + + + ProviderId: + + + + + Mode: + + + + + Reason: + + + + + Type a card name... + + + + + Add rule + + + + + Remove rule + + + + + Refresh + + + + + Card Art Rules + + + TabDeckEditor @@ -7870,7 +8365,7 @@ Please check your shortcut settings! - + Deck: %1 @@ -7878,55 +8373,49 @@ Please check your shortcut settings! TabDeckEditorVisual - + Visual Deck: %1 - + &Visual Deck Editor - - + + Card Info - - + + Deck - - - Filters - - - - + &View - + Printing - + Visible - + Floating - + Reset layout @@ -7934,22 +8423,22 @@ Please check your shortcut settings! TabDeckEditorVisualTabWidget - + Visual Deck View - + Visual Database Display - + Deck Analytics - + Sample Hand @@ -7957,133 +8446,133 @@ Please check your shortcut settings! TabDeckStorage - + Local file system - + Server deck storage - - + + Open in deck editor - + Rename deck or folder - + Upload deck - + Download deck - - - + + + New folder - + Delete - + Open decks folder - + Rename local folder - + Rename local file - + New name: - - - - + + + + Error - + Rename failed - - + + Invalid deck file - + Enter deck name - + This decklist does not have a name. Please enter a name: - + Unnamed deck - + Failed to upload deck to server - + Delete local file - + Are you sure you want to delete the selected files? - + Delete remote decks - + Are you sure you want to delete the selected decks? - - + + Name of new folder: @@ -8155,191 +8644,191 @@ Please enter a name: TabGame - - - + + + Replay - - + + Game - - + + Player List - - + + Card Info - - + + Messages - - + + Replay Timeline - + &Phases - + &Game - + Next &phase - + Next phase with &action - + Next &turn - + Reverse turn order - + &Remove all local arrows - + Rotate View Cl&ockwise - + Rotate View Co&unterclockwise - + Game &information - + Un&concede - - - + + + &Concede - + &Leave game - + C&lose replay - + &Focus Chat - + &Say: - + Selected cards - + &View - + Visible - + Floating - + Reset layout - + Concede - + Are you sure you want to concede this game? - + Unconcede - + You have already conceded. Do you want to return to this game? - + Leave game - + Are you sure you want to leave this game? - + A player has joined game #%1 - + %1 has joined the game - + You have been kicked out of the game. @@ -8398,114 +8887,114 @@ Please enter a name: - + Username: - + IP Address: - + Game Name: - + GameID: - + Message: - + Main Room - + Game Room - + Private Chat - + Past X Days: - + Today - + Last Hour - + Maximum Results: - + At least one filter is required. The more information you put in, the more specific your results will be. - + Get User Logs - + Clear Filters - + Filters - + Log Locations - + Date Range - + Maximum Results - - + + Message History - + Failed to collect message history information. - + There are no messages for the selected filters. @@ -8523,27 +9012,27 @@ The more information you put in, the more specific your results will be. - + %1 - Private chat - + This user is ignoring you, they cannot see your messages in main chat and you cannot join their games. - + Private message from - + %1 has left the server. - + %1 has joined the server. @@ -8551,180 +9040,180 @@ The more information you put in, the more specific your results will be. TabReplays - + Local file system - + Server replay storage - - + + Watch replay - + Rename - - + + New folder - - + + Delete - + Open replays folder - + Download replay - + Toggle expiration lock - + Get replay share code - - + + Look up replay by share code - + Rename local folder - + Rename local file - + New name: - + Error - + Rename failed - + Name of new folder: - + Delete local file - + Are you sure you want to delete the selected files? - + Are you sure you want to delete the selected replays? - + Failed to get code - - + + Either this server does not support replay sharing, or does not permit replay sharing for you. - - - + + + Failed - + Could not get replay code - + Replay Share Code - + Others can use this code to add the replay to their list of remote replays: %1 - + Copy to clipboard - + Replay share code - + Replay code found - + Replay was added, or you already had access to it. - + Replay code not found - + Failed to submit code - + Unexpected error - + Delete remote replay @@ -8737,47 +9226,62 @@ The more information you put in, the more specific your results will be. TabRoom - + + Friends + + + + + Online + + + + + Ignored + + + + &Say: - + Chat - + &Room - + &Leave room - + &Clear chat - + Chat Settings... - + mentioned you. - + Click to view - + You are flooding the chat. Please wait a couple of seconds. @@ -8790,30 +9294,30 @@ The more information you put in, the more specific your results will be. - - - - + + + + Error - + Failed to join the server room: it doesn't exist on the server. - + The server thinks you are in the server room but your client is unable to display it. Try restarting your client. - + You do not have the required permission to join this server room. - + Failed to join the server room due to an unknown error: %1. @@ -8821,97 +9325,97 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Deck Editor - + Visual Deck Editor - + EDHRec - + Archidekt - + Home - + &Visual Deck Storage - + Visual Database Display - + Server - + Account - + Deck Storage - + Game Replays - + Administration - + Logs - + Are you sure? - + There are still open games. Are you sure you want to quit? - + Click to view - + Your buddy %1 has signed on! - + Unknown Event - + The server has sent you a message that your client does not understand. This message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. @@ -8919,38 +9423,38 @@ To update your client, go to Help -> Check for Updates. - + Idle Timeout - + You are about to be logged out due to inactivity. - + Promotion - + You have been promoted. Please log out and back in for changes to take effect. - + Warned - + You have received a warning due to %1. Please refrain from engaging in this activity or further actions may be taken against you. If you have any questions, please private message a moderator. - + You have received the following message from the server. (custom messages like these could be untranslated) @@ -8959,12 +9463,12 @@ Please refrain from engaging in this activity or further actions may be taken ag TabVisualDatabaseDisplay - + Database Display - + Visual Database Display @@ -9001,42 +9505,42 @@ Please refrain from engaging in this activity or further actions may be taken ag TranslateCounterName - + Life - + White - + Blue - + Black - + Red - + Green - + Colorless - + Other @@ -9044,11 +9548,69 @@ Please refrain from engaging in this activity or further actions may be taken ag UpdateDownloader - + Could not open the file for reading. + + UserCardArtSettingsDialog + + + Card Art Settings + + + + + Type a card name... + + + + + Card name: + + + + + Card ProviderId: + + + + + Left margin (%): + + + + + Right margin (%): + + + + + Vertical offset: + + + + + Zoom: + + + + + Parameters + + + + + Preview + + + + + Remove Banner Card + + + UserContextMenu @@ -9166,7 +9728,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Ban History @@ -9181,77 +9743,87 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Failed to collect ban information. - - - + + + Warning History - + Warning Time;Moderator;User Name;Reason - + User has never been warned. - + Failed to collect warning information. - + Failed to get admin notes. - - + + Success - + Successfully promoted user. - + Successfully demoted user. - - - + + Kick Player + + + + + Are you sure you want to kick this player from the game? + + + + + + Failed - + Failed to promote user. - + Failed to demote user. - + Copy hash to clipboard - + Remove this user's messages @@ -9259,109 +9831,116 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInfoBox - + Location: - + Account Age: - + Edit - + Change password - + Change avatar - + Administrator - + Moderator - + Registered user - - + + Unregistered user - + Judge - + Unknown - + The entered password does not match your account. - - - + + + + Information - + User information updated. - - - - - - - - - - + + + + + + + + + + + Error - + User Information - + Real Name: - + User Level: + + + Edit Banner Card + + - + %n Year(s), amount of years (only shown if more than 0) @@ -9370,7 +9949,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + %10%n Day(s) %20 amount of years (if more than 0), amount of days, date in local short format @@ -9379,212 +9958,433 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Enter Password - + Password verification is required in order to change your email address - - - + + + An error occurred while trying to update your user information. - - This server does not permit you to update your user informations. + + The selected card is blacklisted on this server or another error occurred. - - Password changed. + + Banner card removed. - - This server does not permit you to change your password. - - - - - The new password is too short. - - - - - The old password is incorrect. - - - - - Avatar updated. - - - - - This server does not permit you to update your avatar. + + Banner card updated. - An error occured while trying to updater your avatar. + This server does not permit you to update your user informations. + + + + + Password changed. + + + + + This server does not permit you to change your password. + + + + + The new password is too short. + + + + + The old password is incorrect. + + + + + Avatar updated. + + + + + This server does not permit you to update your avatar. + + + + + An error occured while trying to update your avatar. + + + + + This server does not permit you to update your user informations. + An error occured while trying to updater your avatar. + + + + + UserInfoPopup + + + + Games + + + + + Chat + + + + + Open private chat + + + + + Profile + + + + + View user profile + + + + + Show this user's games + + + + + − Buddy + + + + + Remove from buddy list + + + + + + Buddy + + + + + Add to buddy list + + + + + − Ignore + + + + + Remove from ignore list + + + + + + Ignore + + + + + Add to ignore list + + + + + Ban + + + + + Ban from server + + + + + Warn + + + + + Warn user + + + + + Ban log + + + + + View ban history + + + + + Warn log + + + + + View warning history + + + + + Notes + + + + + View admin notes + + + + + − Mod + + + + + Demote from moderator + + + + + + Mod + + + + + Promote to moderator + + + + + − Judge + + + + + Demote from judge + + + + + + Judge + + + + + Promote to judge + + + + + Join game + + + + + Spectate + + + + + + Loading games… + + + + + Could not load games. + + + + + No active games. UserInterfaceSettingsPage - + General interface settings - + &Double-click cards to play them (instead of single-click) - + &Clicking plays all selected cards (instead of just the clicked card) - + &Play all nonlands onto the stack (not the battlefield) by default - + Do not delete &arrows inside of subphases - + Close card view window when last card is removed - + Auto focus search bar when card view window is opened - + Annotate card text on tokens - - Show selection counter during drag selection + + Show selection count during drag selection - - Show total selection counter + + Show total selection count - + + Show subtype breakdown in selection tally + + + + Use tear-off menus, allowing right click menus to persist on screen - + + Keep game chat focused when clicking in game (Note: disables card view search bar) + + + + Notifications settings - + Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating - + Notify in the taskbar when users in your buddy list connect - + Animation settings - + &Tap/untap animation - + Deck editor/storage settings - + Open deck in new tab by default - + Use visual deck storage in game lobby - + Use selection animation for Visual Deck Storage - + When adding a tag in the visual deck storage to a .txt deck: - + do nothing - + ask to convert to .cod - + always convert to .cod - + Default deck editor type - + Classic Deck Editor - + Visual Deck Editor - + Replay settings - + Buffer time for backwards skip via shortcut: @@ -9592,22 +10392,22 @@ Please refrain from engaging in this activity or further actions may be taken ag UserListWidget - + Users connected to server: %1 - + Users in this room: %1 - + Buddies online: %1 / %2 - + Ignored users online: %1 / %2 @@ -9615,35 +10415,45 @@ Please refrain from engaging in this activity or further actions may be taken ag UtilityMenu - + Increment all card counters - + &Untap all permanents - + R&oll die... - + + Flip coin + + + + &Create token... - + C&reate another token - + Cr&eate predefined token + + + C&reate another %1 token + + VisualDatabaseDisplayColorFilterWidget @@ -9695,72 +10505,72 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayFilterToolbarWidget - + Sort by - + Filter by - + Save and load filters - + Filter by exact card name - + Filter by card main-type - + Filter by card sub-type - + Filter by set - + Filter by format legality - + Save/Load - + Name - + Main Type - + Sub Type - + Sets - + Formats @@ -9768,22 +10578,22 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayFormatLegalityFilterWidget - + Show formats with at least: - + cards - + Do not display formats with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) @@ -9801,32 +10611,32 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayMainTypeFilterWidget - + Show main types with at least: - + cards - + Do not display card main-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9834,27 +10644,27 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayNameFilterWidget - + Filter by name... (Exact match) - + Load from Deck - + Apply all card names in currently loaded deck as exact match name filters - + Load from Clipboard - + Apply all card names in clipboard as exact match name filters @@ -9862,7 +10672,7 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayRecentSetFilterSettingsWidget - + Filter to most recent sets @@ -9870,19 +10680,19 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySetFilterWidget - + Search sets... - - + + Mode: Exact Match - - + + Mode: Includes @@ -9890,37 +10700,37 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySubTypeFilterWidget - + Search subtypes... - + Show sub types with at least: - + cards - + Do not display card sub-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9928,23 +10738,23 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayWidget - + Search by card name (or search expressions) - + Visual - + Loading database ... - + Clear all filters @@ -9983,17 +10793,17 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Toggle Layout: Overlap - + Change how cards are displayed within zones (i.e. overlapped or fully visible.) - + Toggle Layout: Flat @@ -10022,17 +10832,17 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckEditorWidget - + Type a card name here for suggestions from the database... - + Quick search and add card - + Search for closest match in the database (with auto-suggestions) and add preferred printing to the deck on pressing enter @@ -10150,43 +10960,43 @@ Please refrain from engaging in this activity or further actions may be taken ag WarningDialog - + Which warning would you like to send? - + Redact all messages from this user in all rooms - + &OK - + &Cancel - + Warn user for misconduct - - + + Error - + User name to send a warning to can not be blank, please specify a user to warn. - + Warning to use can not be blank, please select a valid warning to send. @@ -10194,133 +11004,133 @@ Please refrain from engaging in this activity or further actions may be taken ag WndSets - + Move selected set to the top - + Move selected set up - + Move selected set down - + Move selected set to the bottom - + Search by set name, code, or type - + Default order - + Restore original art priority order - + Enable all sets - + Disable all sets - + Enable selected set(s) - + Disable selected set(s) - + Deck Editor - + Use CTRL+A to select all sets in the view. - + Only cards in enabled sets will appear in the card list of the deck editor. - + Image priority is decided in the following order: - + first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom) %1 is a link to the wiki - + Include cards rebalanced for Alchemy [requires restart] - + Card Art - + How to use custom card art - + Hints - + Note - + Sorting by column allows you to find a set while not changing set priority. - + To enable ordering again, click the column header until this message disappears. - + Use the current sorting as the set priority instead - + Sorts the set priority using the same column - + Manage sets @@ -10328,72 +11138,72 @@ Please refrain from engaging in this activity or further actions may be taken ag ZoneViewWidget - + Search by card name (or search expressions) - + Ungrouped - + Group by Type - + Group by Mana Value - + Group by Color - + Unsorted - + Sort by Name - + Sort by Type - + Sort by Mana Cost - + Sort by Colors - + Sort by P/T - + Sort by Set - + shuffle when closing - + pile view @@ -10401,7 +11211,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English @@ -10409,12 +11219,12 @@ Please refrain from engaging in this activity or further actions may be taken ag main - + Connect on startup - + Debug to file @@ -10428,7 +11238,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Deck Editor @@ -10509,7 +11319,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Replays @@ -10624,129 +11434,129 @@ Please refrain from engaging in this activity or further actions may be taken ag - - + + Load Deck from Clipboard... - + Edit Deck in Clipboard, Annotated - + Edit Deck in Clipboard - + New Deck - + Open Custom Pictures Folder - + Print Deck... - + Delete Card - - + + Reset Layout - + Save Deck - + Save Deck as... - + Save Deck to Clipboard, Annotated - + Save Deck to Clipboard, Annotated (No Set Info) - + Save Deck to Clipboard - + Save Deck to Clipboard (No Set Info) - + Load Local Deck... - + Load Remote Deck... - + Set Ready to Start - + Toggle Sideboard Lock - + Add Green Counter - + Remove Green Counter - + Set Green Counters... - + Add Red Counter - + Remove Red Counter - + Set Red Counters... - + Add Life Counter @@ -10756,724 +11566,744 @@ Please refrain from engaging in this activity or further actions may be taken ag - + + Load deck from online service... + + + + + Load from website... + + + + Unload Deck - + Force Start - + Add Card Counter (F) - + Remove Card Counter (F) - + Set Card Counters (F)... - + Add Card Counter (E) - + Remove Card Counter (E) - + Set Card Counters (E)... - + Add Card Counter(D) - + Remove Card Counter (D) - + Set Card Counters (D)... - + Add Card Counter (C) - + Remove Card Counter (C) - + Set Card Counters (C)... - + Add Card Counter (B) - + Remove Card Counter (B) - + Set Card Counters (B)... - + Add Card Counter (A) - + Remove Card Counter (A) - + Set Card Counters (A)... - + Remove Life Counter - + Set Life Counters... - + Add White Counter - + Remove White Counter - + Set White Counters... - + Add Blue Counter - + Remove Blue Counter - + Set Blue Counters... - + Add Black Counter - + Remove Black Counter - + Set Black Counters... - + Add Colorless Counter - + Remove Colorless Counter - + Set Colorless Counters... - + Add Other Counter - + Remove Other Counter - + Set Other Counters... - + Increment all card counters - + Add Power (+1/+0) - + Remove Power (-1/-0) - + Move Toughness to Power (+1/-1) - + Add Toughness (+0/+1) - + Remove Toughness (-0/-1) - + Move Power to Toughness (-1/+1) - + Add Power and Toughness (+1/+1) - + Remove Power and Toughness (-1/-1) - + Set Power and Toughness... - + Reset Power and Toughness - + Untap - + Upkeep - + Draw - + First Main Phase - + Start Combat - + Attack - + Block - + Damage - + End Combat - + Second Main Phase - + End - + Next Phase - + Next Phase Action - + Next Turn - + Hide Card in Reveal Window - + Tap / Untap Card - + Untap All - + Toggle Skip Untapping Toggle Untap - + Turn Card Over - + Peek Card - + Play Card - + Play Card, Face Down - + Attach Card... - + Unattach Card - + Clone Card - + Create Token... - + Create All Related Tokens - + Create Another Token - + Set Annotation... - + + Reduce Life by Power + + + + Select All Cards in Zone - + Select All Cards in Row - + Select All Cards in Column - + Reveal Selected Cards to All Players - - + + Bottom of Library - - - - + + + + Exile - - - - + + + + Graveyard - - + + Hand - - + + Top of Library - - + + Battlefield, Face Down - + Battlefield - + Library - + Sideboard - + Top Cards of Library - + Bottom Cards of Library - + Close Recent View - - + + Stack - - + + Graveyard (Multiple) - - + + Graveyard (Multiple), Face Down - - + + Exile (Multiple) - - + + Exile (Multiple), Face Down - + Stack Until Found - + Draw Bottom Card - + Draw Multiple Cards from Bottom... - + Draw Arrow... - + Remove Local Arrows - + Leave Game - + Concede - + Roll Dice... - + + Flip Coin + + + + Shuffle Library - + Shuffle Top Cards of Library - + Shuffle Bottom Cards of Library - + Mulligan - + Mulligan (Same hand size) - + Mulligan (Hand size - 1) - + Draw a Card - + Draw Multiple Cards... - + Undo Draw - + Always Reveal Top Card - + Always Look At Top Card - + Sort Hand by Name - + Sort Hand by Type - + Sort Hand by Mana Value - + Reveal Hand to All Players - + Reveal Random Card to All Players - + Rotate View Clockwise - + Rotate View Counterclockwise - + Unfocus Text Box - + Focus Chat - + Clear Chat - + Refresh - + Skip Forward - + Skip Backward - + Skip Forward by a lot - + Skip Backward by a lot - + Play/Pause - + Toggle Fast Forward - + Home - + Visual Deck Storage - + Deck Storage - + Server - + Account - + Administration - + Logs diff --git a/oracle/oracle_en@source.ts b/oracle/oracle_en@source.ts index 943b44a97..b3263f861 100644 --- a/oracle/oracle_en@source.ts +++ b/oracle/oracle_en@source.ts @@ -278,7 +278,7 @@ OracleImporter - + Dummy set containing tokens @@ -286,7 +286,7 @@ OracleWizard - + Oracle Importer