From 73a90bdf38f47e9cb2bbf6a2ce3ff652a1b37476 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sat, 20 Dec 2025 17:46:13 +0100 Subject: [PATCH 01/56] Doxygen: Add bullet points to subpages lists & link webpage on welcome page (#6377) * add bullet points to subpages * Link to webpage from welcome page * Add bullet points to subpages * grouping * Add TODO note to card database documentation Added a TODO note for future updates. * Fix GH alerts commands to be doxygen compatible --- README.md | 8 ++++---- .../card_database_schema_and_parsing.md | 4 +++- doc/doxygen/extra-pages/index.md | 4 +++- .../deck_management/editing_decks.md | 7 +++---- .../extra-pages/user_documentation/index.md | 18 +++++++----------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 47fb3a159..3d01e9f3d 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p - [Official Discord](https://discord.gg/3Z9yzmA) - [reddit r/Cockatrice](https://reddit.com/r/cockatrice) ->[!IMPORTANT] ->For support regarding specific servers, please contact that server's admin/mods and use their dedicated communication channels rather than contacting the team building the software. +> [!IMPORTANT] +> For support regarding specific servers, please contact that server's admin/mods and use their dedicated communication channels rather than contacting the team building the software. # Contribute @@ -131,8 +131,8 @@ You can then make package ``` ->[!NOTE] ->Detailed compiling instructions can be found in the Cockatrice wiki at [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice) +> [!NOTE] +> Detailed compiling instructions can be found in the Cockatrice wiki at [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice)
diff --git a/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md b/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md index 6c5e3512b..1de6fda4f 100644 --- a/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md +++ b/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md @@ -1 +1,3 @@ -@page card_database_schema_and_parsing Card Database Schema and Parsing \ No newline at end of file +@page card_database_schema_and_parsing Card Database Schema and Parsing + +TODO diff --git a/doc/doxygen/extra-pages/index.md b/doc/doxygen/extra-pages/index.md index 66113117f..c370a6fc2 100644 --- a/doc/doxygen/extra-pages/index.md +++ b/doc/doxygen/extra-pages/index.md @@ -5,4 +5,6 @@ This is the **main landing page** of the Cockatrice documentation. - Go to the @subpage user_reference page -- Or check out the @subpage developer_reference \ No newline at end of file +- Review the @subpage developer_reference + +Or check out the [Cockatrice Webpage](https://cockatrice.github.io/). diff --git a/doc/doxygen/extra-pages/user_documentation/deck_management/editing_decks.md b/doc/doxygen/extra-pages/user_documentation/deck_management/editing_decks.md index 030811306..37b613461 100644 --- a/doc/doxygen/extra-pages/user_documentation/deck_management/editing_decks.md +++ b/doc/doxygen/extra-pages/user_documentation/deck_management/editing_decks.md @@ -1,7 +1,6 @@ @page editing_decks Editing Decks -@subpage editing_decks_classic + - @subpage editing_decks_classic + - @subpage editing_decks_visual -@subpage editing_decks_visual - -@subpage editing_decks_printings \ No newline at end of file + - @subpage editing_decks_printings diff --git a/doc/doxygen/extra-pages/user_documentation/index.md b/doc/doxygen/extra-pages/user_documentation/index.md index 8a5c88c37..33104991a 100644 --- a/doc/doxygen/extra-pages/user_documentation/index.md +++ b/doc/doxygen/extra-pages/user_documentation/index.md @@ -1,13 +1,9 @@ @page user_reference User Reference -@subpage search_syntax_help - -@subpage deck_search_syntax_help - -@subpage creating_decks - -@subpage importing_decks - -@subpage editing_decks - -@subpage exporting_decks \ No newline at end of file +- @subpage search_syntax_help +- @subpage deck_search_syntax_help + +- @subpage creating_decks +- @subpage importing_decks +- @subpage editing_decks +- @subpage exporting_decks From a0f977e80c9e3513fb288899bb5c18b9cc02ea75 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:19:33 -0800 Subject: [PATCH 02/56] [DeckList] refactor: pass DeckList by const ref (#6437) * [DeckList] refactor: pass DeckList by const ref * Change getDeckList to return a const ref --- .../interfaces/deck_stats_interface.cpp | 14 +++---- .../network/interfaces/deck_stats_interface.h | 6 +-- .../interfaces/tapped_out_interface.cpp | 14 +++---- .../network/interfaces/tapped_out_interface.h | 6 +-- .../src/interface/deck_loader/deck_loader.cpp | 32 +++++++-------- .../src/interface/deck_loader/deck_loader.h | 12 +++--- .../deck_editor_deck_dock_widget.cpp | 4 +- .../deck_editor_deck_dock_widget.h | 2 +- .../dialogs/dlg_load_deck_from_clipboard.cpp | 4 +- .../widgets/tabs/abstract_tab_deck_editor.cpp | 41 ++++++++----------- .../widgets/tabs/abstract_tab_deck_editor.h | 2 +- .../deck_preview/deck_preview_widget.cpp | 8 ++-- 12 files changed, 70 insertions(+), 75 deletions(-) diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp index 5e9b874d0..0298daa6b 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.cpp @@ -43,23 +43,23 @@ void DeckStatsInterface::queryFinished(QNetworkReply *reply) deleteLater(); } -void DeckStatsInterface::getAnalyzeRequestData(DeckList *deck, QByteArray *data) +void DeckStatsInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data) { DeckList deckWithoutTokens; - copyDeckWithoutTokens(*deck, deckWithoutTokens); + copyDeckWithoutTokens(deck, deckWithoutTokens); QUrl params; QUrlQuery urlQuery; urlQuery.addQueryItem("deck", deckWithoutTokens.writeToString_Plain()); - urlQuery.addQueryItem("decktitle", deck->getName()); + urlQuery.addQueryItem("decktitle", deck.getName()); params.setQuery(urlQuery); - data->append(params.query(QUrl::EncodeReserved).toUtf8()); + data.append(params.query(QUrl::EncodeReserved).toUtf8()); } -void DeckStatsInterface::analyzeDeck(DeckList *deck) +void DeckStatsInterface::analyzeDeck(const DeckList &deck) { QByteArray data; - getAnalyzeRequestData(deck, &data); + getAnalyzeRequestData(deck, data); QNetworkRequest request(QUrl("https://deckstats.net/index.php")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); @@ -68,7 +68,7 @@ void DeckStatsInterface::analyzeDeck(DeckList *deck) manager->post(request, data); } -void DeckStatsInterface::copyDeckWithoutTokens(DeckList &source, DeckList &destination) +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()); diff --git a/cockatrice/src/client/network/interfaces/deck_stats_interface.h b/cockatrice/src/client/network/interfaces/deck_stats_interface.h index fd1691a8b..7dc841027 100644 --- a/cockatrice/src/client/network/interfaces/deck_stats_interface.h +++ b/cockatrice/src/client/network/interfaces/deck_stats_interface.h @@ -28,15 +28,15 @@ private: * closest non-token card instead. So we construct a new deck which has no * tokens. */ - void copyDeckWithoutTokens(DeckList &source, DeckList &destination); + void copyDeckWithoutTokens(const DeckList &source, DeckList &destination); private slots: void queryFinished(QNetworkReply *reply); - void getAnalyzeRequestData(DeckList *deck, QByteArray *data); + void getAnalyzeRequestData(const DeckList &deck, QByteArray &data); public: explicit DeckStatsInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr); - void analyzeDeck(DeckList *deck); + void analyzeDeck(const DeckList &deck); }; #endif diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp index 137c7728c..a30a7f531 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.cpp @@ -67,24 +67,24 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply) deleteLater(); } -void TappedOutInterface::getAnalyzeRequestData(DeckList *deck, QByteArray *data) +void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data) { DeckList mainboard, sideboard; - copyDeckSplitMainAndSide(*deck, mainboard, sideboard); + copyDeckSplitMainAndSide(deck, mainboard, sideboard); QUrl params; QUrlQuery urlQuery; - urlQuery.addQueryItem("name", deck->getName()); + urlQuery.addQueryItem("name", deck.getName()); urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true)); urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true)); params.setQuery(urlQuery); - data->append(params.query(QUrl::EncodeReserved).toUtf8()); + data.append(params.query(QUrl::EncodeReserved).toUtf8()); } -void TappedOutInterface::analyzeDeck(DeckList *deck) +void TappedOutInterface::analyzeDeck(const DeckList &deck) { QByteArray data; - getAnalyzeRequestData(deck, &data); + getAnalyzeRequestData(deck, data); QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); @@ -93,7 +93,7 @@ void TappedOutInterface::analyzeDeck(DeckList *deck) manager->post(request, data); } -void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard) +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()); diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.h b/cockatrice/src/client/network/interfaces/tapped_out_interface.h index 056d7fd9a..0ea9c8358 100644 --- a/cockatrice/src/client/network/interfaces/tapped_out_interface.h +++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.h @@ -30,14 +30,14 @@ private: QNetworkAccessManager *manager; CardDatabase &cardDatabase; - void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard); + void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard); private slots: void queryFinished(QNetworkReply *reply); - void getAnalyzeRequestData(DeckList *deck, QByteArray *data); + void getAnalyzeRequestData(const DeckList &deck, QByteArray &data); public: explicit TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr); - void analyzeDeck(DeckList *deck); + void analyzeDeck(const DeckList &deck); }; #endif diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index e567e45fd..45c3ec1ba 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -287,14 +287,14 @@ static QString toDecklistExportString(const QList &car * @param deckList The decklist to export * @param website The website we're sending the deck to */ -QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website) +QString DeckLoader::exportDeckToDecklist(const DeckList &deckList, DecklistWebsite website) { // Add the base url QString deckString = "https://" + getDomainForWebsite(website) + "/?"; // export all cards in zone - QString mainBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_MAIN})); - QString sideBoardCards = toDecklistExportString(deckList->getCardNodes({DECK_ZONE_SIDE})); + QString mainBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_MAIN})); + QString sideBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_SIDE})); // Remove the extra return at the end of the last cards mainBoardCards.chop(3); @@ -310,7 +310,7 @@ QString DeckLoader::exportDeckToDecklist(const DeckList *deckList, DecklistWebsi return deckString; } -void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, bool addSetNameAndNumber) +void DeckLoader::saveToClipboard(const DeckList &deckList, bool addComments, bool addSetNameAndNumber) { QString buffer; QTextStream stream(&buffer); @@ -320,7 +320,7 @@ void DeckLoader::saveToClipboard(const DeckList *deckList, bool addComments, boo } bool DeckLoader::saveToStream_Plain(QTextStream &out, - const DeckList *deckList, + const DeckList &deckList, bool addComments, bool addSetNameAndNumber) { @@ -329,7 +329,7 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out, } // loop zones - for (auto zoneNode : deckList->getZoneNodes()) { + for (auto zoneNode : deckList.getZoneNodes()) { saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber); // end of zone @@ -339,14 +339,14 @@ bool DeckLoader::saveToStream_Plain(QTextStream &out, return true; } -void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList) +void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList) { - if (!deckList->getName().isEmpty()) { - out << "// " << deckList->getName() << "\n\n"; + if (!deckList.getName().isEmpty()) { + out << "// " << deckList.getName() << "\n\n"; } - if (!deckList->getComments().isEmpty()) { - QStringList commentRows = deckList->getComments().split(QRegularExpression("\n|\r\n|\r")); + if (!deckList.getComments().isEmpty()) { + QStringList commentRows = deckList.getComments().split(QRegularExpression("\n|\r\n|\r")); for (const QString &row : commentRows) { out << "// " << row << "\n"; } @@ -434,7 +434,7 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, } } -bool DeckLoader::convertToCockatriceFormat(QString fileName) +bool DeckLoader::convertToCockatriceFormat(const QString &fileName) { // Change the file extension to .cod QFileInfo fileInfo(fileName); @@ -543,7 +543,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode cursor->movePosition(QTextCursor::End); } -void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList) +void DeckLoader::printDeckList(QPrinter *printer, const DeckList &deckList) { QTextDocument doc; @@ -559,14 +559,14 @@ void DeckLoader::printDeckList(QPrinter *printer, const DeckList *deckList) headerCharFormat.setFontWeight(QFont::Bold); cursor.insertBlock(headerBlockFormat, headerCharFormat); - cursor.insertText(deckList->getName()); + cursor.insertText(deckList.getName()); headerCharFormat.setFontPointSize(12); cursor.insertBlock(headerBlockFormat, headerCharFormat); - cursor.insertText(deckList->getComments()); + cursor.insertText(deckList.getComments()); cursor.insertBlock(headerBlockFormat, headerCharFormat); - for (auto zoneNode : deckList->getZoneNodes()) { + for (auto zoneNode : deckList.getZoneNodes()) { cursor.insertHtml("
"); cursor.insertBlock(headerBlockFormat, headerCharFormat); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.h b/cockatrice/src/interface/deck_loader/deck_loader.h index 4ceb9006b..ec5636995 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.h +++ b/cockatrice/src/interface/deck_loader/deck_loader.h @@ -59,11 +59,11 @@ public: bool saveToFile(const QString &fileName, DeckFileFormat::Format fmt); bool updateLastLoadedTimestamp(const QString &fileName, DeckFileFormat::Format fmt); - static QString exportDeckToDecklist(const DeckList *deckList, DecklistWebsite website); + static QString exportDeckToDecklist(const DeckList &deckList, DecklistWebsite website); - static void saveToClipboard(const DeckList *deckList, bool addComments = true, bool addSetNameAndNumber = true); + static void saveToClipboard(const DeckList &deckList, bool addComments = true, bool addSetNameAndNumber = true); static bool saveToStream_Plain(QTextStream &out, - const DeckList *deckList, + const DeckList &deckList, bool addComments = true, bool addSetNameAndNumber = true); @@ -72,9 +72,9 @@ public: * @param printer The printer to render the decklist to. * @param deckList */ - static void printDeckList(QPrinter *printer, const DeckList *deckList); + static void printDeckList(QPrinter *printer, const DeckList &deckList); - bool convertToCockatriceFormat(QString fileName); + bool convertToCockatriceFormat(const QString &fileName); LoadedDeck &getDeck() { @@ -91,7 +91,7 @@ public: private: static void printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node); - static void saveToStream_DeckHeader(QTextStream &out, const DeckList *deckList); + static void saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList); static void saveToStream_DeckZone(QTextStream &out, const InnerDecklistNode *zoneNode, 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 a9e19195d..facf0c98b 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 @@ -527,9 +527,9 @@ DeckLoader *DeckEditorDeckDockWidget::getDeckLoader() return deckLoader; } -DeckList *DeckEditorDeckDockWidget::getDeckList() +const DeckList &DeckEditorDeckDockWidget::getDeckList() const { - return deckModel->getDeckList(); + return *deckModel->getDeckList(); } /** diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index da175d417..60e1d36d2 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -61,7 +61,7 @@ public slots: void syncDisplayWidgetsToModel(); void sortDeckModelToDeckView(); DeckLoader *getDeckLoader(); - DeckList *getDeckList(); + const DeckList &getDeckList() const; void actIncrement(); bool swapCard(const QModelIndex &idx); void actDecrementCard(const ExactCard &card, QString zoneName); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index 9024b3bdc..b72130c81 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -150,7 +150,7 @@ DlgEditDeckInClipboard::DlgEditDeckInClipboard(const DeckList &_deckList, bool _ * @param addComments Whether to add annotations * @return A QString */ -static QString deckListToString(const DeckList *deckList, bool addComments) +static QString deckListToString(const DeckList &deckList, bool addComments) { QString buffer; QTextStream stream(&buffer); @@ -160,7 +160,7 @@ static QString deckListToString(const DeckList *deckList, bool addComments) void DlgEditDeckInClipboard::actRefresh() { - setText(deckListToString(&deckList, annotated)); + setText(deckListToString(deckList, annotated)); } void DlgEditDeckInClipboard::actOK() 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 cdfa5b529..398208993 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -124,7 +124,7 @@ void AbstractTabDeckEditor::onDeckModified() */ void AbstractTabDeckEditor::onDeckHistorySaveRequested(const QString &modificationReason) { - historyManager->save(deckDockWidget->getDeckList()->createMemento(modificationReason)); + historyManager->save(deckDockWidget->getDeckList().createMemento(modificationReason)); } /** @@ -231,7 +231,7 @@ void AbstractTabDeckEditor::openDeck(const LoadedDeck &deck) void AbstractTabDeckEditor::setDeck(const LoadedDeck &_deck) { deckDockWidget->setDeck(_deck); - CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(getDeckList()->getCardRefList())); + CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(getDeckList().getCardRefList())); setModified(false); aDeckDockVisible->setChecked(true); @@ -245,7 +245,7 @@ DeckLoader *AbstractTabDeckEditor::getDeckLoader() const } /** @brief Returns the currently loaded deck list. */ -DeckList *AbstractTabDeckEditor::getDeckList() const +const DeckList &AbstractTabDeckEditor::getDeckList() const { return deckDockWidget->getDeckList(); } @@ -446,7 +446,7 @@ bool AbstractTabDeckEditor::actSaveDeckAs() dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setDefaultSuffix("cod"); dialog.setNameFilters(DeckLoader::FILE_NAME_FILTERS); - dialog.selectFile(getDeckList()->getName().trimmed()); + dialog.selectFile(getDeckList().getName().trimmed()); if (!dialog.exec()) return false; @@ -591,26 +591,21 @@ void AbstractTabDeckEditor::actLoadDeckFromWebsite() */ void AbstractTabDeckEditor::exportToDecklistWebsite(DeckLoader::DecklistWebsite website) { - if (DeckList *deckList = getDeckList()) { - QString decklistUrlString = DeckLoader::exportDeckToDecklist(deckList, website); - // Check to make sure the string isn't empty. - if (decklistUrlString.isEmpty()) { - // Show an error if the deck is empty, and return. - QMessageBox::critical(this, tr("Error"), tr("There are no cards in your deck to be exported")); - return; - } - - // Encode the string recieved from the model to make sure all characters are encoded. - // first we put it into a qurl object - QUrl decklistUrl = QUrl(decklistUrlString); - // we get the correctly encoded url. - decklistUrlString = decklistUrl.toEncoded(); - // We open the url in the user's default browser - QDesktopServices::openUrl(decklistUrlString); - } else { - // if there's no deck loader object, return an error - QMessageBox::critical(this, tr("Error"), tr("No deck was selected to be exported.")); + QString decklistUrlString = DeckLoader::exportDeckToDecklist(getDeckList(), website); + // Check to make sure the string isn't empty. + if (decklistUrlString.isEmpty()) { + // Show an error if the deck is empty, and return. + QMessageBox::critical(this, tr("Error"), tr("There are no cards in your deck to be exported")); + return; } + + // Encode the string recieved from the model to make sure all characters are encoded. + // first we put it into a qurl object + QUrl decklistUrl = QUrl(decklistUrlString); + // we get the correctly encoded url. + decklistUrlString = decklistUrl.toEncoded(); + // We open the url in the user's default browser + QDesktopServices::openUrl(decklistUrlString); } /** @brief Exports deck to www.decklist.org. */ 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 e1ffa45de..10675dd39 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -122,7 +122,7 @@ public: DeckLoader *getDeckLoader() const; /** @brief Returns the currently active deck list. */ - DeckList *getDeckList() const; + const DeckList &getDeckList() const; /** @brief Sets the modified state of the tab. * @param _windowModified Whether the tab is modified. diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 7177879a8..4c782c989 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -334,13 +334,13 @@ QMenu *DeckPreviewWidget::createRightClickMenu() auto saveToClipboardMenu = menu->addMenu(tr("Save Deck to Clipboard")); connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(&deckLoader->getDeck().deckList, true, true); }); + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, true); }); connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(&deckLoader->getDeck().deckList, true, false); }); + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, false); }); connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(&deckLoader->getDeck().deckList, false, true); }); + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, true); }); connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(&deckLoader->getDeck().deckList, false, false); }); + [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, false); }); menu->addSeparator(); From c12f4e9d2a0cf81ef457221ccc4d80f1fb06f059 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:19:57 -0800 Subject: [PATCH 03/56] [DeckListModel] remove more access to underlying decklist for iteration (#6436) * [DeckListModel] remove more access to underlying decklist for iteration * remove one last direct iteration of decklist --- .../deck_editor/deck_editor_deck_dock_widget.cpp | 8 ++++---- .../widgets/dialogs/dlg_select_set_for_cards.cpp | 6 +++--- ...rchidekt_api_response_deck_display_widget.cpp | 4 +--- .../models/deck_list/deck_list_model.cpp | 16 ++++++++++++++++ .../models/deck_list/deck_list_model.h | 11 +++++++++++ 5 files changed, 35 insertions(+), 10 deletions(-) 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 facf0c98b..93286191f 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 @@ -386,15 +386,15 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() // Collect unique (name, providerId) pairs QSet> bannerCardSet; - QList cardsInDeck = deckModel->getDeckList()->getCardNodes(); + QList cardsInDeck = deckModel->getCardRefs(); - for (auto currentCard : cardsInDeck) { - if (!CardDatabaseManager::query()->getCard(currentCard->toCardRef())) { + for (auto cardRef : cardsInDeck) { + if (!CardDatabaseManager::query()->getCard(cardRef)) { continue; } // Insert one entry per distinct card, ignore copies - bannerCardSet.insert({currentCard->getName(), currentCard->getCardProviderId()}); + bannerCardSet.insert({cardRef.name, cardRef.providerId}); } // Convert to sorted list diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp index 52768e0e7..656abbfdc 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp @@ -178,7 +178,7 @@ void DlgSelectSetForCards::actOK() void DlgSelectSetForCards::actClear() { emit deckAboutToBeModified(tr("Cleared all printing information.")); - model->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData()); + model->forEachCard(CardNodeFunction::ClearPrintingData()); emit deckModified(); accept(); } @@ -186,8 +186,8 @@ void DlgSelectSetForCards::actClear() void DlgSelectSetForCards::actSetAllToPreferred() { emit deckAboutToBeModified(tr("Set all printings to preferred.")); - model->getDeckList()->forEachCard(CardNodeFunction::ClearPrintingData()); - model->getDeckList()->forEachCard(CardNodeFunction::SetProviderIdToPreferred()); + model->forEachCard(CardNodeFunction::ClearPrintingData()); + model->forEachCard(CardNodeFunction::SetProviderIdToPreferred()); emit deckModified(); accept(); } diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index ef29a1732..288cdc425 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -70,9 +70,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); model->getDeckList()->loadFromStream_Plain(deckStream, false); - model->getDeckList()->forEachCard(CardNodeFunction::ResolveProviderId()); - - model->rebuildTree(); + model->forEachCard(CardNodeFunction::ResolveProviderId()); retranslateUi(); } diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 8859a31fe..5fd4d71e8 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -561,6 +561,11 @@ void DeckListModel::setDeckList(DeckList *_deck) rebuildTree(); } +void DeckListModel::forEachCard(const std::function &func) +{ + deckList->forEachCard(func); +} + static QList cardNodesToExactCards(QList nodes) { QList cards; @@ -600,6 +605,17 @@ QList DeckListModel::getCardNames() const return names; } +QList DeckListModel::getCardRefs() const +{ + auto nodes = deckList->getCardNodes(); + + QList cardRefs; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(cardRefs), + [](auto node) { return node->toCardRef(); }); + + return cardRefs; +} + QList DeckListModel::getZones() const { auto zoneNodes = deckList->getZoneNodes(); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 6e8882084..80a519297 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -309,6 +309,13 @@ public: } void setDeckList(DeckList *_deck); + /** + * @brief Apply a function to every card in the deck tree. + * + * @param func Function taking (zone node, card node). + */ + void forEachCard(const std::function &func); + /** * @brief Creates a list consisting of the entries of the model mapped into ExactCards (with each entry looked up * in the card database). @@ -323,6 +330,10 @@ public: * @brief Gets a deduplicated list of all card names that appear in the model */ [[nodiscard]] QList getCardNames() const; + /** + * @brief Gets a deduplicated list of all CardRefs that appear in the model + */ + [[nodiscard]] QList getCardRefs() const; /** * @brief Gets a list of all zone names that appear in the model */ From e80f13b78e25ca90cdb88e14ababeb2c22e6aa52 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 22 Dec 2025 05:48:55 -0800 Subject: [PATCH 04/56] [DeckDockWidget] Refactor to move down some methods in AbstractTabDeckEditor (#6444) * move actSwapCard down * rename method * move actAddCard down --- .../deck_editor_deck_dock_widget.cpp | 56 ++++++++++++++++--- .../deck_editor_deck_dock_widget.h | 10 ++-- .../widgets/tabs/abstract_tab_deck_editor.cpp | 36 +----------- .../widgets/tabs/abstract_tab_deck_editor.h | 6 +- .../tab_deck_editor_visual.cpp | 2 +- 5 files changed, 57 insertions(+), 53 deletions(-) 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 93286191f..3ee7f647a 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 @@ -76,14 +76,14 @@ void DeckEditorDeckDockWidget::createDeckDock() deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); connect(deckView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DeckEditorDeckDockWidget::updateCard); - connect(deckView, &QTreeView::doubleClicked, this, &DeckEditorDeckDockWidget::actSwapCard); + connect(deckView, &QTreeView::doubleClicked, this, &DeckEditorDeckDockWidget::actSwapSelection); deckView->setContextMenuPolicy(Qt::CustomContextMenu); connect(deckView, &QTreeView::customContextMenuRequested, this, &DeckEditorDeckDockWidget::decklistCustomMenu); - connect(&deckViewKeySignals, &KeySignals::onShiftS, this, &DeckEditorDeckDockWidget::actSwapCard); - connect(&deckViewKeySignals, &KeySignals::onEnter, this, &DeckEditorDeckDockWidget::actIncrement); - connect(&deckViewKeySignals, &KeySignals::onCtrlAltEqual, this, &DeckEditorDeckDockWidget::actIncrement); + connect(&deckViewKeySignals, &KeySignals::onShiftS, this, &DeckEditorDeckDockWidget::actSwapSelection); + connect(&deckViewKeySignals, &KeySignals::onEnter, this, &DeckEditorDeckDockWidget::actIncrementSelection); + connect(&deckViewKeySignals, &KeySignals::onCtrlAltEqual, this, &DeckEditorDeckDockWidget::actIncrementSelection); connect(&deckViewKeySignals, &KeySignals::onCtrlAltMinus, this, &DeckEditorDeckDockWidget::actDecrementSelection); - connect(&deckViewKeySignals, &KeySignals::onShiftRight, this, &DeckEditorDeckDockWidget::actIncrement); + connect(&deckViewKeySignals, &KeySignals::onShiftRight, this, &DeckEditorDeckDockWidget::actIncrementSelection); connect(&deckViewKeySignals, &KeySignals::onShiftLeft, this, &DeckEditorDeckDockWidget::actDecrementSelection); connect(&deckViewKeySignals, &KeySignals::onDelete, this, &DeckEditorDeckDockWidget::actRemoveCard); @@ -181,7 +181,7 @@ void DeckEditorDeckDockWidget::createDeckDock() aIncrement = new QAction(QString(), this); aIncrement->setIcon(QPixmap("theme:icons/increment")); - connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement); + connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrementSelection); auto *tbIncrement = new QToolButton(this); tbIncrement->setDefaultAction(aIncrement); @@ -199,7 +199,7 @@ void DeckEditorDeckDockWidget::createDeckDock() aSwapCard = new QAction(QString(), this); aSwapCard->setIcon(QPixmap("theme:icons/swap")); - connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapCard); + connect(aSwapCard, &QAction::triggered, this, &DeckEditorDeckDockWidget::actSwapSelection); auto *tbSwapCard = new QToolButton(this); tbSwapCard->setDefaultAction(aSwapCard); @@ -582,7 +582,32 @@ QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const return selectedRows; } -void DeckEditorDeckDockWidget::actIncrement() +void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString &_zoneName) +{ + if (!card) { + return; + } + + QString zoneName = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : _zoneName; + + emit requestDeckHistorySave(tr("Added (%1): %2 (%3) %4") + .arg(zoneName, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), + card.getPrinting().getProperty("num"))); + + QModelIndex newCardIndex = deckModel->addCard(card, zoneName); + + if (!newCardIndex.isValid()) { + return; + } + + expandAll(); + deckView->clearSelection(); + deckView->setCurrentIndex(newCardIndex); + + emit deckModified(); +} + +void DeckEditorDeckDockWidget::actIncrementSelection() { auto selectedRows = getSelectedCardNodes(); @@ -591,7 +616,20 @@ void DeckEditorDeckDockWidget::actIncrement() } } -void DeckEditorDeckDockWidget::actSwapCard() +void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString &zoneName) +{ + QString providerId = card.getPrinting().getUuid(); + QString collectorNumber = card.getPrinting().getProperty("num"); + + QModelIndex foundCard = deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber); + if (!foundCard.isValid()) { + foundCard = deckModel->findCard(card.getName(), zoneName); + } + + swapCard(foundCard); +} + +void DeckEditorDeckDockWidget::actSwapSelection() { auto selectedRows = getSelectedCardNodes(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 60e1d36d2..6c88cafff 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -62,13 +62,13 @@ public slots: void sortDeckModelToDeckView(); DeckLoader *getDeckLoader(); const DeckList &getDeckList() const; - void actIncrement(); - bool swapCard(const QModelIndex &idx); + void actAddCard(const ExactCard &card, const QString &zoneName); + void actIncrementSelection(); void actDecrementCard(const ExactCard &card, QString zoneName); void actDecrementSelection(); - void actSwapCard(); + void actSwapCard(const ExactCard &card, const QString &zoneName); + void actSwapSelection(); void actRemoveCard(); - void offsetCountAtIndex(const QModelIndex &idx, int offset); void initializeFormats(); void expandAll(); @@ -108,9 +108,11 @@ private: void recursiveExpand(const QModelIndex &index); [[nodiscard]] QModelIndexList getSelectedCardNodes() const; + void offsetCountAtIndex(const QModelIndex &idx, int offset); private slots: void decklistCustomMenu(QPoint point); + bool swapCard(const QModelIndex ¤tIndex); void updateCard(QModelIndex, const QModelIndex ¤t); void updateName(const QString &name); void updateComments(); 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 398208993..8eb1477f1 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -140,23 +140,9 @@ void AbstractTabDeckEditor::onDeckHistoryClearRequested() * @param card Card to add. * @param zoneName Zone to add the card to. */ -void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, QString zoneName) +void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName) { - if (!card) - return; - - if (card.getInfo().getIsToken()) - zoneName = DECK_ZONE_TOKENS; - - onDeckHistorySaveRequested(QString(tr("Added (%1): %2 (%3) %4")) - .arg(zoneName, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), - card.getPrinting().getProperty("num"))); - - QModelIndex newCardIndex = deckDockWidget->deckModel->addCard(card, zoneName); - deckDockWidget->expandAll(); - deckDockWidget->deckView->clearSelection(); - deckDockWidget->deckView->setCurrentIndex(newCardIndex); - setModified(true); + deckDockWidget->actAddCard(card, zoneName); databaseDisplayDockWidget->searchEdit->setSelection(0, databaseDisplayDockWidget->searchEdit->text().length()); } @@ -193,24 +179,6 @@ void AbstractTabDeckEditor::actDecrementCardFromSideboard(const ExactCard &card) emit decrementCard(card, DECK_ZONE_SIDE); } -/** - * @brief Swaps a card in a deck zone. - * @param card Card to swap. - * @param zoneName Zone to swap in. - */ -void AbstractTabDeckEditor::actSwapCard(const ExactCard &card, const QString &zoneName) -{ - QString providerId = card.getPrinting().getUuid(); - QString collectorNumber = card.getPrinting().getProperty("num"); - - QModelIndex foundCard = deckDockWidget->deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber); - if (!foundCard.isValid()) { - foundCard = deckDockWidget->deckModel->findCard(card.getName(), zoneName); - } - - deckDockWidget->swapCard(foundCard); -} - /** * @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 10675dd39..bfbda778c 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -77,7 +77,6 @@ class QAction; * * - actAddCard(const ExactCard &card) — Adds a card to the deck. * - actDecrementCard(const ExactCard &card) — Removes a single instance of a card from the deck. - * - actSwapCard(const ExactCard &card, const QString &zone) — Swaps a card between zones. * - 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. @@ -320,10 +319,7 @@ protected: bool isBlankNewDeck() const; /** @brief Helper function to add a card to a specific deck zone. */ - void addCardHelper(const ExactCard &card, QString zoneName); - - /** @brief Swaps a card in the deck view. */ - void actSwapCard(const ExactCard &card, const QString &zoneName); + void addCardHelper(const ExactCard &card, const QString &zoneName); /** @brief Opens a deck from a file. */ virtual void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation); 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 6dd55d545..57615df94 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 @@ -191,7 +191,7 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, // Double click = swap if (event->type() == QEvent::MouseButtonDblClick && event->button() == Qt::LeftButton) { - actSwapCard(card, zoneName); + deckDockWidget->actSwapCard(card, zoneName); idx = deckDockWidget->deckModel->findCard(card.getName(), zoneName); sel->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect); return; From e557ae0f2a9177961b86299701e4f3dcbde0b286 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:09:50 +0100 Subject: [PATCH 05/56] Bump actions/upload-artifact from 5 to 6 (#6445) --- .github/workflows/desktop-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 06549702d..b89190c8d 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -213,7 +213,7 @@ jobs: - name: Upload artifact id: upload_artifact if: matrix.package != 'skip' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{matrix.distro}}${{matrix.version}}-package path: ${{steps.build.outputs.path}} @@ -450,7 +450,7 @@ jobs: - name: Upload artifact id: upload_artifact if: matrix.make_package - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{matrix.artifact_name}} path: ${{steps.build.outputs.path}} @@ -458,7 +458,7 @@ jobs: - name: Upload pdb database if: matrix.os == 'Windows' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: Windows${{matrix.target}}-debug-pdbs path: | From be17ee190222c763588bd88d27b413f330ae251b Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 23 Dec 2025 06:07:39 -0800 Subject: [PATCH 06/56] [DeckListModel] Refactor to use column num constants (#6441) --- .../card_group_display_widget.cpp | 7 ++--- .../cards/deck_card_zone_display_widget.cpp | 5 ++-- .../deck_editor_database_display_widget.cpp | 8 +++--- .../deck_editor_deck_dock_widget.cpp | 27 ++++++++++--------- .../printing_selector/card_amount_widget.cpp | 14 +++++----- ...idekt_api_response_deck_display_widget.cpp | 12 +++++---- .../visual_deck_editor_widget.cpp | 7 ++--- .../deck_list_sort_filter_proxy_model.cpp | 4 +-- 8 files changed, 46 insertions(+), 38 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 cb83768f4..fdac9d0f0 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 @@ -91,8 +91,9 @@ QWidget *CardGroupDisplayWidget::constructWidgetForIndex(QPersistentModelIndex i if (indexToWidgetMap.contains(index)) { return indexToWidgetMap[index]; } - auto cardName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString(); - auto cardProviderId = deckListModel->data(index.sibling(index.row(), 4), Qt::EditRole).toString(); + auto cardName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + auto cardProviderId = + index.sibling(index.row(), DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::EditRole).toString(); auto widget = new CardInfoPictureWithTextOverlayWidget(getLayoutParent(), true); widget->setScaleFactor(cardSizeWidget->getSlider()->value()); @@ -114,7 +115,7 @@ void CardGroupDisplayWidget::updateCardDisplays() // This doesn't really matter since overwrite the whole lessThan function to just compare dynamically anyway. proxy.setSortRole(Qt::EditRole); - proxy.sort(1, Qt::AscendingOrder); + proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); // 1. trackedIndex is a source index → map it to proxy space QModelIndex proxyParent = proxy.mapFromSource(trackedIndex); 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 132964f13..4742467b1 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 @@ -82,10 +82,11 @@ void DeckCardZoneDisplayWidget::cleanupInvalidCardGroup(CardGroupDisplayWidget * void DeckCardZoneDisplayWidget::constructAppropriateWidget(QPersistentModelIndex index) { - auto categoryName = deckListModel->data(index.sibling(index.row(), 1), Qt::EditRole).toString(); if (indexToWidgetMap.contains(index)) { return; } + + auto categoryName = index.sibling(index.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); if (displayType == DisplayType::Overlap) { auto *displayWidget = new OverlappedCardGroupDisplayWidget( cardGroupContainer, deckListModel, selectionModel, index, zoneName, categoryName, activeGroupCriteria, @@ -120,7 +121,7 @@ void DeckCardZoneDisplayWidget::displayCards() QSortFilterProxyModel proxy; proxy.setSourceModel(deckListModel); proxy.setSortRole(Qt::EditRole); - proxy.sort(1, Qt::AscendingOrder); + proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); // 1. trackedIndex is a source index → map it to proxy space QModelIndex proxyParent = proxy.mapFromSource(trackedIndex); 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 b4b27e48a..f5549a98c 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 @@ -134,13 +134,13 @@ void DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters() void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/) { - const QString cardName = current.sibling(current.row(), 0).data().toString(); - if (!current.isValid()) { return; } - if (!current.model()->hasChildren(current.sibling(current.row(), 0))) { + const QString cardName = current.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); + + if (!current.model()->hasChildren(current.siblingAtColumn(CardDatabaseModel::NameColumn))) { emit cardChanged(CardDatabaseManager::query()->getPreferredCard(cardName)); } } @@ -172,7 +172,7 @@ ExactCard DeckEditorDatabaseDisplayWidget::currentCard() const return {}; } - const QString cardName = currentIndex.sibling(currentIndex.row(), 0).data().toString(); + const QString cardName = currentIndex.siblingAtColumn(CardDatabaseModel::NameColumn).data().toString(); return CardDatabaseManager::query()->getPreferredCard(cardName); } 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 3ee7f647a..1e7319aa3 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 @@ -319,17 +319,17 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard() QModelIndex current = deckView->selectionModel()->currentIndex(); if (!current.isValid()) return {}; - const QString cardName = current.sibling(current.row(), 1).data().toString(); - const QString cardProviderID = current.sibling(current.row(), 4).data().toString(); + const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); const QModelIndex gparent = current.parent().parent(); if (!gparent.isValid()) { return {}; } - const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString(); + const QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - if (!current.model()->hasChildren(current.sibling(current.row(), 0))) { + if (!current.model()->hasChildren(current.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT))) { if (ExactCard selectedCard = CardDatabaseManager::query()->getCard({cardName, cardProviderID})) { return selectedCard; } @@ -665,14 +665,15 @@ bool DeckEditorDeckDockWidget::swapCard(const QModelIndex ¤tIndex) { if (!currentIndex.isValid()) return false; - const QString cardName = currentIndex.sibling(currentIndex.row(), 1).data().toString(); - const QString cardProviderID = currentIndex.sibling(currentIndex.row(), 4).data().toString(); + const QString cardName = currentIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + const QString cardProviderID = + currentIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); const QModelIndex gparent = currentIndex.parent().parent(); if (!gparent.isValid()) return false; - const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString(); + const QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); offsetCountAtIndex(currentIndex, -1); const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; @@ -738,7 +739,7 @@ void DeckEditorDeckDockWidget::actRemoveCard() continue; } QModelIndex sourceIndex = proxy->mapToSource(index); - QString cardName = sourceIndex.sibling(sourceIndex.row(), 1).data().toString(); + QString cardName = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); emit requestDeckHistorySave(QString(tr("Removed \"%1\" (all copies)")).arg(cardName)); @@ -761,11 +762,11 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int of QModelIndex sourceIndex = proxy->mapToSource(idx); - const QModelIndex numberIndex = sourceIndex.sibling(sourceIndex.row(), 0); - const QModelIndex nameIndex = sourceIndex.sibling(sourceIndex.row(), 1); + const QModelIndex numberIndex = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT); + const QModelIndex nameIndex = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME); - const QString cardName = deckModel->data(nameIndex, Qt::EditRole).toString(); - const int count = deckModel->data(numberIndex, Qt::EditRole).toInt(); + const QString cardName = nameIndex.data(Qt::EditRole).toString(); + const int count = numberIndex.data(Qt::EditRole).toInt(); const int new_count = count + offset; const auto reason = @@ -773,7 +774,7 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int of .arg(offset > 0 ? tr("Added") : tr("Removed")) .arg(qAbs(offset)) .arg(cardName) - .arg(deckModel->data(sourceIndex.sibling(sourceIndex.row(), 4), Qt::DisplayRole).toString()); + .arg(sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString()); emit requestDeckHistorySave(reason); 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 3d519e595..e979637f8 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -151,9 +151,10 @@ void CardAmountWidget::addPrinting(const QString &zone) bool replacingProviderless = false; if (existing.isValid()) { - QString providerId = deckModel->data(existing.sibling(existing.row(), 4), Qt::DisplayRole).toString(); + QString providerId = + existing.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); if (providerId.isEmpty()) { - int amount = deckModel->data(existing, Qt::DisplayRole).toInt(); + int amount = existing.data(Qt::DisplayRole).toInt(); extraCopies = amount - 1; // One less because we *always* add one replacingProviderless = true; } @@ -177,9 +178,10 @@ void CardAmountWidget::addPrinting(const QString &zone) recursiveExpand(newCardIndex); // Check if a card without a providerId already exists in the deckModel and replace it, if so. - QString foundProviderId = deckModel->data(existing.sibling(existing.row(), 4), Qt::DisplayRole).toString(); + QString foundProviderId = + existing.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); if (existing.isValid() && existing != newCardIndex && foundProviderId == "") { - auto amount = deckModel->data(existing, Qt::DisplayRole); + auto amount = existing.data(Qt::DisplayRole); for (int i = 0; i < amount.toInt() - 1; i++) { deckModel->addCard(rootCard, zone); } @@ -252,8 +254,8 @@ void CardAmountWidget::offsetCountAtIndex(const QModelIndex &idx, int offset) return; } - const QModelIndex numberIndex = idx.sibling(idx.row(), 0); - const int count = deckModel->data(numberIndex, Qt::EditRole).toInt(); + const QModelIndex numberIndex = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT); + const int count = numberIndex.data(Qt::EditRole).toInt(); const int new_count = count + offset; deckView->setCurrentIndex(numberIndex); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 288cdc425..1547cca74 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -83,7 +83,7 @@ void ArchidektApiResponseDeckDisplayWidget::retranslateUi() void ArchidektApiResponseDeckDisplayWidget::onGroupCriteriaChange(const QString &activeGroupCriteria) { model->setActiveGroupCriteria(DeckListModelGroupCriteria::fromString(activeGroupCriteria)); - model->sort(1, Qt::AscendingOrder); + model->sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); } void ArchidektApiResponseDeckDisplayWidget::actOpenInDeckEditor() @@ -120,7 +120,7 @@ void ArchidektApiResponseDeckDisplayWidget::constructZoneWidgetsFromDeckListMode QSortFilterProxyModel proxy; proxy.setSourceModel(model); proxy.setSortRole(Qt::EditRole); - proxy.sort(1, Qt::AscendingOrder); + proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); for (int i = 0; i < proxy.rowCount(); ++i) { QModelIndex proxyIndex = proxy.index(i, 0); @@ -133,10 +133,12 @@ void ArchidektApiResponseDeckDisplayWidget::constructZoneWidgetsFromDeckListMode continue; } + QString zoneName = + persistent.sibling(persistent.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + DeckCardZoneDisplayWidget *zoneDisplayWidget = - new DeckCardZoneDisplayWidget(zoneContainer, model, nullptr, persistent, - model->data(persistent.sibling(persistent.row(), 1), Qt::EditRole).toString(), - "maintype", {"name"}, DisplayType::Overlap, 20, 10, cardSizeSlider); + new DeckCardZoneDisplayWidget(zoneContainer, model, nullptr, persistent, zoneName, "maintype", {"name"}, + DisplayType::Overlap, 20, 10, cardSizeSlider); connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::sortCriteriaChanged, zoneDisplayWidget, &DeckCardZoneDisplayWidget::onActiveSortCriteriaChanged); 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 76cfe8c8e..7b70e2b6d 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 @@ -264,9 +264,10 @@ void VisualDeckEditorWidget::onCardRemoval(const QModelIndex &parent, int first, void VisualDeckEditorWidget::constructZoneWidgetForIndex(QPersistentModelIndex persistent) { + QString zoneName = + persistent.sibling(persistent.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget( - zoneContainer, deckListModel, selectionModel, persistent, - deckListModel->data(persistent.sibling(persistent.row(), 1), Qt::EditRole).toString(), + zoneContainer, deckListModel, selectionModel, persistent, zoneName, displayOptionsWidget->getActiveGroupCriteria(), displayOptionsWidget->getActiveSortCriteria(), displayOptionsWidget->getDisplayType(), 20, 10, cardSizeWidget); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover); @@ -290,7 +291,7 @@ void VisualDeckEditorWidget::constructZoneWidgetsFromDeckListModel() QSortFilterProxyModel proxy; proxy.setSourceModel(deckListModel); proxy.setSortRole(Qt::EditRole); - proxy.sort(1, Qt::AscendingOrder); + proxy.sort(DeckListModelColumns::CARD_NAME, Qt::AscendingOrder); for (int i = 0; i < proxy.rowCount(); ++i) { QModelIndex proxyIndex = proxy.index(i, 0); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp index 35fd4d9f8..0ec159737 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp @@ -11,8 +11,8 @@ bool DeckListSortFilterProxyModel::lessThan(const QModelIndex &left, const QMode bool rightIsCard = src->data(right, Qt::UserRole + 1).toBool(); if (!leftIsCard || !rightIsCard) { - QString lName = src->data(left.siblingAtColumn(1), Qt::EditRole).toString(); - QString rName = src->data(right.siblingAtColumn(1), Qt::EditRole).toString(); + QString lName = src->data(left.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); + QString rName = src->data(right.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); return lName.localeAwareCompare(rName) < 0; } From 01e8e4d5895408a5eee9a0300988467c5c506db6 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 23 Dec 2025 06:45:27 -0800 Subject: [PATCH 07/56] [DeckDockWidget] Fix swap not auto-expanding tree (#6443) --- .../widgets/deck_editor/deck_editor_deck_dock_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1e7319aa3..625af4dfa 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 @@ -681,7 +681,7 @@ bool DeckEditorDeckDockWidget::swapCard(const QModelIndex ¤tIndex) QModelIndex newCardIndex = card ? deckModel->addCard(card, otherZoneName) // Third argument (true) says create the card no matter what, even if not in DB : deckModel->addPreferredPrintingCard(cardName, otherZoneName, true); - recursiveExpand(proxy->mapToSource(newCardIndex)); + recursiveExpand(proxy->mapFromSource(newCardIndex)); return true; } From e7af1bbec9425146de1d3f92ba942f093d302379 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:00:07 +0100 Subject: [PATCH 08/56] [EDHRec] New layout for commander details (#6405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stuff Took 22 minutes * New layout for commander details. Took 1 hour 18 minutes * Update plate to encompass everything, update font sizes. Took 10 minutes * Include map. Took 2 minutes * Include QSet Took 5 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 4 + ...i_response_card_details_display_widget.cpp | 6 +- ...ponse_commander_details_display_widget.cpp | 33 ++++- ...esponse_commander_details_display_widget.h | 6 +- ...api_response_bracket_navigation_widget.cpp | 99 ++++++++++++++ ...r_api_response_bracket_navigation_widget.h | 37 ++++++ ..._api_response_budget_navigation_widget.cpp | 101 ++++++++++++++ ...er_api_response_budget_navigation_widget.h | 37 ++++++ ...mmander_api_response_navigation_widget.cpp | 123 +++--------------- ...commander_api_response_navigation_widget.h | 17 +-- 10 files changed, 336 insertions(+), 127 deletions(-) create mode 100644 cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 4c3679011..b2d387391 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -287,6 +287,10 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/key_signals.cpp src/interface/logger.cpp + src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp + src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h + src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp + src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h ) add_subdirectory(sounds) diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp index 26ade96dd..c01c7fa43 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp @@ -19,17 +19,17 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi nameLabel = new QLabel(this); nameLabel->setText(toDisplay.name); nameLabel->setAlignment(Qt::AlignHCenter); + nameLabel->setStyleSheet("font-size: 20px; font-weight: bold"); inclusionDisplayWidget = new EdhrecApiResponseCardInclusionDisplayWidget(this, toDisplay); synergyDisplayWidget = new EdhrecApiResponseCardSynergyDisplayWidget(this, toDisplay); - layout->addWidget(nameLabel); - layout->addWidget(cardPictureWidget); - backgroundPlateWidget = new BackgroundPlateWidget(this); auto plateLayout = new QVBoxLayout(backgroundPlateWidget); + plateLayout->addWidget(nameLabel); + plateLayout->addWidget(cardPictureWidget); plateLayout->addWidget(inclusionDisplayWidget); plateLayout->addWidget(synergyDisplayWidget); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp index b15d559f5..515475f3e 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp @@ -3,6 +3,7 @@ #include "../../../../../cards/card_info_picture_widget.h" #include "../../tab_edhrec_main.h" #include "../card_prices/edhrec_api_response_card_prices_display_widget.h" +#include "edhrec_commander_api_response_bracket_navigation_widget.h" #include @@ -12,9 +13,14 @@ EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCom QString baseUrl) : QWidget(parent), commanderDetails(_commanderDetails) { - layout = new QVBoxLayout(this); + layout = new QHBoxLayout(this); setLayout(layout); + commanderLayout = new QHBoxLayout(); + commanderDetailsLayout = new QVBoxLayout(); + commanderDetailsLayout->setAlignment(Qt::AlignCenter); + navigationAndPricesLayout = new QVBoxLayout(); + commanderPicture = new CardInfoPictureWidget(this); commanderPicture->setCard(CardDatabaseManager::query()->getCard({commanderDetails.getName()})); @@ -36,20 +42,35 @@ EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCom commanderDetails.debugPrint(); + commanderName = new QLabel(this); + commanderName->setText(commanderDetails.getName()); + commanderName->setAlignment(Qt::AlignCenter); + commanderName->setStyleSheet("font-size: 28px; font-weight: bold"); + label = new QLabel(this); label->setAlignment(Qt::AlignCenter); + label->setStyleSheet("font-size: 16px"); + salt = new QLabel(this); salt->setAlignment(Qt::AlignCenter); + salt->setStyleSheet("font-size: 16px"); cardPricesDisplayWidget = new EdhrecApiResponseCardPricesDisplayWidget(this, commanderDetails.getPrices()); navigationWidget = new EdhrecCommanderApiResponseNavigationWidget(this, commanderDetails, baseUrl); - layout->addWidget(commanderPicture); - layout->addWidget(label); - layout->addWidget(salt); - layout->addWidget(cardPricesDisplayWidget); - layout->addWidget(navigationWidget); + commanderLayout->addWidget(commanderPicture); + commanderDetailsLayout->addWidget(commanderName); + commanderDetailsLayout->addSpacing(1); + commanderDetailsLayout->addWidget(label); + commanderDetailsLayout->addWidget(salt); + commanderDetailsLayout->addWidget(cardPricesDisplayWidget); + commanderLayout->addLayout(commanderDetailsLayout); + navigationAndPricesLayout->addWidget(navigationWidget); + // navigationAndPricesLayout->addWidget(cardPricesDisplayWidget); + + layout->addLayout(commanderLayout); + layout->addLayout(navigationAndPricesLayout); retranslateUi(); } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h index 295e4228b..8e74588e2 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h @@ -29,8 +29,12 @@ public: private: EdhrecCommanderApiResponseCommanderDetails commanderDetails; - QVBoxLayout *layout; + QHBoxLayout *layout; + QHBoxLayout *commanderLayout; + QVBoxLayout *commanderDetailsLayout; + QVBoxLayout *navigationAndPricesLayout; CardInfoPictureWidget *commanderPicture; + QLabel *commanderName; QLabel *label; QLabel *salt; EdhrecApiResponseCardPricesDisplayWidget *cardPricesDisplayWidget; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp new file mode 100644 index 000000000..2bc727bd2 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp @@ -0,0 +1,99 @@ +#include "edhrec_commander_api_response_bracket_navigation_widget.h" + +#include + +EdhrecCommanderApiResponseBracketNavigationWidget::EdhrecCommanderApiResponseBracketNavigationWidget( + QWidget *parent, + const QString &baseUrl) + : QWidget(parent) +{ + layout = new QGridLayout(this); + setLayout(layout); + + gameChangerLabel = new QLabel(this); + + layout->addWidget(gameChangerLabel, 1, 0, 1, 2); + + for (int i = 0; i < gameChangerOptions.length(); i++) { + QString option = gameChangerOptions.at(i); + QString label = option.isEmpty() ? "All" : option.at(0).toUpper() + option.mid(1); + QPushButton *optionButton = new QPushButton(label, this); + optionButton->setMinimumHeight(84); + optionButton->setStyleSheet("font-size: 24px"); + gameChangerButtons[option] = optionButton; + layout->addWidget(optionButton, 2, i); + connect(optionButton, &QPushButton::clicked, this, [=, this]() { + selectedGameChanger = option; + updateOptionButtonSelection(gameChangerButtons, option); + emit requestNavigation(); + }); + } + + updateOptionButtonSelection(gameChangerButtons, ""); + + retranslateUi(); + applyOptionsFromUrl(baseUrl); +} + +void EdhrecCommanderApiResponseBracketNavigationWidget::retranslateUi() +{ + gameChangerLabel->setText(tr("Game Changers")); +} + +void EdhrecCommanderApiResponseBracketNavigationWidget::applyOptionsFromUrl(const QString &url) +{ + QString cleanedUrl = url; + + // Remove base and file extension + if (cleanedUrl.startsWith("https://json.edhrec.com/pages/")) { + cleanedUrl = cleanedUrl.mid(QString("https://json.edhrec.com/pages/").length()); + } + if (cleanedUrl.endsWith(".json")) { + cleanedUrl.chop(5); + } + + // Expecting something like: "commanders/the-ur-dragon/core/expensive" +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QStringList parts = cleanedUrl.split('/', Qt::SkipEmptyParts); +#else + QStringList parts = cleanedUrl.split('/', QString::SkipEmptyParts); +#endif + + if (parts.size() < 2) { + return; + } + + QString commanderName = parts[1]; + QString gameChangerOpt; + + // Define valid sets + QSet validGameChangers = {"exhibition", "core", "upgraded", "optimized", "cedh"}; + + // Check remaining parts after commander + for (int i = 2; i < parts.size(); ++i) { + QString part = parts[i].toLower(); + if (validGameChangers.contains(part)) { + gameChangerOpt = part; + } + } + + // Validate and apply + if (!gameChangerButtons.contains(gameChangerOpt)) { + gameChangerOpt.clear(); + } + + selectedGameChanger = gameChangerOpt; + + updateOptionButtonSelection(gameChangerButtons, selectedGameChanger); +} + +void EdhrecCommanderApiResponseBracketNavigationWidget::updateOptionButtonSelection( + QMap &buttons, + const QString &selectedKey) +{ + for (auto it = buttons.begin(); it != buttons.end(); ++it) { + it.value()->setStyleSheet(it.key() == selectedKey + ? "background-color: lightblue; font-weight: bold; font-size: 24px;" + : "font-size: 24px"); + } +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h new file mode 100644 index 000000000..713ef2791 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BRACKET_NAVIGATION_WIDGET_H +#define COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BRACKET_NAVIGATION_WIDGET_H + +#include +#include +#include +#include +#include + +class EdhrecCommanderApiResponseBracketNavigationWidget : public QWidget +{ + Q_OBJECT +public: + explicit EdhrecCommanderApiResponseBracketNavigationWidget(QWidget *parent, const QString &baseUrl); + void retranslateUi(); + void applyOptionsFromUrl(const QString &url); + QString getSelectedGameChanger() const + { + return selectedGameChanger; + } + +signals: + void requestNavigation(); + +private: + QGridLayout *layout; + QLabel *gameChangerLabel; + + QStringList gameChangerOptions = {"", "exhibition", "core", "upgraded", "optimized", "cedh"}; + QString selectedGameChanger; + + QMap gameChangerButtons; + + void updateOptionButtonSelection(QMap &buttons, const QString &selectedKey); +}; + +#endif // COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BRACKET_NAVIGATION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp new file mode 100644 index 000000000..8470e6b3f --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp @@ -0,0 +1,101 @@ +#include "edhrec_commander_api_response_budget_navigation_widget.h" + +#include + +EdhrecCommanderApiResponseBudgetNavigationWidget::EdhrecCommanderApiResponseBudgetNavigationWidget( + QWidget *parent, + const QString &baseUrl) + : QWidget(parent) +{ + layout = new QGridLayout(this); + setLayout(layout); + + budgetLabel = new QLabel(this); + + layout->addWidget(budgetLabel, 3, 0, 1, 2); + + for (int i = 0; i < budgetOptions.length(); i++) { + QString option = budgetOptions.at(i); + QString label = option.isEmpty() ? "Any" : option.at(0).toUpper() + option.mid(1); + QPushButton *btn = new QPushButton(label, this); + btn->setMinimumHeight(84); + btn->setStyleSheet("font-size: 24px"); + budgetButtons[option] = btn; + layout->addWidget(btn, 4, i); + connect(btn, &QPushButton::clicked, this, [=, this]() { + selectedBudget = option; + updateOptionButtonSelection(budgetButtons, option); + emit requestNavigation(); + }); + } + + updateOptionButtonSelection(budgetButtons, ""); + + retranslateUi(); + applyOptionsFromUrl(baseUrl); +} + +void EdhrecCommanderApiResponseBudgetNavigationWidget::retranslateUi() +{ + budgetLabel->setText(tr("Budget")); +} + +void EdhrecCommanderApiResponseBudgetNavigationWidget::applyOptionsFromUrl(const QString &url) +{ + QString cleanedUrl = url; + + // Remove base and file extension + if (cleanedUrl.startsWith("https://json.edhrec.com/pages/")) { + cleanedUrl = cleanedUrl.mid(QString("https://json.edhrec.com/pages/").length()); + } + if (cleanedUrl.endsWith(".json")) { + cleanedUrl.chop(5); + } + + // Expecting something like: "commanders/the-ur-dragon/core/expensive" +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QStringList parts = cleanedUrl.split('/', Qt::SkipEmptyParts); +#else + QStringList parts = cleanedUrl.split('/', QString::SkipEmptyParts); +#endif + + if (parts.size() < 2) { + return; + } + + QString commanderName = parts[1]; + QString gameChangerOpt, budgetOpt; + + // Define valid sets + QSet validGameChangers = {"exhibition", "core", "upgraded", "optimized", "cedh"}; + QSet validBudgets = {"budget", "expensive"}; + + // Check remaining parts after commander + for (int i = 2; i < parts.size(); ++i) { + QString part = parts[i].toLower(); + if (validGameChangers.contains(part)) { + gameChangerOpt = part; + } else if (validBudgets.contains(part)) { + budgetOpt = part; + } + } + + if (!budgetButtons.contains(budgetOpt)) { + budgetOpt.clear(); + } + + selectedBudget = budgetOpt; + + updateOptionButtonSelection(budgetButtons, selectedBudget); +} + +void EdhrecCommanderApiResponseBudgetNavigationWidget::updateOptionButtonSelection( + QMap &buttons, + const QString &selectedKey) +{ + for (auto it = buttons.begin(); it != buttons.end(); ++it) { + it.value()->setStyleSheet(it.key() == selectedKey + ? "background-color: lightblue; font-weight: bold; font-size: 24px;" + : "font-size: 24px"); + } +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h new file mode 100644 index 000000000..666edba16 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BUDGET_NAVIGATION_WIDGET_H +#define COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BUDGET_NAVIGATION_WIDGET_H + +#include +#include +#include +#include +#include + +class EdhrecCommanderApiResponseBudgetNavigationWidget : public QWidget +{ + Q_OBJECT +public: + explicit EdhrecCommanderApiResponseBudgetNavigationWidget(QWidget *parent, const QString &baseUrl); + void retranslateUi(); + void applyOptionsFromUrl(const QString &url); + QString getSelectedBudget() const + { + return selectedBudget; + } + +signals: + void requestNavigation(); + +private: + QGridLayout *layout; + QLabel *budgetLabel; + + QStringList budgetOptions = {"", "budget", "expensive"}; + QString selectedBudget; + + QMap budgetButtons; + + void updateOptionButtonSelection(QMap &buttons, const QString &selectedKey); +}; + +#endif // COCKATRICE_EDHREC_COMMANDER_API_RESPONSE_BUDGET_NAVIGATION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp index 3f3b9f1ba..c42d2ed90 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.cpp @@ -11,47 +11,28 @@ EdhrecCommanderApiResponseNavigationWidget::EdhrecCommanderApiResponseNavigation layout = new QGridLayout(this); setLayout(layout); - gameChangerLabel = new QLabel(this); - budgetLabel = new QLabel(this); + bracketNavigationWidget = new EdhrecCommanderApiResponseBracketNavigationWidget(this, baseUrl); + + connect(bracketNavigationWidget, &EdhrecCommanderApiResponseBracketNavigationWidget::requestNavigation, this, + &EdhrecCommanderApiResponseNavigationWidget::actRequestCommanderNavigation); + + budgetNavigationWidget = new EdhrecCommanderApiResponseBudgetNavigationWidget(this, baseUrl); + + connect(budgetNavigationWidget, &EdhrecCommanderApiResponseBudgetNavigationWidget::requestNavigation, this, + &EdhrecCommanderApiResponseNavigationWidget::actRequestCommanderNavigation); comboPushButton = new QPushButton(this); + comboPushButton->setMinimumHeight(84); + comboPushButton->setStyleSheet("font-size: 24px"); averageDeckPushButton = new QPushButton(this); + averageDeckPushButton->setMinimumHeight(84); + averageDeckPushButton->setStyleSheet("font-size: 24px"); layout->addWidget(comboPushButton, 0, 0, 1, 1); layout->addWidget(averageDeckPushButton, 0, 1, 1, 1); - layout->addWidget(gameChangerLabel, 1, 0, 1, 2); - - for (int i = 0; i < gameChangerOptions.length(); i++) { - QString option = gameChangerOptions.at(i); - QString label = option.isEmpty() ? "All" : option.at(0).toUpper() + option.mid(1); - QPushButton *optionButton = new QPushButton(label, this); - gameChangerButtons[option] = optionButton; - layout->addWidget(optionButton, 2, i); - connect(optionButton, &QPushButton::clicked, this, [=, this]() { - selectedGameChanger = option; - updateOptionButtonSelection(gameChangerButtons, option); - actRequestCommanderNavigation(); - }); - } - - layout->addWidget(budgetLabel, 3, 0, 1, 2); - - for (int i = 0; i < budgetOptions.length(); i++) { - QString option = budgetOptions.at(i); - QString label = option.isEmpty() ? "Any" : option.at(0).toUpper() + option.mid(1); - QPushButton *btn = new QPushButton(label, this); - budgetButtons[option] = btn; - layout->addWidget(btn, 4, i); - connect(btn, &QPushButton::clicked, this, [=, this]() { - selectedBudget = option; - updateOptionButtonSelection(budgetButtons, option); - actRequestCommanderNavigation(); - }); - } - - updateOptionButtonSelection(gameChangerButtons, ""); - updateOptionButtonSelection(budgetButtons, ""); + layout->addWidget(bracketNavigationWidget, 1, 0, 1, 2); + layout->addWidget(budgetNavigationWidget, 2, 0, 1, 2); QWidget *currentParent = parentWidget(); TabEdhRecMain *parentTab = nullptr; @@ -73,87 +54,21 @@ EdhrecCommanderApiResponseNavigationWidget::EdhrecCommanderApiResponseNavigation } retranslateUi(); - applyOptionsFromUrl(baseUrl); } void EdhrecCommanderApiResponseNavigationWidget::retranslateUi() { comboPushButton->setText(tr("Combos")); averageDeckPushButton->setText(tr("Average Deck")); - gameChangerLabel->setText(tr("Game Changers")); - budgetLabel->setText(tr("Budget")); -} - -void EdhrecCommanderApiResponseNavigationWidget::applyOptionsFromUrl(const QString &url) -{ - QString cleanedUrl = url; - - // Remove base and file extension - if (cleanedUrl.startsWith("https://json.edhrec.com/pages/")) { - cleanedUrl = cleanedUrl.mid(QString("https://json.edhrec.com/pages/").length()); - } - if (cleanedUrl.endsWith(".json")) { - cleanedUrl.chop(5); - } - - // Expecting something like: "commanders/the-ur-dragon/core/expensive" -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - QStringList parts = cleanedUrl.split('/', Qt::SkipEmptyParts); -#else - QStringList parts = cleanedUrl.split('/', QString::SkipEmptyParts); -#endif - - if (parts.size() < 2) { - return; - } - - QString commanderName = parts[1]; - QString gameChangerOpt, budgetOpt; - - // Define valid sets - QSet validGameChangers = {"core", "upgraded", "optimized"}; - QSet validBudgets = {"budget", "expensive"}; - - // Check remaining parts after commander - for (int i = 2; i < parts.size(); ++i) { - QString part = parts[i].toLower(); - if (validGameChangers.contains(part)) { - gameChangerOpt = part; - } else if (validBudgets.contains(part)) { - budgetOpt = part; - } - } - - // Validate and apply - if (!gameChangerButtons.contains(gameChangerOpt)) { - gameChangerOpt.clear(); - } - if (!budgetButtons.contains(budgetOpt)) { - budgetOpt.clear(); - } - - selectedGameChanger = gameChangerOpt; - selectedBudget = budgetOpt; - - updateOptionButtonSelection(gameChangerButtons, selectedGameChanger); - updateOptionButtonSelection(budgetButtons, selectedBudget); -} - -void EdhrecCommanderApiResponseNavigationWidget::updateOptionButtonSelection(QMap &buttons, - const QString &selectedKey) -{ - for (auto it = buttons.begin(); it != buttons.end(); ++it) { - it.value()->setStyleSheet(it.key() == selectedKey ? "background-color: lightblue; font-weight: bold;" : ""); - } } QString EdhrecCommanderApiResponseNavigationWidget::addNavigationOptionsToUrl(QString baseUrl) { - if (!selectedGameChanger.isEmpty()) { - baseUrl += "/" + selectedGameChanger; + if (!bracketNavigationWidget->getSelectedGameChanger().isEmpty()) { + baseUrl += "/" + bracketNavigationWidget->getSelectedGameChanger(); } - if (!selectedBudget.isEmpty()) { - baseUrl += "/" + selectedBudget; + if (!budgetNavigationWidget->getSelectedBudget().isEmpty()) { + baseUrl += "/" + budgetNavigationWidget->getSelectedBudget(); } return baseUrl; } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h index b97e095f7..10dfa8223 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_navigation_widget.h @@ -8,6 +8,8 @@ #define EDHREC_COMMANDER_API_RESPONSE_NAVIGATION_WIDGET_H #include "edhrec_api_response_commander_details_display_widget.h" +#include "edhrec_commander_api_response_bracket_navigation_widget.h" +#include "edhrec_commander_api_response_budget_navigation_widget.h" #include #include @@ -23,7 +25,6 @@ public: const EdhrecCommanderApiResponseCommanderDetails &_commanderDetails, QString baseUrl); void retranslateUi(); - void applyOptionsFromUrl(const QString &url); public slots: void actRequestCommanderNavigation(); @@ -35,24 +36,14 @@ signals: private: QGridLayout *layout; - QLabel *gameChangerLabel; - QLabel *budgetLabel; - - QStringList gameChangerOptions = {"", "core", "upgraded", "optimized"}; - QStringList budgetOptions = {"", "budget", "expensive"}; - - QString selectedGameChanger; - QString selectedBudget; - - QMap gameChangerButtons; - QMap budgetButtons; + EdhrecCommanderApiResponseBracketNavigationWidget *bracketNavigationWidget; + EdhrecCommanderApiResponseBudgetNavigationWidget *budgetNavigationWidget; QPushButton *comboPushButton; QPushButton *averageDeckPushButton; EdhrecCommanderApiResponseCommanderDetails commanderDetails; - void updateOptionButtonSelection(QMap &buttons, const QString &selectedKey); QString addNavigationOptionsToUrl(QString baseUrl); QString buildComboUrl() const; }; From 421d6b334abf57951ab6c6e8ad28a5010936458e Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:21:47 -0800 Subject: [PATCH 09/56] [DeckDockWidget] Correctly handle auto-expanding tree (#6446) * move method * remove expandAll calls * update recursiveExpand * Refactor DeckModel access * [DeckDockWidget] Correctly handle auto-expand --- .../deck_editor_deck_dock_widget.cpp | 82 ++++++++++--------- .../deck_editor_deck_dock_widget.h | 6 +- .../printing_selector/card_amount_widget.cpp | 43 +--------- .../printing_selector/card_amount_widget.h | 2 - .../models/deck_list/deck_list_model.cpp | 47 ++++++++++- .../models/deck_list/deck_list_model.h | 37 ++++++++- 6 files changed, 131 insertions(+), 86 deletions(-) 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 625af4dfa..57d34be00 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 @@ -156,6 +156,9 @@ void DeckEditorDeckDockWidget::createDeckDock() // Delay the update to avoid race conditions QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox); }); + connect(deckModel, &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand); + connect(deckModel, &DeckListModel::deckReplaced, this, &DeckEditorDeckDockWidget::expandAll); + connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &DeckEditorDeckDockWidget::setBannerCard); bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); @@ -175,8 +178,6 @@ void DeckEditorDeckDockWidget::createDeckDock() deckModel->setActiveGroupCriteria(static_cast( activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - deckView->expandAll(); - deckView->expandAll(); }); aIncrement = new QAction(QString(), this); @@ -506,7 +507,6 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() bannerCardComboBox->blockSignals(false); updateHash(); sortDeckModelToDeckView(); - expandAll(); deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); } @@ -516,8 +516,6 @@ void DeckEditorDeckDockWidget::sortDeckModelToDeckView() deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat()); formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat())); - deckView->expandAll(); - deckView->expandAll(); emit deckChanged(); } @@ -550,17 +548,26 @@ void DeckEditorDeckDockWidget::cleanDeck() deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); } -void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &index) +/** + * @brief Expands all parents of the given index. + * @param sourceIndex The index to expand (model source index) + */ +void DeckEditorDeckDockWidget::recursiveExpand(const QModelIndex &sourceIndex) { - if (index.parent().isValid()) - recursiveExpand(index.parent()); - deckView->expand(index); + auto index = proxy->mapFromSource(sourceIndex); + + while (index.parent().isValid()) { + index = index.parent(); + deckView->expand(index); + } } +/** + * @brief Fully expands all levels of the deck view + */ void DeckEditorDeckDockWidget::expandAll() { - deckView->expandAll(); - deckView->expandAll(); + deckView->expandRecursively(deckView->rootIndex()); } /** @@ -600,7 +607,6 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString & return; } - expandAll(); deckView->clearSelection(); deckView->setCurrentIndex(newCardIndex); @@ -612,7 +618,7 @@ void DeckEditorDeckDockWidget::actIncrementSelection() auto selectedRows = getSelectedCardNodes(); for (const auto &index : selectedRows) { - offsetCountAtIndex(index, 1); + offsetCountAtIndex(index, true); } } @@ -674,14 +680,15 @@ bool DeckEditorDeckDockWidget::swapCard(const QModelIndex ¤tIndex) return false; const QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - offsetCountAtIndex(currentIndex, -1); + offsetCountAtIndex(currentIndex, false); const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; - ExactCard card = CardDatabaseManager::query()->getCard({cardName, cardProviderID}); - QModelIndex newCardIndex = card ? deckModel->addCard(card, otherZoneName) - // Third argument (true) says create the card no matter what, even if not in DB - : deckModel->addPreferredPrintingCard(cardName, otherZoneName, true); - recursiveExpand(proxy->mapFromSource(newCardIndex)); + if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, cardProviderID})) { + deckModel->addCard(card, otherZoneName); + } else { + // Third argument (true) says create the card no matter what, even if not in DB + deckModel->addPreferredPrintingCard(cardName, otherZoneName, true); + } return true; } @@ -703,7 +710,7 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z deckView->clearSelection(); deckView->setCurrentIndex(proxy->mapToSource(idx)); - offsetCountAtIndex(idx, -1); + offsetCountAtIndex(idx, false); } void DeckEditorDeckDockWidget::actDecrementSelection() @@ -717,7 +724,7 @@ void DeckEditorDeckDockWidget::actDecrementSelection() } for (const auto &index : selectedRows) { - offsetCountAtIndex(index, -1); + offsetCountAtIndex(index, false); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -754,7 +761,12 @@ void DeckEditorDeckDockWidget::actRemoveCard() } } -void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int offset) +/** + * @brief Increments or decrements the amount of the card node at the index by 1. + * @param idx The proxy index + * @param isIncrement If true, increments the count. If false, decrements the count + */ +void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement) { if (!idx.isValid() || deckModel->hasChildren(idx)) { return; @@ -762,26 +774,22 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, int of QModelIndex sourceIndex = proxy->mapToSource(idx); - const QModelIndex numberIndex = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT); - const QModelIndex nameIndex = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME); + QString cardName = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + QString providerId = + sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - const QString cardName = nameIndex.data(Qt::EditRole).toString(); - const int count = numberIndex.data(Qt::EditRole).toInt(); - const int new_count = count + offset; - - const auto reason = - QString(tr("%1 %2 × \"%3\" (%4)")) - .arg(offset > 0 ? tr("Added") : tr("Removed")) - .arg(qAbs(offset)) - .arg(cardName) - .arg(sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString()); + const auto reason = QString(tr("%1 %2 × \"%3\" (%4)")) + .arg(isIncrement ? tr("Added") : tr("Removed")) + .arg(1) + .arg(cardName) + .arg(providerId); emit requestDeckHistorySave(reason); - if (new_count <= 0) { - deckModel->removeRow(sourceIndex.row(), sourceIndex.parent()); + if (isIncrement) { + deckModel->incrementAmountAtIndex(sourceIndex); } else { - deckModel->setData(numberIndex, new_count, Qt::EditRole); + deckModel->decrementAmountAtIndex(sourceIndex); } emit deckModified(); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 6c88cafff..30f752203 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -70,7 +70,6 @@ public slots: void actSwapSelection(); void actRemoveCard(); void initializeFormats(); - void expandAll(); signals: void nameChanged(); @@ -106,9 +105,8 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; - void recursiveExpand(const QModelIndex &index); [[nodiscard]] QModelIndexList getSelectedCardNodes() const; - void offsetCountAtIndex(const QModelIndex &idx, int offset); + void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); private slots: void decklistCustomMenu(QPoint point); @@ -124,6 +122,8 @@ private slots: void updateShowBannerCardComboBox(bool visible); void updateShowTagsWidget(bool visible); void syncBannerCardComboBoxSelectionWithDeck(); + void recursiveExpand(const QModelIndex &parent); + void expandAll(); }; #endif // DECK_EDITOR_DECK_DOCK_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 e979637f8..7c5804c37 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -175,7 +175,6 @@ void CardAmountWidget::addPrinting(const QString &zone) // Add the card and expand the list UI auto newCardIndex = deckModel->addCard(rootCard, zone); - recursiveExpand(newCardIndex); // Check if a card without a providerId already exists in the deckModel and replace it, if so. QString foundProviderId = @@ -229,46 +228,6 @@ void CardAmountWidget::removePrintingSideboard() decrementCardHelper(DECK_ZONE_SIDE); } -/** - * @brief Recursively expands the card in the deck view starting from the given index. - * - * @param index The model index of the card to expand. - */ -void CardAmountWidget::recursiveExpand(const QModelIndex &index) -{ - if (index.parent().isValid()) { - recursiveExpand(index.parent()); - } - deckView->expand(index); -} - -/** - * @brief Offsets the card count at the specified index by the given amount. - * - * @param idx The model index of the card. - * @param offset The amount to add or subtract from the card count. - */ -void CardAmountWidget::offsetCountAtIndex(const QModelIndex &idx, int offset) -{ - if (!idx.isValid() || offset == 0) { - return; - } - - const QModelIndex numberIndex = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT); - const int count = numberIndex.data(Qt::EditRole).toInt(); - const int new_count = count + offset; - - deckView->setCurrentIndex(numberIndex); - - if (new_count <= 0) { - deckModel->removeRow(idx.row(), idx.parent()); - } else { - deckModel->setData(numberIndex, new_count, Qt::EditRole); - } - - deckEditor->setModified(true); -} - /** * @brief Helper function to decrement the card count for a given zone. * @@ -288,7 +247,7 @@ void CardAmountWidget::decrementCardHelper(const QString &zone) QModelIndex idx = deckModel->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), rootCard.getPrinting().getProperty("num")); - offsetCountAtIndex(idx, -1); + deckModel->decrementAmountAtIndex(idx); deckEditor->setModified(true); } 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 6d059bc04..b4704cede 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -57,9 +57,7 @@ private: bool hovered; - void offsetCountAtIndex(const QModelIndex &idx, int offset); void decrementCardHelper(const QString &zoneName); - void recursiveExpand(const QModelIndex &index); private slots: void addPrintingMainboard(); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 5fd4d71e8..53d08cd9d 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -436,7 +436,51 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam } sort(lastKnownColumn, lastKnownOrder); emitRecursiveUpdates(parentIndex); - return nodeToIndex(cardNode); + auto index = nodeToIndex(cardNode); + + emit cardAddedAt(index); + + return index; +} + +bool DeckListModel::incrementAmountAtIndex(const QModelIndex &idx) +{ + return offsetAmountAtIndex(idx, 1); +} + +bool DeckListModel::decrementAmountAtIndex(const QModelIndex &idx) +{ + return offsetAmountAtIndex(idx, -1); +} + +bool DeckListModel::offsetAmountAtIndex(const QModelIndex &idx, int offset) +{ + if (!idx.isValid()) { + return false; + } + + auto *node = static_cast(idx.internalPointer()); + auto *card = dynamic_cast(node); + + if (!card) { + return false; + } + + const QModelIndex numberIndex = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT); + const int count = numberIndex.data(Qt::EditRole).toInt(); + const int newCount = count + offset; + + if (newCount <= 0) { + removeRow(idx.row(), idx.parent()); + } else { + setData(numberIndex, newCount, Qt::EditRole); + } + + if (offset > 0) { + emit cardAddedAt(idx); + } + + return true; } int DeckListModel::findSortedInsertRow(InnerDecklistNode *parent, CardInfoPtr cardInfo) const @@ -559,6 +603,7 @@ void DeckListModel::setDeckList(DeckList *_deck) deckList = _deck; } rebuildTree(); + emit deckReplaced(); } void DeckListModel::forEachCard(const std::function &func) diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 80a519297..a85542d97 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -226,6 +226,18 @@ signals: */ void deckHashChanged(); + /** + * @brief Emitted whenever a card is added to the deck, regardless of whether it's an entirely new card or an + * existing card that got incremented. + * @param index The index of the card that got added. + */ + void cardAddedAt(const QModelIndex &index); + + /** + * @brief Emitted whenever the deck in the model has been replaced with a new one + */ + void deckReplaced(); + public: explicit DeckListModel(QObject *parent = nullptr); ~DeckListModel() override; @@ -250,7 +262,6 @@ public: [[nodiscard]] int rowCount(const QModelIndex &parent) const override; [[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - void emitBackgroundUpdates(const QModelIndex &parent); [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override; [[nodiscard]] QModelIndex index(int row, int column, const QModelIndex &parent) const override; [[nodiscard]] QModelIndex parent(const QModelIndex &index) const override; @@ -258,6 +269,12 @@ public: bool setData(const QModelIndex &index, const QVariant &value, int role) override; bool removeRows(int row, int count, const QModelIndex &parent) override; + /** + * Recursively emits the dataChanged signal for all child nodes. + * @param parent The parent node + */ + void emitBackgroundUpdates(const QModelIndex &parent); + /** * @brief Finds a card by name, zone, and optional identifiers. * @param cardName The card's name. @@ -289,6 +306,21 @@ public: */ QModelIndex addCard(const ExactCard &card, const QString &zoneName); + /** + * @brief Increments the `amount` field of the card node at the index by 1. + * @param idx The index of a card node. No-ops if the index is invalid or not a card node + * @return Whether the operation was successful + */ + bool incrementAmountAtIndex(const QModelIndex &idx); + + /** + * @brief Decrements the `amount` field of the card node at the index by 1. + * Removes the node if it causes the amount to fall to 0. + * @param idx The index of a card node. No-ops if the index is invalid or not a card node + * @return Whether the operation was successful + */ + bool decrementAmountAtIndex(const QModelIndex &idx); + /** * @brief Determines the sorted insertion row for a card. * @param parent The parent node where the card will be inserted. @@ -362,6 +394,9 @@ private: const QString &zoneName, const QString &providerId = "", const QString &cardNumber = "") const; + + bool offsetAmountAtIndex(const QModelIndex &idx, int offset); + void emitRecursiveUpdates(const QModelIndex &index); void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); From 521046fb0941bb130e2b122d15a2282d8994c7cf Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 23 Dec 2025 17:48:10 +0100 Subject: [PATCH 10/56] Hashing tests (#5026) * add deck hashing tests * format * fix header * fix cmakelists * fix test * add 5 second timeout to test let the optimising begin * expand tests * remove debug message * manually format * I installed cmake format from the aur * use decklist library * format --- tests/CMakeLists.txt | 11 ++- tests/deck_hash_performance_test.cpp | 81 +++++++++++++++++++ .../clipboard_testing.cpp | 22 +++-- .../clipboard_testing.h | 2 +- .../loading_from_clipboard_test.cpp | 16 ++++ 5 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 tests/deck_hash_performance_test.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6a5eacf54..d80ccce4f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,16 +1,20 @@ enable_testing() + add_test(NAME dummy_test COMMAND dummy_test) add_test(NAME expression_test COMMAND expression_test) - add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) +add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) +set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) + # Find GTest add_executable(dummy_test dummy_test.cpp) add_executable(expression_test expression_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) find_package(GTest) @@ -41,6 +45,7 @@ if(NOT GTEST_FOUND) add_dependencies(expression_test gtest) add_dependencies(test_age_formatting gtest) add_dependencies(password_hash_test gtest) + add_dependencies(deck_hash_performance_test gtest) endif() include_directories(${GTEST_INCLUDE_DIRS}) @@ -50,6 +55,10 @@ target_link_libraries(test_age_formatting Threads::Threads ${GTEST_BOTH_LIBRARIE target_link_libraries( password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) +target_link_libraries( + deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} + ${TEST_QT_MODULES} +) add_subdirectory(carddatabase) add_subdirectory(loading_from_clipboard) diff --git a/tests/deck_hash_performance_test.cpp b/tests/deck_hash_performance_test.cpp new file mode 100644 index 000000000..154283e8e --- /dev/null +++ b/tests/deck_hash_performance_test.cpp @@ -0,0 +1,81 @@ +#include "gtest/gtest.h" +#include +#include + +static constexpr int amount = 1e5; +QString repeatDeck; +QString numberDeck; +QString uniquesDeck; +QString uniquesXorDeck; +QString duplicatesDeck; + +TEST(DeckHashTest, RepeatTest) +{ + DeckList decklist(repeatDeck); + for (int i = 0; i < amount; ++i) { + decklist.getDeckHash(); + decklist.refreshDeckHash(); + } + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "5cac19qm") << "The hash does not match!"; +} + +TEST(DeckHashTest, NumberTest) +{ + DeckList decklist(numberDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "e0m38p19") << "The hash does not match!"; +} + +TEST(DeckHashTest, UniquesTest) +{ + DeckList decklist(uniquesDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "88prk025") << "The hash does not match!"; +} + +TEST(DeckHashTest, UniquesTestXor) +{ + DeckList decklist(uniquesXorDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "hkn6q4pf") << "The hash does not match!"; +} + +TEST(DeckHashTest, DuplicatesTest) +{ + DeckList decklist(duplicatesDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "ekt6tg1h") << "The hash does not match!"; +} + +int main(int argc, char **argv) +{ + const QString deckStart = + R"()"; + const QString deckEnd = R"()"; + + repeatDeck = + deckStart + + R"()" + + deckEnd; + numberDeck = deckStart + QString(R"()").arg(amount) + deckEnd; + + QStringList deckString{deckStart}; + QStringList deckStringXor = deckString; + int len = QString::number(amount).length(); + for (int i = 0; i < amount; ++i) { + // creates already sorted list + deckString << R"()"; + // xor in order to mess with sorting + deckStringXor << R"()"; + } + deckString << deckEnd; + deckStringXor << deckEnd; + uniquesDeck = deckString.join(""); + uniquesXorDeck = deckStringXor.join(""); + + duplicatesDeck = deckStart + QString(R"()").repeated(amount) + deckEnd; + + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/loading_from_clipboard/clipboard_testing.cpp b/tests/loading_from_clipboard/clipboard_testing.cpp index 2342bc088..29535c1dc 100644 --- a/tests/loading_from_clipboard/clipboard_testing.cpp +++ b/tests/loading_from_clipboard/clipboard_testing.cpp @@ -3,22 +3,32 @@ #include #include -void testEmpty(const QString &clipboard) +DeckList getDeckList(const QString &clipboard) { - QString cp(clipboard); DeckList deckList; + QString cp(clipboard); QTextStream stream(&cp); // text stream requires local copy deckList.loadFromStream_Plain(stream, false); + return deckList; +} + +void testEmpty(const QString &clipboard) +{ + DeckList deckList = getDeckList(clipboard); ASSERT_TRUE(deckList.getCardList().isEmpty()); } +void testHash(const QString &clipboard, const std::string &hash) +{ + DeckList deckList = getDeckList(clipboard); + + ASSERT_EQ(deckList.getDeckHash().toStdString(), hash); +} + void testDeck(const QString &clipboard, const Result &result) { - QString cp(clipboard); - DeckList deckList; - QTextStream stream(&cp); // text stream requires local copy - deckList.loadFromStream_Plain(stream, false); + DeckList deckList = getDeckList(clipboard); ASSERT_EQ(result.name, deckList.getName().toStdString()); ASSERT_EQ(result.comments, deckList.getComments().toStdString()); diff --git a/tests/loading_from_clipboard/clipboard_testing.h b/tests/loading_from_clipboard/clipboard_testing.h index 5e9cff915..41d8ea794 100644 --- a/tests/loading_from_clipboard/clipboard_testing.h +++ b/tests/loading_from_clipboard/clipboard_testing.h @@ -21,7 +21,7 @@ struct Result }; void testEmpty(const QString &clipboard); - +void testHash(const QString &clipboard, const std::string &hash); void testDeck(const QString &clipboard, const Result &result); #endif // CLIPBOARD_TESTING_H diff --git a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp index 6f9762be9..fcfbb22db 100644 --- a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp +++ b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp @@ -203,6 +203,22 @@ TEST(LoadingFromClipboardTest, emptyMainBoard) testEmpty(clipboard); } +TEST(LoadingFromClipboardTest, emptyHash) +{ + QString clipboard(""); + + testHash(clipboard, "r8sq7riu"); +} + +TEST(LoadingFromClipboardTest, deckHash) +{ + QString clipboard("1 Mountain\n" + "2 Island\n" + "SB: 3 Forest\n"); + + testHash(clipboard, "5cac19qm"); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); From 70f9982c29d0951eb911cab43a3b13c6b4de8cf9 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:58:23 -0800 Subject: [PATCH 11/56] Bump minimum Qt version from 5.8 to 5.15 (#6442) * Bump minimum Qt version from 5.8 to 5.15 * remove version check * remove version checks --- cmake/FindQtRuntime.cmake | 2 +- .../src/client/settings/shortcut_treeview.cpp | 7 +------ cockatrice/src/game/dialogs/dlg_create_token.cpp | 8 -------- .../src/game/player/player_event_handler.cpp | 7 ------- .../board/abstract_graphics_item.cpp | 7 +------ .../card_picture_loader_worker.cpp | 2 -- cockatrice/src/interface/logger.cpp | 15 --------------- .../widgets/server/chat_view/chat_view.cpp | 4 ---- .../widgets/server/chat_view/chat_view.h | 3 --- .../src/interface/widgets/server/games_model.cpp | 4 +--- ...api_response_deck_listings_display_widget.cpp | 3 --- .../widgets/tabs/api/archidekt/tab_archidekt.cpp | 3 --- ...er_api_response_bracket_navigation_widget.cpp | 4 ---- ...der_api_response_budget_navigation_widget.cpp | 4 ---- .../widgets/tabs/api/edhrec/tab_edhrec.cpp | 3 --- .../widgets/tabs/api/edhrec/tab_edhrec_main.cpp | 3 --- cockatrice/src/interface/window_main.cpp | 4 ---- cockatrice/src/main.cpp | 2 -- .../card/database/card_database_querier.cpp | 4 ---- .../models/database/card_set/card_sets_model.cpp | 4 ---- .../network/client/remote/remote_client.cpp | 4 ---- .../server/remote/game/server_cardzone.cpp | 4 ---- .../network/server/remote/game/server_game.cpp | 4 ---- .../network/server/remote/game/server_game.h | 4 ---- servatrice/src/isl_interface.cpp | 8 -------- servatrice/src/servatrice.cpp | 9 --------- servatrice/src/servatrice_database_interface.cpp | 4 ---- servatrice/src/server_logger.cpp | 4 ---- servatrice/src/serversocketinterface.cpp | 16 ---------------- servatrice/src/settingscache.cpp | 7 +------ servatrice/src/smtp/qxtsmtp.cpp | 4 ---- 31 files changed, 5 insertions(+), 156 deletions(-) diff --git a/cmake/FindQtRuntime.cmake b/cmake/FindQtRuntime.cmake index 6be08a694..c205ebdcf 100644 --- a/cmake/FindQtRuntime.cmake +++ b/cmake/FindQtRuntime.cmake @@ -59,7 +59,7 @@ if(Qt6_FOUND) endif() else() find_package( - Qt5 5.8.0 + Qt5 5.15.2 COMPONENTS ${REQUIRED_QT_COMPONENTS} QUIET HINTS ${Qt5_DIR} ) diff --git a/cockatrice/src/client/settings/shortcut_treeview.cpp b/cockatrice/src/client/settings/shortcut_treeview.cpp index 6b329b23d..b909d47ac 100644 --- a/cockatrice/src/client/settings/shortcut_treeview.cpp +++ b/cockatrice/src/client/settings/shortcut_treeview.cpp @@ -150,12 +150,7 @@ void ShortcutTreeView::currentChanged(const QModelIndex ¤t, const QModelIn */ void ShortcutTreeView::updateSearchString(const QString &searchString) { -#if QT_VERSION > QT_VERSION_CHECK(5, 14, 0) - const auto skipEmptyParts = Qt::SkipEmptyParts; -#else - const auto skipEmptyParts = QString::SkipEmptyParts; -#endif - QStringList searchWords = searchString.split(" ", skipEmptyParts); + QStringList searchWords = searchString.split(" ", Qt::SkipEmptyParts); auto escapeRegex = [](const QString &s) { return QRegularExpression::escape(s); }; std::transform(searchWords.begin(), searchWords.end(), searchWords.begin(), escapeRegex); diff --git a/cockatrice/src/game/dialogs/dlg_create_token.cpp b/cockatrice/src/game/dialogs/dlg_create_token.cpp index 1f6f9b08c..836c0c16a 100644 --- a/cockatrice/src/game/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game/dialogs/dlg_create_token.cpp @@ -117,11 +117,7 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa chooseTokenFromDeckRadioButton->setDisabled(true); // No tokens in deck = no need for option } else { chooseTokenFromDeckRadioButton->setChecked(true); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) cardDatabaseDisplayModel->setCardNameSet(QSet(predefinedTokens.begin(), predefinedTokens.end())); -#else - cardDatabaseDisplayModel->setCardNameSet(QSet::fromList(predefinedTokens)); -#endif } auto *tokenChooseLayout = new QVBoxLayout; @@ -223,11 +219,7 @@ void DlgCreateToken::actChooseTokenFromAll(bool checked) void DlgCreateToken::actChooseTokenFromDeck(bool checked) { if (checked) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) cardDatabaseDisplayModel->setCardNameSet(QSet(predefinedTokens.begin(), predefinedTokens.end())); -#else - cardDatabaseDisplayModel->setCardNameSet(QSet::fromList(predefinedTokens)); -#endif } } diff --git a/cockatrice/src/game/player/player_event_handler.cpp b/cockatrice/src/game/player/player_event_handler.cpp index 4cf814d86..331605918 100644 --- a/cockatrice/src/game/player/player_event_handler.cpp +++ b/cockatrice/src/game/player/player_event_handler.cpp @@ -78,14 +78,7 @@ void PlayerEventHandler::eventShuffle(const Event_Shuffle &event) void PlayerEventHandler::eventRollDie(const Event_RollDie &event) { if (!event.values().empty()) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QList rolls(event.values().begin(), event.values().end()); -#else - QList rolls; - for (const auto &value : event.values()) { - rolls.append(value); - } -#endif std::sort(rolls.begin(), rolls.end()); emit logRollDie(player, static_cast(event.sides()), rolls); } else if (event.value()) { diff --git a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp index 8ced8d5bc..05f4a41ab 100644 --- a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp @@ -17,12 +17,7 @@ void AbstractGraphicsItem::paintNumberEllipse(int number, font.setWeight(QFont::Bold); QFontMetrics fm(font); - double w = 1.3 * -#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) - fm.horizontalAdvance(numStr); -#else - fm.width(numStr); -#endif + double w = 1.3 * fm.horizontalAdvance(numStr); double h = fm.height() * 1.3; if (w < h) w = h; diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index 77fddc9de..128b03c95 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -21,9 +21,7 @@ CardPictureLoaderWorker::CardPictureLoaderWorker() // We need a timeout to ensure requests don't hang indefinitely in case of // cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397 // Use Qt's default timeout (30s, as of 2023-02-22) -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) networkManager->setTransferTimeout(); -#endif cache = new QNetworkDiskCache(this); cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath()); cache->setMaximumCacheSize(1024L * 1024L * diff --git a/cockatrice/src/interface/logger.cpp b/cockatrice/src/interface/logger.cpp index 37e07acec..d6df065e8 100644 --- a/cockatrice/src/interface/logger.cpp +++ b/cockatrice/src/interface/logger.cpp @@ -57,17 +57,10 @@ void Logger::openLogfileSession() return; } fileStream.setDevice(&fileHandle); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) fileStream << "Log session started at " << QDateTime::currentDateTime().toString() << Qt::endl; fileStream << getClientVersion() << Qt::endl; fileStream << getSystemArchitecture() << Qt::endl; fileStream << getClientInstallInfo() << Qt::endl; -#else - fileStream << "Log session started at " << QDateTime::currentDateTime().toString() << endl; - fileStream << getClientVersion() << endl; - fileStream << getSystemArchitecture() << endl; - fileStream << getClientInstallInfo() << endl; -#endif logToFileEnabled = true; } @@ -77,11 +70,7 @@ void Logger::closeLogfileSession() return; logToFileEnabled = false; -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) fileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << Qt::endl; -#else - fileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << endl; -#endif fileHandle.close(); } @@ -103,11 +92,7 @@ void Logger::internalLog(const QString &message) std::cerr << message.toStdString() << std::endl; // Print to stdout if (logToFileEnabled) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) fileStream << message << Qt::endl; // Print to fileStream -#else - fileStream << message << endl; // Print to fileStream -#endif } } diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index 694085771..50375c936 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -236,11 +236,7 @@ void ChatView::appendMessage(QString message, cursor.setCharFormat(defaultFormat); bool mentionEnabled = SettingsCache::instance().getChatMention(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) highlightedWords = SettingsCache::instance().getHighlightWords().split(' ', Qt::SkipEmptyParts); -#else - highlightedWords = SettingsCache::instance().getHighlightWords().split(' ', QString::SkipEmptyParts); -#endif // parse the message while (message.size()) { diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h index 00fcc4f5e..6cf8370ed 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -28,9 +28,6 @@ class UserListProxy; class UserMessagePosition { public: -#if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0)) - UserMessagePosition() = default; // older qt versions require a default constructor to use in containers -#endif UserMessagePosition(QTextCursor &cursor); int relativePosition; QTextBlock block; diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index cdac71b28..05d363fee 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -421,10 +421,8 @@ bool GamesProxyModel::filterAcceptsRow(int sourceRow) const { #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) static const QDate epochDate = QDateTime::fromSecsSinceEpoch(0, QTimeZone::UTC).date(); -#elif (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) - static const QDate epochDate = QDateTime::fromSecsSinceEpoch(0, Qt::UTC).date(); #else - static const QDate epochDate = QDateTime::fromTime_t(0, Qt::UTC).date(); + static const QDate epochDate = QDateTime::fromSecsSinceEpoch(0, Qt::UTC).date(); #endif auto *model = qobject_cast(sourceModel()); if (!model) diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp index 5747ce90d..8746093c7 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp @@ -16,10 +16,7 @@ ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsD flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); imageNetworkManager = new QNetworkAccessManager(this); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) imageNetworkManager->setTransferTimeout(); // Use Qt's default timeout -#endif - imageNetworkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); // Add widgets for deck listings diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index e6615fa7b..3769cd9a2 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -27,10 +27,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { networkManager = new QNetworkAccessManager(this); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) networkManager->setTransferTimeout(); // Use Qt's default timeout -#endif - networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *))); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp index 2bc727bd2..c3ab23e41 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp @@ -53,11 +53,7 @@ void EdhrecCommanderApiResponseBracketNavigationWidget::applyOptionsFromUrl(cons } // Expecting something like: "commanders/the-ur-dragon/core/expensive" -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList parts = cleanedUrl.split('/', Qt::SkipEmptyParts); -#else - QStringList parts = cleanedUrl.split('/', QString::SkipEmptyParts); -#endif if (parts.size() < 2) { return; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp index 8470e6b3f..1845c020d 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp @@ -53,11 +53,7 @@ void EdhrecCommanderApiResponseBudgetNavigationWidget::applyOptionsFromUrl(const } // Expecting something like: "commanders/the-ur-dragon/core/expensive" -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList parts = cleanedUrl.split('/', Qt::SkipEmptyParts); -#else - QStringList parts = cleanedUrl.split('/', QString::SkipEmptyParts); -#endif if (parts.size() < 2) { return; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp index 93dbd6bc5..10389f4eb 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp @@ -15,10 +15,7 @@ TabEdhRec::TabEdhRec(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { networkManager = new QNetworkAccessManager(this); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) networkManager->setTransferTimeout(); // Use Qt's default timeout -#endif - networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); connect(networkManager, &QNetworkAccessManager::finished, this, &TabEdhRec::processApiJson); } diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp index fb26732f6..f861d8afd 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp @@ -37,10 +37,7 @@ static bool canBeCommander(const CardInfoPtr &cardInfo) TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) { networkManager = new QNetworkAccessManager(this); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) networkManager->setTransferTimeout(); // Use Qt's default timeout -#endif - networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *))); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 4c9922c80..41113185c 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -499,11 +499,7 @@ QString MainWindow::extractInvalidUsernameMessage(QString &in) if (words.startsWith("\n")) { out += tr("no unacceptable language as specified by these server rules:", "note that the following lines will not be translated"); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) for (QString &line : words.split("\n", Qt::SkipEmptyParts)) { -#else - for (QString &line : words.split("\n", QString::SkipEmptyParts)) { -#endif out += "
  • " + line + "
  • "; } } else { diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 29dacee74..7092a3fd7 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -260,10 +260,8 @@ int main(int argc, char *argv[]) qCInfo(MainLog) << "MainWindow constructor finished"; ui.setWindowIcon(QPixmap("theme:cockatrice")); -#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) // set name of the app desktop file; used by wayland to load the window icon QGuiApplication::setDesktopFileName("cockatrice"); -#endif SettingsCache::instance().setClientID(generateClientID()); diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp index b2a675b99..26e515a2d 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -328,11 +328,7 @@ QMap CardDatabaseQuerier::getAllSubCardTypesWithCount() const QStringList parts = type.split(" — "); if (parts.size() > 1) { // Ensure there are subtypes -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); -#else - QStringList subtypes = parts[1].split(" ", QString::SkipEmptyParts); -#endif for (const QString &subtype : subtypes) { typeCounts[subtype]++; 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 2af815246..b678e8276 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 @@ -283,11 +283,7 @@ bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &source auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent); auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) const auto filter = filterRegularExpression(); -#else - const auto filter = filterRegExp(); -#endif return (sourceModel()->data(typeIndex).toString().contains(filter) || sourceModel()->data(nameIndex).toString().contains(filter) || diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index c0167a875..4b5d3f1b8 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -42,11 +42,7 @@ RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSe connect(socket, &QTcpSocket::connected, this, &RemoteClient::slotConnected); connect(socket, &QTcpSocket::readyRead, this, &RemoteClient::readData); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) connect(socket, &QTcpSocket::errorOccurred, this, &RemoteClient::slotSocketError); -#else - connect(socket, qOverload(&QTcpSocket::error), this, &RemoteClient::slotSocketError); -#endif websocket = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this); connect(websocket, &QWebSocket::binaryMessageReceived, this, &RemoteClient::websocketMessageReceived); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp index 68dcadd35..f2a35e548 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp @@ -63,11 +63,7 @@ void Server_CardZone::shuffle(int start, int end) for (int i = end; i > start; i--) { int j = rng->rand(start, i); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) cards.swapItemsAt(j, i); -#else - cards.swap(j, i); -#endif } playersWithWritePermission.clear(); } 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 e53838695..5cd6c8bf8 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -73,11 +73,7 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal), shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) gameMutex() -#else - gameMutex(QMutex::Recursive) -#endif { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); 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 033542fad..64374019c 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -90,11 +90,7 @@ private slots: void doStartGameIfReady(bool forceStartGame = false); public: -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) mutable QRecursiveMutex gameMutex; -#else - mutable QMutex gameMutex; -#endif Server_Game(const ServerInfo_User &_creatorInfo, int _gameId, const QString &_description, diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 2269ff314..bcf71a98a 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -112,11 +112,7 @@ void IslInterface::initServer() socket->startServerEncryption(); if (!socket->waitForEncrypted(5000)) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) QList sslErrors(socket->sslHandshakeErrors()); -#else - QList sslErrors(socket->sslErrors()); -#endif if (sslErrors.isEmpty()) qDebug() << "[ISL] SSL handshake timeout, terminating connection"; else @@ -193,11 +189,7 @@ void IslInterface::initClient() return; } if (!socket->waitForEncrypted(5000)) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) QList sslErrors(socket->sslHandshakeErrors()); -#else - QList sslErrors(socket->sslErrors()); -#endif if (sslErrors.isEmpty()) qDebug() << "[ISL] SSL handshake timeout, terminating connection"; else diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index ea3d783bd..410bf4ed9 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -254,13 +254,8 @@ bool Servatrice::initServer() qDebug() << "Accept registered users only:" << getRegOnlyServerEnabled(); qDebug() << "Registration enabled:" << getRegistrationEnabled(); if (getRegistrationEnabled()) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList emailBlackListFilters = getEmailBlackList().split(",", Qt::SkipEmptyParts); QStringList emailWhiteListFilters = getEmailWhiteList().split(",", Qt::SkipEmptyParts); -#else - QStringList emailBlackListFilters = getEmailBlackList().split(",", QString::SkipEmptyParts); - QStringList emailWhiteListFilters = getEmailWhiteList().split(",", QString::SkipEmptyParts); -#endif qDebug() << "Email blacklist:" << emailBlackListFilters; qDebug() << "Email whitelist:" << emailWhiteListFilters; qDebug() << "Require email address to register:" << getRequireEmailForRegistrationEnabled(); @@ -564,11 +559,7 @@ void Servatrice::setRequiredFeatures(const QString &featureList) FeatureSet features; serverRequiredFeatureList.clear(); features.initalizeFeatureList(serverRequiredFeatureList); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList listReqFeatures = featureList.split(",", Qt::SkipEmptyParts); -#else - QStringList listReqFeatures = featureList.split(",", QString::SkipEmptyParts); -#endif if (!listReqFeatures.isEmpty()) for (const QString &reqFeature : listReqFeatures) { features.enableRequiredFeature(serverRequiredFeatureList, reqFeature); diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index bad16bec3..fa9b14f31 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -163,11 +163,7 @@ bool Servatrice_DatabaseInterface::usernameIsValid(const QString &user, QString bool allowPunctuationPrefix = settingsCache->value("users/allowpunctuationprefix", false).toBool(); QString allowedPunctuation = settingsCache->value("users/allowedpunctuation", "_").toString(); QString disallowedWordsStr = settingsCache->value("users/disallowedwords", "").toString(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList disallowedWords = disallowedWordsStr.split(",", Qt::SkipEmptyParts); -#else - QStringList disallowedWords = disallowedWordsStr.split(",", QString::SkipEmptyParts); -#endif disallowedWords.removeDuplicates(); QVariant displayDisallowedWords = settingsCache->value("users/displaydisallowedwords"); QString disallowedRegExpStr; diff --git a/servatrice/src/server_logger.cpp b/servatrice/src/server_logger.cpp index 9e40ac7b4..79d8cdfe0 100644 --- a/servatrice/src/server_logger.cpp +++ b/servatrice/src/server_logger.cpp @@ -57,11 +57,7 @@ void ServerLogger::logMessage(const QString &message, void *caller) // filter out all log entries based on values in configuration file bool shouldWeWriteLog = settingsCache->value("server/writelog", 1).toBool(); QString logFilters = settingsCache->value("server/logfilters").toString(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList listlogFilters = logFilters.split(",", Qt::SkipEmptyParts); -#else - QStringList listlogFilters = logFilters.split(",", QString::SkipEmptyParts); -#endif bool shouldWeSkipLine = false; if (!shouldWeWriteLog) diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index bc686ad28..619d36d3a 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -972,11 +972,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnList(const Comma Response_WarnList *re = new Response_WarnList; QString officialWarnings = settingsCache->value("server/officialwarnings").toString(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList warningsList = officialWarnings.split(",", Qt::SkipEmptyParts); -#else - QStringList warningsList = officialWarnings.split(",", QString::SkipEmptyParts); -#endif for (const QString &warning : warningsList) { re->add_warning(warning.toStdString()); } @@ -1172,13 +1168,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C const auto parsedEmailParts = EmailParser::parseEmailAddress(nameFromStdString(cmd.email())); const auto emailUser = parsedEmailParts.first; const auto emailDomain = parsedEmailParts.second; -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) const QStringList emailBlackListFilters = emailBlackList.split(",", Qt::SkipEmptyParts); const QStringList emailWhiteListFilters = emailWhiteList.split(",", Qt::SkipEmptyParts); -#else - const QStringList emailBlackListFilters = emailBlackList.split(",", QString::SkipEmptyParts); - const QStringList emailWhiteListFilters = emailWhiteList.split(",", QString::SkipEmptyParts); -#endif bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool(); if (requireEmailForRegistration && emailUser.isEmpty()) { @@ -1930,13 +1921,8 @@ TcpServerSocketInterface::TcpServerSocketInterface(Servatrice *_server, socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); -#else - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, - SLOT(catchSocketError(QAbstractSocket::SocketError))); -#endif } TcpServerSocketInterface::~TcpServerSocketInterface() @@ -2109,10 +2095,8 @@ void WebsocketServerSocketInterface::initConnection(void *_socket) } socket = (QWebSocket *)_socket; socket->setParent(this); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) // https://bugreports.qt.io/browse/QTBUG-70693 socket->setMaxAllowedIncomingMessageSize(1500000); // 1.5MB -#endif address = socket->peerAddress(); diff --git a/servatrice/src/settingscache.cpp b/servatrice/src/settingscache.cpp index 14bfaebfb..f6dcd5fc8 100644 --- a/servatrice/src/settingscache.cpp +++ b/servatrice/src/settingscache.cpp @@ -11,12 +11,7 @@ SettingsCache::SettingsCache(const QString &fileName, QSettings::Format format, // first, figure out if we are running in portable mode isPortableBuild = QFile::exists(qApp->applicationDirPath() + "/portable.dat"); - QStringList disallowedRegExpStr = -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - value("users/disallowedregexp", "").toString().split(",", Qt::SkipEmptyParts); -#else - value("users/disallowedregexp", "").toString().split(",", QString::SkipEmptyParts); -#endif + QStringList disallowedRegExpStr = value("users/disallowedregexp", "").toString().split(",", Qt::SkipEmptyParts); disallowedRegExpStr.removeDuplicates(); for (const QString ®ExpStr : disallowedRegExpStr) { disallowedRegExp.append(QRegularExpression(QString("\\A%1\\z").arg(regExpStr))); diff --git a/servatrice/src/smtp/qxtsmtp.cpp b/servatrice/src/smtp/qxtsmtp.cpp index 951492d7a..6326b101d 100644 --- a/servatrice/src/smtp/qxtsmtp.cpp +++ b/servatrice/src/smtp/qxtsmtp.cpp @@ -334,11 +334,7 @@ void QxtSmtpPrivate::authenticate() state = Authenticated; emit qxt_p().authenticated(); } else { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList auth = extensions["AUTH"].toUpper().split(' ', Qt::SkipEmptyParts); -#else - QStringList auth = extensions["AUTH"].toUpper().split(' ', QString::SkipEmptyParts); -#endif if (auth.contains("CRAM-MD5")) { authCramMD5(); } else if (auth.contains("PLAIN")) { From ca3f6bba0234ba098d49cd7cd91c0659bcd813dd Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 26 Dec 2025 04:29:35 -0800 Subject: [PATCH 12/56] [Refactor] Move prev/next card logic out of PrintingSelector (#6450) --- .../deck_editor_deck_dock_widget.cpp | 46 ++++++++++++++++ .../deck_editor_deck_dock_widget.h | 3 ++ ...k_editor_printing_selector_dock_widget.cpp | 4 ++ .../printing_selector/printing_selector.cpp | 52 ------------------- .../printing_selector/printing_selector.h | 13 +++-- ...rinting_selector_card_selection_widget.cpp | 4 +- 6 files changed, 65 insertions(+), 57 deletions(-) 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 57d34be00..8ded2c50a 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 @@ -548,6 +548,52 @@ void DeckEditorDeckDockWidget::cleanDeck() deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); } +void DeckEditorDeckDockWidget::selectPrevCard() +{ + changeSelectedCard(-1); +} + +void DeckEditorDeckDockWidget::selectNextCard() +{ + changeSelectedCard(1); +} + +/** + * @brief Selects a card based on the change direction. + * + * @param changeBy The direction to change, -1 for previous, 1 for next. + */ +void DeckEditorDeckDockWidget::changeSelectedCard(int changeBy) +{ + if (changeBy == 0) { + return; + } + + // Get the current index of the selected item + auto deckViewCurrentIndex = deckView->currentIndex(); + + auto nextIndex = deckViewCurrentIndex.siblingAtRow(deckViewCurrentIndex.row() + changeBy); + if (!nextIndex.isValid()) { + nextIndex = deckViewCurrentIndex; + + // Increment to the next valid index, skipping header rows + AbstractDecklistNode *node; + do { + if (changeBy > 0) { + nextIndex = deckView->indexBelow(nextIndex); + } else { + nextIndex = deckView->indexAbove(nextIndex); + } + node = static_cast(nextIndex.internalPointer()); + } while (node && node->isDeckHeader()); + } + + if (nextIndex.isValid()) { + deckView->setCurrentIndex(nextIndex); + deckView->setFocus(Qt::FocusReason::MouseFocusReason); + } +} + /** * @brief Expands all parents of the given index. * @param sourceIndex The index to expand (model source index) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 30f752203..a50434f0b 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -56,6 +56,8 @@ public: public slots: void cleanDeck(); + void selectPrevCard(); + void selectNextCard(); void updateBannerCardComboBox(); void setDeck(const LoadedDeck &_deck); void syncDisplayWidgetsToModel(); @@ -122,6 +124,7 @@ private slots: void updateShowBannerCardComboBox(bool visible); void updateShowTagsWidget(bool visible); void syncBannerCardComboBoxSelectionWithDeck(); + void changeSelectedCard(int changeBy); void recursiveExpand(const QModelIndex &parent); void expandAll(); }; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp index 0c068e043..03760c22d 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp @@ -33,6 +33,10 @@ void DeckEditorPrintingSelectorDockWidget::createPrintingSelectorDock() installEventFilter(deckEditor); connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged); + connect(printingSelector, &PrintingSelector::prevCardRequested, deckEditor->getDeckDockWidget(), + &DeckEditorDeckDockWidget::selectPrevCard); + connect(printingSelector, &PrintingSelector::nextCardRequested, deckEditor->getDeckDockWidget(), + &DeckEditorDeckDockWidget::selectNextCard); } void DeckEditorPrintingSelectorDockWidget::retranslateUi() diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index f27101b93..f6851a9c7 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -138,58 +138,6 @@ void PrintingSelector::setCard(const CardInfoPtr &newCard, const QString &_curre flowWidget->repaint(); } -/** - * @brief Selects the previous card in the list. - */ -void PrintingSelector::selectPreviousCard() -{ - selectCard(-1); -} - -/** - * @brief Selects the next card in the list. - */ -void PrintingSelector::selectNextCard() -{ - selectCard(1); -} - -/** - * @brief Selects a card based on the change direction. - * - * @param changeBy The direction to change, -1 for previous, 1 for next. - */ -void PrintingSelector::selectCard(const int changeBy) -{ - if (changeBy == 0) { - return; - } - - // Get the current index of the selected item - auto deckViewCurrentIndex = deckView->currentIndex(); - - auto nextIndex = deckViewCurrentIndex.siblingAtRow(deckViewCurrentIndex.row() + changeBy); - if (!nextIndex.isValid()) { - nextIndex = deckViewCurrentIndex; - - // Increment to the next valid index, skipping header rows - AbstractDecklistNode *node; - do { - if (changeBy > 0) { - nextIndex = deckView->indexBelow(nextIndex); - } else { - nextIndex = deckView->indexAbove(nextIndex); - } - node = static_cast(nextIndex.internalPointer()); - } while (node && node->isDeckHeader()); - } - - if (nextIndex.isValid()) { - deckView->setCurrentIndex(nextIndex); - deckView->setFocus(Qt::FocusReason::MouseFocusReason); - } -} - /** * @brief Loads and displays all sets for the current selected card. */ diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index fbe3b5b06..0de67619f 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -48,13 +48,21 @@ public: public slots: void retranslateUi(); void updateDisplay(); - void selectPreviousCard(); - void selectNextCard(); void toggleVisibilityNavigationButtons(bool _state); private slots: void printingsInDeckChanged(); +signals: + /** + * Requests the previous card in the list + */ + void prevCardRequested(); + /** + * Requests the next card in the list + */ + void nextCardRequested(); + private: QVBoxLayout *layout; SettingsButtonWidget *displayOptionsWidget; @@ -73,7 +81,6 @@ private: QString currentZone; QTimer *widgetLoadingBufferTimer; int currentIndex = 0; - void selectCard(int changeBy); }; #endif // PRINTING_SELECTOR_H diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp index fc17cecd0..317ce83b6 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp @@ -42,8 +42,8 @@ PrintingSelectorCardSelectionWidget::PrintingSelectorCardSelectionWidget(Printin */ void PrintingSelectorCardSelectionWidget::connectSignals() { - connect(previousCardButton, &QPushButton::clicked, parent, &PrintingSelector::selectPreviousCard); - connect(nextCardButton, &QPushButton::clicked, parent, &PrintingSelector::selectNextCard); + connect(previousCardButton, &QPushButton::clicked, parent, &PrintingSelector::prevCardRequested); + connect(nextCardButton, &QPushButton::clicked, parent, &PrintingSelector::nextCardRequested); } void PrintingSelectorCardSelectionWidget::selectSetForCards() From 96c82a03771b1e74b686a7e8e8a896a08f86ecff Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 29 Dec 2025 03:03:44 -0800 Subject: [PATCH 13/56] [Refactor] Clean up some PrintingSelector widgets (#6451) * remove currentZone from PrintingSelector * don't store constructor args in fields if they're just passed * simplify some methods * refactor * clean up initializeFormats * more refactoring in CardAmountWidget --- .../deck_editor_deck_dock_widget.cpp | 7 +- .../dialogs/dlg_select_set_for_cards.cpp | 30 +++++--- .../all_zones_card_amount_widget.cpp | 3 +- .../all_zones_card_amount_widget.h | 4 - .../printing_selector/card_amount_widget.cpp | 76 ++++++++++--------- .../printing_selector/printing_selector.cpp | 8 +- .../printing_selector/printing_selector.h | 3 +- .../printing_selector_card_display_widget.cpp | 25 +++--- .../printing_selector_card_display_widget.h | 17 ++--- .../printing_selector_card_overlay_widget.cpp | 15 ++-- .../printing_selector_card_overlay_widget.h | 3 - ...e_and_collectors_number_display_widget.cpp | 3 +- .../widgets/tabs/abstract_tab_deck_editor.cpp | 2 +- .../widgets/tabs/tab_deck_editor.cpp | 3 +- .../tab_deck_editor_visual.cpp | 3 +- 15 files changed, 92 insertions(+), 110 deletions(-) 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 8ded2c50a..409d43e64 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 @@ -284,16 +284,15 @@ void DeckEditorDeckDockWidget::createDeckDock() void DeckEditorDeckDockWidget::initializeFormats() { - QMap allFormats = CardDatabaseManager::query()->getAllFormatsWithCount(); + QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys(); formatComboBox->clear(); // Remove "Loading Database..." formatComboBox->setEnabled(true); // Populate with formats formatComboBox->addItem("", ""); - for (auto it = allFormats.constBegin(); it != allFormats.constEnd(); ++it) { - QString displayText = QString("%1").arg(it.key()); - formatComboBox->addItem(displayText, it.key()); // store the raw key in itemData + for (auto formatName : allFormats) { + formatComboBox->addItem(formatName, formatName); // store the raw key in itemData } if (!deckModel->getDeckList()->getGameFormat().isEmpty()) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp index 656abbfdc..587407065 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp @@ -143,6 +143,22 @@ void DlgSelectSetForCards::retranslateUi() setAllToPreferredButton->setText(tr("Set all to preferred")); } +static bool swapPrinting(DeckListModel *model, const QString &modifiedSet, const QString &cardName) +{ + QModelIndex idx = model->findCard(cardName, DECK_ZONE_MAIN); + if (!idx.isValid()) { + return false; + } + int amount = model->data(idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT), Qt::DisplayRole).toInt(); + model->removeRow(idx.row(), idx.parent()); + CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(cardName); + PrintingInfo printing = CardDatabaseManager::query()->getSpecificPrinting(cardName, modifiedSet, ""); + for (int i = 0; i < amount; i++) { + model->addCard(ExactCard(cardInfo, printing), DECK_ZONE_MAIN); + } + return true; +} + void DlgSelectSetForCards::actOK() { QMap modifiedSetsAndCardsMap = getModifiedCards(); @@ -155,20 +171,10 @@ void DlgSelectSetForCards::actOK() for (QString modifiedSet : modifiedSetsAndCardsMap.keys()) { for (QString card : modifiedSetsAndCardsMap.value(modifiedSet)) { - QModelIndex find_card = model->findCard(card, DECK_ZONE_MAIN); - if (!find_card.isValid()) { - continue; - } - int amount = - model->data(find_card.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT), Qt::DisplayRole).toInt(); - model->removeRow(find_card.row(), find_card.parent()); - CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(card); - PrintingInfo printing = CardDatabaseManager::query()->getSpecificPrinting(card, modifiedSet, ""); - for (int i = 0; i < amount; i++) { - model->addCard(ExactCard(cardInfo, printing), DECK_ZONE_MAIN); - } + swapPrinting(model, modifiedSet, card); } } + if (!modifiedSetsAndCardsMap.isEmpty()) { emit deckModified(); } 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 5b67ae727..d8bd88b37 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 @@ -23,8 +23,7 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, QTreeView *deckView, QSlider *cardSizeSlider, const ExactCard &rootCard) - : QWidget(parent), deckEditor(deckEditor), deckModel(deckModel), deckView(deckView), cardSizeSlider(cardSizeSlider), - rootCard(rootCard) + : QWidget(parent), cardSizeSlider(cardSizeSlider) { layout = new QVBoxLayout(this); layout->setAlignment(Qt::AlignHCenter); 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 1a8ec8bfd..6ce10cf2e 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 @@ -36,11 +36,7 @@ public slots: private: QVBoxLayout *layout; - AbstractTabDeckEditor *deckEditor; - DeckListModel *deckModel; - QTreeView *deckView; QSlider *cardSizeSlider; - ExactCard rootCard; QLabel *zoneLabelMainboard; CardAmountWidget *buttonBoxMainboard; QLabel *zoneLabelSideboard; 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 7c5804c37..7371976eb 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -137,6 +137,31 @@ void CardAmountWidget::updateCardCount() layout->activate(); } +static QModelIndex addAndReplacePrintings(DeckListModel *model, + const QModelIndex &existing, + const ExactCard &rootCard, + const QString &zone, + int extraCopies, + bool replaceProviderless) +{ + auto newCardIndex = model->addCard(rootCard, zone); + if (!newCardIndex.isValid()) { + return {}; + } + + // Check if a card without a providerId already exists in the deckModel and replace it, if so. + if (existing.isValid() && existing != newCardIndex && replaceProviderless) { + for (int i = 0; i < extraCopies; i++) { + model->addCard(rootCard, zone); + } + model->removeRow(existing.row(), existing.parent()); + } + + // Set Index and Focus as if the user had just clicked the new card and modify the deckEditor saveState + return model->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), + rootCard.getPrinting().getProperty("num")); +} + /** * @brief Adds a printing of the card to the specified zone (Mainboard or Sideboard). * @@ -144,26 +169,24 @@ void CardAmountWidget::updateCardCount() */ void CardAmountWidget::addPrinting(const QString &zone) { - int addedCount = 1; // Check if we will need to add extra copies due to replacing copies without providerIds QModelIndex existing = deckModel->findCard(rootCard.getName(), zone); + int extraCopies = 0; bool replacingProviderless = false; if (existing.isValid()) { - QString providerId = + QString foundProviderId = existing.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - if (providerId.isEmpty()) { + if (foundProviderId.isEmpty()) { int amount = existing.data(Qt::DisplayRole).toInt(); extraCopies = amount - 1; // One less because we *always* add one replacingProviderless = true; } } - addedCount += extraCopies; - QString reason = QString("Added %1 copies of '%2 (%3) %4' to %5 [ProviderID: %6]%7") - .arg(addedCount) + .arg(1 + extraCopies) .arg(rootCard.getName()) .arg(rootCard.getPrinting().getSet()->getShortName()) .arg(rootCard.getPrinting().getProperty("num")) @@ -174,26 +197,13 @@ void CardAmountWidget::addPrinting(const QString &zone) emit deckModified(reason); // Add the card and expand the list UI - auto newCardIndex = deckModel->addCard(rootCard, zone); + auto newCardIndex = addAndReplacePrintings(deckModel, existing, rootCard, zone, extraCopies, replacingProviderless); - // Check if a card without a providerId already exists in the deckModel and replace it, if so. - QString foundProviderId = - existing.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - if (existing.isValid() && existing != newCardIndex && foundProviderId == "") { - auto amount = existing.data(Qt::DisplayRole); - for (int i = 0; i < amount.toInt() - 1; i++) { - deckModel->addCard(rootCard, zone); - } - deckModel->removeRow(existing.row(), existing.parent()); + if (newCardIndex.isValid()) { + deckView->setCurrentIndex(newCardIndex); + deckView->setFocus(Qt::FocusReason::MouseFocusReason); + deckEditor->setModified(true); } - - // Set Index and Focus as if the user had just clicked the new card and modify the deckEditor saveState - newCardIndex = deckModel->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), - rootCard.getPrinting().getProperty("num")); - - deckView->setCurrentIndex(newCardIndex); - deckView->setFocus(Qt::FocusReason::MouseFocusReason); - deckEditor->setModified(true); } /** @@ -259,22 +269,14 @@ void CardAmountWidget::decrementCardHelper(const QString &zone) */ int CardAmountWidget::countCardsInZone(const QString &deckZone) { - if (rootCard.getPrinting().getUuid().isEmpty()) { - return 0; // Cards without uuids/providerIds CANNOT match another card, they are undefined for us. - } + QString uuid = rootCard.getPrinting().getUuid(); - if (!deckModel) { - return -1; + if (uuid.isEmpty()) { + return 0; // Cards without uuids/providerIds CANNOT match another card, they are undefined for us. } QList cards = deckModel->getCardsForZone(deckZone); - int count = 0; - for (auto currentCard : cards) { - if (currentCard.getPrinting().getUuid() == rootCard.getPrinting().getProperty("uuid")) { - count++; - } - } - - return count; + return std::count_if(cards.cbegin(), cards.cend(), + [&uuid](const ExactCard &card) { return card.getPrinting().getUuid() == uuid; }); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index f6851a9c7..e64d6a009 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -115,9 +115,8 @@ void PrintingSelector::updateDisplay() * @brief Sets the current card for the selector and updates the display. * * @param newCard The new card to set. - * @param _currentZone The current zone the card is in. */ -void PrintingSelector::setCard(const CardInfoPtr &newCard, const QString &_currentZone) +void PrintingSelector::setCard(const CardInfoPtr &newCard) { if (newCard.isNull()) { return; @@ -129,7 +128,6 @@ void PrintingSelector::setCard(const CardInfoPtr &newCard, const QString &_curre } selectedCard = newCard; - currentZone = _currentZone; if (isVisible()) { updateDisplay(); } @@ -166,8 +164,8 @@ void PrintingSelector::getAllSetsForCurrentCard() connect(widgetLoadingBufferTimer, &QTimer::timeout, this, [=, this]() mutable { for (int i = 0; i < BATCH_SIZE && currentIndex < printingsToUse.size(); ++i, ++currentIndex) { auto card = ExactCard(selectedCard, printingsToUse[currentIndex]); - auto *cardDisplayWidget = new PrintingSelectorCardDisplayWidget( - this, deckEditor, deckModel, deckView, cardSizeWidget->getSlider(), card, currentZone); + auto *cardDisplayWidget = new PrintingSelectorCardDisplayWidget(this, deckEditor, deckModel, deckView, + cardSizeWidget->getSlider(), card); flowWidget->addWidget(cardDisplayWidget); cardDisplayWidget->clampSetNameToPicture(); connect(cardDisplayWidget, &PrintingSelectorCardDisplayWidget::cardPreferenceChanged, this, diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index 0de67619f..e34ce3fe6 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -33,7 +33,7 @@ class PrintingSelector : public QWidget public: PrintingSelector(QWidget *parent, AbstractTabDeckEditor *deckEditor); - void setCard(const CardInfoPtr &newCard, const QString &_currentZone); + void setCard(const CardInfoPtr &newCard); void getAllSetsForCurrentCard(); [[nodiscard]] DeckListModel *getDeckModel() const { @@ -78,7 +78,6 @@ private: DeckListModel *deckModel; QTreeView *deckView; CardInfoPtr selectedCard; - QString currentZone; QTimer *widgetLoadingBufferTimer; int currentIndex = 0; }; 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 24c45a7e3..86d6659a8 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 @@ -17,22 +17,19 @@ * display. * * @param parent The parent widget for this display. - * @param _deckEditor The TabDeckEditor instance for deck management. - * @param _deckModel The DeckListModel instance providing deck data. - * @param _deckView The QTreeView instance displaying the deck. - * @param _cardSizeSlider The slider controlling the size of the displayed card. - * @param _rootCard The root card object, representing the card to be displayed. - * @param _currentZone The current zone in which the card is located. + * @param deckEditor The TabDeckEditor instance for deck management. + * @param deckModel The DeckListModel instance providing deck data. + * @param deckView The QTreeView instance displaying the deck. + * @param cardSizeSlider The slider controlling the size of the displayed card. + * @param rootCard The root card object, representing the card to be displayed. */ PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *parent, - AbstractTabDeckEditor *_deckEditor, - DeckListModel *_deckModel, - QTreeView *_deckView, - QSlider *_cardSizeSlider, - const ExactCard &_rootCard, - QString &_currentZone) - : QWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), deckView(_deckView), - cardSizeSlider(_cardSizeSlider), rootCard(_rootCard), currentZone(_currentZone) + AbstractTabDeckEditor *deckEditor, + DeckListModel *deckModel, + QTreeView *deckView, + QSlider *cardSizeSlider, + const ExactCard &rootCard) + : QWidget(parent) { layout = new QVBoxLayout(this); setLayout(layout); 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 2f6bd6920..608c2df5c 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 @@ -20,12 +20,11 @@ class PrintingSelectorCardDisplayWidget : public QWidget public: PrintingSelectorCardDisplayWidget(QWidget *parent, - AbstractTabDeckEditor *_deckEditor, - DeckListModel *_deckModel, - QTreeView *_deckView, - QSlider *_cardSizeSlider, - const ExactCard &_rootCard, - QString &_currentZone); + AbstractTabDeckEditor *deckEditor, + DeckListModel *deckModel, + QTreeView *deckView, + QSlider *cardSizeSlider, + const ExactCard &rootCard); public slots: void clampSetNameToPicture(); @@ -36,12 +35,6 @@ signals: private: QVBoxLayout *layout; SetNameAndCollectorsNumberDisplayWidget *setNameAndCollectorsNumberDisplayWidget; - AbstractTabDeckEditor *deckEditor; - DeckListModel *deckModel; - QTreeView *deckView; - QSlider *cardSizeSlider; - ExactCard rootCard; - QString currentZone; PrintingSelectorCardOverlayWidget *overlayWidget; }; 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 5f3a44b30..04ab07a59 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 @@ -22,19 +22,18 @@ * * @param parent The parent widget for this overlay. * @param _deckEditor The TabDeckEditor instance for deck management. - * @param _deckModel The DeckListModel instance providing deck data. - * @param _deckView The QTreeView instance displaying the deck. - * @param _cardSizeSlider The slider controlling the size of the card. + * @param deckModel The DeckListModel instance providing deck data. + * @param deckView The QTreeView instance displaying the deck. + * @param cardSizeSlider The slider controlling the size of the card. * @param _rootCard The root card object that contains information about the card. */ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, - DeckListModel *_deckModel, - QTreeView *_deckView, - QSlider *_cardSizeSlider, + DeckListModel *deckModel, + QTreeView *deckView, + QSlider *cardSizeSlider, const ExactCard &_rootCard) - : QWidget(parent), deckEditor(_deckEditor), deckModel(_deckModel), deckView(_deckView), - cardSizeSlider(_cardSizeSlider), rootCard(_rootCard) + : QWidget(parent), deckEditor(_deckEditor), rootCard(_rootCard) { // Set up the main layout auto *mainLayout = new QVBoxLayout(this); 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 10df9d53e..3bd5ce247 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 @@ -48,9 +48,6 @@ private: AllZonesCardAmountWidget *allZonesCardAmountWidget; QLabel *pinBadge = nullptr; AbstractTabDeckEditor *deckEditor; - DeckListModel *deckModel; - QTreeView *deckView; - QSlider *cardSizeSlider; ExactCard rootCard; }; diff --git a/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp index b1a6a61c1..5962680cd 100644 --- a/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp @@ -13,7 +13,7 @@ SetNameAndCollectorsNumberDisplayWidget::SetNameAndCollectorsNumberDisplayWidget const QString &_setName, const QString &_collectorsNumber, QSlider *_cardSizeSlider) - : QWidget(parent) + : QWidget(parent), cardSizeSlider(_cardSizeSlider) { // Set up the layout for the widget layout = new QVBoxLayout(this); @@ -35,7 +35,6 @@ SetNameAndCollectorsNumberDisplayWidget::SetNameAndCollectorsNumberDisplayWidget collectorsNumber->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); // Store the card size slider and connect its signal to the font size adjustment slot - cardSizeSlider = _cardSizeSlider; connect(cardSizeSlider, &QSlider::valueChanged, this, &SetNameAndCollectorsNumberDisplayWidget::adjustFontSize); // Add labels to the layout 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 8eb1477f1..cb2002199 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -101,7 +101,7 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta void AbstractTabDeckEditor::updateCard(const ExactCard &card) { cardInfoDockWidget->updateCard(card); - printingSelectorDockWidget->printingSelector->setCard(card.getCardPtr(), DECK_ZONE_MAIN); + printingSelectorDockWidget->printingSelector->setCard(card.getCardPtr()); } /** @brief Placeholder: called when the deck changes. */ diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 4fa12259c..4e3d0c57d 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -158,8 +158,7 @@ void TabDeckEditor::refreshShortcuts() */ void TabDeckEditor::showPrintingSelector() { - printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr(), - DECK_ZONE_MAIN); + printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); printingSelectorDockWidget->printingSelector->updateDisplay(); aPrintingSelectorDockVisible->setChecked(true); printingSelectorDockWidget->setVisible(true); 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 57615df94..a124eaa01 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 @@ -266,8 +266,7 @@ bool TabDeckEditorVisual::actSaveDeckAs() /** @brief Shows the printing selector dock and updates it with the current card. */ void TabDeckEditorVisual::showPrintingSelector() { - printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr(), - DECK_ZONE_MAIN); + printingSelectorDockWidget->printingSelector->setCard(cardInfoDockWidget->cardInfo->getCard().getCardPtr()); printingSelectorDockWidget->printingSelector->updateDisplay(); aPrintingSelectorDockVisible->setChecked(true); printingSelectorDockWidget->setVisible(true); From 296866a67525cc3e524c04420afea00729be8799 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 29 Dec 2025 08:19:03 -0800 Subject: [PATCH 14/56] [DeckListModel] Refactor api for offset count (#6454) --- .../deck_editor_deck_dock_widget.cpp | 7 ++----- .../printing_selector/card_amount_widget.cpp | 2 +- .../models/deck_list/deck_list_model.cpp | 12 +----------- .../models/deck_list/deck_list_model.h | 18 +++++------------- 4 files changed, 9 insertions(+), 30 deletions(-) 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 409d43e64..543f1a1c0 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 @@ -831,11 +831,8 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i emit requestDeckHistorySave(reason); - if (isIncrement) { - deckModel->incrementAmountAtIndex(sourceIndex); - } else { - deckModel->decrementAmountAtIndex(sourceIndex); - } + int offset = isIncrement ? 1 : -1; + deckModel->offsetCountAtIndex(sourceIndex, offset); emit deckModified(); } 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 7371976eb..623b797a7 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -257,7 +257,7 @@ void CardAmountWidget::decrementCardHelper(const QString &zone) QModelIndex idx = deckModel->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), rootCard.getPrinting().getProperty("num")); - deckModel->decrementAmountAtIndex(idx); + deckModel->offsetCountAtIndex(idx, -1); deckEditor->setModified(true); } diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 53d08cd9d..130f05c8a 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -443,17 +443,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam return index; } -bool DeckListModel::incrementAmountAtIndex(const QModelIndex &idx) -{ - return offsetAmountAtIndex(idx, 1); -} - -bool DeckListModel::decrementAmountAtIndex(const QModelIndex &idx) -{ - return offsetAmountAtIndex(idx, -1); -} - -bool DeckListModel::offsetAmountAtIndex(const QModelIndex &idx, int offset) +bool DeckListModel::offsetCountAtIndex(const QModelIndex &idx, int offset) { if (!idx.isValid()) { return false; diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index a85542d97..7bb504e60 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -307,19 +307,13 @@ public: QModelIndex addCard(const ExactCard &card, const QString &zoneName); /** - * @brief Increments the `amount` field of the card node at the index by 1. - * @param idx The index of a card node. No-ops if the index is invalid or not a card node + * @brief Changes the `amount` field in the card node at the index by the amount. + * Removes the node if it causes the amount to fall to 0 or below. + * @param idx The index of a card node. No-ops if the index is invalid or not a card node. + * @param offset The amount to change the amount field by. * @return Whether the operation was successful */ - bool incrementAmountAtIndex(const QModelIndex &idx); - - /** - * @brief Decrements the `amount` field of the card node at the index by 1. - * Removes the node if it causes the amount to fall to 0. - * @param idx The index of a card node. No-ops if the index is invalid or not a card node - * @return Whether the operation was successful - */ - bool decrementAmountAtIndex(const QModelIndex &idx); + bool offsetCountAtIndex(const QModelIndex &idx, int offset); /** * @brief Determines the sorted insertion row for a card. @@ -395,8 +389,6 @@ private: const QString &providerId = "", const QString &cardNumber = "") const; - bool offsetAmountAtIndex(const QModelIndex &idx, int offset); - void emitRecursiveUpdates(const QModelIndex &index); void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); From 9d0bb0d51a0cbfb89a3d350561d9bfbae28a1b13 Mon Sep 17 00:00:00 2001 From: Bruno Alexandre Rosa <1791393+brunoalr@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:21:59 -0300 Subject: [PATCH 15/56] fix: manage ccache caches manually on macos (#6449) * fix: manage ccache caches manually on macos * install ccache * fix issues shown by bugbot * readd cache size limit --- .github/workflows/desktop-build.yml | 32 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index b89190c8d..a26e82ee5 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -342,6 +342,11 @@ jobs: name: ${{matrix.os}} ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} needs: configure runs-on: ${{matrix.runner}} + 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: 500M steps: - name: Checkout @@ -356,16 +361,20 @@ jobs: with: msbuild-architecture: x64 - # Using jianmingyong/ccache-action to setup ccache without using brew - # It tries to download a binary of ccache from GitHub Release and falls back to building from source if it fails - name: Setup ccache + if: matrix.use_ccache == 1 && matrix.os == 'macOS' + run: brew install ccache + + - name: Restore compiler cache (ccache) if: matrix.use_ccache == 1 - uses: jianmingyong/ccache-action@v1 + id: ccache_restore + uses: actions/cache/restore@v5 + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: - install-type: "binary" - ccache-key-prefix: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}} - max-size: 500M - gh-token: ${{ secrets.GITHUB_TOKEN }} + path: ${{env.CCACHE_DIR}} + key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}} + restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}- - name: Install Qt ${{matrix.qt_version}} uses: jurplel/install-qt-action@v4 @@ -403,6 +412,15 @@ jobs: TARGET_MACOS_VERSION: ${{ matrix.override_target }} run: .ci/compile.sh --server --test --vcpkg + - name: Save compiler cache (ccache) + if: github.ref == 'refs/heads/master' && matrix.use_ccache == 1 + uses: actions/cache/save@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}} + - name: Sign app bundle if: matrix.os == 'macOS' && matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null) env: From ce4a3bf11825ae01ce7225e6100d0533c0b8ff1f Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 30 Dec 2025 06:16:02 +0100 Subject: [PATCH 16/56] compile in debug mode on ubuntu 22.04 (#6418) * compile in debug mode on ubuntu 22.04 * Update card_info_display_widget.cpp Use c++ instead of c-style cast --------- Co-authored-by: BruebachL <44814898+BruebachL@users.noreply.github.com> --- .github/workflows/desktop-build.yml | 1 - .../src/interface/widgets/cards/card_info_display_widget.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index a26e82ee5..9f4ee4984 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -142,7 +142,6 @@ jobs: - distro: Ubuntu version: 22.04 package: DEB - test: skip # Running tests on all distros is superfluous - distro: Ubuntu version: 24.04 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 819bc57d0..f8b581e33 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_display_widget.cpp @@ -27,7 +27,7 @@ CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *pa layout->addWidget(text, 0, Qt::AlignCenter); setLayout(layout); - setFrameStyle(QFrame::Panel | QFrame::Raised); + setFrameStyle(static_cast(QFrame::Panel) | QFrame::Raised); int pixmapHeight = QGuiApplication::primaryScreen()->geometry().height() / 3; int pixmapWidth = static_cast(pixmapHeight / aspectRatio); From cb2cf31cecf3588159bd8ef31d9f608579ad6632 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 29 Dec 2025 22:13:34 -0800 Subject: [PATCH 17/56] [DeckListModel] Clean up recursive updates (#6457) --- .../models/deck_list/deck_list_model.cpp | 33 +++++++++---------- .../models/deck_list/deck_list_model.h | 18 ++++++---- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 130f05c8a..2ccb95690 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -178,22 +178,6 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const } } -void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent) -{ - int rows = rowCount(parent); - if (rows == 0) - return; - - QModelIndex topLeft = index(0, 0, parent); - QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent); - emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole}); - - for (int r = 0; r < rows; ++r) { - QModelIndex child = index(r, 0, parent); - emitBackgroundUpdates(child); - } -} - QVariant DeckListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const { if ((role != Qt::DisplayRole) || (orientation != Qt::Horizontal)) { @@ -252,6 +236,22 @@ Qt::ItemFlags DeckListModel::flags(const QModelIndex &index) const return result; } +void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent) +{ + int rows = rowCount(parent); + if (rows == 0) + return; + + QModelIndex topLeft = index(0, 0, parent); + QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent); + emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole}); + + for (int r = 0; r < rows; ++r) { + QModelIndex child = index(r, 0, parent); + emitBackgroundUpdates(child); + } +} + void DeckListModel::emitRecursiveUpdates(const QModelIndex &index) { if (!index.isValid()) { @@ -294,7 +294,6 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con deckList->refreshDeckHash(); emit deckHashChanged(); - emit dataChanged(index, index); return true; } diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 7bb504e60..724a4d9d8 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -269,12 +269,6 @@ public: bool setData(const QModelIndex &index, const QVariant &value, int role) override; bool removeRows(int row, int count, const QModelIndex &parent) override; - /** - * Recursively emits the dataChanged signal for all child nodes. - * @param parent The parent node - */ - void emitBackgroundUpdates(const QModelIndex &parent); - /** * @brief Finds a card by name, zone, and optional identifiers. * @param cardName The card's name. @@ -389,7 +383,19 @@ private: const QString &providerId = "", const QString &cardNumber = "") const; + /** + * @brief Recursively emits the dataChanged signal with role as Qt::BackgroundRole for all indices that are children + * of the given node. This is used to update the background color when changing formats. + * @param parent The parent node + */ + void emitBackgroundUpdates(const QModelIndex &parent); + + /** + * @brief Recursively emits the dataChanged signal for the given node and all parent nodes. + * @param index The parent node + */ void emitRecursiveUpdates(const QModelIndex &index); + void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); template T getNode(const QModelIndex &index) const From daa7db7ce310e4a66b57221e636d9ab78c7c5cbd Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 29 Dec 2025 22:43:34 -0800 Subject: [PATCH 18/56] [DeckDockWidget] Fix tree unexpanding when changing group by (#6458) --- .../widgets/deck_editor/deck_editor_deck_dock_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 543f1a1c0..3ca171a15 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 @@ -157,7 +157,7 @@ void DeckEditorDeckDockWidget::createDeckDock() QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox); }); connect(deckModel, &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand); - connect(deckModel, &DeckListModel::deckReplaced, this, &DeckEditorDeckDockWidget::expandAll); + connect(deckModel, &DeckListModel::modelReset, this, &DeckEditorDeckDockWidget::expandAll); connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &DeckEditorDeckDockWidget::setBannerCard); From 968be8a06f6d2eb50f140992f7214428f16d2f9b Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 31 Dec 2025 03:00:23 -0800 Subject: [PATCH 19/56] Fix bug with next/prev buttons in PrintingSelector (#6453) * Hacky fix and debug messages * remove debug * add todo --- .../widgets/deck_editor/deck_editor_deck_dock_widget.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) 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 3ca171a15..b8ebc1419 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 @@ -571,6 +571,15 @@ void DeckEditorDeckDockWidget::changeSelectedCard(int changeBy) // Get the current index of the selected item auto deckViewCurrentIndex = deckView->currentIndex(); + // For some reason, if the deckModel is modified but the view is not manually reselected, + // currentIndex will return an index for the underlying deckModel instead of the proxy. + // That index will return an invalid index when indexBelow/indexAbove crosses a header node, + // causing the selection to fail to move down. + /// \todo Figure out why it's happening so we can do a proper fix instead of a hacky workaround + if (deckViewCurrentIndex.model() == proxy->sourceModel()) { + deckViewCurrentIndex = proxy->mapFromSource(deckViewCurrentIndex); + } + auto nextIndex = deckViewCurrentIndex.siblingAtRow(deckViewCurrentIndex.row() + changeBy); if (!nextIndex.isValid()) { nextIndex = deckViewCurrentIndex; From d722b2569c157735632a9df9425b9a067839b630 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 31 Dec 2025 03:01:49 -0800 Subject: [PATCH 20/56] [DeckListModel] Refactor: general code cleanup (#6460) * change one usage * move method * move format check code * make group criteria method static * move method * make method private * more comments --- .../printing_selector/card_amount_widget.cpp | 4 +- .../libcockatrice/card/card_info.cpp | 10 ++++ .../libcockatrice/card/card_info.h | 9 ++++ .../models/deck_list/deck_list_model.cpp | 53 +++++++++---------- .../models/deck_list/deck_list_model.h | 49 +++++++---------- 5 files changed, 66 insertions(+), 59 deletions(-) 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 623b797a7..d01725cc4 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -151,9 +151,7 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model, // Check if a card without a providerId already exists in the deckModel and replace it, if so. if (existing.isValid() && existing != newCardIndex && replaceProviderless) { - for (int i = 0; i < extraCopies; i++) { - model->addCard(rootCard, zone); - } + model->offsetCountAtIndex(newCardIndex, extraCopies); model->removeRow(existing.row(), existing.parent()); } diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 3054a10cf..acfaea8c8 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -75,6 +75,16 @@ QString CardInfo::getCorrectedName() const return result.remove(rmrx).replace(spacerx, space); } +bool CardInfo::isLegalInFormat(const QString &format) const +{ + if (format.isEmpty()) { + return true; + } + + QString formatLegality = getProperty("format-" + format); + return formatLegality == "legal" || formatLegality == "restricted"; +} + void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo _info) { if (!_set->contains(smartThis)) { diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 00e8fec37..13b2b8a49 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -291,6 +291,15 @@ public: */ [[nodiscard]] QString getCorrectedName() const; + /** + * @brief Checks if the card is legal in the given format. + * A card is considered legal in a format if its properties map contains an entry for "format-", with value + * "legal" or "restricted". + * @param format The format's name. If empty, will always return true. + * @return Whether the card is legal in the given format. + */ + [[nodiscard]] bool isLegalInFormat(const QString &format) const; + /** * @brief Adds a printing to a specific set. * diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 2ccb95690..6f479616e 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -18,13 +18,19 @@ DeckListModel::~DeckListModel() delete root; } -QString DeckListModel::getGroupCriteriaForCard(CardInfoPtr info) const +/** + * @brief Extract the value from the card that is used for the group criteria. + * @param info Pointer to card information. + * @param criteria The group criteria + * @return String representing the value of the criteria. + */ +static QString extractGroupCriteriaValue(const CardInfoPtr &info, DeckListModelGroupCriteria::Type criteria) { if (!info) { return "unknown"; } - switch (activeGroupCriteria) { + switch (criteria) { case DeckListModelGroupCriteria::MAIN_TYPE: return info->getMainCardType(); case DeckListModelGroupCriteria::MANA_COST: @@ -56,7 +62,7 @@ void DeckListModel::rebuildTree() } CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(currentCard->getName()); - QString groupCriteria = getGroupCriteriaForCard(info); + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); auto *groupNode = dynamic_cast(node->findChild(groupCriteria)); @@ -353,7 +359,7 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, return nullptr; } - QString groupCriteria = getGroupCriteriaForCard(info); + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); InnerDecklistNode *groupNode = dynamic_cast(zoneNode->findChild(groupCriteria)); if (!groupNode) { return nullptr; @@ -406,7 +412,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam CardInfoPtr cardInfo = card.getCardPtr(); PrintingInfo printingInfo = card.getPrinting(); - QString groupCriteria = getGroupCriteriaForCard(cardInfo); + QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode); const QModelIndex parentIndex = nodeToIndex(groupNode); @@ -420,7 +426,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"), - printingInfo.getProperty("uuid"), isCardLegalForCurrentFormat(cardInfo)); + printingInfo.getProperty("uuid"), cardInfo->isLegalInFormat(deckList->getGameFormat())); beginInsertRows(parentIndex, insertRow, insertRow); cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); @@ -472,7 +478,7 @@ bool DeckListModel::offsetCountAtIndex(const QModelIndex &idx, int offset) return true; } -int DeckListModel::findSortedInsertRow(InnerDecklistNode *parent, CardInfoPtr cardInfo) const +int DeckListModel::findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const { if (!cardInfo) { return parent->size(); // fallback: append at end @@ -661,18 +667,6 @@ QList DeckListModel::getZones() const return zones; } -bool DeckListModel::isCardLegalForCurrentFormat(const CardInfoPtr cardInfo) -{ - if (!deckList->getGameFormat().isEmpty()) { - if (cardInfo->getProperties().contains("format-" + deckList->getGameFormat())) { - QString formatLegality = cardInfo->getProperty("format-" + deckList->getGameFormat()); - return formatLegality == "legal" || formatLegality == "restricted"; - } - return false; - } - return true; -} - static int maxAllowedForLegality(const FormatRules &format, const QString &legality) { for (const AllowedCount &c : format.allowedCounts) { @@ -683,25 +677,29 @@ static int maxAllowedForLegality(const FormatRules &format, const QString &legal return -1; // unknown legality → treat as illegal } -bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardInfo, int quantity) +static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, int quantity) { - auto formatRules = CardDatabaseManager::query()->getFormat(deckList->getGameFormat()); + if (format.isEmpty()) { + return true; + } + + auto formatRules = CardDatabaseManager::query()->getFormat(format); if (!formatRules) { return true; } // Exceptions always win - if (cardHasAnyException(*cardInfo, *formatRules)) { + if (cardHasAnyException(cardInfo, *formatRules)) { return true; } - const QString legalityProp = "format-" + deckList->getGameFormat(); - if (!cardInfo->getProperties().contains(legalityProp)) { + const QString legalityProp = "format-" + format; + if (!cardInfo.getProperties().contains(legalityProp)) { return false; } - const QString legality = cardInfo->getProperty(legalityProp); + const QString legality = cardInfo.getProperty(legalityProp); int maxAllowed = maxAllowedForLegality(*formatRules, legality); @@ -735,10 +733,11 @@ void DeckListModel::refreshCardFormatLegalities() continue; } - bool legal = isCardLegalForCurrentFormat(exactCard.getCardPtr()); + QString format = deckList->getGameFormat(); + bool legal = exactCard.getInfo().isLegalInFormat(format); if (legal) { - legal = isCardQuantityLegalForCurrentFormat(exactCard.getCardPtr(), currentCard->getNumber()); + legal = isCardQuantityLegalForFormat(format, exactCard.getInfo(), currentCard->getNumber()); } currentCard->setFormatLegality(legal); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 724a4d9d8..b6292d689 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -217,7 +217,12 @@ public slots: */ void rebuildTree(); -public slots: + /** + * @brief Sets the criteria used to group cards in the model. + * @param newCriteria The new grouping criteria. + */ + void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria); + void setActiveFormat(const QString &_format); signals: @@ -251,14 +256,8 @@ public: return nodeToIndex(root); } - /** - * @brief Returns the value of the grouping category for a card based on the current criteria. - * @param info Pointer to card information. - * @return String representing the value of the current grouping criteria for the card. - */ - [[nodiscard]] QString getGroupCriteriaForCard(CardInfoPtr info) const; - - // Qt model overrides + /// @name Qt model overrides + ///@{ [[nodiscard]] int rowCount(const QModelIndex &parent) const override; [[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; @@ -268,6 +267,8 @@ public: [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; bool removeRows(int row, int count, const QModelIndex &parent) override; + void sort(int column, Qt::SortOrder order) override; + ///@} /** * @brief Finds a card by name, zone, and optional identifiers. @@ -309,16 +310,6 @@ public: */ bool offsetCountAtIndex(const QModelIndex &idx, int offset); - /** - * @brief Determines the sorted insertion row for a card. - * @param parent The parent node where the card will be inserted. - * @param cardInfo The card info to insert. - * @return Row index where the card should be inserted to maintain sort order. - */ - int findSortedInsertRow(InnerDecklistNode *parent, CardInfoPtr cardInfo) const; - - void sort(int column, Qt::SortOrder order) override; - /** * @brief Removes all cards and resets the model. */ @@ -359,16 +350,6 @@ public: */ [[nodiscard]] QList getZones() const; - bool isCardLegalForCurrentFormat(CardInfoPtr cardInfo); - bool isCardQuantityLegalForCurrentFormat(CardInfoPtr cardInfo, int quantity); - void refreshCardFormatLegalities(); - - /** - * @brief Sets the criteria used to group cards in the model. - * @param newCriteria The new grouping criteria. - */ - void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria); - private: DeckList *deckList; /**< Pointer to the deck loader providing the underlying data. */ InnerDecklistNode *root; /**< Root node of the model tree. */ @@ -383,6 +364,14 @@ private: const QString &providerId = "", const QString &cardNumber = "") const; + /** + * @brief Determines the sorted insertion row for a card. + * @param parent The parent node where the card will be inserted. + * @param cardInfo The card info to insert. + * @return Row index where the card should be inserted to maintain sort order. + */ + int findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const; + /** * @brief Recursively emits the dataChanged signal with role as Qt::BackgroundRole for all indices that are children * of the given node. This is used to update the background color when changing formats. @@ -404,6 +393,8 @@ private: return dynamic_cast(root); return dynamic_cast(static_cast(index.internalPointer())); } + + void refreshCardFormatLegalities(); }; #endif From db3bdb586b63bc1ef79f6f6d41b54b31fac14031 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 31 Dec 2025 04:13:32 -0800 Subject: [PATCH 21/56] [CardInfo] clean up signatures (#6462) --- .../libcockatrice/card/card_info.cpp | 18 +++++++++--------- .../libcockatrice/card/card_info.h | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index acfaea8c8..ae73ef2db 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -85,7 +85,7 @@ bool CardInfo::isLegalInFormat(const QString &format) const return formatLegality == "legal" || formatLegality == "restricted"; } -void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo _info) +void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) { if (!_set->contains(smartThis)) { _set->append(smartThis); @@ -166,7 +166,7 @@ QString CardInfo::simplifyName(const QString &name) return simpleName; } -const QChar CardInfo::getColorChar() const +QChar CardInfo::getColorChar() const { QString colors = getColors(); switch (colors.size()) { @@ -188,7 +188,7 @@ void CardInfo::resetReverseRelatedCards2Me() } // Back-compatibility methods. Remove ASAP -const QString CardInfo::getCardType() const +QString CardInfo::getCardType() const { return getProperty(Mtg::CardType); } @@ -196,11 +196,11 @@ void CardInfo::setCardType(const QString &value) { setProperty(Mtg::CardType, value); } -const QString CardInfo::getCmc() const +QString CardInfo::getCmc() const { return getProperty(Mtg::ConvertedManaCost); } -const QString CardInfo::getColors() const +QString CardInfo::getColors() const { return getProperty(Mtg::Colors); } @@ -208,19 +208,19 @@ void CardInfo::setColors(const QString &value) { setProperty(Mtg::Colors, value); } -const QString CardInfo::getLoyalty() const +QString CardInfo::getLoyalty() const { return getProperty(Mtg::Loyalty); } -const QString CardInfo::getMainCardType() const +QString CardInfo::getMainCardType() const { return getProperty(Mtg::MainCardType); } -const QString CardInfo::getManaCost() const +QString CardInfo::getManaCost() const { return getProperty(Mtg::ManaCost); } -const QString CardInfo::getPowTough() const +QString CardInfo::getPowTough() const { return getProperty(Mtg::PowTough); } diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 13b2b8a49..b81cf51dc 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -267,18 +267,18 @@ public: } //@} - [[nodiscard]] const QChar getColorChar() const; + [[nodiscard]] QChar getColorChar() const; /** @name Legacy/Convenience Property Accessors */ //@{ - [[nodiscard]] const QString getCardType() const; + [[nodiscard]] QString getCardType() const; void setCardType(const QString &value); - [[nodiscard]] const QString getCmc() const; - [[nodiscard]] const QString getColors() const; + [[nodiscard]] QString getCmc() const; + [[nodiscard]] QString getColors() const; void setColors(const QString &value); - [[nodiscard]] const QString getLoyalty() const; - [[nodiscard]] const QString getMainCardType() const; - [[nodiscard]] const QString getManaCost() const; - [[nodiscard]] const QString getPowTough() const; + [[nodiscard]] QString getLoyalty() const; + [[nodiscard]] QString getMainCardType() const; + [[nodiscard]] QString getManaCost() const; + [[nodiscard]] QString getPowTough() const; void setPowTough(const QString &value); //@} @@ -308,7 +308,7 @@ public: * @param _set The set to which the card should be added. * @param _info Optional printing information. */ - void addToSet(const CardSetPtr &_set, PrintingInfo _info = PrintingInfo()); + void addToSet(const CardSetPtr &_set, const PrintingInfo &_info = PrintingInfo()); /** * @brief Combines legality properties from a provided map. From 0085015ebe59d3b0942ebf11f6dd32b64529ab28 Mon Sep 17 00:00:00 2001 From: Alex Okonechnikov <36140593+okonech@users.noreply.github.com> Date: Wed, 31 Dec 2025 11:48:30 -0500 Subject: [PATCH 22/56] manual drag override (#6461) * manual drag override * fix styles * pr comments * better close button rect calc --- .../src/game/zones/view_zone_widget.cpp | 194 ++++++++++++++++-- cockatrice/src/game/zones/view_zone_widget.h | 36 +++- 2 files changed, 210 insertions(+), 20 deletions(-) diff --git a/cockatrice/src/game/zones/view_zone_widget.cpp b/cockatrice/src/game/zones/view_zone_widget.cpp index 91abb4663..e15a466c3 100644 --- a/cockatrice/src/game/zones/view_zone_widget.cpp +++ b/cockatrice/src/game/zones/view_zone_widget.cpp @@ -13,12 +13,20 @@ #include #include #include +#include #include #include #include +#include #include #include +namespace +{ +constexpr qreal kTitleBarHeight = 24.0; +constexpr qreal kMinVisibleWidth = 100.0; +} // namespace + /** * @param _player the player the cards were revealed to. * @param _origZone the zone the cards were revealed from. @@ -241,33 +249,182 @@ void ZoneViewWidget::retranslateUi() pileViewCheckBox.setText(tr("pile view")); } -void ZoneViewWidget::moveEvent(QGraphicsSceneMoveEvent * /* event */) +void ZoneViewWidget::stopWindowDrag() { - if (!scene()) + if (!draggingWindow) return; - int titleBarHeight = 24; + draggingWindow = false; + ungrabMouse(); +} - QPointF scenePos = pos(); +void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event) +{ + draggingWindow = true; + dragStartItemPos = pos(); + dragStartScreenPos = event->screenPos(); + dragView = findDragView(event->widget()); - if (scenePos.x() < 0) { - scenePos.setX(0); - } else { - qreal maxw = scene()->sceneRect().width() - 100; - if (scenePos.x() > maxw) - scenePos.setX(maxw); + // need to grab mouse to receive events and not miss initial movement + grabMouse(); +} + +QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const +{ + const QRectF frameRectF = windowFrameRect(); + const QRect titleBarRect(frameRectF.toRect().x(), frameRectF.toRect().y(), frameRectF.toRect().width(), + static_cast(kTitleBarHeight)); + + // query the style for the close button position (handles macOS top-left placement) + if (styleWidget) { + QStyleOptionTitleBar opt; + opt.initFrom(styleWidget); + opt.rect = titleBarRect; + opt.text = windowTitle(); + opt.icon = styleWidget->windowIcon(); + opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint; + opt.subControls = QStyle::SC_TitleBarCloseButton; + opt.activeSubControls = QStyle::SC_TitleBarCloseButton; + opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState; + if (styleWidget->isActiveWindow()) + opt.state |= QStyle::State_Active; + const QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton, + styleWidget); + if (r.isValid() && !r.isEmpty()) { + return QRectF(r); + } } - if (scenePos.y() < titleBarHeight) { - scenePos.setY(titleBarHeight); - } else { - qreal maxh = scene()->sceneRect().height() - titleBarHeight; - if (scenePos.y() > maxh) - scenePos.setY(maxh); + // fallback: square at right end of titlebar (Windows/Linux style) + return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight); +} + +QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const +{ + QWidget *current = eventWidget; + while (current) { + if (auto *view = qobject_cast(current)) + return view; + current = current->parentWidget(); } - if (scenePos != pos()) - setPos(scenePos); + if (scene() && !scene()->views().isEmpty()) + return scene()->views().constFirst(); + + return nullptr; +} + +QPointF ZoneViewWidget::calcDraggedWindowPos(const QPoint &screenPos, + const QPointF &scenePos, + const QPointF &buttonDownScenePos) const +{ + if (dragView && dragView->viewport()) { + const QPoint vpStart = dragView->viewport()->mapFromGlobal(dragStartScreenPos); + const QPoint vpNow = dragView->viewport()->mapFromGlobal(screenPos); + const QPointF sceneStart = dragView->mapToScene(vpStart); + const QPointF sceneNow = dragView->mapToScene(vpNow); + return dragStartItemPos + (sceneNow - sceneStart); + } + + return dragStartItemPos + (scenePos - buttonDownScenePos); +} + +bool ZoneViewWidget::windowFrameEvent(QEvent *event) +{ + if (event->type() == QEvent::UngrabMouse) { + stopWindowDrag(); + return QGraphicsWidget::windowFrameEvent(event); + } + + auto *me = dynamic_cast(event); + if (!me) + return QGraphicsWidget::windowFrameEvent(event); + + switch (event->type()) { + case QEvent::GraphicsSceneMousePress: + if (me->button() == Qt::LeftButton && windowFrameSectionAt(me->pos()) == Qt::TitleBarArea) { + // avoid drag on close button + if (closeButtonRect(me->widget()).contains(me->pos())) { + me->accept(); + close(); + return true; + } + startWindowDrag(me); + me->accept(); + return true; + } + break; + + case QEvent::GraphicsSceneMouseMove: + if (draggingWindow) { + if (!(me->buttons() & Qt::LeftButton)) { + stopWindowDrag(); + } else { + setPos( + calcDraggedWindowPos(me->screenPos(), me->scenePos(), me->buttonDownScenePos(Qt::LeftButton))); + } + me->accept(); + return true; + } + break; + + case QEvent::GraphicsSceneMouseRelease: + if (draggingWindow && me->button() == Qt::LeftButton) { + stopWindowDrag(); + me->accept(); + return true; + } + break; + + default: + break; + } + + return QGraphicsWidget::windowFrameEvent(event); +} + +void ZoneViewWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + // move if the scene routes moves while dragging + if (draggingWindow && (event->buttons() & Qt::LeftButton)) { + setPos(calcDraggedWindowPos(event->screenPos(), event->scenePos(), event->buttonDownScenePos(Qt::LeftButton))); + event->accept(); + return; + } + + QGraphicsWidget::mouseMoveEvent(event); +} + +void ZoneViewWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (draggingWindow && event->button() == Qt::LeftButton) { + stopWindowDrag(); + event->accept(); + return; + } + + QGraphicsWidget::mouseReleaseEvent(event); +} + +QVariant ZoneViewWidget::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == QGraphicsItem::ItemPositionChange && scene()) { + // Keep grab area in main view + const QRectF sceneRect = scene()->sceneRect(); + const QPointF requestedPos = value.toPointF(); + QPointF desiredPos = requestedPos; + + const qreal minX = sceneRect.left(); + const qreal maxX = qMax(minX, sceneRect.right() - kMinVisibleWidth); + const qreal minY = sceneRect.top() + kTitleBarHeight; + const qreal maxY = qMax(minY, sceneRect.bottom() - kTitleBarHeight); + + desiredPos.setX(qBound(minX, desiredPos.x(), maxX)); + desiredPos.setY(qBound(minY, desiredPos.y(), maxY)); + return desiredPos; + } + + return QGraphicsWidget::itemChange(change, value); } void ZoneViewWidget::resizeEvent(QGraphicsSceneResizeEvent *event) @@ -350,6 +507,7 @@ void ZoneViewWidget::handleScrollBarChange(int value) void ZoneViewWidget::closeEvent(QCloseEvent *event) { + stopWindowDrag(); disconnect(zone, &ZoneViewZone::closed, this, 0); // manually call zone->close in order to remove it from the origZones views zone->close(); diff --git a/cockatrice/src/game/zones/view_zone_widget.h b/cockatrice/src/game/zones/view_zone_widget.h index 272ff5560..4ed8f74d8 100644 --- a/cockatrice/src/game/zones/view_zone_widget.h +++ b/cockatrice/src/game/zones/view_zone_widget.h @@ -3,7 +3,6 @@ * @ingroup GameGraphicsZones * @brief TODO: Document this. */ - #ifndef ZONEVIEWWIDGET_H #define ZONEVIEWWIDGET_H @@ -14,6 +13,7 @@ #include #include #include +#include #include class QLabel; @@ -28,6 +28,8 @@ class ServerInfo_Card; class QGraphicsSceneMouseEvent; class QGraphicsSceneWheelEvent; class QStyleOption; +class QGraphicsView; +class QWidget; class ScrollableGraphicsProxyWidget : public QGraphicsProxyWidget { @@ -66,6 +68,33 @@ private: int extraHeight; Player *player; + bool draggingWindow = false; + QPoint dragStartScreenPos; + QPointF dragStartItemPos; + QPointer dragView; + + void stopWindowDrag(); + void startWindowDrag(QGraphicsSceneMouseEvent *event); + QRectF closeButtonRect(QWidget *styleWidget) const; + /** + * @brief Resolves the QGraphicsView to use for drag coordinate mapping + * + * @param eventWidget QWidget that originated the mouse event + * @return The resolved QGraphicsView + */ + QGraphicsView *findDragView(QWidget *eventWidget) const; + /** + * @brief Calculates the desired widget position while dragging + * + * @param screenPos Global screen coordinates of the current mouse position + * @param scenePos Scene coordinates of the current mouse position + * @param buttonDownScenePos Scene coordinates of the initial mouse press position + * + * @return The new widget position in scene coordinates + */ + QPointF + calcDraggedWindowPos(const QPoint &screenPos, const QPointF &scenePos, const QPointF &buttonDownScenePos) const; + void resizeScrollbar(qreal newZoneHeight); signals: void closePressed(ZoneViewWidget *zv); @@ -76,7 +105,6 @@ private slots: void resizeToZoneContents(bool forceInitialHeight = false); void handleScrollBarChange(int value); void zoneDeleted(); - void moveEvent(QGraphicsSceneMoveEvent * /* event */) override; void resizeEvent(QGraphicsSceneResizeEvent * /* event */) override; void expandWindow(); @@ -101,6 +129,10 @@ public: protected: void closeEvent(QCloseEvent *event) override; void initStyleOption(QStyleOption *option) const override; + bool windowFrameEvent(QEvent *event) override; + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; }; From b2dd8eed3fbec6539dfbc8a61ce2d2e70664f229 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 31 Dec 2025 08:54:47 -0800 Subject: [PATCH 23/56] [TabDeckEditor] Create class to centralize deck state (#6459) * create new file * use QSharedPointer in DeckListModel * [TabDeckEditor] Create class to centralize deck state * delete method * update docs --- cockatrice/CMakeLists.txt | 1 + .../deck_editor_deck_dock_widget.cpp | 298 ++++----------- .../deck_editor_deck_dock_widget.h | 38 +- .../deck_list_history_manager_widget.cpp | 77 ++-- .../deck_list_history_manager_widget.h | 11 +- .../deck_editor/deck_state_manager.cpp | 361 ++++++++++++++++++ .../widgets/deck_editor/deck_state_manager.h | 297 ++++++++++++++ .../dialogs/dlg_select_set_for_cards.cpp | 49 +-- .../dialogs/dlg_select_set_for_cards.h | 6 +- .../all_zones_card_amount_widget.cpp | 14 +- .../all_zones_card_amount_widget.h | 4 +- .../printing_selector/card_amount_widget.cpp | 44 +-- .../printing_selector/card_amount_widget.h | 8 +- .../printing_selector/printing_selector.cpp | 16 +- .../printing_selector/printing_selector.h | 13 +- .../printing_selector_card_display_widget.cpp | 9 +- .../printing_selector_card_display_widget.h | 3 +- .../printing_selector_card_overlay_widget.cpp | 9 +- .../printing_selector_card_overlay_widget.h | 3 +- ...rinting_selector_card_selection_widget.cpp | 14 +- .../printing_selector_card_selection_widget.h | 3 +- .../printing_selector_card_sorting_widget.cpp | 2 +- .../printing_selector_card_sorting_widget.h | 2 +- .../widgets/tabs/abstract_tab_deck_editor.cpp | 135 +++---- .../widgets/tabs/abstract_tab_deck_editor.h | 36 +- ...idekt_api_response_deck_display_widget.cpp | 5 +- .../widgets/tabs/tab_deck_editor.cpp | 5 +- .../tab_deck_editor_visual.cpp | 19 +- ...al_database_display_name_filter_widget.cpp | 3 +- .../models/deck_list/deck_list_model.cpp | 15 +- .../models/deck_list/deck_list_model.h | 10 +- 31 files changed, 933 insertions(+), 577 deletions(-) create mode 100644 cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp create mode 100644 cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index b2d387391..c6a29969f 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -156,6 +156,7 @@ set(cockatrice_SOURCES src/interface/widgets/deck_editor/deck_editor_filter_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp src/interface/widgets/deck_editor/deck_list_style_proxy.cpp + src/interface/widgets/deck_editor/deck_state_manager.cpp src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/banner_widget.cpp 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 b8ebc1419..62195974b 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 @@ -1,8 +1,8 @@ #include "deck_editor_deck_dock_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../deck_loader/deck_loader.h" #include "deck_list_style_proxy.h" +#include "deck_state_manager.h" #include #include @@ -38,7 +38,7 @@ static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo) } DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent) - : QDockWidget(parent), deckEditor(parent) + : QDockWidget(parent), deckEditor(parent), deckStateManager(parent->deckStateManager) { setObjectName("deckDock"); @@ -52,19 +52,19 @@ DeckEditorDeckDockWidget::DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent void DeckEditorDeckDockWidget::createDeckDock() { - deckModel = new DeckListModel(this); - deckModel->setObjectName("deckModel"); - connect(deckModel, &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash); - - deckLoader = new DeckLoader(this); + connect(getModel(), &DeckListModel::deckHashChanged, this, &DeckEditorDeckDockWidget::updateHash); proxy = new DeckListStyleProxy(this); - proxy->setSourceModel(deckModel); + proxy->setSourceModel(getModel()); - historyManagerWidget = new DeckListHistoryManagerWidget(deckModel, proxy, deckEditor->getHistoryManager(), this); + historyManagerWidget = new DeckListHistoryManagerWidget(deckStateManager, proxy, this); connect(historyManagerWidget, &DeckListHistoryManagerWidget::requestDisplayWidgetSync, this, &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); + connect(deckStateManager, &DeckStateManager::focusIndexChanged, this, &DeckEditorDeckDockWidget::setSelectedIndex); + connect(deckStateManager, &DeckStateManager::deckReplaced, this, + &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); + deckView = new QTreeView(); deckView->setObjectName("deckView"); deckView->setModel(proxy); @@ -97,7 +97,7 @@ void DeckEditorDeckDockWidget::createDeckDock() nameDebounceTimer = new QTimer(this); nameDebounceTimer->setSingleShot(true); nameDebounceTimer->setInterval(300); // debounce duration in ms - connect(nameDebounceTimer, &QTimer::timeout, this, [this]() { updateName(nameEdit->text()); }); + connect(nameDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeName); connect(nameEdit, &LineEditUnfocusable::textChanged, this, [this]() { nameDebounceTimer->start(); // restart debounce timer @@ -141,7 +141,7 @@ void DeckEditorDeckDockWidget::createDeckDock() commentsDebounceTimer = new QTimer(this); commentsDebounceTimer->setSingleShot(true); commentsDebounceTimer->setInterval(400); // longer debounce for multi-line - connect(commentsDebounceTimer, &QTimer::timeout, this, [this]() { updateComments(); }); + connect(commentsDebounceTimer, &QTimer::timeout, this, &DeckEditorDeckDockWidget::writeComments); connect(commentsEdit, &QTextEdit::textChanged, this, [this]() { commentsDebounceTimer->start(); // restart debounce timer @@ -152,21 +152,21 @@ void DeckEditorDeckDockWidget::createDeckDock() bannerCardLabel->setText(tr("Banner Card")); bannerCardLabel->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); bannerCardComboBox = new QComboBox(this); - connect(deckModel, &DeckListModel::dataChanged, this, [this]() { + connect(getModel(), &DeckListModel::dataChanged, this, [this]() { // Delay the update to avoid race conditions QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox); }); - connect(deckModel, &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand); - connect(deckModel, &DeckListModel::modelReset, this, &DeckEditorDeckDockWidget::expandAll); + connect(getModel(), &DeckListModel::cardAddedAt, this, &DeckEditorDeckDockWidget::recursiveExpand); + connect(getModel(), &DeckListModel::modelReset, this, &DeckEditorDeckDockWidget::expandAll); connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &DeckEditorDeckDockWidget::setBannerCard); + &DeckEditorDeckDockWidget::writeBannerCard); bannerCardComboBox->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); - deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()->getTags()); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, {}); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible()); - connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, - &DeckEditorDeckDockWidget::setTags); + connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, deckStateManager, + &DeckStateManager::setTags); activeGroupCriteriaLabel = new QLabel(this); @@ -175,9 +175,9 @@ void DeckEditorDeckDockWidget::createDeckDock() activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST); activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR); connect(activeGroupCriteriaComboBox, QOverload::of(&QComboBox::currentIndexChanged), [this]() { - deckModel->setActiveGroupCriteria(static_cast( + getModel()->setActiveGroupCriteria(static_cast( activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); - deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); + getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); }); aIncrement = new QAction(QString(), this); @@ -295,9 +295,10 @@ void DeckEditorDeckDockWidget::initializeFormats() formatComboBox->addItem(formatName, formatName); // store the raw key in itemData } - if (!deckModel->getDeckList()->getGameFormat().isEmpty()) { - deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat()); - formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat())); + QString format = deckStateManager->getMetadata().gameFormat; + if (!format.isEmpty()) { + getModel()->setActiveFormat(format); + formatComboBox->setCurrentIndex(formatComboBox->findData(format)); } else { // Ensure no selection is visible initially formatComboBox->setCurrentIndex(-1); @@ -306,11 +307,10 @@ void DeckEditorDeckDockWidget::initializeFormats() connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { if (index >= 0) { QString formatKey = formatComboBox->itemData(index).toString(); - deckModel->setActiveFormat(formatKey); + deckStateManager->setFormat(formatKey); } else { - deckModel->setActiveFormat(QString()); // clear format if deselected + deckStateManager->setFormat(""); // clear format if deselected } - emit deckModified(); }); } @@ -340,43 +340,37 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard() void DeckEditorDeckDockWidget::updateCard(const QModelIndex /*¤t*/, const QModelIndex & /*previous*/) { if (ExactCard card = getCurrentCard()) { - emit cardChanged(card); + emit selectedCardChanged(card); } } -void DeckEditorDeckDockWidget::updateName(const QString &name) +/** + * @brief Writes the contents of the name textBox to the DeckStateManager + */ +void DeckEditorDeckDockWidget::writeName() { - emit requestDeckHistorySave( - QString(tr("Rename deck to \"%1\" from \"%2\"")).arg(name).arg(deckLoader->getDeck().deckList.getName())); - deckModel->getDeckList()->setName(name); - deckEditor->setModified(name.isEmpty()); - emit nameChanged(); - emit deckModified(); + QString name = nameEdit->text(); + deckStateManager->setName(name); } -void DeckEditorDeckDockWidget::updateComments() +/** + * @brief Writes the contents of the comments textBox to the DeckStateManager + */ +void DeckEditorDeckDockWidget::writeComments() { - emit requestDeckHistorySave(tr("Updated comments (was %1 chars, now %2 chars)") - .arg(deckLoader->getDeck().deckList.getComments().size()) - .arg(commentsEdit->toPlainText().size())); - - deckModel->getDeckList()->setComments(commentsEdit->toPlainText()); - deckEditor->setModified(commentsEdit->toPlainText().isEmpty()); - emit commentsChanged(); - emit deckModified(); + QString comments = commentsEdit->toPlainText(); + deckStateManager->setComments(comments); } void DeckEditorDeckDockWidget::updateHash() { - hashLabel->setText(deckModel->getDeckList()->getDeckHash()); - emit hashChanged(); - emit deckModified(); + hashLabel->setText(deckStateManager->getDeckHash()); } void DeckEditorDeckDockWidget::updateBannerCardComboBox() { // Store current banner card identity - CardRef wanted = deckModel->getDeckList()->getBannerCard(); + CardRef wanted = deckStateManager->getMetadata().bannerCard; // Block signals temporarily bool wasBlocked = bannerCardComboBox->blockSignals(true); @@ -386,7 +380,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() // Collect unique (name, providerId) pairs QSet> bannerCardSet; - QList cardsInDeck = deckModel->getCardRefs(); + QList cardsInDeck = getModel()->getCardRefs(); for (auto cardRef : cardsInDeck) { if (!CardDatabaseManager::query()->getCard(cardRef)) { @@ -415,7 +409,6 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() // Handle results if (restoreIndex != -1) { bannerCardComboBox->setCurrentIndex(restoreIndex); - syncDeckListBannerCardWithComboBox(); } else { // Add a placeholder "-" and set it as the current selection bannerCardComboBox->insertItem(0, "-"); @@ -426,25 +419,14 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox() bannerCardComboBox->blockSignals(wasBlocked); } -void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */) +/** + * @brief Writes the selected bannerCard to the DeckStateManager + */ +void DeckEditorDeckDockWidget::writeBannerCard(int index) { - emit requestDeckHistorySave(tr("Banner card changed")); - syncDeckListBannerCardWithComboBox(); - deckEditor->setModified(true); - emit deckModified(); -} - -void DeckEditorDeckDockWidget::setTags(const QStringList &tags) -{ - deckModel->getDeckList()->setTags(tags); - deckEditor->setModified(true); - emit deckModified(); -} - -void DeckEditorDeckDockWidget::syncDeckListBannerCardWithComboBox() -{ - auto [name, id] = bannerCardComboBox->currentData().value>(); - deckModel->getDeckList()->setBannerCard({name, id}); + auto [name, id] = bannerCardComboBox->itemData(index).value>(); + CardRef bannerCard = {name, id}; + deckStateManager->setBannerCard(bannerCard); } void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible) @@ -460,7 +442,7 @@ void DeckEditorDeckDockWidget::updateShowTagsWidget(const bool visible) void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() { - if (deckModel->getDeckList()->getBannerCard().name == "") { + if (deckStateManager->getMetadata().bannerCard.name == "") { if (bannerCardComboBox->findText("-") != -1) { bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText("-")); } else { @@ -468,36 +450,26 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() bannerCardComboBox->setCurrentIndex(0); } } else { - bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().name); + bannerCardComboBox->setCurrentText(deckStateManager->getMetadata().bannerCard.name); } } -/** - * Sets the currently active deck for this tab - * @param _deck The deck. - */ -void DeckEditorDeckDockWidget::setDeck(const LoadedDeck &_deck) +void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex) { - deckLoader->setDeck(_deck); - deckModel->setDeckList(&deckLoader->getDeck().deckList); - connect(deckLoader, &DeckLoader::deckLoaded, deckModel, &DeckListModel::rebuildTree); - - emit requestDeckHistoryClear(); - historyManagerWidget->setDeckListModel(deckModel); - - syncDisplayWidgetsToModel(); - - emit deckChanged(); + deckView->clearSelection(); + deckView->setCurrentIndex(newCardIndex); + recursiveExpand(newCardIndex); + deckView->setFocus(Qt::FocusReason::MouseFocusReason); } void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() { nameEdit->blockSignals(true); - nameEdit->setText(deckModel->getDeckList()->getName()); + nameEdit->setText(deckStateManager->getMetadata().name); nameEdit->blockSignals(false); commentsEdit->blockSignals(true); - commentsEdit->setText(deckModel->getDeckList()->getComments()); + commentsEdit->setText(deckStateManager->getMetadata().comments); commentsEdit->blockSignals(false); bannerCardComboBox->blockSignals(true); @@ -507,44 +479,22 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() updateHash(); sortDeckModelToDeckView(); - deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); + deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); } void DeckEditorDeckDockWidget::sortDeckModelToDeckView() { - deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat()); - formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat())); - - emit deckChanged(); -} - -DeckLoader *DeckEditorDeckDockWidget::getDeckLoader() -{ - return deckLoader; -} - -const DeckList &DeckEditorDeckDockWidget::getDeckList() const -{ - return *deckModel->getDeckList(); + getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); + getModel()->setActiveFormat(deckStateManager->getMetadata().gameFormat); + formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); } /** - * Resets the tab to the state for a blank new tab. + * @brief Convenience method to get the underlying model instance from the DeckStateManager */ -void DeckEditorDeckDockWidget::cleanDeck() +DeckListModel *DeckEditorDeckDockWidget::getModel() const { - deckModel->cleanList(); - nameEdit->setText(QString()); - emit nameChanged(); - commentsEdit->setText(QString()); - emit commentsChanged(); - hashLabel->setText(QString()); - emit hashChanged(); - emit deckModified(); - emit deckChanged(); - updateBannerCardComboBox(); - deckTagsDisplayWidget->setTags(deckModel->getDeckList()->getTags()); + return deckStateManager->getModel(); } void DeckEditorDeckDockWidget::selectPrevCard() @@ -635,7 +585,7 @@ QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const auto selectedRows = deckView->selectionModel()->selectedRows(); const auto notLeafNode = [this](const QModelIndex &index) { - return deckModel->hasChildren(proxy->mapToSource(index)); + return getModel()->hasChildren(proxy->mapToSource(index)); }; selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end()); @@ -650,21 +600,7 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString & } QString zoneName = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : _zoneName; - - emit requestDeckHistorySave(tr("Added (%1): %2 (%3) %4") - .arg(zoneName, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), - card.getPrinting().getProperty("num"))); - - QModelIndex newCardIndex = deckModel->addCard(card, zoneName); - - if (!newCardIndex.isValid()) { - return; - } - - deckView->clearSelection(); - deckView->setCurrentIndex(newCardIndex); - - emit deckModified(); + deckStateManager->addCard(card, zoneName); } void DeckEditorDeckDockWidget::actIncrementSelection() @@ -681,12 +617,12 @@ void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString QString providerId = card.getPrinting().getUuid(); QString collectorNumber = card.getPrinting().getProperty("num"); - QModelIndex foundCard = deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber); + QModelIndex foundCard = getModel()->findCard(card.getName(), zoneName, providerId, collectorNumber); if (!foundCard.isValid()) { - foundCard = deckModel->findCard(card.getName(), zoneName); + foundCard = getModel()->findCard(card.getName(), zoneName); } - swapCard(foundCard); + deckStateManager->swapCardAtIndex(foundCard); } void DeckEditorDeckDockWidget::actSwapSelection() @@ -699,54 +635,15 @@ void DeckEditorDeckDockWidget::actSwapSelection() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - bool isModified = false; for (const auto ¤tIndex : selectedRows) { - if (swapCard(currentIndex)) { - isModified = true; - } + deckStateManager->swapCardAtIndex(currentIndex); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); - if (isModified) { - emit deckModified(); - } - update(); } -/** - * Swaps the card at the index between the maindeck and sideboard - * - * @param currentIndex The index to swap. - * @return True if the swap was successful - */ -bool DeckEditorDeckDockWidget::swapCard(const QModelIndex ¤tIndex) -{ - if (!currentIndex.isValid()) - return false; - const QString cardName = currentIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); - const QString cardProviderID = - currentIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); - const QModelIndex gparent = currentIndex.parent().parent(); - - if (!gparent.isValid()) - return false; - - const QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - offsetCountAtIndex(currentIndex, false); - const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; - - if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, cardProviderID})) { - deckModel->addCard(card, otherZoneName); - } else { - // Third argument (true) says create the card no matter what, even if not in DB - deckModel->addPreferredPrintingCard(cardName, otherZoneName, true); - } - - return true; -} - void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString zoneName) { if (!card) @@ -754,17 +651,7 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z if (card.getInfo().getIsToken()) zoneName = DECK_ZONE_TOKENS; - QString providerId = card.getPrinting().getUuid(); - QString collectorNumber = card.getPrinting().getProperty("num"); - - QModelIndex idx = deckModel->findCard(card.getName(), zoneName, providerId, collectorNumber); - if (!idx.isValid()) { - return; - } - - deckView->clearSelection(); - deckView->setCurrentIndex(proxy->mapToSource(idx)); - offsetCountAtIndex(idx, false); + deckStateManager->decrementCard(card, zoneName); } void DeckEditorDeckDockWidget::actDecrementSelection() @@ -794,25 +681,11 @@ void DeckEditorDeckDockWidget::actRemoveCard() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - bool isModified = false; - for (const auto &index : selectedRows) { - if (!index.isValid() || deckModel->hasChildren(index)) { - continue; - } - QModelIndex sourceIndex = proxy->mapToSource(index); - QString cardName = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); - - emit requestDeckHistorySave(QString(tr("Removed \"%1\" (all copies)")).arg(cardName)); - - deckModel->removeRow(sourceIndex.row(), sourceIndex.parent()); - isModified = true; + for (const auto &row : selectedRows) { + deckStateManager->removeCardAtIndex(row); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); - - if (isModified) { - emit deckModified(); - } } /** @@ -822,28 +695,17 @@ void DeckEditorDeckDockWidget::actRemoveCard() */ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement) { - if (!idx.isValid() || deckModel->hasChildren(idx)) { + if (!idx.isValid() || getModel()->hasChildren(idx)) { return; } QModelIndex sourceIndex = proxy->mapToSource(idx); - QString cardName = sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); - QString providerId = - sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); - - const auto reason = QString(tr("%1 %2 × \"%3\" (%4)")) - .arg(isIncrement ? tr("Added") : tr("Removed")) - .arg(1) - .arg(cardName) - .arg(providerId); - - emit requestDeckHistorySave(reason); - - int offset = isIncrement ? 1 : -1; - deckModel->offsetCountAtIndex(sourceIndex, offset); - - emit deckModified(); + if (isIncrement) { + deckStateManager->incrementCountAtIndex(sourceIndex); + } else { + deckStateManager->decrementCountAtIndex(sourceIndex); + } } void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point) diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index a50434f0b..1a82b00d1 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -28,22 +28,14 @@ class DeckEditorDeckDockWidget : public QDockWidget Q_OBJECT public: explicit DeckEditorDeckDockWidget(AbstractTabDeckEditor *parent); - DeckLoader *deckLoader; + DeckListStyleProxy *proxy; - DeckListModel *deckModel; QTreeView *deckView; QComboBox *bannerCardComboBox; void createDeckDock(); ExactCard getCurrentCard(); void retranslateUi(); - QString getDeckName() - { - return nameEdit->text(); - } - QString getSimpleDeckName() - { - return nameEdit->text().simplified(); - } + QComboBox *getGroupByComboBox() { return activeGroupCriteriaComboBox; @@ -55,15 +47,11 @@ public: } public slots: - void cleanDeck(); void selectPrevCard(); void selectNextCard(); void updateBannerCardComboBox(); - void setDeck(const LoadedDeck &_deck); void syncDisplayWidgetsToModel(); void sortDeckModelToDeckView(); - DeckLoader *getDeckLoader(); - const DeckList &getDeckList() const; void actAddCard(const ExactCard &card, const QString &zoneName); void actIncrementSelection(); void actDecrementCard(const ExactCard &card, QString zoneName); @@ -74,17 +62,12 @@ public slots: void initializeFormats(); signals: - void nameChanged(); - void commentsChanged(); - void hashChanged(); - void deckChanged(); - void deckModified(); - void requestDeckHistorySave(const QString &modificationReason); - void requestDeckHistoryClear(); - void cardChanged(const ExactCard &_card); + void selectedCardChanged(const ExactCard &card); private: AbstractTabDeckEditor *deckEditor; + DeckStateManager *deckStateManager; + DeckListHistoryManagerWidget *historyManagerWidget; KeySignals deckViewKeySignals; QLabel *nameLabel; @@ -107,18 +90,17 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; + DeckListModel *getModel() const; [[nodiscard]] QModelIndexList getSelectedCardNodes() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); private slots: void decklistCustomMenu(QPoint point); - bool swapCard(const QModelIndex ¤tIndex); void updateCard(QModelIndex, const QModelIndex ¤t); - void updateName(const QString &name); - void updateComments(); - void setBannerCard(int); - void setTags(const QStringList &tags); - void syncDeckListBannerCardWithComboBox(); + void writeName(); + void writeComments(); + void writeBannerCard(int); + void setSelectedIndex(const QModelIndex &newCardIndex); void updateHash(); void refreshShortcuts(); void updateShowBannerCardComboBox(bool visible); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp index c68934ea6..d2d748753 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.cpp @@ -1,10 +1,11 @@ #include "deck_list_history_manager_widget.h" -DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckListModel, +#include "deck_state_manager.h" + +DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckStateManager *_deckStateManager, DeckListStyleProxy *_styleProxy, - DeckListHistoryManager *manager, QWidget *parent) - : QWidget(parent), deckListModel(_deckListModel), styleProxy(_styleProxy), historyManager(manager) + : QWidget(parent), deckStateManager(_deckStateManager), styleProxy(_styleProxy) { layout = new QHBoxLayout(this); @@ -43,8 +44,7 @@ DeckListHistoryManagerWidget::DeckListHistoryManagerWidget(DeckListModel *_deckL connect(historyList, &QListWidget::itemClicked, this, &DeckListHistoryManagerWidget::onListClicked); - connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, - &DeckListHistoryManagerWidget::refreshList); + connect(deckStateManager, &DeckStateManager::historyChanged, this, &DeckListHistoryManagerWidget::refreshList); refreshList(); retranslateUi(); @@ -58,15 +58,12 @@ void DeckListHistoryManagerWidget::retranslateUi() historyLabel->setText(tr("Click on an entry to revert to that point in the history.")); } -void DeckListHistoryManagerWidget::setDeckListModel(DeckListModel *_deckListModel) -{ - deckListModel = _deckListModel; -} - void DeckListHistoryManagerWidget::refreshList() { historyList->clear(); + DeckListHistoryManager *historyManager = deckStateManager->getHistoryManager(); + // Fill redo section first (oldest redo at top, newest redo closest to divider) const auto redoStack = historyManager->getRedoStack(); for (int i = 0; i < redoStack.size(); ++i) { // iterate forward @@ -98,36 +95,7 @@ void DeckListHistoryManagerWidget::refreshList() redoButton->setEnabled(historyManager->canRedo()); } -void DeckListHistoryManagerWidget::doUndo() -{ - if (!historyManager->canUndo()) { - return; - } - - historyManager->undo(deckListModel->getDeckList()); - deckListModel->rebuildTree(); - emit deckListModel->layoutChanged(); - emit requestDisplayWidgetSync(); - - refreshList(); -} - -void DeckListHistoryManagerWidget::doRedo() -{ - if (!historyManager->canRedo()) { - return; - } - - historyManager->redo(deckListModel->getDeckList()); - deckListModel->rebuildTree(); - - emit deckListModel->layoutChanged(); - emit requestDisplayWidgetSync(); - - refreshList(); -} - -void DeckListHistoryManagerWidget::onListClicked(QListWidgetItem *item) +void DeckListHistoryManagerWidget::onListClicked(const QListWidgetItem *item) { // Ignore non-selectable items (like divider) if (!(item->flags() & Qt::ItemIsSelectable)) { @@ -138,23 +106,24 @@ void DeckListHistoryManagerWidget::onListClicked(QListWidgetItem *item) int index = item->data(Qt::UserRole + 1).toInt(); if (mode == "redo") { - const auto redoStack = historyManager->getRedoStack(); + const auto redoStack = deckStateManager->getHistoryManager()->getRedoStack(); int steps = redoStack.size() - index; - for (int i = 0; i < steps; ++i) { - historyManager->redo(deckListModel->getDeckList()); - } + deckStateManager->redo(steps); } else if (mode == "undo") { - const auto undoStack = historyManager->getUndoStack(); - int steps = undoStack.size() - 1 - index; - for (int i = 0; i < steps + 1; ++i) { - historyManager->undo(deckListModel->getDeckList()); - } + const auto undoStack = deckStateManager->getHistoryManager()->getUndoStack(); + int steps = undoStack.size() - index; + deckStateManager->undo(steps); } - deckListModel->rebuildTree(); - - emit deckListModel->layoutChanged(); - emit requestDisplayWidgetSync(); - refreshList(); +} + +void DeckListHistoryManagerWidget::doUndo() +{ + deckStateManager->undo(); +} + +void DeckListHistoryManagerWidget::doRedo() +{ + deckStateManager->redo(); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h index 0e208ad2b..ab53912e2 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_history_manager_widget.h @@ -14,6 +14,8 @@ #include #include +class DeckStateManager; + class DeckListHistoryManagerWidget : public QWidget { Q_OBJECT @@ -25,22 +27,19 @@ public slots: void retranslateUi(); public: - explicit DeckListHistoryManagerWidget(DeckListModel *deckListModel, + explicit DeckListHistoryManagerWidget(DeckStateManager *deckStateManager, DeckListStyleProxy *styleProxy, - DeckListHistoryManager *manager, QWidget *parent = nullptr); - void setDeckListModel(DeckListModel *_deckListModel); private slots: void refreshList(); - void onListClicked(QListWidgetItem *item); + void onListClicked(const QListWidgetItem *item); void doUndo(); void doRedo(); private: - DeckListModel *deckListModel; + DeckStateManager *deckStateManager; DeckListStyleProxy *styleProxy; - DeckListHistoryManager *historyManager; QHBoxLayout *layout; QAction *aUndo; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp new file mode 100644 index 000000000..628d33d51 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -0,0 +1,361 @@ +#include "deck_state_manager.h" + +#include +#include + +DeckStateManager::DeckStateManager(QObject *parent) + : QObject(parent), deckList(QSharedPointer(new DeckList)), + deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this)) +{ + connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] { + setModified(true); + emit historyChanged(); + }); + connect(deckListModel, &DeckListModel::rowsInserted, this, &DeckStateManager::uniqueCardsChanged); + connect(deckListModel, &DeckListModel::rowsRemoved, this, &DeckStateManager::uniqueCardsChanged); +} + +const DeckList &DeckStateManager::getDeckList() const +{ + return *deckList.get(); +} + +LoadedDeck DeckStateManager::toLoadedDeck() const +{ + return {getDeckList(), lastLoadInfo}; +} + +DeckList::Metadata const &DeckStateManager::getMetadata() const +{ + return deckList->getMetadata(); +} + +QString DeckStateManager::getSimpleDeckName() const +{ + return deckList->getMetadata().name.simplified(); +} + +QString DeckStateManager::getDeckHash() const +{ + return deckList->getDeckHash(); +} + +bool DeckStateManager::isModified() const +{ + return modified; +} + +void DeckStateManager::setModified(bool state) +{ + if (state == modified) { + return; + } + + modified = state; + emit isModifiedChanged(modified); +} + +bool DeckStateManager::isBlankNewDeck() const +{ + return !isModified() && deckList->isBlankDeck(); +} + +void DeckStateManager::replaceDeck(const LoadedDeck &deck) +{ + lastLoadInfo = deck.lastLoadInfo; + deckList = QSharedPointer(new DeckList(deck.deckList)); + deckListModel->setDeckList(deckList); + + historyManager->clear(); + + setModified(false); + emit deckReplaced(); +} + +void DeckStateManager::clearDeck() +{ + replaceDeck(LoadedDeck()); +} + +bool DeckStateManager::modifyDeck(const QString &reason, const std::function &operation) +{ + DeckListMemento memento = deckList->createMemento(reason); + bool success = operation(deckListModel); + + if (success) { + historyManager->save(memento); + doCardModified(); + } + + return success; +} + +QModelIndex DeckStateManager::modifyDeck(const QString &reason, + const std::function &operation) +{ + DeckListMemento memento = deckList->createMemento(reason); + QModelIndex idx = operation(deckListModel); + + if (idx.isValid()) { + historyManager->save(memento); + doCardModified(); + } + + return idx; +} + +void DeckStateManager::setName(const QString &name) +{ + QString previous = deckList->getName(); + if (previous == name) { + return; + } + + requestHistorySave(tr("Rename deck to \"%1\" from \"%2\"").arg(name).arg(previous)); + deckList->setName(name); + + doMetadataModified(); +} + +void DeckStateManager::setComments(const QString &comments) +{ + QString previous = deckList->getComments(); + if (previous == comments) { + return; + } + + requestHistorySave(tr("Updated comments (was %1 chars, now %2 chars)").arg(previous.size()).arg(comments.size())); + deckList->setComments(comments); + + doMetadataModified(); +} + +void DeckStateManager::setBannerCard(const CardRef &bannerCard) +{ + CardRef previous = deckList->getBannerCard(); + if (previous == bannerCard) { + return; + } + + requestHistorySave(tr("Set banner card to %1 (%2)").arg(bannerCard.name).arg(bannerCard.providerId)); + deckList->setBannerCard(bannerCard); + + doMetadataModified(); +} + +void DeckStateManager::setTags(const QStringList &tags) +{ + QStringList previous = deckList->getTags(); + if (previous == tags) { + return; + } + + requestHistorySave(tr("Tags changed")); + deckList->setTags(tags); + + doMetadataModified(); +} + +void DeckStateManager::setFormat(const QString &format) +{ + if (deckList->getMetadata().gameFormat == format) { + return; + } + + requestHistorySave(tr("Set format to %1").arg(format)); + deckListModel->setActiveFormat(format); + + doMetadataModified(); +} + +QModelIndex DeckStateManager::addCard(const ExactCard &card, const QString &zoneName) +{ + if (!card) { + return {}; + } + + QString reason = tr("Added (%1): %2 (%3) %4") + .arg(zoneName, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), + card.getPrinting().getProperty("num")); + + QModelIndex idx = modifyDeck(reason, [&card, &zoneName](auto model) { return model->addCard(card, zoneName); }); + + if (idx.isValid()) { + emit focusIndexChanged(idx); + } + + return idx; +} + +QModelIndex DeckStateManager::decrementCard(const ExactCard &card, const QString &zoneName) +{ + if (!card) + return {}; + + QString providerId = card.getPrinting().getUuid(); + QString collectorNumber = card.getPrinting().getProperty("num"); + + QModelIndex idx = deckListModel->findCard(card.getName(), zoneName, providerId, collectorNumber); + if (!idx.isValid()) { + return {}; + } + + bool success = offsetCountAtIndex(idx, false); + + if (!success) { + return {}; + } + + if (idx.isValid()) { + emit focusIndexChanged(idx); + } + + return idx; +} + +static bool doSwapCard(DeckListModel *model, + const QModelIndex &idx, + const QString &cardName, + const QString &providerId, + const QString &otherZone) +{ + bool success = model->offsetCountAtIndex(idx, -1); + if (!success) { + return false; + } + + if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId})) { + model->addCard(card, otherZone); + } else { + // Third argument (true) says create the card no matter what, even if not in DB + model->addPreferredPrintingCard(cardName, otherZone, true); + } + + return true; +} + +bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) +{ + if (!idx.isValid()) + return false; + + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); + QModelIndex gparent = idx.parent().parent(); + + if (!gparent.isValid()) + return false; + + QString zoneName = gparent.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; + + QString reason = tr("Moved to %1 1 × \"%2\" (%3)") // + .arg(otherZoneName) + .arg(cardName) + .arg(providerId); + + return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) { + return doSwapCard(model, idx, cardName, providerId, otherZoneName); + }); +} + +bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx) +{ + if (!idx.isValid() || deckListModel->hasChildren(idx)) { + return false; + } + + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + + QString reason = tr("Removed \"%1\" (all copies)").arg(cardName); + + return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); }); +} + +bool DeckStateManager::incrementCountAtIndex(const QModelIndex &idx) +{ + return offsetCountAtIndex(idx, 1); +} + +bool DeckStateManager::decrementCountAtIndex(const QModelIndex &idx) +{ + return offsetCountAtIndex(idx, -1); +} + +bool DeckStateManager::offsetCountAtIndex(const QModelIndex &idx, int offset) +{ + if (!idx.isValid()) { + return false; + } + + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); + + QString reason = tr("%1 1 × \"%2\" (%3)") // + .arg(offset > 0 ? tr("Added") : tr("Removed")) + .arg(cardName) + .arg(providerId); + + return modifyDeck(reason, [&idx, &offset](auto model) { return model->offsetCountAtIndex(idx, offset); }); +} + +void DeckStateManager::undo(int steps) +{ + if (!historyManager->canUndo()) { + return; + } + + for (int i = 0; i < steps; i++) { + if (!historyManager->canUndo()) { + continue; + } + historyManager->undo(deckList.get()); + } + + deckListModel->rebuildTree(); + + emit deckListModel->layoutChanged(); +} + +void DeckStateManager::redo(int steps) +{ + if (!historyManager->canRedo()) { + return; + } + + for (int i = 0; i < steps; i++) { + if (!historyManager->canRedo()) { + continue; + } + historyManager->redo(deckList.get()); + } + + deckListModel->rebuildTree(); + + emit deckListModel->layoutChanged(); +} + +void DeckStateManager::requestHistorySave(const QString &reason) +{ + historyManager->save(deckList->createMemento(reason)); +} + +/** + * @brief Handles updating state and emitting signals whenever the cards are modified + */ +void DeckStateManager::doCardModified() +{ + setModified(true); + emit cardModified(); + emit deckModified(); +} + +/** + * @brief Handles updating state and emitting signals whenever the metadata is modified + */ +void DeckStateManager::doMetadataModified() +{ + setModified(true); + emit metadataModified(); + emit deckModified(); +} diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h new file mode 100644 index 000000000..0f3ba3255 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -0,0 +1,297 @@ +#ifndef COCKATRICE_DECK_STATE_MANAGER_H +#define COCKATRICE_DECK_STATE_MANAGER_H + +#include "../../deck_loader/loaded_deck.h" +#include "deck_list_model.h" + +#include +#include + +class DeckListHistoryManager; + +/** + * @brief This class centralizes the management of the state of the deck in the deck editor tab. + * It is responsible for owning and managing the DeckListModel, underlying DeckList, load info, and edit history. + * + * Although this class provides getters for the underlying DeckListModel, you should generally refrain from directly + * modifying the returned model. Outside modifications to the deck state should be done through @link + * DeckStateManager::modifyDeck and the metadata setters. + * Those methods ensure that the history is recorded and correct signals are emitted. + */ +class DeckStateManager : public QObject +{ + Q_OBJECT + + LoadedDeck::LoadInfo lastLoadInfo; + QSharedPointer deckList; + DeckListModel *deckListModel; + DeckListHistoryManager *historyManager; + + bool modified = false; + +public: + explicit DeckStateManager(QObject *parent = nullptr); + + /** + * Gets the underlying HistoryManager. + * @return The DeckListHistoryManager instance + */ + DeckListHistoryManager *getHistoryManager() const + { + return historyManager; + } + + /** + * @brief Gets the underlying DeckListModel. + * You should generally refrain modifying the returned model directly. + * However, it's fine (and intended) to perform queries on the returned model. + * @return The DeckListModel instance + */ + DeckListModel *getModel() const + { + return deckListModel; + } + + /** + * @brief Gets a view of the current deck. + */ + const DeckList &getDeckList() const; + + /** + * @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo. + * + * @return A new LoadedDeck instance. + */ + LoadedDeck toLoadedDeck() const; + + /** + * @brief Gets a view of the metadata in the DeckList + */ + DeckList::Metadata const &getMetadata() const; + + /** + * @brief Gets the deck's simplified name. + */ + QString getSimpleDeckName() const; + + /** + * @brief Gets the deck hash. + */ + QString getDeckHash() const; + + /** + * @brief Checks if the deck has been modified since it was last saved + */ + bool isModified() const; + + /** + * @brief Sets the new isModified state, emitting a signal if the state changed. + * This class will automatically update its isModified state, but you may need to set it manually to handle, for + * example, saving. + * @param state The state + */ + void setModified(bool state); + + /** + * @brief Checks if the deck state is as if it was a new deck + */ + bool isBlankNewDeck() const; + + /** + * @brief Overwrites the current deck with a new deck, resetting all history + * @param deck The new deck. + */ + void replaceDeck(const LoadedDeck &deck); + + /** + * @brief Resets the deck to a blank new deck, resetting all history. + */ + void clearDeck(); + + /** + * @brief Sets the lastLoadInfo. + * @param loadInfo The lastLoadInfo + */ + void setLastLoadInfo(const LoadedDeck::LoadInfo &loadInfo) + { + lastLoadInfo = loadInfo; + } + + /** + * @brief Modifies the cards in the deck, in a wrapped operation that is saved to the history. + * + * The operation is a function that accepts a DeckListModel that it operates upon, and returns a bool. + * + * This method will pass the underlying DeckListModel into the operation function. The function can call methods on + * the model to modify the deck. + * The function should return a bool to indicate success/failure. + * + * If the operation returns true, the state of the deck before the operation is ran is saved to the history, and the + * isModified state is updated. + * If the operation returns false, the history and isModified state is not updated. + * + * Note that even if the operation fails, any modifications to the model will already have been made. + * It's recommended for the operation to always return true if any modification has already been made to the model, + * as not doing that may cause the state to become desynced. + * + * @param reason The reason to display in the history + * @param operation The modification operation. + * @return The bool returned from the operation + */ + bool modifyDeck(const QString &reason, const std::function &operation); + + /** + * @brief Modifies the cards in the deck, in a wrapped operation that is saved to the history. + * + * The operation is a function that accepts a DeckListModel that it operates upon, and returns a QModelIndex. + * If the index is invalid, then the operation is considered to be a failure. + * + * See the other @link DeckStateManager::modifyDeck for more info about the behavior of this method. + * + * @param reason The reason to display in the history + * @param operation The modification operation. + * @return The QModelIndex returned from the operation + */ + QModelIndex modifyDeck(const QString &reason, const std::function &operation); + + /// @name Metadata setters + /// @brief These methods set the metadata. Will no-op if the new value is the same as the current value. + /// Saves the operation to history if successful. + ///@{ + void setName(const QString &name); + void setComments(const QString &comments); + void setBannerCard(const CardRef &bannerCard); + void setTags(const QStringList &tags); + void setFormat(const QString &format); + ///@} + + /** + * @brief Adds the given card to the given zone. + * Saves the operation to history if successful. + * + * @param card The card to add + * @param zoneName The zone to add the card to + * @return The index of the added card + */ + QModelIndex addCard(const ExactCard &card, const QString &zoneName); + + /** + * @brief Removes 1 copy of the given card from the given zone. + * Saves the operation to history if successful. + * + * @param card The card to remove + * @param zoneName The zone to remove the card from + * @return The index of the removed card. Will be invalid if the last copy was removed. + */ + QModelIndex decrementCard(const ExactCard &card, const QString &zoneName); + + /** + * @brief Swaps one copy of the card at the given index between the maindeck and sideboard. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool swapCardAtIndex(const QModelIndex &idx); + + /** + * @brief Removes all copies of the card at the given index. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool removeCardAtIndex(const QModelIndex &idx); + + /** + * @brief Increments the number of copies of the card at the given index by 1. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool incrementCountAtIndex(const QModelIndex &idx); + + /** + * @brief Decrements the number of copies of the card at the given index by 1. + * No-ops if index is invalid or not a card node. + * Saves the operation to history if successful. + * + * @param idx The model index + * @return Whether the operation was successfully performed + */ + bool decrementCountAtIndex(const QModelIndex &idx); + + /** + * Undoes n steps of the history, setting the decklist state and updating the current step in the historyManager. + * @param steps Number of steps to undo. + */ + void undo(int steps = 1); + + /** + * Redoes n steps of the history, setting the decklist state and updating the current step in the historyManager. + * @param steps Number of steps to redo. + */ + void redo(int steps = 1); + +public slots: + /** + * Saves the current decklist state to history. + * @param reason The reason that is shown in the history. + */ + void requestHistorySave(const QString &reason); + +private: + bool offsetCountAtIndex(const QModelIndex &idx, int offset); + void doCardModified(); + void doMetadataModified(); + +signals: + /** + * A modification has been made to the cards in the deck + */ + void cardModified(); + + /** + * A card that wasn't previously in the deck was added to the deck, or the last copy of a card was removed from the + * deck. + */ + void uniqueCardsChanged(); + + /** + * A modification has been made to the metadata in the deck + */ + void metadataModified(); + + /** + * A modification has been made to the cards or metadata in the deck + */ + void deckModified(); + + /** + * The history has been greatly changed and needs to be reloaded. + */ + void historyChanged(); + + /** + * The deck has been completely changed. + */ + void deckReplaced(); + + /** + * The isModified state of the deck has changed + * @param isModified the new state + */ + void isModifiedChanged(bool isModified); + + /** + * The selected card on any views connected to this deck should be changed to this index. + * @param index The model index + */ + void focusIndexChanged(QModelIndex index); +}; + +#endif // COCKATRICE_DECK_STATE_MANAGER_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp index 587407065..6ed6b67a4 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp @@ -2,6 +2,7 @@ #include "../../deck_loader/card_node_function.h" #include "../../deck_loader/deck_loader.h" +#include "../deck_editor/deck_state_manager.h" #include "../interface/widgets/cards/card_info_picture_widget.h" #include "../interface/widgets/general/layout_containers/flow_widget.h" @@ -21,7 +22,8 @@ #include #include -DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckListModel *_model) : QDialog(parent), model(_model) +DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *deckStateManger) + : QDialog(parent), deckStateManager(deckStateManger) { setMinimumSize(500, 500); setAcceptDrops(true); @@ -165,36 +167,39 @@ void DlgSelectSetForCards::actOK() if (modifiedSetsAndCardsMap.isEmpty()) { accept(); // Nothing to do - } else { - emit deckAboutToBeModified(tr("Bulk modified printings.")); + return; } - for (QString modifiedSet : modifiedSetsAndCardsMap.keys()) { - for (QString card : modifiedSetsAndCardsMap.value(modifiedSet)) { - swapPrinting(model, modifiedSet, card); + auto bulkModify = [&modifiedSetsAndCardsMap](DeckListModel *model) { + for (QString modifiedSet : modifiedSetsAndCardsMap.keys()) { + for (QString card : modifiedSetsAndCardsMap.value(modifiedSet)) { + swapPrinting(model, modifiedSet, card); + } } - } + return true; + }; + + deckStateManager->modifyDeck(tr("Bulk modified printings."), bulkModify); - if (!modifiedSetsAndCardsMap.isEmpty()) { - emit deckModified(); - } accept(); } void DlgSelectSetForCards::actClear() { - emit deckAboutToBeModified(tr("Cleared all printing information.")); - model->forEachCard(CardNodeFunction::ClearPrintingData()); - emit deckModified(); + deckStateManager->modifyDeck(tr("Cleared all printing information."), [](auto model) { + model->forEachCard(CardNodeFunction::ClearPrintingData()); + return true; + }); accept(); } void DlgSelectSetForCards::actSetAllToPreferred() { - emit deckAboutToBeModified(tr("Set all printings to preferred.")); - model->forEachCard(CardNodeFunction::ClearPrintingData()); - model->forEachCard(CardNodeFunction::SetProviderIdToPreferred()); - emit deckModified(); + deckStateManager->modifyDeck(tr("Set all printings to preferred."), [](auto model) { + model->forEachCard(CardNodeFunction::ClearPrintingData()); + model->forEachCard(CardNodeFunction::SetProviderIdToPreferred()); + return true; + }); accept(); } @@ -227,10 +232,8 @@ void DlgSelectSetForCards::sortSetsByCount() QMap DlgSelectSetForCards::getSetsForCards() { QMap setCounts; - if (!model) - return setCounts; - QList cardNames = model->getCardNames(); + QList cardNames = deckStateManager->getModel()->getCardNames(); for (auto cardName : cardNames) { CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(cardName); @@ -269,7 +272,7 @@ void DlgSelectSetForCards::updateCardLists() } } - QList cardNames = model->getCardNames(); + QList cardNames = deckStateManager->getModel()->getCardNames(); for (auto cardName : cardNames) { bool found = false; @@ -351,10 +354,8 @@ void DlgSelectSetForCards::dropEvent(QDropEvent *event) QMap DlgSelectSetForCards::getCardsForSets() { QMap setCards; - if (!model) - return setCards; - QList cardNames = model->getCardNames(); + QList cardNames = deckStateManager->getModel()->getCardNames(); for (auto cardName : cardNames) { CardInfoPtr infoPtr = CardDatabaseManager::query()->getCardInfo(cardName); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h index 5cdef5a30..795366b57 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.h @@ -18,6 +18,7 @@ #include #include +class DeckStateManager; class SetEntryWidget; // Forward declaration class DlgSelectSetForCards : public QDialog @@ -25,7 +26,7 @@ class DlgSelectSetForCards : public QDialog Q_OBJECT public: - explicit DlgSelectSetForCards(QWidget *parent, DeckListModel *_model); + explicit DlgSelectSetForCards(QWidget *parent, DeckStateManager *deckStateManager); void retranslateUi(); void sortSetsByCount(); QMap getCardsForSets(); @@ -37,7 +38,6 @@ public: signals: void widgetOrderChanged(); void orderChanged(); - void deckAboutToBeModified(const QString &reason); void deckModified(); public slots: @@ -61,7 +61,7 @@ private: QLabel *modifiedCardsLabel; QWidget *listContainer; QListWidget *listWidget; - DeckListModel *model; + DeckStateManager *deckStateManager; QMap setEntries; QPushButton *clearButton; QPushButton *setAllToPreferredButton; 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 d8bd88b37..b1346e4fd 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 @@ -11,16 +11,12 @@ * UI elements for managing card counts in both the mainboard and sideboard zones. * * @param parent The parent widget. - * @param deckEditor Pointer to the TabDeckEditor. - * @param deckModel Pointer to the DeckListModel. - * @param deckView Pointer to the QTreeView for the deck display. + * @param deckStateManager Pointer to the DeckStateManager * @param cardSizeSlider Pointer to the QSlider used for dynamic font resizing. * @param rootCard The root card for the widget. */ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard) : QWidget(parent), cardSizeSlider(cardSizeSlider) @@ -32,11 +28,9 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, setContentsMargins(5, 5, 5, 5); // Padding around the text zoneLabelMainboard = new ShadowBackgroundLabel(this, tr("Mainboard")); - buttonBoxMainboard = - new CardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard, DECK_ZONE_MAIN); + buttonBoxMainboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_MAIN); zoneLabelSideboard = new ShadowBackgroundLabel(this, tr("Sideboard")); - buttonBoxSideboard = - new CardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard, DECK_ZONE_SIDE); + buttonBoxSideboard = new CardAmountWidget(this, deckStateManager, cardSizeSlider, rootCard, DECK_ZONE_SIDE); layout->addWidget(zoneLabelMainboard, 0, Qt::AlignHCenter | Qt::AlignBottom); layout->addWidget(buttonBoxMainboard, 0, Qt::AlignHCenter | Qt::AlignTop); 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 6ce10cf2e..5a03c5f4a 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 @@ -18,9 +18,7 @@ class AllZonesCardAmountWidget : public QWidget Q_OBJECT public: explicit AllZonesCardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard); int getMainboardAmount(); 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 d01725cc4..62c8e2d60 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -1,5 +1,7 @@ #include "card_amount_widget.h" +#include "../deck_editor/deck_state_manager.h" + #include #include @@ -7,22 +9,17 @@ * @brief Constructs a widget for displaying and controlling the card count in a specific zone. * * @param parent The parent widget. - * @param deckEditor Pointer to the TabDeckEditor instance. - * @param deckModel Pointer to the DeckListModel instance. - * @param deckView Pointer to the QTreeView displaying the deck. * @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). */ CardAmountWidget::CardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard, const QString &zoneName) - : QWidget(parent), deckEditor(deckEditor), deckModel(deckModel), deckView(deckView), cardSizeSlider(cardSizeSlider), - rootCard(rootCard), zoneName(zoneName), hovered(false) + : QWidget(parent), deckStateManager(deckStateManager), cardSizeSlider(cardSizeSlider), rootCard(rootCard), + zoneName(zoneName), hovered(false) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -56,15 +53,10 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, layout->addWidget(incrementButton); // React to model changes - connect(deckModel, &DeckListModel::dataChanged, this, &CardAmountWidget::updateCardCount); - connect(deckModel, &QAbstractItemModel::rowsRemoved, this, &CardAmountWidget::updateCardCount); + connect(deckStateManager, &DeckStateManager::cardModified, this, &CardAmountWidget::updateCardCount); // Connect slider for dynamic font size adjustment connect(cardSizeSlider, &QSlider::valueChanged, this, &CardAmountWidget::adjustFontSize); - - if (deckEditor) { - connect(this, &CardAmountWidget::deckModified, deckEditor, &AbstractTabDeckEditor::onDeckHistorySaveRequested); - } } /** @@ -168,7 +160,7 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model, void CardAmountWidget::addPrinting(const QString &zone) { // Check if we will need to add extra copies due to replacing copies without providerIds - QModelIndex existing = deckModel->findCard(rootCard.getName(), zone); + QModelIndex existing = deckStateManager->getModel()->findCard(rootCard.getName(), zone); int extraCopies = 0; bool replacingProviderless = false; @@ -192,15 +184,13 @@ void CardAmountWidget::addPrinting(const QString &zone) .arg(rootCard.getPrinting().getUuid()) .arg(replacingProviderless ? " (replaced providerless printings)" : ""); - emit deckModified(reason); - // Add the card and expand the list UI - auto newCardIndex = addAndReplacePrintings(deckModel, existing, rootCard, zone, extraCopies, replacingProviderless); + QModelIndex newCardIndex = deckStateManager->modifyDeck(reason, [&](auto model) { + return addAndReplacePrintings(model, existing, rootCard, zone, extraCopies, replacingProviderless); + }); if (newCardIndex.isValid()) { - deckView->setCurrentIndex(newCardIndex); - deckView->setFocus(Qt::FocusReason::MouseFocusReason); - deckEditor->setModified(true); + emit deckStateManager->focusIndexChanged(newCardIndex); } } @@ -250,13 +240,11 @@ void CardAmountWidget::decrementCardHelper(const QString &zone) .arg(zone == DECK_ZONE_MAIN ? "mainboard" : "sideboard") .arg(rootCard.getPrinting().getUuid()); - emit deckModified(reason); - - QModelIndex idx = deckModel->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), + deckStateManager->modifyDeck(reason, [this, &zone](auto model) { + QModelIndex idx = model->findCard(rootCard.getName(), zone, rootCard.getPrinting().getUuid(), rootCard.getPrinting().getProperty("num")); - - deckModel->offsetCountAtIndex(idx, -1); - deckEditor->setModified(true); + return model->offsetCountAtIndex(idx, -1); + }); } /** @@ -273,7 +261,7 @@ int CardAmountWidget::countCardsInZone(const QString &deckZone) return 0; // Cards without uuids/providerIds CANNOT match another card, they are undefined for us. } - QList cards = deckModel->getCardsForZone(deckZone); + QList cards = deckStateManager->getModel()->getCardsForZone(deckZone); return std::count_if(cards.cbegin(), cards.cend(), [&uuid](const ExactCard &card) { return card.getPrinting().getUuid() == uuid; }); 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 b4704cede..983416782 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -27,9 +27,7 @@ signals: public: explicit CardAmountWidget(QWidget *parent, - AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard, const QString &zoneName); @@ -44,9 +42,7 @@ protected: void showEvent(QShowEvent *event) override; private: - AbstractTabDeckEditor *deckEditor; - DeckListModel *deckModel; - QTreeView *deckView; + DeckStateManager *deckStateManager; QSlider *cardSizeSlider; ExactCard rootCard; QString zoneName; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index e64d6a009..95d6b2cdf 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -3,6 +3,7 @@ #include "../../../client/settings/cache_settings.h" #include "../../../interface/card_picture_loader/card_picture_loader.h" #include "../../../interface/widgets/dialogs/dlg_select_set_for_cards.h" +#include "../deck_editor/deck_state_manager.h" #include "printing_selector_card_display_widget.h" #include "printing_selector_card_search_widget.h" #include "printing_selector_card_selection_widget.h" @@ -21,12 +22,9 @@ * * @param parent The parent widget for the PrintingSelector. * @param deckEditor The TabDeckEditor instance used for managing the deck. - * @param deckModel The DeckListModel instance that provides data for the deck's contents. - * @param deckView The QTreeView instance used to display the deck and its contents. */ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deckEditor) - : QWidget(parent), deckEditor(_deckEditor), deckModel(deckEditor->deckDockWidget->deckModel), - deckView(deckEditor->deckDockWidget->deckView) + : QWidget(parent), deckEditor(_deckEditor), deckStateManager(_deckEditor->deckStateManager) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout = new QVBoxLayout(this); @@ -74,13 +72,12 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck layout->addWidget(flowWidget); - cardSelectionBar = new PrintingSelectorCardSelectionWidget(this); + cardSelectionBar = new PrintingSelectorCardSelectionWidget(this, deckStateManager); cardSelectionBar->setVisible(SettingsCache::instance().getPrintingSelectorNavigationButtonsVisible()); layout->addWidget(cardSelectionBar); // Connect deck model data change signal to update display - connect(deckModel, &DeckListModel::rowsInserted, this, &PrintingSelector::printingsInDeckChanged); - connect(deckModel, &DeckListModel::rowsRemoved, this, &PrintingSelector::printingsInDeckChanged); + connect(deckStateManager, &DeckStateManager::uniqueCardsChanged, this, &PrintingSelector::printingsInDeckChanged); retranslateUi(); } @@ -152,7 +149,8 @@ void PrintingSelector::getAllSetsForCurrentCard() QList printingsToUse; if (SettingsCache::instance().getBumpSetsWithCardsInDeckToTop()) { - printingsToUse = sortToolBar->prependPrintingsInDeck(filteredPrintings, selectedCard, deckModel); + printingsToUse = + sortToolBar->prependPrintingsInDeck(filteredPrintings, selectedCard, deckStateManager->getModel()); } else { printingsToUse = filteredPrintings; } @@ -164,7 +162,7 @@ void PrintingSelector::getAllSetsForCurrentCard() connect(widgetLoadingBufferTimer, &QTimer::timeout, this, [=, this]() mutable { for (int i = 0; i < BATCH_SIZE && currentIndex < printingsToUse.size(); ++i, ++currentIndex) { auto card = ExactCard(selectedCard, printingsToUse[currentIndex]); - auto *cardDisplayWidget = new PrintingSelectorCardDisplayWidget(this, deckEditor, deckModel, deckView, + auto *cardDisplayWidget = new PrintingSelectorCardDisplayWidget(this, deckEditor, deckStateManager, cardSizeWidget->getSlider(), card); flowWidget->addWidget(cardDisplayWidget); cardDisplayWidget->clampSetNameToPicture(); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index e34ce3fe6..e1c07addf 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -21,6 +21,7 @@ #define BATCH_SIZE 10 +class DeckStateManager; class PrintingSelectorCardSearchWidget; class PrintingSelectorCardSelectionWidget; class PrintingSelectorCardSortingWidget; @@ -35,15 +36,6 @@ public: void setCard(const CardInfoPtr &newCard); void getAllSetsForCurrentCard(); - [[nodiscard]] DeckListModel *getDeckModel() const - { - return deckModel; - } - - [[nodiscard]] AbstractTabDeckEditor *getDeckEditor() const - { - return deckEditor; - } public slots: void retranslateUi(); @@ -75,8 +67,7 @@ private: CardSizeWidget *cardSizeWidget; PrintingSelectorCardSelectionWidget *cardSelectionBar; AbstractTabDeckEditor *deckEditor; - DeckListModel *deckModel; - QTreeView *deckView; + DeckStateManager *deckStateManager; CardInfoPtr selectedCard; QTimer *widgetLoadingBufferTimer; int currentIndex = 0; 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 86d6659a8..92cf2437c 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 @@ -18,15 +18,13 @@ * * @param parent The parent widget for this display. * @param deckEditor The TabDeckEditor instance for deck management. - * @param deckModel The DeckListModel instance providing deck data. - * @param deckView The QTreeView instance displaying the deck. + * @param deckStateManager The DeckStateManager instance providing deck data. * @param cardSizeSlider The slider controlling the size of the displayed card. * @param rootCard The root card object, representing the card to be displayed. */ PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard) : QWidget(parent) @@ -36,8 +34,7 @@ PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *pa setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // Create the overlay widget for the card display - overlayWidget = - new PrintingSelectorCardOverlayWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, rootCard); + overlayWidget = new PrintingSelectorCardOverlayWidget(this, deckEditor, deckStateManager, cardSizeSlider, rootCard); connect(overlayWidget, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this, [this]() { emit cardPreferenceChanged(); }); 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 608c2df5c..2637b0e57 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 @@ -21,8 +21,7 @@ class PrintingSelectorCardDisplayWidget : public QWidget public: PrintingSelectorCardDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard); 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 04ab07a59..ac36f2cf4 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 @@ -22,15 +22,13 @@ * * @param parent The parent widget for this overlay. * @param _deckEditor The TabDeckEditor instance for deck management. - * @param deckModel The DeckListModel instance providing deck data. - * @param deckView The QTreeView instance displaying the deck. + * @param deckStateManager The DeckStateManager instance providing deck data. * @param cardSizeSlider The slider controlling the size of the card. * @param _rootCard The root card object that contains information about the card. */ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, - DeckListModel *deckModel, - QTreeView *deckView, + DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &_rootCard) : QWidget(parent), deckEditor(_deckEditor), rootCard(_rootCard) @@ -58,8 +56,7 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa updatePinBadgeVisibility(); // Add AllZonesCardAmountWidget - allZonesCardAmountWidget = - new AllZonesCardAmountWidget(this, deckEditor, deckModel, deckView, cardSizeSlider, _rootCard); + allZonesCardAmountWidget = new AllZonesCardAmountWidget(this, deckStateManager, cardSizeSlider, _rootCard); allZonesCardAmountWidget->raise(); // Ensure it's on top of the picture // Set initial visibility based on amounts 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 3bd5ce247..8550612bd 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 @@ -20,8 +20,7 @@ class PrintingSelectorCardOverlayWidget : public QWidget public: explicit PrintingSelectorCardOverlayWidget(QWidget *parent, AbstractTabDeckEditor *_deckEditor, - DeckListModel *_deckModel, - QTreeView *_deckView, + DeckStateManager *_deckStateManager, QSlider *_cardSizeSlider, const ExactCard &_rootCard); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp index 317ce83b6..2eb2ef245 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp @@ -11,7 +11,9 @@ * * @param parent The parent PrintingSelector widget responsible for managing card selection. */ -PrintingSelectorCardSelectionWidget::PrintingSelectorCardSelectionWidget(PrintingSelector *parent) : parent(parent) +PrintingSelectorCardSelectionWidget::PrintingSelectorCardSelectionWidget(PrintingSelector *parent, + DeckStateManager *deckStateManager) + : parent(parent), deckStateManager(deckStateManager) { cardSelectionBarLayout = new QHBoxLayout(this); cardSelectionBarLayout->setContentsMargins(9, 0, 9, 0); @@ -48,12 +50,6 @@ void PrintingSelectorCardSelectionWidget::connectSignals() void PrintingSelectorCardSelectionWidget::selectSetForCards() { - auto *setSelectionDialog = new DlgSelectSetForCards(nullptr, parent->getDeckModel()); - connect(setSelectionDialog, &DlgSelectSetForCards::deckAboutToBeModified, parent->getDeckEditor(), - &AbstractTabDeckEditor::onDeckHistorySaveRequested); - connect(setSelectionDialog, &DlgSelectSetForCards::deckModified, parent->getDeckEditor(), - &AbstractTabDeckEditor::onDeckModified); - if (!setSelectionDialog->exec()) { - return; - } + auto *setSelectionDialog = new DlgSelectSetForCards(nullptr, deckStateManager); + setSelectionDialog->exec(); } diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h index a1176a76c..ecd5c83e3 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_selection_widget.h @@ -18,7 +18,7 @@ class PrintingSelectorCardSelectionWidget : public QWidget Q_OBJECT public: - explicit PrintingSelectorCardSelectionWidget(PrintingSelector *parent); + explicit PrintingSelectorCardSelectionWidget(PrintingSelector *parent, DeckStateManager *deckStateManager); void connectSignals(); @@ -27,6 +27,7 @@ public slots: private: PrintingSelector *parent; + DeckStateManager *deckStateManager; QHBoxLayout *cardSelectionBarLayout; QPushButton *previousCardButton; QPushButton *selectSetForCardsButton; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp index af7cedbeb..725e5df90 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp @@ -183,7 +183,7 @@ QList PrintingSelectorCardSortingWidget::prependPinnedPrintings(co */ QList PrintingSelectorCardSortingWidget::prependPrintingsInDeck(const QList &printings, const CardInfoPtr &selectedCard, - DeckListModel *deckModel) + const DeckListModel *deckModel) { if (!selectedCard) { return {}; diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h index d0faea4ac..b5a00b81e 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.h @@ -23,7 +23,7 @@ public: QList prependPinnedPrintings(const QList &printings, const QString &cardName); QList prependPrintingsInDeck(const QList &printings, const CardInfoPtr &selectedCard, - DeckListModel *deckModel); + const DeckListModel *deckModel); public slots: void updateSortOrder(); 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 cb2002199..29bcc1ab1 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -12,6 +12,7 @@ #include "../../../client/settings/cache_settings.h" #include "../client/network/interfaces/deck_stats_interface.h" #include "../client/network/interfaces/tapped_out_interface.h" +#include "../deck_editor/deck_state_manager.h" #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/pixel_map_generator.h" #include "../interface/widgets/dialogs/dlg_load_deck.h" @@ -52,7 +53,7 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta { setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); - historyManager = new DeckListHistoryManager(this); + deckStateManager = new DeckStateManager(this); databaseDisplayDockWidget = new DeckEditorDatabaseDisplayWidget(this); deckDockWidget = new DeckEditorDeckDockWidget(this); @@ -64,14 +65,8 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta }); // Connect deck signals to this tab - connect(deckDockWidget, &DeckEditorDeckDockWidget::deckChanged, this, &AbstractTabDeckEditor::onDeckChanged); - connect(deckDockWidget, &DeckEditorDeckDockWidget::deckModified, this, &AbstractTabDeckEditor::onDeckModified); - connect(deckDockWidget, &DeckEditorDeckDockWidget::requestDeckHistorySave, this, - &AbstractTabDeckEditor::onDeckHistorySaveRequested); - connect(deckDockWidget, &DeckEditorDeckDockWidget::requestDeckHistoryClear, this, - &AbstractTabDeckEditor::onDeckHistoryClearRequested); - connect(deckDockWidget, &DeckEditorDeckDockWidget::cardChanged, this, &AbstractTabDeckEditor::updateCard); - connect(this, &AbstractTabDeckEditor::decrementCard, deckDockWidget, &DeckEditorDeckDockWidget::actDecrementCard); + connect(deckStateManager, &DeckStateManager::isModifiedChanged, this, &AbstractTabDeckEditor::onDeckModified); + connect(deckDockWidget, &DeckEditorDeckDockWidget::selectedCardChanged, this, &AbstractTabDeckEditor::updateCard); // Connect database display signals to this tab connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, this, @@ -107,7 +102,6 @@ void AbstractTabDeckEditor::updateCard(const ExactCard &card) /** @brief Placeholder: called when the deck changes. */ void AbstractTabDeckEditor::onDeckChanged() { - historyManager->clear(); } /** @@ -115,24 +109,8 @@ void AbstractTabDeckEditor::onDeckChanged() */ void AbstractTabDeckEditor::onDeckModified() { - setModified(!isBlankNewDeck()); - deckMenu->setSaveStatus(!isBlankNewDeck()); -} - -/** - * @brief Marks the tab as modified and updates the save menu status. - */ -void AbstractTabDeckEditor::onDeckHistorySaveRequested(const QString &modificationReason) -{ - historyManager->save(deckDockWidget->getDeckList().createMemento(modificationReason)); -} - -/** - * @brief Marks the tab as modified and updates the save menu status. - */ -void AbstractTabDeckEditor::onDeckHistoryClearRequested() -{ - historyManager->clear(); + deckMenu->setSaveStatus(!deckStateManager->isBlankNewDeck()); + emit tabTextChanged(this, getTabText()); } /** @@ -142,7 +120,7 @@ void AbstractTabDeckEditor::onDeckHistoryClearRequested() */ void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName) { - deckDockWidget->actAddCard(card, zoneName); + deckStateManager->addCard(card, zoneName); databaseDisplayDockWidget->searchEdit->setSelection(0, databaseDisplayDockWidget->searchEdit->text().length()); } @@ -170,13 +148,13 @@ void AbstractTabDeckEditor::actAddCardToSideboard(const ExactCard &card) /** @brief Decrements a card from the main deck. */ void AbstractTabDeckEditor::actDecrementCard(const ExactCard &card) { - emit decrementCard(card, DECK_ZONE_MAIN); + deckStateManager->decrementCard(card, DECK_ZONE_MAIN); } /** @brief Decrements a card from the sideboard. */ void AbstractTabDeckEditor::actDecrementCardFromSideboard(const ExactCard &card) { - emit decrementCard(card, DECK_ZONE_SIDE); + deckStateManager->decrementCard(card, DECK_ZONE_SIDE); } /** @@ -198,45 +176,13 @@ void AbstractTabDeckEditor::openDeck(const LoadedDeck &deck) */ void AbstractTabDeckEditor::setDeck(const LoadedDeck &_deck) { - deckDockWidget->setDeck(_deck); - CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(getDeckList().getCardRefList())); - setModified(false); + deckStateManager->replaceDeck(_deck); + CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(_deck.deckList.getCardRefList())); aDeckDockVisible->setChecked(true); deckDockWidget->setVisible(aDeckDockVisible->isChecked()); } -/** @brief Returns the currently loaded deck. */ -DeckLoader *AbstractTabDeckEditor::getDeckLoader() const -{ - return deckDockWidget->getDeckLoader(); -} - -/** @brief Returns the currently loaded deck list. */ -const DeckList &AbstractTabDeckEditor::getDeckList() const -{ - return deckDockWidget->getDeckList(); -} - -/** - * @brief Sets the modified state of the tab. - * @param _modified True if tab is modified, false otherwise. - */ -void AbstractTabDeckEditor::setModified(bool _modified) -{ - modified = _modified; - emit tabTextChanged(this, getTabText()); -} - -/** - * @brief Returns true if the tab is a blank newly created deck. - */ -bool AbstractTabDeckEditor::isBlankNewDeck() const -{ - const LoadedDeck &loadedDeck = deckDockWidget->getDeckLoader()->getDeck(); - return !modified && loadedDeck.isEmpty(); -} - /** @brief Creates a new deck. Handles opening in new tab if needed. */ void AbstractTabDeckEditor::actNewDeck() { @@ -255,9 +201,8 @@ void AbstractTabDeckEditor::actNewDeck() /** @brief Clears the current deck and resets modified flag. */ void AbstractTabDeckEditor::cleanDeckAndResetModified() { + deckStateManager->clearDeck(); deckMenu->setSaveStatus(false); - deckDockWidget->cleanDeck(); - setModified(false); } /** @@ -268,13 +213,13 @@ void AbstractTabDeckEditor::cleanDeckAndResetModified() AbstractTabDeckEditor::DeckOpenLocation AbstractTabDeckEditor::confirmOpen(const bool openInSameTabIfBlank) { if (SettingsCache::instance().getOpenDeckInNewTab()) { - if (openInSameTabIfBlank && isBlankNewDeck()) + if (openInSameTabIfBlank && deckStateManager->isBlankNewDeck()) return SAME_TAB; else return NEW_TAB; } - if (!modified) + if (!deckStateManager->isModified()) return SAME_TAB; tabSupervisor->setCurrentWidget(this); @@ -325,7 +270,6 @@ void AbstractTabDeckEditor::actLoadDeck() QString fileName = dialog.selectedFiles().at(0); openDeckFromFile(fileName, deckOpenLocation); - deckDockWidget->updateBannerCardComboBox(); } /** @@ -371,7 +315,7 @@ void AbstractTabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLo */ bool AbstractTabDeckEditor::actSaveDeck() { - const LoadedDeck &loadedDeck = getDeckLoader()->getDeck(); + const auto loadedDeck = deckStateManager->toLoadedDeck(); if (loadedDeck.lastLoadInfo.remoteDeckId != LoadedDeck::LoadInfo::NON_REMOTE_ID) { QString deckString = loadedDeck.deckList.writeToString_Native(); if (deckString.length() > MAX_FILE_LENGTH) { @@ -392,8 +336,10 @@ bool AbstractTabDeckEditor::actSaveDeck() if (loadedDeck.lastLoadInfo.fileName.isEmpty()) return actSaveDeckAs(); - if (getDeckLoader()->saveToFile(loadedDeck.lastLoadInfo.fileName, loadedDeck.lastLoadInfo.fileFormat)) { - setModified(false); + auto deckLoader = DeckLoader(this); + deckLoader.setDeck(loadedDeck); + if (deckLoader.saveToFile(loadedDeck.lastLoadInfo.fileName, loadedDeck.lastLoadInfo.fileFormat)) { + deckStateManager->setModified(false); return true; } @@ -409,12 +355,14 @@ bool AbstractTabDeckEditor::actSaveDeck() */ bool AbstractTabDeckEditor::actSaveDeckAs() { + LoadedDeck loadedDeck = deckStateManager->toLoadedDeck(); + QFileDialog dialog(this, tr("Save deck")); dialog.setDirectory(SettingsCache::instance().getDeckPath()); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setDefaultSuffix("cod"); dialog.setNameFilters(DeckLoader::FILE_NAME_FILTERS); - dialog.selectFile(getDeckList().getName().trimmed()); + dialog.selectFile(loadedDeck.deckList.getName().trimmed()); if (!dialog.exec()) return false; @@ -422,14 +370,18 @@ bool AbstractTabDeckEditor::actSaveDeckAs() QString fileName = dialog.selectedFiles().at(0); DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(fileName); - if (!getDeckLoader()->saveToFile(fileName, fmt)) { + DeckLoader deckLoader = DeckLoader(this); + deckLoader.setDeck(loadedDeck); + if (!deckLoader.saveToFile(fileName, fmt)) { QMessageBox::critical( this, tr("Error"), tr("The deck could not be saved.\nPlease check that the directory is writable and try again.")); return false; } - setModified(false); + deckStateManager->setLastLoadInfo({.fileName = fileName, .fileFormat = fmt}); + + deckStateManager->setModified(false); SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName); return true; } @@ -443,7 +395,7 @@ void AbstractTabDeckEditor::saveDeckRemoteFinished(const Response &response) if (response.response_code() != Response::RespOk) QMessageBox::critical(this, tr("Error"), tr("The deck could not be saved.")); else - setModified(false); + deckStateManager->setModified(false); } /** @@ -464,7 +416,7 @@ void AbstractTabDeckEditor::actLoadDeckFromClipboard() emit openDeckEditor({.deckList = dlg.getDeckList()}); } else { setDeck({.deckList = dlg.getDeckList()}); - setModified(true); + deckStateManager->setModified(true); } deckMenu->setSaveStatus(true); @@ -476,12 +428,13 @@ void AbstractTabDeckEditor::actLoadDeckFromClipboard() */ void AbstractTabDeckEditor::editDeckInClipboard(bool annotated) { - DlgEditDeckInClipboard dlg(getDeckLoader()->getDeck().deckList, annotated, this); + LoadedDeck loadedDeck = deckStateManager->toLoadedDeck(); + DlgEditDeckInClipboard dlg(loadedDeck.deckList, annotated, this); if (!dlg.exec()) return; - setDeck({dlg.getDeckList(), getDeckLoader()->getDeck().lastLoadInfo}); - setModified(true); + setDeck({dlg.getDeckList(), loadedDeck.lastLoadInfo}); + deckStateManager->setModified(true); deckMenu->setSaveStatus(true); } @@ -500,25 +453,25 @@ void AbstractTabDeckEditor::actEditDeckInClipboardRaw() /** @brief Saves deck to clipboard with set info and annotation. */ void AbstractTabDeckEditor::actSaveDeckToClipboard() { - DeckLoader::saveToClipboard(getDeckList(), true, true); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), true, true); } /** @brief Saves deck to clipboard with annotation, without set info. */ void AbstractTabDeckEditor::actSaveDeckToClipboardNoSetInfo() { - DeckLoader::saveToClipboard(getDeckList(), true, false); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), true, false); } /** @brief Saves deck to clipboard without annotations, with set info. */ void AbstractTabDeckEditor::actSaveDeckToClipboardRaw() { - DeckLoader::saveToClipboard(getDeckList(), false, true); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), false, true); } /** @brief Saves deck to clipboard without annotations or set info. */ void AbstractTabDeckEditor::actSaveDeckToClipboardRawNoSetInfo() { - DeckLoader::saveToClipboard(getDeckList(), false, false); + DeckLoader::saveToClipboard(deckStateManager->getDeckList(), false, false); } /** @brief Prints the deck using a QPrintPreviewDialog. */ @@ -526,7 +479,7 @@ void AbstractTabDeckEditor::actPrintDeck() { auto *dlg = new QPrintPreviewDialog(this); connect(dlg, &QPrintPreviewDialog::paintRequested, this, - [this](QPrinter *printer) { DeckLoader::printDeckList(printer, getDeckList()); }); + [this](QPrinter *printer) { DeckLoader::printDeckList(printer, deckStateManager->getDeckList()); }); dlg->exec(); } @@ -547,7 +500,7 @@ void AbstractTabDeckEditor::actLoadDeckFromWebsite() emit openDeckEditor({.deckList = dlg.getDeck()}); } else { setDeck({.deckList = dlg.getDeck()}); - setModified(true); + deckStateManager->setModified(true); } deckMenu->setSaveStatus(true); @@ -559,7 +512,7 @@ void AbstractTabDeckEditor::actLoadDeckFromWebsite() */ void AbstractTabDeckEditor::exportToDecklistWebsite(DeckLoader::DecklistWebsite website) { - QString decklistUrlString = DeckLoader::exportDeckToDecklist(getDeckList(), website); + QString decklistUrlString = DeckLoader::exportDeckToDecklist(deckStateManager->getDeckList(), website); // Check to make sure the string isn't empty. if (decklistUrlString.isEmpty()) { // Show an error if the deck is empty, and return. @@ -592,14 +545,14 @@ void AbstractTabDeckEditor::actExportDeckDecklistXyz() void AbstractTabDeckEditor::actAnalyzeDeckDeckstats() { auto *interface = new DeckStatsInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(), this); - interface->analyzeDeck(getDeckList()); + interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Analyzes the deck using TappedOut. */ void AbstractTabDeckEditor::actAnalyzeDeckTappedout() { auto *interface = new TappedOutInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(), this); - interface->analyzeDeck(getDeckList()); + interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Applies a new filter tree to the database display. */ @@ -658,7 +611,7 @@ bool AbstractTabDeckEditor::eventFilter(QObject *o, QEvent *e) /** @brief Shows a confirmation dialog before closing. */ bool AbstractTabDeckEditor::confirmClose() { - if (modified) { + if (deckStateManager->isModified()) { tabSupervisor->setCurrentWidget(this); int ret = createSaveConfirmationWindow()->exec(); if (ret == QMessageBox::Save) 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 bfbda778c..69b2b4a1c 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -19,6 +19,7 @@ #include +class DeckStateManager; class CardDatabaseModel; class CardDatabaseDisplayModel; @@ -117,30 +118,13 @@ public: */ void openDeck(const LoadedDeck &deck); - /** @brief Returns the currently active deck loader. */ - DeckLoader *getDeckLoader() const; - - /** @brief Returns the currently active deck list. */ - const DeckList &getDeckList() const; - - /** @brief Sets the modified state of the tab. - * @param _windowModified Whether the tab is modified. - */ - void setModified(bool _windowModified); - DeckEditorDeckDockWidget *getDeckDockWidget() const { return deckDockWidget; } - DeckListHistoryManager *getHistoryManager() const - { - return historyManager; - } - - DeckListHistoryManager *historyManager; - // UI Elements + DeckStateManager *deckStateManager; DeckEditorMenu *deckMenu; ///< Menu for deck operations DeckEditorDatabaseDisplayWidget *databaseDisplayDockWidget; ///< Database dock DeckEditorCardInfoDockWidget *cardInfoDockWidget; ///< Card info dock @@ -155,14 +139,6 @@ public slots: /** @brief Called when the deck is modified. */ virtual void onDeckModified(); - /** @brief Called when a widget is about to modify the state of the DeckList. - * @param modificationReason The reason for the state modification - */ - virtual void onDeckHistorySaveRequested(const QString &modificationReason); - - /** @brief Called when a widget would like to clear the history. */ - virtual void onDeckHistoryClearRequested(); - /** @brief Updates the card info panel. * @param card The card to display. */ @@ -202,9 +178,6 @@ signals: /** @brief Emitted before the tab is closed. */ void deckEditorClosing(AbstractTabDeckEditor *tab); - /** @brief Emitted when a card should be decremented. */ - void decrementCard(const ExactCard &card, QString zoneName); - protected slots: /** @brief Starts a new deck in this tab. */ virtual void actNewDeck(); @@ -315,9 +288,6 @@ protected: */ QMessageBox *createSaveConfirmationWindow(); - /** @brief Returns true if the tab is a blank newly created deck. */ - bool isBlankNewDeck() const; - /** @brief Helper function to add a card to a specific deck zone. */ void addCardHelper(const ExactCard &card, const QString &zoneName); @@ -330,8 +300,6 @@ protected: QAction *aResetLayout; QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating; QAction *aFilterDockVisible, *aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating; - - bool modified = false; ///< Whether the deck/tab has unsaved changes }; #endif // TAB_GENERIC_DECK_EDITOR_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 1547cca74..3a2468368 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -68,7 +68,10 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi model = new DeckListModel(this); connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); - model->getDeckList()->loadFromStream_Plain(deckStream, false); + + auto decklist = QSharedPointer(new DeckList); + decklist->loadFromStream_Plain(deckStream, false); + model->setDeckList(decklist); model->forEachCard(CardNodeFunction::ResolveProviderId()); diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 4e3d0c57d..286252e13 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -1,6 +1,7 @@ #include "tab_deck_editor.h" #include "../../../client/settings/cache_settings.h" +#include "../deck_editor/deck_state_manager.h" #include "../filters/filter_builder.h" #include "../interface/pixel_map_generator.h" #include "../interface/widgets/cards/card_info_frame_widget.h" @@ -114,8 +115,8 @@ void TabDeckEditor::createMenus() */ QString TabDeckEditor::getTabText() const { - QString result = tr("Deck: %1").arg(deckDockWidget->getSimpleDeckName()); - if (modified) + QString result = tr("Deck: %1").arg(deckStateManager->getSimpleDeckName()); + if (deckStateManager->isModified()) result.prepend("* "); return result; } 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 a124eaa01..f3d573d27 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 "../../deck_editor/deck_state_manager.h" #include "../../filters/filter_builder.h" #include "../../interface/pixel_map_generator.h" #include "../../interface/widgets/cards/card_info_frame_widget.h" @@ -61,7 +62,7 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame = new QVBoxLayout; centralWidget->setLayout(centralFrame); - tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckDockWidget->deckModel, + tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseDisplayDockWidget->databaseModel, databaseDisplayDockWidget->databaseDisplayModel); @@ -85,7 +86,7 @@ void TabDeckEditorVisual::onDeckChanged() AbstractTabDeckEditor::onDeckModified(); tabContainer->visualDeckView->constructZoneWidgetsFromDeckListModel(); tabContainer->deckAnalytics->refreshDisplays(); - tabContainer->sampleHandWidget->setDeckModel(deckDockWidget->deckModel); + tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel()); } /** @brief Creates menus for deck editing and view options, including dock actions. */ @@ -149,8 +150,8 @@ void TabDeckEditorVisual::createMenus() /** @brief Returns the tab text, prepending a mark if the deck has unsaved changes. */ QString TabDeckEditorVisual::getTabText() const { - QString result = tr("Visual Deck: %1").arg(deckDockWidget->getSimpleDeckName()); - if (modified) + QString result = tr("Visual Deck: %1").arg(deckStateManager->getSimpleDeckName()); + if (deckStateManager->isModified()) result.prepend("* "); return result; } @@ -166,9 +167,9 @@ void TabDeckEditorVisual::changeModelIndexAndCardInfo(const ExactCard &activeCar void TabDeckEditorVisual::changeModelIndexToCard(const ExactCard &activeCard) { QString cardName = activeCard.getName(); - QModelIndex index = deckDockWidget->deckModel->findCard(cardName, DECK_ZONE_MAIN); + QModelIndex index = deckStateManager->getModel()->findCard(cardName, DECK_ZONE_MAIN); if (!index.isValid()) { - index = deckDockWidget->deckModel->findCard(cardName, DECK_ZONE_SIDE); + index = deckStateManager->getModel()->findCard(cardName, DECK_ZONE_SIDE); } if (!deckDockWidget->getSelectionModel()->hasSelection()) { deckDockWidget->getSelectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); @@ -182,7 +183,7 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, auto card = instance->getCard(); // Get the model index for the card - QModelIndex idx = deckDockWidget->deckModel->findCard(card.getName(), zoneName); + QModelIndex idx = deckStateManager->getModel()->findCard(card.getName(), zoneName); if (!idx.isValid()) { return; } @@ -191,8 +192,8 @@ void TabDeckEditorVisual::processMainboardCardClick(QMouseEvent *event, // Double click = swap if (event->type() == QEvent::MouseButtonDblClick && event->button() == Qt::LeftButton) { - deckDockWidget->actSwapCard(card, zoneName); - idx = deckDockWidget->deckModel->findCard(card.getName(), zoneName); + deckStateManager->swapCardAtIndex(idx); + idx = deckStateManager->getModel()->findCard(card.getName(), zoneName); sel->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect); return; } 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 7551954f3..5098696dd 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 @@ -2,6 +2,7 @@ #include "../../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" +#include "../deck_editor/deck_state_manager.h" #include @@ -60,7 +61,7 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi() void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck() { - DeckListModel *deckListModel = deckEditor->deckDockWidget->deckModel; + DeckListModel *deckListModel = deckEditor->deckStateManager->getModel(); if (!deckListModel) return; diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 6f479616e..ece3bc2f8 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -5,14 +5,15 @@ DeckListModel::DeckListModel(QObject *parent) : QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder) { - // This class will leak the decklist object. We cannot safely delete it in the dtor because the deckList field is a - // non-owning pointer and another deckList might have been assigned to it. - // `DeckListModel::cleanList` also leaks for the same reason. - // TODO: fix the leak - deckList = new DeckList; + deckList = QSharedPointer(new DeckList()); root = new InnerDecklistNode; } +DeckListModel::DeckListModel(QObject *parent, const QSharedPointer &deckList) : DeckListModel(parent) +{ + setDeckList(deckList); +} + DeckListModel::~DeckListModel() { delete root; @@ -586,13 +587,13 @@ void DeckListModel::setActiveFormat(const QString &_format) void DeckListModel::cleanList() { - setDeckList(new DeckList); + setDeckList(QSharedPointer(new DeckList())); } /** * @param _deck The deck. */ -void DeckListModel::setDeckList(DeckList *_deck) +void DeckListModel::setDeckList(const QSharedPointer &_deck) { if (deckList != _deck) { deckList = _deck; diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index b6292d689..e6f10c072 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -245,6 +245,7 @@ signals: public: explicit DeckListModel(QObject *parent = nullptr); + explicit DeckListModel(QObject *parent, const QSharedPointer &deckList); ~DeckListModel() override; /** @@ -314,11 +315,12 @@ public: * @brief Removes all cards and resets the model. */ void cleanList(); - [[nodiscard]] DeckList *getDeckList() const + + [[nodiscard]] QSharedPointer getDeckList() const { return deckList; } - void setDeckList(DeckList *_deck); + void setDeckList(const QSharedPointer &_deck); /** * @brief Apply a function to every card in the deck tree. @@ -351,8 +353,8 @@ public: [[nodiscard]] QList getZones() const; private: - DeckList *deckList; /**< Pointer to the deck loader providing the underlying data. */ - InnerDecklistNode *root; /**< Root node of the model tree. */ + QSharedPointer deckList; /**< Pointer to the decklist providing the underlying data. */ + InnerDecklistNode *root; /**< Root node of the model tree. */ DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE; int lastKnownColumn; /**< Last column used for sorting. */ Qt::SortOrder lastKnownOrder; /**< Last known sort order. */ From 9f90de2242ec30e519169a00873ff5fac0f163b6 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 31 Dec 2025 17:55:31 +0100 Subject: [PATCH 24/56] change the release channel based on version string (#6447) * change the release channel based on version string * Apply suggestions from code review * format --- .../src/client/settings/cache_settings.cpp | 19 +++++++++++++++++-- cockatrice/src/interface/window_main.cpp | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 7a518df67..fde8e9b34 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -2,6 +2,7 @@ #include "../network/update/client/release_channel.h" #include "card_counter_settings.h" +#include "version_string.h" #include #include @@ -198,7 +199,13 @@ SettingsCache::SettingsCache() mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).toBool(); - checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool(); + if (settings->contains("personal/startupUpdateCheck")) { + checkUpdatesOnStartup = settings->value("personal/startupUpdateCheck", true).toBool(); + } else if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) { + checkUpdatesOnStartup = false; // do not run auto updater on custom version + } else { + checkUpdatesOnStartup = true; // default to run auto updater + } startupCardUpdateCheckPromptForUpdate = settings->value("personal/startupCardUpdateCheckPromptForUpdate", true).toBool(); startupCardUpdateCheckAlwaysUpdate = settings->value("personal/startupCardUpdateCheckAlwaysUpdate", false).toBool(); @@ -206,7 +213,15 @@ SettingsCache::SettingsCache() lastCardUpdateCheck = settings->value("personal/lastCardUpdateCheck", QDateTime::currentDateTime().date()).toDate(); notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool(); notifyAboutNewVersion = settings->value("personal/newversionnotification", true).toBool(); - updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt(); + + if (settings->contains("personal/updatereleasechannel")) { + updateReleaseChannel = settings->value("personal/updatereleasechannel").toInt(); + } else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) { + // default to beta if this is a beta release + updateReleaseChannel = 1; + } else { + updateReleaseChannel = 0; // stable + } lang = settings->value("personal/lang").toString(); keepalive = settings->value("personal/keepalive", 3).toInt(); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 41113185c..2e135d170 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -938,9 +938,25 @@ void MainWindow::startupConfigCheck() const auto reloadOk0 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); }); } - qCInfo(WindowMainStartupShortcutsLog) << "[MainWindow] Migrating shortcuts after update detected."; + qCInfo(WindowMainStartupShortcutsLog) << "Migrating shortcuts after update detected."; SettingsCache::instance().shortcuts().migrateShortcuts(); + if (SettingsCache::instance().getCheckUpdatesOnStartup()) { + if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) { + qCInfo(WindowMainStartupShortcutsLog) << "Update has changed to custom version, disabling auto update"; + SettingsCache::instance().setCheckUpdatesOnStartup(Qt::Unchecked); + } else { + int channel = 0; + if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) { + channel = 1; + } + if (SettingsCache::instance().getUpdateReleaseChannelIndex() != channel) { + qCInfo(WindowMainStartupShortcutsLog) << "Update has changed beta state, updating release channel."; + SettingsCache::instance().setUpdateReleaseChannelIndex(channel); + } + } + } + SettingsCache::instance().setClientVersion(VERSION_STRING); } else { // previous config from this version found From 28c800dd3795f736d72b86ea3bbe8b6bd420f9ec Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 31 Dec 2025 17:56:24 +0100 Subject: [PATCH 25/56] Docs: Add & link xsd schema information (#6439) * Add xsd scheme information * further references as list --- .../card_database_schema_and_parsing.md | 13 +++++++++++++ .../deck_management/creating_decks.md | 8 +++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md b/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md index 1de6fda4f..7b4bdafc3 100644 --- a/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md +++ b/doc/doxygen/extra-pages/developer_documentation/card_database_schema_and_parsing.md @@ -1,3 +1,16 @@ @page card_database_schema_and_parsing Card Database Schema and Parsing +# Card Database Schemas + +Cockatrice uses `XML files` to store information of available cards to be used in the app (`cards.xml`). +The token file follows the schema of the card database (`tokens.xml`), too. + +Saved decks (`.xml`) use a different schema. + +- [XSD Schema for `Card Databases`](https://github.com/Cockatrice/Cockatrice/blob/master/doc/carddatabase_v4/cards.xsd) v4 +- [XSD Schema for `Decks`](https://github.com/Cockatrice/Cockatrice/blob/master/doc/deck.xsd) v1 + + +# Card Database Parsing + TODO diff --git a/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md b/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md index bfe578afd..153320c3d 100644 --- a/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md +++ b/doc/doxygen/extra-pages/user_documentation/deck_management/creating_decks.md @@ -8,10 +8,8 @@ by selecting "Deck Editor" or "Visual Deck Editor" under the "Tabs" application # Further References -See @ref editing_decks for information on how to modify the attributes and contents of a deck in the Deck Editor +- See @ref editing_decks for information on how to modify the attributes and contents of a deck in the Deck Editor widgets. - -See @ref exporting_decks for information on how to store and persist your deck either in-client or to external +- See @ref exporting_decks for information on how to store and persist your deck either in-client or to external services. - -See @ref importing_decks for information on how to import existing decks either in-client or from external services \ No newline at end of file +- See @ref importing_decks for information on how to import existing decks either in-client or from external services From 36d82807652961a80787f1ba939adff04ff76be8 Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 31 Dec 2025 17:57:19 +0100 Subject: [PATCH 26/56] Readme: Reorder `Contribute` section (#6435) * Reorder contribute section * fix * Wording updates in related section --- README.md | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3d01e9f3d..c08aee0e7 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ Latest beta version: # Related Repositories -- [Magic-Token](https://github.com/Cockatrice/Magic-Token): MtG token data to use in Cockatrice -- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Script to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) to use in Cockatrice +- [Magic-Token](https://github.com/Cockatrice/Magic-Token): File with MtG token data for use in Cockatrice +- [Magic-Spoiler](https://github.com/Cockatrice/Magic-Spoiler): Code to generate MtG spoiler data from [MTGJSON](https://github.com/mtgjson/mtgjson) for use in Cockatrice - [cockatrice.github.io](https://github.com/Cockatrice/cockatrice.github.io): Code of the official Cockatrice webpage -- [Cockatrice @Flathub](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration for our Linux `flatpak` package +- [io.github.Cockatrice.cockatrice](https://github.com/flathub/io.github.Cockatrice.cockatrice): Configuration of our Linux `flatpak` package hosted at [Flathub](https://flathub.org/en/apps/io.github.Cockatrice.cockatrice) # Community Resources [![Discord](https://img.shields.io/discord/314987288398659595?label=Discord&logo=discord&logoColor=white)](https://discord.gg/3Z9yzmA) @@ -55,7 +55,6 @@ Latest beta version: Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other projet contributors (`#dev` channel) or fellow users of the app. Come here to talk about the application, features, or just to hang out. - [Official Website](https://cockatrice.github.io) - [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) -- [Official Code Documentation](https://cockatrice.github.io/docs) - [Official Discord](https://discord.gg/3Z9yzmA) - [reddit r/Cockatrice](https://reddit.com/r/cockatrice) @@ -64,6 +63,23 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p # Contribute +

    + Code | + Documentation | + Translation +

    + +#### Repository Activity +![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats") + +
    +Kudos to all our amazing contributors ❤️ +
    + + +
    + Made with contrib.rocks +
    ### Code @@ -79,21 +95,17 @@ We'll happily advice on how best to implement a feature, or we can show you wher You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code). +### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush) + +There are various places where useful information for different needs are maintained: +- [Official Code Documentation](https://cockatrice.github.io/docs/) +- [Official Wiki](https://github.com/Cockatrice/Cockatrice/wiki) `Community supported` +- [Official Webpage](https://cockatrice.github.io/) +- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file` + Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide. -#### Repository Activity -![Cockatrice Repo Analytics](https://repobeats.axiom.co/api/embed/c7cec938789a5bbaeb4182a028b4dbb96db8f181.svg "Cockatrice Repo Analytics by Repobeats") - -
    -Kudos to all our amazing contributors ❤️ -
    - - -
    - Made with contrib.rocks -
    - -### Translations [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/) +### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/) Cockatrice uses Transifex to manage translations. You can help us bring Cockatrice, Oracle and Webatrice to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://explore.transifex.com/cockatrice/cockatrice/).
    From df9a8b22723cbda5e8d5bd9742457d3efc194d10 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Wed, 31 Dec 2025 19:45:49 +0100 Subject: [PATCH 27/56] [VDE] Deck Analytics Widgets overhaul (#6463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [VDE] Deck Analytics Widgets overhaul Took 2 minutes Took 3 minutes Took 3 minutes * Qt5 version guards. Took 33 minutes Took 3 seconds * Include QtMath Took 3 minutes Took 8 seconds * Use getCards() Took 4 minutes * Non pointer stuff Took 52 seconds * Add a newline to the tooltip Took 2 minutes Took 27 seconds * Fix build failure on macOS 15 * Rename some things. Took 17 minutes Took 11 seconds Took 18 seconds * Address overloads, fix default configuration. Took 1 hour 9 minutes Took 8 seconds * Fix mana curve default config. Took 4 minutes * Namespace to Qt libs Took 5 minutes * Selection overlay is transparent for mouse events. Took 2 minutes * Brace initialize. Took 8 minutes * Debian 11. Took 5 minutes --------- Co-authored-by: Lukas Brübach Co-authored-by: RickyRister --- cockatrice/CMakeLists.txt | 36 +- .../abstract_analytics_panel_widget.cpp | 48 +++ .../abstract_analytics_panel_widget.h | 61 +++ .../add_analytics_panel_dialog.cpp | 32 ++ .../add_analytics_panel_dialog.h | 29 ++ .../analytics_panel_widget_factory.cpp | 33 ++ .../analytics_panel_widget_factory.h | 44 +++ .../analytics_panel_widget_registrar.cpp | 1 + .../analytics_panel_widget_registrar.h | 17 + .../draw_probability_config.cpp | 28 ++ .../draw_probability_config.h | 19 + .../draw_probability_config_dialog.cpp | 92 +++++ .../draw_probability_config_dialog.h | 44 +++ .../draw_probability_widget.cpp | 236 +++++++++++ .../draw_probability_widget.h | 54 +++ .../mana_base/mana_base_config.cpp | 32 ++ .../mana_base/mana_base_config.h | 19 + .../mana_base/mana_base_config_dialog.cpp | 67 ++++ .../mana_base/mana_base_config_dialog.h | 42 ++ .../mana_base/mana_base_widget.cpp | 115 ++++++ .../mana_base/mana_base_widget.h | 51 +++ .../mana_curve/mana_curve_category_widget.cpp | 121 ++++++ .../mana_curve/mana_curve_category_widget.h | 32 ++ .../mana_curve/mana_curve_config.cpp | 41 ++ .../mana_curve/mana_curve_config.h | 21 + .../mana_curve/mana_curve_config_dialog.cpp | 91 +++++ .../mana_curve/mana_curve_config_dialog.h | 44 +++ .../mana_curve/mana_curve_total_widget.cpp | 78 ++++ .../mana_curve/mana_curve_total_widget.h | 32 ++ .../mana_curve/mana_curve_widget.cpp | 148 +++++++ .../mana_curve/mana_curve_widget.h | 50 +++ .../mana_devotion/mana_devotion_config.cpp | 31 ++ .../mana_devotion/mana_devotion_config.h | 18 + .../mana_devotion_config_dialog.cpp | 62 +++ .../mana_devotion_config_dialog.h | 42 ++ .../mana_devotion/mana_devotion_widget.cpp | 123 ++++++ .../mana_devotion/mana_devotion_widget.h | 45 +++ .../mana_distribution_config.cpp | 36 ++ .../mana_distribution_config.h | 20 + .../mana_distribution_config_dialog.cpp | 83 ++++ .../mana_distribution_config_dialog.h | 45 +++ ...ana_distribution_single_display_widget.cpp | 49 +++ .../mana_distribution_single_display_widget.h | 28 ++ .../mana_distribution_widget.cpp | 129 ++++++ .../mana_distribution_widget.h | 45 +++ .../deck_analytics/deck_analytics_widget.cpp | 307 +++++++++++++-- .../deck_analytics/deck_analytics_widget.h | 63 ++- .../deck_list_statistics_analyzer.cpp | 141 ++++++- .../deck_list_statistics_analyzer.h | 129 +++++- .../deck_analytics/mana_base_widget.cpp | 71 ---- .../widgets/deck_analytics/mana_base_widget.h | 38 -- .../deck_analytics/mana_curve_widget.cpp | 68 ---- .../deck_analytics/mana_curve_widget.h | 37 -- .../deck_analytics/mana_devotion_widget.cpp | 66 ---- .../deck_analytics/mana_devotion_widget.h | 37 -- .../deck_analytics/resizable_panel.cpp | 367 ++++++++++++++++++ .../widgets/deck_analytics/resizable_panel.h | 79 ++++ .../bars/bar_chart_background_widget.cpp | 43 ++ .../charts/bars/bar_chart_background_widget.h | 23 ++ .../display/charts/bars/bar_chart_widget.cpp | 215 ++++++++++ .../display/charts/bars/bar_chart_widget.h | 52 +++ .../display/{ => charts/bars}/bar_widget.cpp | 0 .../display/{ => charts/bars}/bar_widget.h | 0 .../display/{ => charts/bars}/color_bar.cpp | 1 - .../display/{ => charts/bars}/color_bar.h | 0 .../{ => charts/bars}/percent_bar_widget.cpp | 0 .../{ => charts/bars}/percent_bar_widget.h | 0 .../charts/bars/segmented_bar_widget.cpp | 140 +++++++ .../charts/bars/segmented_bar_widget.h | 38 ++ .../general/display/charts/pies/color_pie.cpp | 205 ++++++++++ .../general/display/charts/pies/color_pie.h | 44 +++ ...api_response_deck_entry_display_widget.cpp | 2 +- ...i_response_card_inclusion_display_widget.h | 2 +- ...api_response_card_synergy_display_widget.h | 2 +- .../tab_deck_editor_visual.cpp | 2 +- .../tab_deck_editor_visual_tab_widget.cpp | 7 +- .../tab_deck_editor_visual_tab_widget.h | 3 +- .../visual_deck_editor_sample_hand_widget.cpp | 11 +- .../visual_deck_editor_sample_hand_widget.h | 8 +- .../libcockatrice/utility/color.h | 37 ++ .../libcockatrice/utility/qt_utils.h | 14 + 81 files changed, 4372 insertions(+), 394 deletions(-) create mode 100644 cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h delete mode 100644 cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp delete mode 100644 cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h delete mode 100644 cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp delete mode 100644 cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h delete mode 100644 cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp delete mode 100644 cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h create mode 100644 cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp create mode 100644 cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h create mode 100644 cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp create mode 100644 cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h create mode 100644 cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp create mode 100644 cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h rename cockatrice/src/interface/widgets/general/display/{ => charts/bars}/bar_widget.cpp (100%) rename cockatrice/src/interface/widgets/general/display/{ => charts/bars}/bar_widget.h (100%) rename cockatrice/src/interface/widgets/general/display/{ => charts/bars}/color_bar.cpp (99%) rename cockatrice/src/interface/widgets/general/display/{ => charts/bars}/color_bar.h (100%) rename cockatrice/src/interface/widgets/general/display/{ => charts/bars}/percent_bar_widget.cpp (100%) rename cockatrice/src/interface/widgets/general/display/{ => charts/bars}/percent_bar_widget.h (100%) create mode 100644 cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp create mode 100644 cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h create mode 100644 cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp create mode 100644 cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index c6a29969f..8ebd177db 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -144,11 +144,31 @@ set(cockatrice_SOURCES src/interface/widgets/cards/card_size_widget.cpp src/interface/widgets/cards/deck_card_zone_display_widget.cpp src/interface/widgets/cards/deck_preview_card_picture_widget.cpp + src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp + src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp + src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp src/interface/widgets/deck_analytics/deck_analytics_widget.cpp src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp - src/interface/widgets/deck_analytics/mana_base_widget.cpp - src/interface/widgets/deck_analytics/mana_curve_widget.cpp - src/interface/widgets/deck_analytics/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/resizable_panel.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp + src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp + 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/deck_list_history_manager_widget.cpp src/interface/widgets/deck_editor/deck_editor_card_info_dock_widget.cpp src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -160,13 +180,17 @@ set(cockatrice_SOURCES src/interface/widgets/general/background_sources.cpp src/interface/widgets/general/display/background_plate_widget.cpp src/interface/widgets/general/display/banner_widget.cpp - src/interface/widgets/general/display/bar_widget.cpp - src/interface/widgets/general/display/color_bar.cpp src/interface/widgets/general/display/dynamic_font_size_label.cpp src/interface/widgets/general/display/dynamic_font_size_push_button.cpp src/interface/widgets/general/display/labeled_input.cpp - src/interface/widgets/general/display/percent_bar_widget.cpp src/interface/widgets/general/display/shadow_background_label.cpp + src/interface/widgets/general/display/charts/bars/bar_widget.cpp + src/interface/widgets/general/display/charts/bars/color_bar.cpp + src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp + src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp + src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp + src/interface/widgets/general/display/charts/pies/color_pie.cpp src/interface/widgets/general/home_styled_button.cpp src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp new file mode 100644 index 000000000..bad883d27 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp @@ -0,0 +1,48 @@ +#include "abstract_analytics_panel_widget.h" + +#include "deck_list_statistics_analyzer.h" + +#include + +AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : QWidget(parent), analyzer(analyzer) +{ + layout = new QVBoxLayout(this); + + bannerAndSettingsContainer = new QWidget(this); + + bannerAndSettingsLayout = new QHBoxLayout(bannerAndSettingsContainer); + bannerAndSettingsContainer->setLayout(bannerAndSettingsLayout); + bannerWidget = new BannerWidget(this, "Analytics Widget", Qt::Vertical, 100); + bannerWidget->setMaximumHeight(100); + + bannerAndSettingsLayout->addWidget(bannerWidget, 1); + + // config button + configureButton = new QPushButton(tr("Configure"), this); + configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); + bannerAndSettingsLayout->addWidget(configureButton, 0); + + layout->addWidget(bannerAndSettingsContainer); + + connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &AbstractAnalyticsPanelWidget::updateDisplay); +} + +bool AbstractAnalyticsPanelWidget::applyConfigFromDialog() +{ + QDialog *dlg = createConfigDialog(this); + if (!dlg) { + return false; + } + + bool ok = dlg->exec() == QDialog::Accepted; + if (ok) { + // dialog must expose its final config as JSON + auto newCfg = extractConfigFromDialog(dlg); + loadConfig(newCfg); + updateDisplay(); + } + dlg->deleteLater(); + return ok; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h new file mode 100644 index 000000000..23374a9e1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.h @@ -0,0 +1,61 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H + +#include "../general/display/banner_widget.h" + +#include +#include +#include +#include + +class DeckListStatisticsAnalyzer; + +class AbstractAnalyticsPanelWidget : public QWidget +{ + Q_OBJECT +public slots: + virtual void updateDisplay() = 0; + // Widgets must return a config dialog + virtual QDialog *createConfigDialog(QWidget *parent) = 0; + +public: + explicit AbstractAnalyticsPanelWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + void setDisplayTitle(const QString &title) + { + displayTitle = title; + if (bannerWidget) { + bannerWidget->setText(displayTitle); + } + } + + QString displayTitleText() const + { + return displayTitle; + } + + virtual QJsonObject saveConfig() const + { + return {}; + } + virtual void loadConfig(const QJsonObject &) + { + } + + // Unified helper to run config dialog and update widget + bool applyConfigFromDialog(); + + // Dialog → JSON must be supplied by each subclass + virtual QJsonObject extractConfigFromDialog(QDialog *dlg) const = 0; + +protected: + DeckListStatisticsAnalyzer *analyzer; + QVBoxLayout *layout; + QWidget *bannerAndSettingsContainer; + QHBoxLayout *bannerAndSettingsLayout; + QString displayTitle; + BannerWidget *bannerWidget; + QPushButton *configureButton; +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_BASE_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp new file mode 100644 index 000000000..c83e6d982 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.cpp @@ -0,0 +1,32 @@ +#include "add_analytics_panel_dialog.h" + +#include "analytics_panel_widget_factory.h" + +#include +#include + +AddAnalyticsPanelDialog::AddAnalyticsPanelDialog(QWidget *parent) : QDialog(parent) +{ + setWindowTitle(tr("Add Analytics Panel")); + + layout = new QVBoxLayout(this); + + typeCombo = new QComboBox(this); + + // Populate using descriptors + const auto widgets = AnalyticsPanelWidgetFactory::instance().availableWidgets(); + + for (const auto &desc : widgets) { + // Show translated title to user + typeCombo->addItem(desc.title, desc.type); + } + + layout->addWidget(typeCombo); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h new file mode 100644 index 000000000..aa44734c2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/add_analytics_panel_dialog.h @@ -0,0 +1,29 @@ + +#ifndef COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H +#define COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H + +#include "analytics_panel_widget_factory.h" + +#include +#include +#include +#include + +class AddAnalyticsPanelDialog : public QDialog +{ + Q_OBJECT +public: + explicit AddAnalyticsPanelDialog(QWidget *parent); + + QString selectedType() const + { + return typeCombo->currentData().toString(); + } + +private: + QVBoxLayout *layout; + QComboBox *typeCombo; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_ADD_ANALYTICS_PANEL_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp new file mode 100644 index 000000000..7af641689 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.cpp @@ -0,0 +1,33 @@ +#include "analytics_panel_widget_factory.h" + +#include "abstract_analytics_panel_widget.h" + +AnalyticsPanelWidgetFactory &AnalyticsPanelWidgetFactory::instance() +{ + static AnalyticsPanelWidgetFactory f; + return f; +} + +void AnalyticsPanelWidgetFactory::registerWidget(const Descriptor &desc) +{ + widgets.insert(desc.type, desc); +} + +AbstractAnalyticsPanelWidget * +AnalyticsPanelWidgetFactory::create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const +{ + auto it = widgets.find(type); + if (it == widgets.end()) + return nullptr; + + auto w = it->creator(parent, analyzer); + + w->setDisplayTitle(it->title); + + return w; +} + +QList AnalyticsPanelWidgetFactory::availableWidgets() const +{ + return widgets.values(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h new file mode 100644 index 000000000..6c5856d70 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_factory.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_FACTORY_H + +#include +#include +#include +#include +#include + +class AbstractAnalyticsPanelWidget; +class DeckListStatisticsAnalyzer; + +class AnalyticsPanelWidgetFactory +{ +public: + using Creator = std::function; + + struct Descriptor + { + QString type; // stable ID ("manaProdDevotion") + QString title; // translated, user-facing + Creator creator; + }; + + static AnalyticsPanelWidgetFactory &instance(); + + // NEW: richer registration + void registerWidget(const Descriptor &desc); + + AbstractAnalyticsPanelWidget * + create(const QString &type, QWidget *parent, DeckListStatisticsAnalyzer *analyzer) const; + + // NEW: expose widgets to UI + QList availableWidgets() const; + +private: + AnalyticsPanelWidgetFactory() = default; // Ensure private constructor + AnalyticsPanelWidgetFactory(const AnalyticsPanelWidgetFactory &) = delete; + AnalyticsPanelWidgetFactory &operator=(const AnalyticsPanelWidgetFactory &) = delete; + + QMap widgets; +}; + +#endif diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp new file mode 100644 index 000000000..d4129a3d0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.cpp @@ -0,0 +1 @@ +#include "analytics_panel_widget_registrar.h" diff --git a/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h new file mode 100644 index 000000000..70d6df94f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analytics_panel_widget_registrar.h @@ -0,0 +1,17 @@ +#ifndef COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H +#define COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H + +#include "analytics_panel_widget_factory.h" + +class AnalyticsPanelWidgetRegistrar +{ +public: + AnalyticsPanelWidgetRegistrar(const QString &type, + const QString &title, + AnalyticsPanelWidgetFactory::Creator creator) + { + AnalyticsPanelWidgetFactory::instance().registerWidget({type, title, creator}); + } +}; + +#endif // COCKATRICE_DECK_ANALYTICS_WIDGET_REGISTRAR_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp new file mode 100644 index 000000000..51129a4b4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp @@ -0,0 +1,28 @@ +#include "draw_probability_config.h" + +QJsonObject DrawProbabilityConfig::toJson() const +{ + QJsonObject o; + o["criteria"] = criteria; + o["atLeast"] = atLeast; + o["quantity"] = quantity; + o["drawn"] = drawn; + return o; +} +DrawProbabilityConfig DrawProbabilityConfig::fromJson(const QJsonObject &o) +{ + DrawProbabilityConfig cfg; + if (o.contains("criteria")) { + cfg.criteria = o["criteria"].toString(); + } + if (o.contains("atLeast")) { + cfg.atLeast = o["atLeast"].toBool(true); + } + if (o.contains("quantity")) { + cfg.quantity = o["quantity"].toInt(1); + } + if (o.contains("drawn")) { + cfg.drawn = o["drawn"].toInt(7); + } + return cfg; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h new file mode 100644 index 000000000..bbe61a68e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_DRAW_PROBABILITY_CONFIG_H +#define COCKATRICE_DRAW_PROBABILITY_CONFIG_H + +#include +#include + +struct DrawProbabilityConfig +{ + QString criteria = "name"; // name, type, subtype, cmc + bool atLeast = true; // true = at least, false = exactly + int quantity = 1; // N + int drawn = 7; // M + + QJsonObject toJson() const; + + static DrawProbabilityConfig fromJson(const QJsonObject &o); +}; + +#endif diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp new file mode 100644 index 000000000..71f88c0fc --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp @@ -0,0 +1,92 @@ +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include + +DrawProbabilityConfigDialog::DrawProbabilityConfigDialog(QWidget *parent) : QDialog(parent) +{ + form = new QFormLayout(this); + + // Criteria + labelCriteria = new QLabel(this); + criteria = new QComboBox(this); + criteria->addItem(QString(), "name"); + criteria->addItem(QString(), "type"); + criteria->addItem(QString(), "subtype"); + criteria->addItem(QString(), "cmc"); + form->addRow(labelCriteria, criteria); + + // Exactness + labelExactness = new QLabel(this); + exactness = new QComboBox(this); + exactness->addItem(QString(), true); + exactness->addItem(QString(), false); + form->addRow(labelExactness, exactness); + + // Quantity + labelQuantity = new QLabel(this); + quantity = new QSpinBox(this); + quantity->setRange(1, 60); + form->addRow(labelQuantity, quantity); + + // Drawn + labelDrawn = new QLabel(this); + drawn = new QSpinBox(this); + drawn->setRange(1, 60); + drawn->setValue(7); + form->addRow(labelDrawn, drawn); + + // Button box + auto *bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + form->addWidget(bb); + + connect(bb, &QDialogButtonBox::accepted, this, &DrawProbabilityConfigDialog::accept); + connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject); + + retranslateUi(); +} + +void DrawProbabilityConfigDialog::retranslateUi() +{ + setWindowTitle(tr("Draw Probability Settings")); + + labelCriteria->setText(tr("Criteria:")); + criteria->setItemText(0, tr("Card Name")); + criteria->setItemText(1, tr("Type")); + criteria->setItemText(2, tr("Subtype")); + criteria->setItemText(3, tr("Mana Value")); + + labelExactness->setText(tr("Exactness:")); + exactness->setItemText(0, tr("At least")); + exactness->setItemText(1, tr("Exactly")); + + labelQuantity->setText(tr("Quantity (N):")); + labelDrawn->setText(tr("Cards drawn (M):")); + + // i18n-friendly suffixes + quantity->setSuffix(tr(" cards")); + drawn->setSuffix(tr(" cards")); +} + +void DrawProbabilityConfigDialog::setFromConfig(const DrawProbabilityConfig &_config) +{ + cfg = _config; + + criteria->setCurrentIndex(criteria->findData(_config.criteria)); + exactness->setCurrentIndex(exactness->findData(_config.atLeast)); + quantity->setValue(_config.quantity); + drawn->setValue(_config.drawn); +} + +void DrawProbabilityConfigDialog::accept() +{ + cfg.criteria = criteria->currentData().toString(); + cfg.atLeast = exactness->currentData().toBool(); + cfg.quantity = quantity->value(); + cfg.drawn = drawn->value(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h new file mode 100644 index 000000000..44b1f0eec --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.h @@ -0,0 +1,44 @@ +#pragma once + +#include "draw_probability_config.h" + +#include +#include + +class QComboBox; +class QSpinBox; +class QLabel; + +class DrawProbabilityConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit DrawProbabilityConfigDialog(QWidget *parent = nullptr); + + void retranslateUi(); + + void setFromConfig(const DrawProbabilityConfig &_config); + DrawProbabilityConfig result() const + { + return cfg; + } + +protected: + void accept() override; + +private: + DrawProbabilityConfig cfg; + + QFormLayout *form; + + // Widgets + QComboBox *criteria; + QComboBox *exactness; + QSpinBox *quantity; + QSpinBox *drawn; + + QLabel *labelCriteria; + QLabel *labelExactness; + QLabel *labelQuantity; + QLabel *labelDrawn; +}; diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp new file mode 100644 index 000000000..a8bec834f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp @@ -0,0 +1,236 @@ +#include "draw_probability_widget.h" + +#include "draw_probability_config_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : AbstractAnalyticsPanelWidget(parent, analyzer) +{ + controls = new QWidget(this); + controlLayout = new QHBoxLayout(controls); + + labelPrefix = new QLabel(this); + controlLayout->addWidget(labelPrefix); + + criteriaCombo = new QComboBox(this); + // Give these things item-data so we can translate the actual user-facing strings + criteriaCombo->addItem(QString(), "name"); + criteriaCombo->addItem(QString(), "type"); + criteriaCombo->addItem(QString(), "subtype"); + criteriaCombo->addItem(QString(), "cmc"); + controlLayout->addWidget(criteriaCombo); + + exactnessCombo = new QComboBox(this); + exactnessCombo->addItem(QString(), true); // At least + exactnessCombo->addItem(QString(), false); // Exactly + controlLayout->addWidget(exactnessCombo); + + quantitySpin = new QSpinBox(this); + quantitySpin->setRange(1, 60); + controlLayout->addWidget(quantitySpin); + + labelMiddle = new QLabel(this); + controlLayout->addWidget(labelMiddle); + + drawnSpin = new QSpinBox(this); + drawnSpin->setRange(1, 60); + drawnSpin->setValue(7); + controlLayout->addWidget(drawnSpin); + + labelSuffix = new QLabel(this); + controlLayout->addWidget(labelSuffix); + + labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + controlLayout->addStretch(1); + layout->addWidget(controls); + + // Table + resultTable = new QTableWidget(this); + resultTable->setColumnCount(3); + resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + layout->addWidget(resultTable); + + // Connections + connect(criteriaCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + config.criteria = criteriaCombo->currentData().toString(); + updateDisplay(); + }); + + connect(exactnessCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { + config.atLeast = exactnessCombo->currentData().toBool(); + updateDisplay(); + }); + + connect(quantitySpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { + config.quantity = v; + updateDisplay(); + }); + + connect(drawnSpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int v) { + config.drawn = v; + updateDisplay(); + }); + + retranslateUi(); + applyConfigToToolbar(); + updateFilterOptions(); +} + +void DrawProbabilityWidget::retranslateUi() +{ + bannerWidget->setText(tr("Draw Probability")); + + labelPrefix->setText(tr("Probability of drawing")); + + criteriaCombo->setItemText(0, tr("Card Name")); + criteriaCombo->setItemText(1, tr("Type")); + criteriaCombo->setItemText(2, tr("Subtype")); + criteriaCombo->setItemText(3, tr("Mana Value")); + + exactnessCombo->setItemText(0, tr("At least")); + exactnessCombo->setItemText(1, tr("Exactly")); + + labelMiddle->setText(tr("card(s) having drawn at least")); + labelSuffix->setText(tr("cards")); + + resultTable->setHorizontalHeaderLabels({tr("Category"), tr("Qty"), tr("Odds (%)")}); +} + +QDialog *DrawProbabilityWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new DrawProbabilityConfigDialog(parent); + dlg->setFromConfig(config); + return dlg; +} + +QJsonObject DrawProbabilityWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *dp = qobject_cast(dlg); + return dp ? dp->result().toJson() : QJsonObject{}; +} + +void DrawProbabilityWidget::applyConfigToToolbar() +{ + auto setComboByData = [](QComboBox *combo, const QVariant &value) { + int idx = combo->findData(value); + if (idx >= 0) { + combo->setCurrentIndex(idx); + } + }; + + setComboByData(criteriaCombo, config.criteria); + setComboByData(exactnessCombo, config.atLeast); + + quantitySpin->setValue(config.quantity); + drawnSpin->setValue(config.drawn); +} + +void DrawProbabilityWidget::updateDisplay() +{ + updateFilterOptions(); +} + +void DrawProbabilityWidget::loadConfig(const QJsonObject &cfg) +{ + config = DrawProbabilityConfig::fromJson(cfg); + applyConfigToToolbar(); + updateFilterOptions(); +} + +void DrawProbabilityWidget::updateFilterOptions() +{ + if (!analyzer->getModel()->getDeckList()) { + return; + } + + const QString criteria = config.criteria; + const bool atLeast = config.atLeast; + const int quantity = config.quantity; + const int drawn = config.drawn; + + QMap categoryCounts; + int totalDeckCards = 0; + + const auto nodes = analyzer->getModel()->getDeckList()->getCardNodes(); + for (auto *node : nodes) { + CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr(); + if (!info) { + continue; + } + + totalDeckCards += node->getNumber(); + + QStringList categories; + if (criteria == "name") { + categories << info->getName(); + } else if (criteria == "type") { + categories = info->getMainCardType().split(' ', Qt::SkipEmptyParts); + } else if (criteria == "subtype") { + categories = info->getCardType().split(' ', Qt::SkipEmptyParts); + } else if (criteria == "cmc") { + categories << QString::number(info->getCmc().toInt()); + } + + for (const QString &cat : categories) { + categoryCounts[cat] += node->getNumber(); + } + } + + resultTable->setRowCount(categoryCounts.size()); + + int row = 0; + for (auto it = categoryCounts.cbegin(); it != categoryCounts.cend(); ++it, ++row) { + const QString &cat = it.key(); + const int copies = it.value(); + + double probability = 0.0; + if (atLeast) { + for (int k = quantity; k <= drawn && k <= copies; ++k) { + probability += hypergeometricProbability(totalDeckCards, copies, drawn, k); + } + } else { + probability = hypergeometricProbability(totalDeckCards, copies, drawn, quantity); + } + + resultTable->setItem(row, 0, new QTableWidgetItem(cat)); + resultTable->setItem(row, 1, new QTableWidgetItem(QString::number(copies))); + resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2))); + } +} + +double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k) +{ + if (k < 0 || k > n || K > N || n > N) { + return 0.0; + } + + double logP = 0.0; + for (int i = 1; i <= k; ++i) { + logP += qLn(double(K - k + i) / i); + } + for (int i = 1; i <= n - k; ++i) { + logP += qLn(double(N - K - (n - k) + i) / i); + } + for (int i = 1; i <= n; ++i) { + logP -= qLn(double(N - n + i) / i); + } + + return qExp(logP); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h new file mode 100644 index 000000000..80015999f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H +#define COCKATRICE_DRAW_PROBABILITY_WIDGET_H + +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "draw_probability_config.h" + +#include +#include +#include +#include + +class DrawProbabilityWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT +public: + DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + QDialog *createConfigDialog(QWidget *parent) override; + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + void applyConfigToToolbar(); + +public slots: + void updateDisplay() override; + void loadConfig(const QJsonObject &cfg) override; + void retranslateUi(); + +private slots: + void updateFilterOptions(); + +private: + DrawProbabilityConfig config; + + QWidget *controls; + QHBoxLayout *controlLayout; + QLabel *labelPrefix; + QLabel *labelMiddle; + QLabel *labelSuffix; + QLineEdit *cardNameEdit; + QComboBox *criteriaCombo; // Card Name / Type / Subtype / Mana Value + QComboBox *filterCombo; // The actual value + QComboBox *exactnessCombo; // At least / Exactly + QSpinBox *quantitySpin; // N + QSpinBox *drawnSpin; // M + + QSpinBox *manaValueSpin; + + QTableWidget *resultTable; + + double hypergeometricProbability(int N, int K, int n, int k); + double calculateProbability(int totalCards, int copies, int drawn, bool atLeast); +}; + +#endif // COCKATRICE_DRAW_PROBABILITY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp new file mode 100644 index 000000000..2f9f60752 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp @@ -0,0 +1,32 @@ +#include "mana_base_config.h" + +QJsonObject ManaBaseConfig::toJson() const +{ + QJsonObject jsonObject; + QJsonArray jsonArray; + jsonObject["displayType"] = displayType; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + return jsonObject; +} + +ManaBaseConfig ManaBaseConfig::fromJson(const QJsonObject &o) + +{ + ManaBaseConfig config; + + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h new file mode 100644 index 000000000..d01f88b8b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.h @@ -0,0 +1,19 @@ + +#ifndef COCKATRICE_MANA_BASE_CONFIG_H +#define COCKATRICE_MANA_BASE_CONFIG_H + +#include +#include +#include + +struct ManaBaseConfig +{ + QString displayType; // "pie" or "bar" or "combinedBar" + QStringList filters; // which colors to show, empty = all + + QJsonObject toJson() const; + + static ManaBaseConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_BASE_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp new file mode 100644 index 000000000..3317486ea --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp @@ -0,0 +1,67 @@ +#include "mana_base_config_dialog.h" + +#include + +ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaBaseConfig initial, + QWidget *parent) + : QDialog(parent), config(initial) +{ + layout = new QVBoxLayout(this); + + displayTypeLabel = new QLabel(this); + layout->addWidget(displayTypeLabel); + + displayType = new QComboBox(this); + layout->addWidget(displayType); + + filterLabel = new QLabel(this); + layout->addWidget(filterLabel); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + layout->addWidget(filterList); + + QStringList colors = analyzer->getManaBase().keys(); + colors.sort(); + filterList->addItems(colors); + + // select initial filters + for (int i = 0; i < filterList->count(); ++i) { + if (config.filters.contains(filterList->item(i)->text())) + filterList->item(i)->setSelected(true); + } + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &ManaBaseConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaBaseConfigDialog::reject); + + retranslateUi(); +} + +void ManaBaseConfigDialog::retranslateUi() +{ + setWindowTitle(tr("Mana Base Configuration")); + + displayTypeLabel->setText(tr("Display type:")); + + displayType->clear(); + displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")}); + + filterLabel->setText(tr("Filter Colors (optional):")); + + buttons->button(QDialogButtonBox::Ok)->setText(tr("OK")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); +} + +void ManaBaseConfigDialog::accept() +{ + config.displayType = displayType->currentText(); + config.filters.clear(); + for (auto *item : filterList->selectedItems()) { + config.filters << item->text(); + } + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h new file mode 100644 index 000000000..a816f154f --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.h @@ -0,0 +1,42 @@ + +#ifndef COCKATRICE_MANA_BASE_ADD_DIALOG_H +#define COCKATRICE_MANA_BASE_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_base_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ManaBaseConfigDialog : public QDialog +{ + Q_OBJECT +public: + ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig initial = {}, QWidget *parent = nullptr); + void retranslateUi(); + + void accept() override; + + ManaBaseConfig result() const + { + return config; + } + +private: + ManaBaseConfig config; + QVBoxLayout *layout; + QLabel *displayTypeLabel; + QComboBox *displayType; + QLabel *filterLabel; + QListWidget *filterList; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_MANA_BASE_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp new file mode 100644 index 000000000..d38314b8c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp @@ -0,0 +1,115 @@ +#include "mana_base_widget.h" + +#include "../../../general/display/charts/bars/bar_widget.h" +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../analytics_panel_widget_registrar.h" +#include "mana_base_config_dialog.h" + +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaBase{ + "manaBase", ManaBaseWidget::tr("Mana Base"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg)) +{ + barContainer = new QWidget(this); + barLayout = new QHBoxLayout(barContainer); + layout->addWidget(barContainer); + + updateDisplay(); +} + +void ManaBaseWidget::updateDisplay() +{ + // Clear previous widgets + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + auto &pipCount = analyzer->getProductionPipCount(); + auto &cardCount = analyzer->getProductionCardCount(); + + QHash manaMap; + for (auto key : pipCount.keys()) { + manaMap[key] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash filtered; + for (auto f : config.filters) { + if (manaMap.contains(f)) { + filtered[f] = manaMap[f]; + } + } + manaMap = filtered; + } + + // Determine maximum for bar charts + int highest = 1; + for (auto val : manaMap) { + highest = std::max(highest, val); + } + + // Convert to QMap for ColorBar / ColorPie (sorted) + QMap mapSorted; + for (auto it = manaMap.begin(); it != manaMap.end(); ++it) { + mapSorted.insert(it.key(), it.value()); + } + + // Choose display mode + if (config.displayType == "bar") { + QHash colors = {{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)}, + {"B", QColor(21, 11, 0)}, {"R", QColor(211, 32, 42)}, + {"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)}}; + + for (auto color : manaMap.keys()) { + QString label = QString("%1 %2 (%3)").arg(color).arg(manaMap[color]).arg(cardCount.value(color)); + + BarWidget *bar = new BarWidget(label, manaMap[color], highest, colors.value(color, Qt::gray), this); + + barLayout->addWidget(bar); + } + } else if (config.displayType == "combinedBar") { + ColorBar *cb = new ColorBar(mapSorted, this); + cb->setMinimumHeight(30); + barLayout->addWidget(cb); + } else if (config.displayType == "pie") { + ColorPie *pie = new ColorPie(mapSorted, this); + pie->setMinimumSize(200, 200); + barLayout->addWidget(pie); + } + + update(); +} +QSize ManaBaseWidget::sizeHint() const +{ + return QSize(800, 150); +} + +QDialog *ManaBaseWidget::createConfigDialog(QWidget *parent) +{ + ManaBaseConfigDialog *dlg = new ManaBaseConfigDialog(analyzer, config, parent); + return dlg; +} + +QJsonObject ManaBaseWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h new file mode 100644 index 000000000..39380c07e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.h @@ -0,0 +1,51 @@ +/** + * @file mana_base_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_BASE_WIDGET_H +#define MANA_BASE_WIDGET_H + +#include "../../../general/display/banner_widget.h" +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_base_config.h" + +#include +#include +#include +#include +#include + +class ManaBaseWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaBaseConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaBaseConfig::fromJson(o); + updateDisplay(); + } + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaBaseConfig config; + QWidget *barContainer; + QHBoxLayout *barLayout; +}; + +#endif // MANA_BASE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp new file mode 100644 index 000000000..a0ccedc74 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.cpp @@ -0,0 +1,121 @@ +#include "mana_curve_category_widget.h" + +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config.h" +#include "mana_curve_total_widget.h" + +constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row + +ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent) +{ + layout = new QVBoxLayout(this); + layout->setSpacing(4); + layout->setContentsMargins(0, 0, 0, 0); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +// Same as minimum for now +QSize ManaCurveCategoryWidget::sizeHint() const +{ + if (layout->isEmpty()) { + return QSize(0, 0); + } + + // Calculate exact height needed for all rows + int rowCount = layout->count(); + + int totalHeight = rowCount * MIN_ROW_HEIGHT; + totalHeight += (rowCount - 1) * layout->spacing(); + + return QSize(0, totalHeight); +} + +QSize ManaCurveCategoryWidget::minimumSizeHint() const +{ + if (layout->isEmpty()) { + return QSize(0, 0); + } + + // Calculate actual minimum based on number of rows + int rowCount = layout->count(); + + int totalHeight = rowCount * MIN_ROW_HEIGHT; + totalHeight += (rowCount - 1) * layout->spacing(); + + return QSize(0, totalHeight); +} + +void ManaCurveCategoryWidget::updateDisplay(int minCmc, + int maxCmc, + int highest, + QHash> qCategoryCounts, + QHash> qCategoryCards, + const ManaCurveConfig &config) +{ + // Clear previous content + QtUtils::clearLayoutRec(layout); + + if (!config.showCategoryRows) { + return; // nothing to show + } + + // Collect categories + QStringList categories = qCategoryCounts.keys(); + + // Apply filters + if (!config.filters.isEmpty()) { + QStringList filtered; + for (const QString &cat : categories) { + if (config.filters.contains(cat)) { + filtered.append(cat); + } + } + categories = filtered; + } + + std::sort(categories.begin(), categories.end()); + + for (const QString &cat : categories) { + QWidget *row = new QWidget(this); + row->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + row->setFixedHeight(MIN_ROW_HEIGHT); + + QHBoxLayout *rowLayout = new QHBoxLayout(row); + rowLayout->setContentsMargins(0, 0, 0, 0); + rowLayout->setSpacing(4); + + QLabel *categoryLabel = new QLabel(cat, row); + categoryLabel->setFixedWidth(80); + rowLayout->addWidget(categoryLabel); + + QVector catBars; + const auto cmcCounts = qCategoryCounts.value(cat); + const auto cmcCards = qCategoryCards.value(cat); + + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + int val = cmcCounts.value(cmc, 0); + QStringList cards = cmcCards.value(cmc); + + QVector segments; + if (val > 0) { + segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)}); + } + + catBars.push_back({QString::number(cmc), segments}); + } + + auto *catChart = new BarChartWidget(row); + catChart->setHighest(highest); + catChart->setBars(catBars); + catChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + rowLayout->addWidget(catChart); + layout->addWidget(row); + } + + // Update geometry after adding all widgets + updateGeometry(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h new file mode 100644 index 000000000..c6aa6f1f0 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_category_widget.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H +#define COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H + +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class ManaCurveCategoryWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveCategoryWidget(QWidget *parent); + void updateDisplay(int minCmc, + int maxCmc, + int highest, + QHash> qCategoryCounts, + QHash> qCategoryCards, + const ManaCurveConfig &config); + +public slots: + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +private: + QVBoxLayout *layout; +}; + +#endif // COCKATRICE_MANA_CURVE_CATEGORY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp new file mode 100644 index 000000000..0dfef1b66 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.cpp @@ -0,0 +1,41 @@ +#include "mana_curve_config.h" + +QJsonObject ManaCurveConfig::toJson() const +{ + QJsonObject jsonObject; + jsonObject["groupBy"] = groupBy; + QJsonArray jsonArray; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + jsonObject["showMain"] = showMain; + jsonObject["showCategoryRows"] = showCategoryRows; + return jsonObject; +} + +ManaCurveConfig ManaCurveConfig::fromJson(const QJsonObject &o) +{ + ManaCurveConfig config; + + if (o.contains("groupBy")) { + config.groupBy = o["groupBy"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + if (o.contains("showMain")) { + config.showMain = o["showMain"].toBool(true); + } + + if (o.contains("showCategoryRows")) { + config.showCategoryRows = o["showCategoryRows"].toBool(true); + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h new file mode 100644 index 000000000..55222dd98 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config.h @@ -0,0 +1,21 @@ + +#ifndef COCKATRICE_MANA_CURVE_CONFIG_H +#define COCKATRICE_MANA_CURVE_CONFIG_H + +#include +#include +#include + +struct ManaCurveConfig +{ + QString groupBy = "type"; // "type", "color", "subtype", etc. + QStringList filters; // empty = all + bool showMain = true; + bool showCategoryRows = true; + + QJsonObject toJson() const; + + static ManaCurveConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_CURVE_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp new file mode 100644 index 000000000..38199656c --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.cpp @@ -0,0 +1,91 @@ +#include "mana_curve_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +ManaCurveConfigDialog::ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent) + : QDialog(parent), analyzer(analyzer) +{ + auto *lay = new QVBoxLayout(this); + + labelGroupBy = new QLabel(this); + lay->addWidget(labelGroupBy); + + groupBy = new QComboBox(this); + lay->addWidget(groupBy); + + labelFilters = new QLabel(this); + lay->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + lay->addWidget(filterList); + + showMain = new QCheckBox(this); + showMain->setChecked(true); + lay->addWidget(showMain); + + showCatRows = new QCheckBox(this); + showCatRows->setChecked(true); + lay->addWidget(showCatRows); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + lay->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaCurveConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaCurveConfigDialog::reject); + + // populate dynamic data + QStringList cats = analyzer->getManaCurveByType().keys(); + cats.append(analyzer->getManaCurveByColor().keys()); + cats.removeDuplicates(); + cats.sort(); + filterList->addItems(cats); + + groupBy->addItems({"type", "color", "subtype", "power", "toughness"}); + + retranslateUi(); +} + +void ManaCurveConfigDialog::retranslateUi() +{ + labelGroupBy->setText(tr("Group By:")); + groupBy->setItemText(0, tr("type")); + groupBy->setItemText(1, tr("color")); + groupBy->setItemText(2, tr("subtype")); + groupBy->setItemText(3, tr("power")); + groupBy->setItemText(4, tr("toughness")); + + labelFilters->setText(tr("Filters (optional):")); + + showMain->setText(tr("Show main bar row")); + showCatRows->setText(tr("Show per-category rows")); +} + +void ManaCurveConfigDialog::setFromConfig(const ManaCurveConfig &cfg) +{ + groupBy->setCurrentText(cfg.groupBy); + // restore filters + for (int i = 0; i < filterList->count(); ++i) + filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text())); + + showMain->setChecked(cfg.showMain); + showCatRows->setChecked(cfg.showCategoryRows); +} + +void ManaCurveConfigDialog::accept() +{ + cfg.groupBy = groupBy->currentText(); + + cfg.filters.clear(); + for (auto *item : filterList->selectedItems()) + cfg.filters << item->text(); + + cfg.showMain = showMain->isChecked(); + cfg.showCategoryRows = showCatRows->isChecked(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h new file mode 100644 index 000000000..17e6776e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_config_dialog.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_MANA_CURVE_ADD_DIALOG_H +#define COCKATRICE_MANA_CURVE_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class QListWidget; +class QCheckBox; +class QComboBox; + +class ManaCurveConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit ManaCurveConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr); + void retranslateUi(); + void setFromConfig(const ManaCurveConfig &cfg); + + ManaCurveConfig result() const + { + return cfg; + } + +private: + ManaCurveConfig cfg; + DeckListStatisticsAnalyzer *analyzer; + + QLabel *labelGroupBy; + QComboBox *groupBy; + QLabel *labelFilters; + QListWidget *filterList; + QDialogButtonBox *buttons; + QCheckBox *showMain; + QCheckBox *showCatRows; + +private slots: + void accept() override; +}; + +#endif // COCKATRICE_MANA_CURVE_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp new file mode 100644 index 000000000..f059ff873 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.cpp @@ -0,0 +1,78 @@ +#include "mana_curve_total_widget.h" + +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config.h" + +#include + +ManaCurveTotalWidget::ManaCurveTotalWidget(QWidget *parent) : QWidget(parent) +{ + layout = new QHBoxLayout(this); + + label = new QLabel(this); + label->setFixedWidth(80); + layout->addWidget(label); + + barChart = new BarChartWidget(this); + layout->addWidget(barChart, 1); + + setMinimumHeight(200); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +QSize ManaCurveTotalWidget::sizeHint() const +{ + return {0, 280}; +} + +QSize ManaCurveTotalWidget::minimumSizeHint() const +{ + return {0, 200}; +} + +void ManaCurveTotalWidget::updateDisplay(const QString &categoryName, + int minCmc, + int maxCmc, + int highest, + const QMap> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config) +{ + QVector mainBars; + mainBars.reserve(maxCmc - minCmc + 1); + + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + QVector segments; + + const auto cmcIt = cmcMap.constFind(cmc); + if (cmcIt != cmcMap.cend()) { + for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) { + const QString &category = it.key(); + + if (!config.filters.isEmpty() && !config.filters.contains(category)) + continue; + + const int value = it.value(); + + QStringList cards; + const auto catIt = cardsMap.constFind(category); + if (catIt != cardsMap.cend()) + cards = catIt->value(cmc); + + segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)}); + } + } + + std::sort(segments.begin(), segments.end(), + [](const BarSegment &a, const BarSegment &b) { return a.category < b.category; }); + + mainBars.push_back({QString::number(cmc), segments}); + } + + label->setText(categoryName); + + barChart->setHighest(highest); + barChart->setBars(mainBars); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h new file mode 100644 index 000000000..203f75345 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_total_widget.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H +#define COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "mana_curve_config.h" + +#include +#include +#include + +class ManaCurveTotalWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ManaCurveTotalWidget(QWidget *parent); + QSize sizeHint() const; + QSize minimumSizeHint() const; + void updateDisplay(const QString &categoryName, + int minCmc, + int maxCmc, + int highest, + const QMap> &cmcMap, + const QMap> &cardsMap, + const ManaCurveConfig &config); + +private: + QHBoxLayout *layout; + QLabel *label; + BarChartWidget *barChart; +}; + +#endif // COCKATRICE_MANA_CURVE_TOTAL_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp new file mode 100644 index 000000000..e09ecfe87 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.cpp @@ -0,0 +1,148 @@ +#include "mana_curve_widget.h" + +#include "../../../general/display/charts/bars/bar_chart_background_widget.h" +#include "../../../general/display/charts/bars/bar_chart_widget.h" +#include "../../../general/display/charts/bars/segmented_bar_widget.h" +#include "../../analytics_panel_widget_registrar.h" +#include "../../deck_list_statistics_analyzer.h" +#include "libcockatrice/utility/color.h" +#include "libcockatrice/utility/qt_utils.h" +#include "mana_curve_config_dialog.h" + +#include +#include +#include +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaCurve{ + "manaCurve", ManaCurveWidget::tr("Mana Curve"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(cfg) +{ + setLayout(layout); + + totalWidget = new ManaCurveTotalWidget(this); + totalWidget->setHidden(true); + layout->addWidget(totalWidget); + + categoryWidget = new ManaCurveCategoryWidget(this); + categoryWidget->setHidden(true); + layout->addWidget(categoryWidget); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(analyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay); + + updateDisplay(); +} + +QDialog *ManaCurveWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new ManaCurveConfigDialog(analyzer, parent); + dlg->setFromConfig(config); + return dlg; +} + +QJsonObject ManaCurveWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + return mc ? mc->result().toJson() : QJsonObject{}; +} + +static void buildMapsByCategory(const QHash> &categoryCounts, + const QHash> &categoryCards, + QMap> &outCmcMap, + QMap> &outCardsMap) +{ + outCmcMap.clear(); + outCardsMap.clear(); + + for (auto catIt = categoryCounts.cbegin(); catIt != categoryCounts.cend(); ++catIt) { + const QString &category = catIt.key(); + const auto &countsByCmc = catIt.value(); + + for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it) + outCmcMap[it.key()][category] = it.value(); + } + + for (auto catIt = categoryCards.cbegin(); catIt != categoryCards.cend(); ++catIt) { + const QString &category = catIt.key(); + const auto &cardsByCmc = catIt.value(); + + for (auto it = cardsByCmc.cbegin(); it != cardsByCmc.cend(); ++it) + outCardsMap[category][it.key()] = it.value(); + } +} + +static void findGlobalCmcRange(const QHash> &categoryCounts, int &minCmc, int &maxCmc) +{ + minCmc = 0; + maxCmc = 0; + + for (const auto &countsByCmc : categoryCounts) { + for (auto it = countsByCmc.cbegin(); it != countsByCmc.cend(); ++it) + maxCmc = qMax(maxCmc, it.key()); + } +} + +void ManaCurveWidget::updateDisplay() +{ + QHash> categoryCounts; + QHash> categoryCards; + + if (config.groupBy == "color") { + categoryCounts = analyzer->getManaCurveByColor(); + categoryCards = analyzer->getManaCurveCardsByColor(); + } else if (config.groupBy == "subtype") { + categoryCounts = analyzer->getManaCurveBySubtype(); + categoryCards = analyzer->getManaCurveCardsBySubtype(); + } else if (config.groupBy == "power") { + categoryCounts = analyzer->getManaCurveByPower(); + categoryCards = analyzer->getManaCurveCardsByPower(); + } else { + categoryCounts = analyzer->getManaCurveByType(); + categoryCards = analyzer->getManaCurveCardsByType(); + } + + QMap> cmcMap; + QMap> cardsMap; + buildMapsByCategory(categoryCounts, categoryCards, cmcMap, cardsMap); + + int minCmc = 0; + int maxCmc = 0; + findGlobalCmcRange(categoryCounts, minCmc, maxCmc); + + int highest = 1; + for (int cmc = minCmc; cmc <= maxCmc; ++cmc) { + int sum = 0; + + const auto cmcIt = cmcMap.constFind(cmc); + if (cmcIt != cmcMap.cend()) { + for (auto it = cmcIt->cbegin(); it != cmcIt->cend(); ++it) { + if (!config.filters.isEmpty() && !config.filters.contains(it.key())) { + continue; + } + + sum += it.value(); + } + } + + highest = qMax(highest, sum); + } + + totalWidget->updateDisplay(config.groupBy, minCmc, maxCmc, highest, cmcMap, cardsMap, config); + + totalWidget->setVisible(config.showMain); + + categoryWidget->updateDisplay(minCmc, maxCmc, highest, categoryCounts, categoryCards, config); + + categoryWidget->setVisible(config.showCategoryRows); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h new file mode 100644 index 000000000..da59da9a8 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_curve/mana_curve_widget.h @@ -0,0 +1,50 @@ +/** + * @file mana_curve_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_CURVE_WIDGET_H +#define MANA_CURVE_WIDGET_H + +#include "../../abstract_analytics_panel_widget.h" +#include "mana_curve_category_widget.h" +#include "mana_curve_config.h" +#include "mana_curve_total_widget.h" + +#include + +class SegmentedBarWidget; +class DeckListStatisticsAnalyzer; + +class ManaCurveWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + // QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaCurveConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaCurveConfig::fromJson(o); + updateDisplay(); + }; + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaCurveConfig config; + ManaCurveTotalWidget *totalWidget; + ManaCurveCategoryWidget *categoryWidget; +}; + +#endif // MANA_CURVE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp new file mode 100644 index 000000000..ee57cb1e2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.cpp @@ -0,0 +1,31 @@ +#include "mana_devotion_config.h" + +QJsonObject ManaDevotionConfig::toJson() const +{ + QJsonObject jsonObject; + QJsonArray jsonArray; + jsonObject["displayType"] = displayType; + for (auto &filter : filters) { + jsonArray.append(filter); + } + jsonObject["filters"] = jsonArray; + return jsonObject; +} + +ManaDevotionConfig ManaDevotionConfig::fromJson(const QJsonObject &o) +{ + ManaDevotionConfig config; + + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) { + config.filters << v.toString(); + } + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h new file mode 100644 index 000000000..08be146e5 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config.h @@ -0,0 +1,18 @@ +#ifndef COCKATRICE_MANA_DEVOTION_CONFIG_H +#define COCKATRICE_MANA_DEVOTION_CONFIG_H + +#include +#include +#include + +struct ManaDevotionConfig +{ + QString displayType; // "pie" or "bar" or "combinedBar" + QStringList filters; // which colors to show, empty = all + + QJsonObject toJson() const; + + static ManaDevotionConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_DEVOTION_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp new file mode 100644 index 000000000..80fd03928 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.cpp @@ -0,0 +1,62 @@ +#include "mana_devotion_config_dialog.h" + +ManaDevotionConfigDialog::ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaDevotionConfig initial, + QWidget *parent) + : QDialog(parent), config(initial) +{ + layout = new QVBoxLayout(this); + + labelDisplayType = new QLabel(this); + layout->addWidget(labelDisplayType); + + displayType = new QComboBox(this); + layout->addWidget(displayType); + + labelFilters = new QLabel(this); + layout->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + + QStringList colors = analyzer->getDevotionPipCount().keys(); + colors.sort(); + filterList->addItems(colors); + layout->addWidget(filterList); + + // select initial filters + for (int i = 0; i < filterList->count(); ++i) { + if (config.filters.contains(filterList->item(i)->text())) + filterList->item(i)->setSelected(true); + } + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaDevotionConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaDevotionConfigDialog::reject); + + // populate combo box items + displayType->addItems({"pie", "bar", "combinedBar"}); + + retranslateUi(); +} + +void ManaDevotionConfigDialog::retranslateUi() +{ + labelDisplayType->setText(tr("Display type:")); + displayType->setItemText(0, tr("pie")); + displayType->setItemText(1, tr("bar")); + displayType->setItemText(2, tr("combinedBar")); + + labelFilters->setText(tr("Filter Colors (optional):")); +} + +void ManaDevotionConfigDialog::accept() +{ + config.displayType = displayType->currentText(); + config.filters.clear(); + for (auto *item : filterList->selectedItems()) { + config.filters << item->text(); + } + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h new file mode 100644 index 000000000..fd804e2af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_config_dialog.h @@ -0,0 +1,42 @@ + +#ifndef COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H +#define COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H + +#include "../../deck_list_statistics_analyzer.h" +#include "mana_devotion_config.h" + +#include +#include +#include +#include +#include +#include +#include + +class ManaDevotionConfigDialog : public QDialog +{ + Q_OBJECT +public: + ManaDevotionConfigDialog(DeckListStatisticsAnalyzer *analyzer, + ManaDevotionConfig initial = {}, + QWidget *parent = nullptr); + void retranslateUi(); + + void accept() override; + + ManaDevotionConfig result() const + { + return config; + } + +private: + ManaDevotionConfig config; + QVBoxLayout *layout; + QLabel *labelDisplayType; + QComboBox *displayType; + QLabel *labelFilters; + QListWidget *filterList; + QDialogButtonBox *buttons; +}; + +#endif // COCKATRICE_MANA_DEVOTION_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp new file mode 100644 index 000000000..709577ff2 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.cpp @@ -0,0 +1,123 @@ +#include "mana_devotion_widget.h" + +#include "../../../general/display/charts/bars/bar_widget.h" +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../analytics_panel_widget_registrar.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_devotion_config_dialog.h" + +#include +#include + +namespace +{ + +AnalyticsPanelWidgetRegistrar registerManaDevotion{ + "manaDevotion", ManaDevotionWidget::tr("Mana Devotion"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }}; + +} // anonymous namespace + +ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg) + : AbstractAnalyticsPanelWidget(parent, analyzer), config(std::move(cfg)) +{ + barContainer = new QWidget(this); + barLayout = new QHBoxLayout(barContainer); + barContainer->setLayout(barLayout); + layout->addWidget(barContainer); + + updateDisplay(); +} + +void ManaDevotionWidget::updateDisplay() +{ + // Clear previous widgets + while (QLayoutItem *item = barLayout->takeAt(0)) { + if (item->widget()) { + item->widget()->deleteLater(); + } + delete item; + } + + auto &pipCount = analyzer->getDevotionPipCount(); + auto &cardCount = analyzer->getDevotionCardCount(); + + // Convert keys to single QChar form + QHash devoMap; + for (auto key : pipCount.keys()) { + devoMap[key[0]] = pipCount[key]; + } + + // Apply filters + if (!config.filters.isEmpty()) { + QHash filtered; + for (auto f : config.filters) { + if (devoMap.contains(f[0])) { + filtered[f[0]] = devoMap[f[0]]; + } + } + devoMap = filtered; + } + + // Determine maximum for bar charts + int highest = 1; + for (auto val : devoMap) { + highest = std::max(highest, val); + } + + // Convert to QMap for ColorBar / ColorPie + QMap mapSorted; + for (auto it = devoMap.begin(); it != devoMap.end(); ++it) { + mapSorted.insert(QString(it.key()), it.value()); + } + + // Color map + QHash colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)}, + {'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)}, + {'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}}; + + // Choose display mode + if (config.displayType == "bar") { + // One BarWidget per devotion color + for (auto c : devoMap.keys()) { + QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c))); + + BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this); + + barLayout->addWidget(bar); + } + } else if (config.displayType == "combinedBar") { + // Stacked devotion bar + ColorBar *cb = new ColorBar(mapSorted, this); + cb->setMinimumHeight(30); + barLayout->addWidget(cb); + } else if (config.displayType == "pie") { + // Devotion pie chart + ColorPie *pie = new ColorPie(mapSorted, this); + pie->setMinimumSize(200, 200); + barLayout->addWidget(pie); + } + + update(); +} + +QDialog *ManaDevotionWidget::createConfigDialog(QWidget *parent) +{ + ManaDevotionConfigDialog *dlg = new ManaDevotionConfigDialog(analyzer, config, parent); + return dlg; +} + +QJsonObject ManaDevotionWidget::extractConfigFromDialog(QDialog *dlg) const +{ + auto *mc = qobject_cast(dlg); + if (!mc) { + return {}; + } + return mc->result().toJson(); +} + +QSize ManaDevotionWidget::sizeHint() const +{ + return QSize(800, 150); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h new file mode 100644 index 000000000..833f12938 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_devotion/mana_devotion_widget.h @@ -0,0 +1,45 @@ +/** + * @file mana_devotion_widget.h + * @ingroup DeckEditorAnalyticsWidgets + * @brief TODO: Document this. + */ + +#ifndef MANA_DEVOTION_WIDGET_H +#define MANA_DEVOTION_WIDGET_H +#include "../../../general/display/banner_widget.h" +#include "../../abstract_analytics_panel_widget.h" +#include "mana_devotion_config.h" + +#include + +class ManaDevotionWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT + +public slots: + QSize sizeHint() const override; + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + +public: + ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer, ManaDevotionConfig cfg = {}); + + QJsonObject saveConfig() const override + { + return config.toJson(); + } + void loadConfig(const QJsonObject &o) override + { + config = ManaDevotionConfig::fromJson(o); + updateDisplay(); + } + + QJsonObject extractConfigFromDialog(QDialog *dlg) const override; + +private: + ManaDevotionConfig config; + QWidget *barContainer; + QHBoxLayout *barLayout; +}; + +#endif // MANA_DEVOTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp new file mode 100644 index 000000000..f70f32d5b --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.cpp @@ -0,0 +1,36 @@ +#include "mana_distribution_config.h" + +QJsonObject ManaDistributionConfig::toJson() const +{ + QJsonObject o; + o["displayType"] = displayType; + + QJsonArray jsonArray; + for (auto &s : filters) { + jsonArray.append(s); + } + o["filters"] = jsonArray; + + o["showColorRows"] = showColorRows; + return o; +} + +ManaDistributionConfig ManaDistributionConfig::fromJson(const QJsonObject &o) +{ + ManaDistributionConfig config; + if (o.contains("displayType")) { + config.displayType = o["displayType"].toString(); + } + + if (o.contains("filters")) { + config.filters.clear(); + for (auto v : o["filters"].toArray()) + config.filters << v.toString(); + } + + if (o.contains("showColorRows")) { + config.showColorRows = o["showColorRows"].toBool(true); + } + + return config; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h new file mode 100644 index 000000000..d1aa0f48e --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_CONFIG_H +#define COCKATRICE_MANA_DISTRIBUTION_CONFIG_H + +#include +#include +#include +#include + +struct ManaDistributionConfig +{ + QString displayType = "pie"; // "pie" or "bar" + QStringList filters; // empty = all colors + bool showColorRows = true; + + QJsonObject toJson() const; + + static ManaDistributionConfig fromJson(const QJsonObject &o); +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_CONFIG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp new file mode 100644 index 000000000..7fe4d94e4 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.cpp @@ -0,0 +1,83 @@ +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include +#include +#include + +static const QStringList kColors = {"W", "U", "B", "R", "G", "C"}; + +ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent) + : QDialog(parent), analyzer(analyzer) +{ + auto *lay = new QVBoxLayout(this); + + // Labels + labelDisplayType = new QLabel(this); + lay->addWidget(labelDisplayType); + + displayType = new QComboBox(this); + lay->addWidget(displayType); + + labelFilters = new QLabel(this); + lay->addWidget(labelFilters); + + filterList = new QListWidget(this); + filterList->setSelectionMode(QAbstractItemView::MultiSelection); + filterList->addItems(kColors); // dynamic/fixed, no translation needed + lay->addWidget(filterList); + + showColorRows = new QCheckBox(this); + showColorRows->setChecked(true); + lay->addWidget(showColorRows); + + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + lay->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, this, &ManaDistributionConfigDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &ManaDistributionConfigDialog::reject); + + displayType->addItems({"pie", "bar"}); // combo items + + retranslateUi(); +} + +void ManaDistributionConfigDialog::retranslateUi() +{ + labelDisplayType->setText(tr("Top display type:")); + displayType->setItemText(0, tr("pie")); + displayType->setItemText(1, tr("bar")); + + labelFilters->setText(tr("Colors:")); + + showColorRows->setText(tr("Show per-color rows")); + + // QDialogButtonBox buttons are automatically translated +} + +void ManaDistributionConfigDialog::setFromConfig(const ManaDistributionConfig &cfgIn) +{ + cfg = cfgIn; + + displayType->setCurrentText(cfg.displayType); + + for (int i = 0; i < filterList->count(); ++i) + filterList->item(i)->setSelected(cfg.filters.contains(filterList->item(i)->text())); + + showColorRows->setChecked(cfg.showColorRows); +} + +void ManaDistributionConfigDialog::accept() +{ + cfg.displayType = displayType->currentText(); + + // Filters + cfg.filters.clear(); + for (auto *item : filterList->selectedItems()) + cfg.filters << item->text(); + + cfg.showColorRows = showColorRows->isChecked(); + + QDialog::accept(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h new file mode 100644 index 000000000..57c88e29d --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_config_dialog.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H +#define COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H + +#include "mana_distribution_config.h" + +#include +#include +#include +#include + +class QComboBox; +class QListWidget; +class QCheckBox; +class DeckListStatisticsAnalyzer; + +class ManaDistributionConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent = nullptr); + void retranslateUi(); + + void setFromConfig(const ManaDistributionConfig &cfg); + const ManaDistributionConfig &config() const + { + return cfg; + } + +public slots: + void accept() override; + +private: + DeckListStatisticsAnalyzer *analyzer; + + QLabel *labelDisplayType; + QComboBox *displayType; + QLabel *labelFilters; + QListWidget *filterList; + QCheckBox *showColorRows; + QDialogButtonBox *buttons; + + ManaDistributionConfig cfg; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_ADD_DIALOG_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp new file mode 100644 index 000000000..8a5e14858 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.cpp @@ -0,0 +1,49 @@ +#include "mana_distribution_single_display_widget.h" + +#include "../../../cards/additional_info/mana_symbol_widget.h" + +#include + +ManaDistributionSingleDisplayWidget::ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent) + : QWidget(parent) +{ + auto layout = new QVBoxLayout(this); + layout->setAlignment(Qt::AlignHCenter); + + symbolLabel = new ManaSymbolWidget(this, colorSymbol, true, false); + symbolLabel->setFixedSize(40, 40); + + devotionBar = new QProgressBar(this); + devotionBar->setRange(0, 100); + devotionBar->setTextVisible(false); + + devotionLabel = new QLabel(this); + devotionLabel->setAlignment(Qt::AlignCenter); + + productionBar = new QProgressBar(this); + productionBar->setRange(0, 100); + productionBar->setTextVisible(false); + + productionLabel = new QLabel(this); + productionLabel->setAlignment(Qt::AlignCenter); + + layout->addWidget(symbolLabel); + layout->addWidget(devotionBar); + layout->addWidget(devotionLabel); + layout->addWidget(productionBar); + layout->addWidget(productionLabel); + + setLayout(layout); +} + +void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int percent) +{ + devotionBar->setValue(percent); + devotionLabel->setText(QString(tr("%1 pips (%2 cards)")).arg(pips).arg(cards)); +} + +void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent) +{ + productionBar->setValue(percent); + productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards)); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h new file mode 100644 index 000000000..e33d0cec1 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_single_display_widget.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H +#define COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H + +#include +#include +#include +#include + +class ManaDistributionSingleDisplayWidget : public QWidget +{ + Q_OBJECT +public: + explicit ManaDistributionSingleDisplayWidget(const QString &colorSymbol, QWidget *parent = nullptr); + + void setDevotion(int pips, int cards, int percent); + void setProduction(int pips, int cards, int percent); + +private: + QLabel *symbolLabel; + + QProgressBar *devotionBar; + QLabel *devotionLabel; + + QProgressBar *productionBar; + QLabel *productionLabel; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_SINGLE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp new file mode 100644 index 000000000..8eaea1426 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.cpp @@ -0,0 +1,129 @@ +#include "mana_distribution_widget.h" + +#include "../../analytics_panel_widget_registrar.h" +#include "mana_distribution_config_dialog.h" + +#include +#include +#include +#include + +namespace +{ +AnalyticsPanelWidgetRegistrar registerManaDistribution{ + "manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"), + [](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }}; + +} // anonymous namespace + +static const QStringList kColors = {"W", "U", "B", "R", "G", "C"}; + +ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) + : AbstractAnalyticsPanelWidget(parent, analyzer) +{ + container = new QWidget(this); + containerLayout = new QVBoxLayout(container); + + devotionBarTop = new ColorBar({}, this); + devotionPieTop = new ColorPie({}, this); + productionBarTop = new ColorBar({}, this); + productionPieTop = new ColorPie({}, this); + + containerLayout->addWidget(devotionBarTop); + containerLayout->addWidget(devotionPieTop); + containerLayout->addWidget(productionBarTop); + containerLayout->addWidget(productionPieTop); + + devotionPieTop->hide(); + productionPieTop->hide(); + + row = new QHBoxLayout(); + containerLayout->addLayout(row); + + for (const QString &c : kColors) { + auto *w = new ManaDistributionSingleDisplayWidget(c, this); + row->addWidget(w); + rows[c] = w; + } + + layout->addWidget(container); +} + +void ManaDistributionWidget::updateDisplay() +{ + const auto &devPips = analyzer->getDevotionPipCount(); + const auto &devCards = analyzer->getDevotionCardCount(); + const auto &prodPips = analyzer->getProductionPipCount(); + const auto &prodCards = analyzer->getProductionCardCount(); + + QStringList filtered = config.filters.isEmpty() ? kColors : config.filters; + + QMap devMap, prodMap; + for (const QString &c : filtered) { + devMap[c] = devPips.value(c, 0); + prodMap[c] = prodPips.value(c, 0); + } + + bool showPie = (config.displayType == "pie"); + + devotionBarTop->setVisible(!showPie); + productionBarTop->setVisible(!showPie); + + devotionPieTop->setVisible(showPie); + productionPieTop->setVisible(showPie); + + if (showPie) { + devotionPieTop->setColors(devMap); + productionPieTop->setColors(prodMap); + } else { + devotionBarTop->setColors(devMap); + productionBarTop->setColors(prodMap); + } + + for (const QString &c : kColors) { + auto *w = rows.value(c); + + if (!w) { + continue; + } + + bool visible = config.showColorRows && filtered.contains(c); + w->setVisible(visible); + if (!visible) { + continue; + } + + int dp = devPips.value(c, 0); + int dc = devCards.value(c, 0); + int pp = prodPips.value(c, 0); + int pc = prodCards.value(c, 0); + + // Compute percentages + int totalDev = 0; + int totalProd = 0; + for (const QString &cc : filtered) { + totalDev += devPips.value(cc, 0); + totalProd += prodPips.value(cc, 0); + } + + int devPct = (totalDev > 0) ? int(100.0 * dp / totalDev) : 0; + int prodPct = (totalProd > 0) ? int(100.0 * pp / totalProd) : 0; + + w->setDevotion(dp, dc, devPct); + w->setProduction(pp, pc, prodPct); + } +} + +QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent) +{ + auto *dlg = new ManaDistributionConfigDialog(analyzer, parent); + dlg->setWindowTitle(tr("Mana Distribution Settings")); + dlg->setFromConfig(config); + + connect(dlg, &QDialog::accepted, [this, dlg]() { + config = dlg->config(); + updateDisplay(); + }); + + return dlg; +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h new file mode 100644 index 000000000..2d834f3af --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/mana_distribution/mana_distribution_widget.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_MANA_DISTRIBUTION_WIDGET_H +#define COCKATRICE_MANA_DISTRIBUTION_WIDGET_H + +#include "../../../general/display/charts/bars/color_bar.h" +#include "../../../general/display/charts/pies/color_pie.h" +#include "../../abstract_analytics_panel_widget.h" +#include "../../deck_list_statistics_analyzer.h" +#include "mana_distribution_config.h" +#include "mana_distribution_single_display_widget.h" + +#include +#include +#include +#include + +class ManaDistributionWidget : public AbstractAnalyticsPanelWidget +{ + Q_OBJECT +public: + explicit ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + + void updateDisplay() override; + QDialog *createConfigDialog(QWidget *parent) override; + QJsonObject extractConfigFromDialog(QDialog *) const override + { + return {}; + } + +private: + ManaDistributionConfig config; + + QWidget *container; + QVBoxLayout *containerLayout; + + QVBoxLayout *topLayout; + ColorBar *devotionBarTop; + ColorPie *devotionPieTop; + ColorBar *productionBarTop; + ColorPie *productionPieTop; + + QHBoxLayout *row; + QMap rows; +}; + +#endif // COCKATRICE_MANA_DISTRIBUTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp index 45393bb5d..ea61302f0 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp @@ -1,35 +1,298 @@ #include "deck_analytics_widget.h" -DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListModel *_deckListModel) - : QWidget(parent), deckListModel(_deckListModel) -{ - mainLayout = new QVBoxLayout(); - setLayout(mainLayout); +#include "abstract_analytics_panel_widget.h" +#include "add_analytics_panel_dialog.h" +#include "analytics_panel_widget_factory.h" +#include "analyzer_modules/mana_base/mana_base_config.h" +#include "analyzer_modules/mana_curve/mana_curve_config.h" +#include "analyzer_modules/mana_devotion/mana_devotion_config.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" +#include +#include +#include +#include +#include +#include +#include + +DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer) + : QWidget(parent), statsAnalyzer(_statsAnalyzer) +{ + layout = new QVBoxLayout(this); + + // Controls + controlContainer = new QWidget(this); + controlLayout = new QHBoxLayout(controlContainer); + addButton = new QPushButton(this); + removeButton = new QPushButton(this); + saveButton = new QPushButton(this); + loadButton = new QPushButton(this); + controlLayout->addWidget(addButton); + controlLayout->addWidget(removeButton); + controlLayout->addWidget(saveButton); + controlLayout->addWidget(loadButton); + + layout->addWidget(controlContainer); + + connect(addButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onAddPanel); + connect(removeButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::onRemoveSelected); + connect(saveButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::saveLayout); + connect(loadButton, &QPushButton::clicked, this, &DeckAnalyticsWidget::loadLayout); + + // Scroll area and container scrollArea = new QScrollArea(this); - scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setWidgetResizable(true); - mainLayout->addWidget(scrollArea); + scrollArea->setFrameShape(QFrame::NoFrame); - container = new QWidget(scrollArea); - containerLayout = new QVBoxLayout(container); - container->setLayout(containerLayout); - scrollArea->setWidget(container); + panelContainer = new QWidget(scrollArea); + panelLayout = new QVBoxLayout(panelContainer); + panelLayout->setSpacing(8); + panelLayout->setContentsMargins(4, 4, 4, 4); + panelLayout->addStretch(1); // push panels up - deckListStatisticsAnalyzer = new DeckListStatisticsAnalyzer(this, deckListModel); + scrollArea->setWidget(panelContainer); + layout->addWidget(scrollArea); - manaCurveWidget = new ManaCurveWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaCurveWidget); + loadLayout(); - manaDevotionWidget = new ManaDevotionWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaDevotionWidget); - - manaBaseWidget = new ManaBaseWidget(this, deckListStatisticsAnalyzer); - containerLayout->addWidget(manaBaseWidget); + retranslateUi(); } -void DeckAnalyticsWidget::refreshDisplays() +void DeckAnalyticsWidget::retranslateUi() { - deckListStatisticsAnalyzer->update(); + addButton->setText(tr("Add Panel")); + removeButton->setText(tr("Remove Panel")); + saveButton->setText(tr("Save Layout")); + loadButton->setText(tr("Load Layout")); } + +void DeckAnalyticsWidget::updateDisplays() +{ + statsAnalyzer->analyze(); +} + +void DeckAnalyticsWidget::onAddPanel() +{ + AddAnalyticsPanelDialog dlg(this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + QString selection = dlg.selectedType(); + if (selection.isEmpty()) { + return; + } + + AbstractAnalyticsPanelWidget *analyticsWidget = + AnalyticsPanelWidgetFactory::instance().create(selection, this, statsAnalyzer); + if (!analyticsWidget) { + return; + } + + if (!analyticsWidget->applyConfigFromDialog()) { + analyticsWidget->deleteLater(); + return; + } + + addPanelInstance(selection, analyticsWidget, analyticsWidget->saveConfig()); +} + +void DeckAnalyticsWidget::addPanelInstance(const QString &typeId, + AbstractAnalyticsPanelWidget *panel, + const QJsonObject &cfg) +{ + panel->loadConfig(cfg); + panel->updateDisplay(); + + auto *resPanel = new ResizablePanel(typeId, panel, panelContainer); + panelWrappers.push_back(resPanel); + + panelLayout->insertWidget(panelLayout->count() - 1, resPanel); + + // Event filter for selection + resPanel->installEventFilter(this); + panel->installEventFilter(this); + + // Connect drag-drop signals + connect(resPanel, &ResizablePanel::dropRequested, this, &DeckAnalyticsWidget::onPanelDropped); +} + +void DeckAnalyticsWidget::onRemoveSelected() +{ + int idx = indexOfSelectedWrapper(); + if (idx < 0) { + return; + } + + ResizablePanel *panel = panelWrappers.takeAt(idx); + selectWrapper(nullptr); + + panel->deleteLater(); +} + +void DeckAnalyticsWidget::saveLayout() +{ + QJsonArray arr; + + for (auto *wrapper : panelWrappers) { + QJsonObject entry; + entry["type"] = wrapper->getTypeId(); + entry["config"] = wrapper->panel->saveConfig(); + entry["height"] = wrapper->getCurrentHeight(); + arr.append(entry); + } + + QSettings s; + s.setValue("deckAnalytics/layout", QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact))); +} + +void DeckAnalyticsWidget::loadLayout() +{ + if (!loadLayoutInternal()) { + addDefaultPanels(); + } +} + +void DeckAnalyticsWidget::addDefaultPanels() +{ + struct DefaultPanel + { + QString type; + QJsonObject cfg; + }; + + // Prepare configs + QJsonObject manaCurveCfg = ManaCurveConfig{}.toJson(); + QJsonObject manaBaseCfg = ManaBaseConfig{"combinedBar", {}}.toJson(); + QJsonObject manaDevotionCfg = ManaDevotionConfig{"combinedBar", {}}.toJson(); + QVector defaults = { + {"manaCurve", manaCurveCfg}, {"manaBase", manaBaseCfg}, {"manaDevotion", manaDevotionCfg}}; + + for (auto &d : defaults) { + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(d.type, this, statsAnalyzer); + if (!w) { + continue; + } + + w->loadConfig(d.cfg); + addPanelInstance(d.type, w, d.cfg); + } +} + +bool DeckAnalyticsWidget::loadLayoutInternal() +{ + QSettings s; + QString layoutData = s.value("deckAnalytics/layout").toString(); + if (layoutData.isEmpty()) { + return false; + } + + QJsonDocument doc = QJsonDocument::fromJson(layoutData.toUtf8()); + if (!doc.isArray()) { + return false; + } + + clearPanels(); + + for (auto v : doc.array()) { + if (!v.isObject()) { + continue; + } + QJsonObject o = v.toObject(); + QString type = o["type"].toString(); + QJsonObject cfg = o["config"].toObject(); + + AbstractAnalyticsPanelWidget *w = AnalyticsPanelWidgetFactory::instance().create(type, this, statsAnalyzer); + if (!w) { + continue; + } + + addPanelInstance(type, w, cfg); + + // Restore height AFTER adding the panel + if (o.contains("height")) { + panelWrappers.last()->setHeightFromSaved(o["height"].toInt()); + } + } + + return true; +} + +void DeckAnalyticsWidget::clearPanels() +{ + selectWrapper(nullptr); + while (!panelWrappers.isEmpty()) { + ResizablePanel *p = panelWrappers.takeLast(); + p->deleteLater(); + } +} + +bool DeckAnalyticsWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress) { + for (auto *p : panelWrappers) { + if (obj == p || obj == p->panel) { + selectWrapper(p); + break; + } + } + } + return QWidget::eventFilter(obj, event); +} + +void DeckAnalyticsWidget::selectWrapper(ResizablePanel *w) +{ + // Same wrapper + if (selectedWrapper == w) { + return; + } + // Deselect the old one + if (selectedWrapper) { + selectedWrapper->setSelected(false); + } + // Set current + selectedWrapper = w; + // Finally, select new + if (selectedWrapper) { + selectedWrapper->setSelected(true); + } +} + +int DeckAnalyticsWidget::indexOfSelectedWrapper() const +{ + if (!selectedWrapper) { + return -1; + } + return panelWrappers.indexOf(selectedWrapper); +} + +void DeckAnalyticsWidget::onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore) +{ + int draggedIdx = panelWrappers.indexOf(dragged); + int targetIdx = panelWrappers.indexOf(target); + + if (draggedIdx == -1 || targetIdx == -1 || draggedIdx == targetIdx) { + return; + } + + // Remove dragged panel from list and layout + panelWrappers.removeAt(draggedIdx); + panelLayout->removeWidget(dragged); + + // Adjust target index if needed + if (draggedIdx < targetIdx) { + targetIdx--; + } + + // Calculate insertion position + int insertIdx = insertBefore ? targetIdx : targetIdx + 1; + + // Insert back into list and layout + panelWrappers.insert(insertIdx, dragged); + panelLayout->insertWidget(insertIdx, dragged); + + // Clear selection + selectWrapper(nullptr); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h index 524362aed..31ee36fbb 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h @@ -1,44 +1,71 @@ /** * @file deck_analytics_widget.h * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. + * @brief Main analytics widget container with resizable panels for deck statistics. */ #ifndef DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H -#include "mana_base_widget.h" -#include "mana_curve_widget.h" -#include "mana_devotion_widget.h" +#include "abstract_analytics_panel_widget.h" +#include "deck_list_statistics_analyzer.h" +#include "resizable_panel.h" -#include +#include #include +#include +#include #include -#include + +class LayoutInspector; class DeckAnalyticsWidget : public QWidget { Q_OBJECT +public slots: + void updateDisplays(); + public: - explicit DeckAnalyticsWidget(QWidget *parent, DeckListModel *deckListModel); - void setDeckList(const DeckList &_deckListModel); - std::map analyzeManaCurve(); - void refreshDisplays(); + explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer); + void retranslateUi(); + +private slots: + void onAddPanel(); + void onRemoveSelected(); + void onPanelDropped(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore); + void saveLayout(); + void loadLayout(); + void addDefaultPanels(); + bool loadLayoutInternal(); + void clearPanels(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void selectWrapper(ResizablePanel *panel); + int indexOfSelectedWrapper() const; private: - DeckListModel *deckListModel; - DeckListStatisticsAnalyzer *deckListStatisticsAnalyzer; - QVBoxLayout *mainLayout; + void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {}); - QWidget *container; - QVBoxLayout *containerLayout; + QVBoxLayout *layout; + QWidget *controlContainer; + QHBoxLayout *controlLayout; + + QPushButton *addButton; + QPushButton *removeButton; + QPushButton *saveButton; + QPushButton *loadButton; QScrollArea *scrollArea; + QWidget *panelContainer; + QVBoxLayout *panelLayout; - ManaCurveWidget *manaCurveWidget; - ManaDevotionWidget *manaDevotionWidget; - ManaBaseWidget *manaBaseWidget; + QVector panelWrappers; + ResizablePanel *selectedWrapper = nullptr; + + DeckListStatisticsAnalyzer *statsAnalyzer; + LayoutInspector *insp = nullptr; }; #endif // DECK_ANALYTICS_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp index 3ca18d21c..7d0259c72 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp @@ -9,38 +9,93 @@ DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *_model, - DeckListStatisticsAnalyzerConfig cfg) - : QObject(parent), model(_model), config(cfg) + DeckListStatisticsAnalyzerConfig _config) + : QObject(parent), model(_model), config(_config) { - connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::update); + connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::analyze); } -void DeckListStatisticsAnalyzer::update() +void DeckListStatisticsAnalyzer::analyze() { - manaBaseMap.clear(); - manaCurveMap.clear(); - manaDevotionMap.clear(); + clearData(); QList cards = model->getCards(); - for (const ExactCard &card : cards) { - // ---- Mana curve ---- + for (auto card : cards) { + auto info = card.getInfo(); + const int cmc = info.getCmc().toInt(); + + // Convert once + QStringList types = info.getMainCardType().split(' '); + QStringList subtypes = info.getCardType().split('-').last().split(" "); + QString colors = info.getColors(); + int power = info.getPowTough().split("/").first().toInt(); + int toughness = info.getPowTough().split("/").last().toInt(); + + // For each copy of card + // ---------------- Mana Curve ---------------- if (config.computeManaCurve) { - manaCurveMap[card.getInfo().getCmc().toInt()]++; + manaCurveMap[cmc]++; } - // ---- Mana base ---- + // per-type curve + for (auto &t : types) { + manaCurveByType[t][cmc]++; + manaCurveCardsByType[t][cmc].append(info.getName()); + } + + // Per-subtype curve + for (auto &st : subtypes) { + manaCurveBySubtype[st][cmc]++; + manaCurveCardsBySubtype[st][cmc].append(info.getName()); + } + + // per-color curve + for (auto &c : colors) { + manaCurveByColor[c][cmc]++; + manaCurveCardsByColor[c][cmc].append(info.getName()); + } + + // Power/toughness + manaCurveByPower[QString::number(power)][cmc]++; + manaCurveCardsByPower[QString::number(power)][cmc].append(info.getName()); + manaCurveByToughness[QString::number(toughness)][cmc]++; + manaCurveCardsByToughness[QString::number(toughness)][cmc].append(info.getName()); + + // ========== Category Counts =========== + for (auto &t : types) { + typeCount[t]++; + } + for (auto &st : subtypes) { + subtypeCount[st]++; + } + for (auto &c : colors) { + colorCount[c]++; + } + manaValueCount[cmc]++; + + // ---------------- Mana Base ---------------- if (config.computeManaBase) { - auto mana = determineManaProduction(card.getInfo().getText()); - for (auto it = mana.begin(); it != mana.end(); ++it) + auto prod = determineManaProduction(info.getText()); + for (auto it = prod.begin(); it != prod.end(); ++it) { + if (it.value() > 0) { + productionPipCount[it.key()] += it.value(); + productionCardCount[it.key()]++; + } manaBaseMap[it.key()] += it.value(); + } } - // ---- Devotion ---- + // ---------------- Devotion ---------------- if (config.computeDevotion) { - auto devo = countManaSymbols(card.getInfo().getManaCost()); - for (auto &d : devo) + auto devo = countManaSymbols(info.getManaCost()); + for (auto &d : devo) { + if (d.second > 0) { + devotionPipCount[QString(d.first)] += d.second; + devotionCardCount[QString(d.first)]++; + } manaDevotionMap[d.first] += d.second; + } } } @@ -112,3 +167,57 @@ std::unordered_map DeckListStatisticsAnalyzer::countManaSymbols(const return manaCounts; } + +// Hypergeometric probability: P(X=k) +double DeckListStatisticsAnalyzer::hypergeometric(int N, int K, int n, int k) +{ + if (k < 0 || k > n || K > N) { + return 0.0; + } + + auto choose = [](int n, int r) -> double { + if (r > n) + return 0.0; + if (r == 0 || r == n) + return 1.0; + double res = 1.0; + for (int i = 1; i <= r; ++i) { + res *= (n - r + i); + res /= i; + } + return res; + }; + + return choose(K, k) * choose(N - K, n - k) / choose(N, n); +} + +void DeckListStatisticsAnalyzer::clearData() +{ + manaBaseMap.clear(); + manaCurveMap.clear(); + manaDevotionMap.clear(); + + devotionPipCount.clear(); + devotionCardCount.clear(); + + productionPipCount.clear(); + productionCardCount.clear(); + + manaCurveByType.clear(); + manaCurveBySubtype.clear(); + manaCurveByColor.clear(); + manaCurveByPower.clear(); + manaCurveByToughness.clear(); + + manaCurveCardsByType.clear(); + manaCurveCardsBySubtype.clear(); + manaCurveCardsByColor.clear(); + manaCurveCardsByPower.clear(); + manaCurveCardsByToughness.clear(); + + typeCount.clear(); + subtypeCount.clear(); + colorCount.clear(); + rarityCount.clear(); + manaValueCount.clear(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h index 8fa033971..946bb0117 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.h @@ -14,6 +14,9 @@ struct DeckListStatisticsAnalyzerConfig bool computeManaBase = true; bool computeManaCurve = true; bool computeDevotion = true; + bool computeCategories = true; + bool computeCurveBreakdowns = true; + bool computeProbabilities = true; }; class DeckListStatisticsAnalyzer : public QObject @@ -23,9 +26,9 @@ class DeckListStatisticsAnalyzer : public QObject public: explicit DeckListStatisticsAnalyzer(QObject *parent, DeckListModel *model, - DeckListStatisticsAnalyzerConfig cfg = DeckListStatisticsAnalyzerConfig()); + DeckListStatisticsAnalyzerConfig _config = DeckListStatisticsAnalyzerConfig()); - void update(); + void analyze(); [[nodiscard]] const QHash &getManaBase() const { @@ -40,6 +43,96 @@ public: return manaDevotionMap; } + const QHash &getDevotionPipCount() const + { + return devotionPipCount; + } + const QHash &getDevotionCardCount() const + { + return devotionCardCount; + } + + const QHash &getProductionPipCount() const + { + return productionPipCount; + } + const QHash &getProductionCardCount() const + { + return productionCardCount; + } + + const QHash &getTypeCount() const + { + return typeCount; + } + const QHash &getSubtypeCount() const + { + return subtypeCount; + } + const QHash &getColorCount() const + { + return colorCount; + } + const QHash &getRarityCount() const + { + return rarityCount; + } + const QHash &getManaValueCount() const + { + return manaValueCount; + } + + const QHash> &getManaCurveByType() const + { + return manaCurveByType; + } + const QHash> &getManaCurveBySubtype() const + { + return manaCurveBySubtype; + } + const QHash> &getManaCurveByColor() const + { + return manaCurveByColor; + } + const QHash> &getManaCurveByPower() const + { + return manaCurveByPower; + } + const QHash> &getManaCurveByToughness() const + { + return manaCurveByToughness; + } + + const QHash> &getManaCurveCardsByType() const + { + return manaCurveCardsByType; + } + + const QHash> &getManaCurveCardsBySubtype() const + { + return manaCurveCardsBySubtype; + } + + const QHash> &getManaCurveCardsByColor() const + { + return manaCurveCardsByColor; + } + + const QHash> &getManaCurveCardsByPower() const + { + return manaCurveCardsByPower; + } + + const QHash> &getManaCurveCardsByToughness() const + { + return manaCurveCardsByToughness; + } + + DeckListModel *getModel() const + { + return model; + } + signals: void statsUpdated(); @@ -47,14 +140,42 @@ private: DeckListModel *model; DeckListStatisticsAnalyzerConfig config; - // Internal result containers QHash manaBaseMap; std::unordered_map manaCurveMap; std::unordered_map manaDevotionMap; - // Internal helper functions + QHash devotionPipCount; // W/U/B/R/G total symbols + QHash devotionCardCount; // how many cards provide devotion + + QHash productionPipCount; // mana produced by cards + QHash productionCardCount; // number of producers + + QHash typeCount; + QHash subtypeCount; + QHash colorCount; + QHash rarityCount; + QHash manaValueCount; + + QHash> manaCurveByType; + QHash> manaCurveBySubtype; + QHash> manaCurveByColor; + QHash> manaCurveByPower; + QHash> manaCurveByToughness; + + QHash> manaCurveCardsByType; + QHash> manaCurveCardsBySubtype; + QHash> manaCurveCardsByColor; + QHash> manaCurveCardsByPower; + QHash> manaCurveCardsByToughness; + + // Not storing card info — only numeric results. + QHash>> probabilityExact; + QHash>> probabilityAtLeast; + QHash determineManaProduction(const QString &); std::unordered_map countManaSymbols(const QString &); + double hypergeometric(int N, int K, int n, int k); + void clearData(); }; #endif // COCKATRICE_DECK_LIST_STATISTICS_ANALYZER_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp deleted file mode 100644 index 2e530db0b..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "mana_base_widget.h" - -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include -#include - -ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Base"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barContainer = new QWidget(this); - barLayout = new QHBoxLayout(barContainer); - layout->addWidget(barContainer); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaBaseWidget::updateDisplay); - - retranslateUi(); -} - -void ManaBaseWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Base")); -} - -void ManaBaseWidget::updateDisplay() -{ - // Clear the layout first - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - - auto manaBaseMap = deckStatAnalyzer->getManaBase(); - - int highestEntry = 0; - for (auto entry : manaBaseMap) { - if (entry > highestEntry) { - highestEntry = entry; - } - } - - // Define color mapping for mana types - QHash manaColors; - manaColors.insert("W", QColor(248, 231, 185)); - manaColors.insert("U", QColor(14, 104, 171)); - manaColors.insert("B", QColor(21, 11, 0)); - manaColors.insert("R", QColor(211, 32, 42)); - manaColors.insert("G", QColor(0, 115, 62)); - manaColors.insert("C", QColor(150, 150, 150)); - - for (auto manaColor : manaBaseMap.keys()) { - QColor barColor = manaColors.value(manaColor, Qt::gray); - BarWidget *barWidget = new BarWidget(QString(manaColor), manaBaseMap[manaColor], highestEntry, barColor, this); - barLayout->addWidget(barWidget); - } - - update(); -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h deleted file mode 100644 index 079449353..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_base_widget.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file mana_base_widget.h - * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. - */ - -#ifndef MANA_BASE_WIDGET_H -#define MANA_BASE_WIDGET_H - -#include "../general/display/banner_widget.h" -#include "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include -#include - -class ManaBaseWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - BannerWidget *bannerWidget; - QVBoxLayout *layout; - QWidget *barContainer; - QHBoxLayout *barLayout; -}; - -#endif // MANA_BASE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp deleted file mode 100644 index c094eb590..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "mana_curve_widget.h" - -#include "../../../main.h" -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include - -ManaCurveWidget::ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Curve"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barContainer = new QWidget(this); - barLayout = new QHBoxLayout(barContainer); - layout->addWidget(barContainer); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaCurveWidget::updateDisplay); - - retranslateUi(); -} - -void ManaCurveWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Curve")); -} - -void ManaCurveWidget::updateDisplay() -{ - // Clear the layout first - if (barLayout != nullptr) { - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - } - - auto manaCurveMap = deckStatAnalyzer->getManaCurve(); - - int highestEntry = 0; - for (const auto &entry : manaCurveMap) { - if (entry.second > highestEntry) { - highestEntry = entry.second; - } - } - - // Convert unordered_map to ordered map to ensure sorting by CMC - std::map sortedManaCurve(manaCurveMap.begin(), manaCurveMap.end()); - - // Add new widgets to the layout in sorted order - for (const auto &entry : sortedManaCurve) { - BarWidget *barWidget = - new BarWidget(QString::number(entry.first), entry.second, highestEntry, QColor(122, 122, 122), this); - barLayout->addWidget(barWidget); - } - - update(); // Update the widget display -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h deleted file mode 100644 index fad1fb0f8..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_curve_widget.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file mana_curve_widget.h - * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. - */ - -#ifndef MANA_CURVE_WIDGET_H -#define MANA_CURVE_WIDGET_H - -#include "../general/display/banner_widget.h" -#include "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include - -class ManaCurveWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaCurveWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - QVBoxLayout *layout; - BannerWidget *bannerWidget; - QWidget *barContainer; - QHBoxLayout *barLayout; -}; - -#endif // MANA_CURVE_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp deleted file mode 100644 index 476bb8077..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "mana_devotion_widget.h" - -#include "../../deck_loader/deck_loader.h" -#include "../general/display/banner_widget.h" -#include "../general/display/bar_widget.h" - -#include -#include -#include -#include -#include - -ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *_deckStatAnalyzer) - : QWidget(parent), deckStatAnalyzer(_deckStatAnalyzer) -{ - layout = new QVBoxLayout(this); - setLayout(layout); - - bannerWidget = new BannerWidget(this, tr("Mana Devotion"), Qt::Vertical, 100); - bannerWidget->setMaximumHeight(100); - layout->addWidget(bannerWidget); - - barLayout = new QHBoxLayout(); - layout->addLayout(barLayout); - - connect(deckStatAnalyzer, &DeckListStatisticsAnalyzer::statsUpdated, this, &ManaDevotionWidget::updateDisplay); - - retranslateUi(); -} - -void ManaDevotionWidget::retranslateUi() -{ - bannerWidget->setText(tr("Mana Devotion")); -} - -void ManaDevotionWidget::updateDisplay() -{ - // Clear the layout first - QLayoutItem *item; - while ((item = barLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); - delete item; - } - - auto manaDevotionMap = deckStatAnalyzer->getDevotion(); - - int highestEntry = 0; - for (auto entry : manaDevotionMap) { - if (highestEntry < entry.second) { - highestEntry = entry.second; - } - } - - // Define color mapping for devotion bars - std::unordered_map manaColors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)}, - {'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)}, - {'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}}; - - for (auto entry : manaDevotionMap) { - QColor barColor = manaColors.count(entry.first) ? manaColors[entry.first] : Qt::gray; - BarWidget *barWidget = new BarWidget(QString(entry.first), entry.second, highestEntry, barColor, this); - barLayout->addWidget(barWidget); - } - - update(); // Update the widget display -} diff --git a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h b/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h deleted file mode 100644 index ff2e86159..000000000 --- a/cockatrice/src/interface/widgets/deck_analytics/mana_devotion_widget.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file mana_devotion_widget.h - * @ingroup DeckEditorAnalyticsWidgets - * @brief TODO: Document this. - */ - -#ifndef MANA_DEVOTION_WIDGET_H -#define MANA_DEVOTION_WIDGET_H - -#include "../general/display/banner_widget.h" -#include "deck_list_statistics_analyzer.h" - -#include -#include -#include -#include -#include - -class ManaDevotionWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyzer *deckStatAnalyzer); - void updateDisplay(); - -public slots: - void retranslateUi(); - -private: - DeckListStatisticsAnalyzer *deckStatAnalyzer; - BannerWidget *bannerWidget; - QVBoxLayout *layout; - QHBoxLayout *barLayout; -}; - -#endif // MANA_DEVOTION_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp new file mode 100644 index 000000000..a0c971a75 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.cpp @@ -0,0 +1,367 @@ +#include "resizable_panel.h" + +#include "libcockatrice/utility/qt_utils.h" + +#include +#include +#include +#include + +ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent) + : QWidget(parent), panel(analyticsPanel), typeId(_typeId) +{ + setAcceptDrops(true); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // Frame for selection highlight + frame = new QFrame(this); + frame->setFrameShape(QFrame::Box); + frame->setLineWidth(2); + frame->setStyleSheet("border: none;"); + + auto *frameLayout = new QVBoxLayout(frame); + frameLayout->setContentsMargins(0, 0, 0, 0); + frameLayout->setSpacing(0); + + // Add the analytics panel + frameLayout->addWidget(analyticsPanel); + + dropIndicator = new QFrame(frame); + dropIndicator->setStyleSheet("background-color: #3daee9;"); + dropIndicator->setFixedHeight(3); + dropIndicator->hide(); // hidden by default + dropIndicator->raise(); // make sure it's above children + + selectionOverlay = new QFrame(frame); + selectionOverlay->setStyleSheet("background-color: rgba(61,174,233,50);"); // semi-transparent blue + selectionOverlay->hide(); // hidden by default + selectionOverlay->raise(); // make sure it is above children + selectionOverlay->setAttribute(Qt::WA_TransparentForMouseEvents); + + // Bottom bar with drag button and resize handle + auto *bottomBar = new QWidget(frame); + auto *bottomLayout = new QHBoxLayout(bottomBar); + bottomLayout->setContentsMargins(0, 0, 0, 0); + bottomLayout->setSpacing(0); + + // Drag button on the left + dragButton = new QPushButton("☰", bottomBar); + dragButton->setFixedSize(40, 8); + dragButton->setCursor(Qt::OpenHandCursor); + dragButton->setStyleSheet("QPushButton { " + "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a4a4a, stop:1 #3a3a3a); " + "border: none; color: #888; font-size: 10px; }" + "QPushButton:hover { background: #5a5a5a; }"); + bottomLayout->addWidget(dragButton); + + // Resize handle fills the rest + resizeHandle = new QWidget(bottomBar); + resizeHandle->setFixedHeight(8); + resizeHandle->setCursor(Qt::SizeVerCursor); + resizeHandle->setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:0, y2:1, " + "stop:0 #3a3a3a, stop:1 #2a2a2a);"); + bottomLayout->addWidget(resizeHandle, 1); + + frameLayout->addWidget(bottomBar); + + mainLayout->addWidget(frame); + + // Set size policy + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + // Calculate initial height - use panel's size hint if available + int panelHint = analyticsPanel->sizeHint().height(); + int panelMin = analyticsPanel->minimumSizeHint().height(); + + // Start with the larger of panel's hint and panel's minimum hint + currentHeight = qMax(panelHint + 8, panelMin + 8); + updateSizeConstraints(); + + // Install event filters + dragButton->installEventFilter(this); + resizeHandle->installEventFilter(this); + + // Timer for auto-scroll during drag + autoScrollTimer = new QTimer(this); + autoScrollTimer->setInterval(50); + connect(autoScrollTimer, &QTimer::timeout, this, &ResizablePanel::performAutoScroll); +} + +void ResizablePanel::setSelected(bool selected) +{ + if (selected) { + selectionOverlay->setGeometry(0, 0, width(), height()); + selectionOverlay->show(); + } else { + selectionOverlay->hide(); + } +} + +void ResizablePanel::setHeightFromSaved(int h) +{ + if (h > 0) { + currentHeight = qMax(h, getMinimumAllowedHeight()); + updateSizeConstraints(); + } +} + +int ResizablePanel::getCurrentHeight() const +{ + return currentHeight; +} + +QSize ResizablePanel::sizeHint() const +{ + return QSize(width(), currentHeight); +} + +QSize ResizablePanel::minimumSizeHint() const +{ + return QSize(0, getMinimumAllowedHeight()); +} + +// ===================================================================================================================== +// Event Handling +// ===================================================================================================================== + +bool ResizablePanel::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == dragButton) { + if (event->type() == QEvent::MouseButtonPress) { + auto *mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + dragStartPos = mouseEvent->globalPosition().toPoint(); +#else + dragStartPos = mouseEvent->globalPos(); +#endif + isDraggingPanel = false; + dragButton->setCursor(Qt::ClosedHandCursor); + } + return false; + } else if (event->type() == QEvent::MouseMove) { + auto *mouseEvent = static_cast(event); + if (mouseEvent->buttons() & Qt::LeftButton) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint currentPos = mouseEvent->globalPosition().toPoint(); +#else + QPoint currentPos = mouseEvent->globalPos(); +#endif + int distance = (currentPos - dragStartPos).manhattanLength(); + if (distance >= 5 && !isDraggingPanel) { + isDraggingPanel = true; + startDrag(); + return true; + } + } + return false; + } else if (event->type() == QEvent::MouseButtonRelease) { + dragButton->setCursor(Qt::OpenHandCursor); + isDraggingPanel = false; + return false; + } + } + + if (obj == resizeHandle) { + if (event->type() == QEvent::MouseButtonPress) { + auto *mouseEvent = static_cast(event); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + resizeStartY = mouseEvent->globalPosition().y(); +#else + resizeStartY = mouseEvent->globalPos().y(); +#endif + isResizing = true; + resizeStartHeight = currentHeight; + resizeHandle->grabMouse(); + return true; + } else if (event->type() == QEvent::MouseMove && isResizing) { + auto *mouseEvent = static_cast(event); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + int deltaY = mouseEvent->globalPosition().y() - resizeStartY; +#else + int deltaY = mouseEvent->globalPos().y() - resizeStartY; +#endif + int newHeight = resizeStartHeight + deltaY; + + int minAllowed = getMinimumAllowedHeight(); + newHeight = qMax(newHeight, minAllowed); + + currentHeight = newHeight; + updateSizeConstraints(); + + return true; + } else if (event->type() == QEvent::MouseButtonRelease) { + isResizing = false; + resizeHandle->releaseMouse(); + return true; + } + } + + return QWidget::eventFilter(obj, event); +} + +void ResizablePanel::dragEnterEvent(QDragEnterEvent *event) +{ + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + event->acceptProposedAction(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + showDropIndicator(event->position().y()); +#else + showDropIndicator(event->pos().y()); +#endif + } +} + +void ResizablePanel::dragMoveEvent(QDragMoveEvent *event) +{ + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + event->acceptProposedAction(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + showDropIndicator(event->position().y()); + lastDragPos = mapToGlobal(event->position().toPoint()); +#else + showDropIndicator(event->pos().y()); + lastDragPos = mapToGlobal(event->pos()); +#endif + + if (!autoScrollTimer->isActive()) { + autoScrollTimer->start(); + } + } +} + +void ResizablePanel::dragLeaveEvent(QDragLeaveEvent *event) +{ + Q_UNUSED(event); + hideDropIndicator(); + autoScrollTimer->stop(); +} + +void ResizablePanel::dropEvent(QDropEvent *event) +{ + hideDropIndicator(); + autoScrollTimer->stop(); + + if (event->mimeData()->hasFormat("application/x-resizablepanel")) { + QByteArray data = event->mimeData()->data("application/x-resizablepanel"); + quintptr ptr = *reinterpret_cast(data.constData()); + ResizablePanel *draggedPanel = reinterpret_cast(ptr); + + if (draggedPanel && draggedPanel != this) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + bool insertBefore = (event->position().y() < height() / 2); +#else + bool insertBefore = (event->pos().y() < height() / 2); +#endif + emit dropRequested(draggedPanel, this, insertBefore); + event->acceptProposedAction(); + } + } +} + +void ResizablePanel::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + if (selectionOverlay->isVisible()) { + selectionOverlay->setGeometry(0, 0, width(), height()); + } + + if (dropIndicator->isVisible()) { + dropIndicator->setGeometry(0, dropIndicator->y(), width(), dropIndicator->height()); + } +} + +// ===================================================================================================================== +// Private Helpers +// ===================================================================================================================== + +int ResizablePanel::getMinimumAllowedHeight() const +{ + QSize panelMin = panel->minimumSizeHint(); + int panelMinHeight = (panelMin.isValid() && panelMin.height() > 0) ? panelMin.height() : 100; + return panelMinHeight + 8; +} + +void ResizablePanel::updateSizeConstraints() +{ + setMinimumHeight(currentHeight); + setMaximumHeight(currentHeight); + updateGeometry(); +} + +void ResizablePanel::startDrag() +{ + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + + quintptr ptr = reinterpret_cast(this); + QByteArray data(reinterpret_cast(&ptr), sizeof(ptr)); + mimeData->setData("application/x-resizablepanel", data); + + drag->setMimeData(mimeData); + + QPixmap pixmap(width(), 40); + pixmap.fill(QColor(58, 58, 58, 200)); + drag->setPixmap(pixmap); + drag->setHotSpot(QPoint(width() / 2, 20)); + + emit dragStarted(this); + + autoScrollTimer->start(); + + Qt::DropAction result = drag->exec(Qt::MoveAction); + Q_UNUSED(result); + + autoScrollTimer->stop(); + dragButton->setCursor(Qt::OpenHandCursor); + isDraggingPanel = false; +} + +void ResizablePanel::performAutoScroll() +{ + QScrollArea *scrollArea = QtUtils::findParentOfType(this); + + if (!scrollArea) { + return; + } + + QScrollBar *scrollBar = scrollArea->verticalScrollBar(); + if (!scrollBar) { + return; + } + + QRect scrollRect = scrollArea->viewport()->rect(); + QPoint scrollTopLeft = scrollArea->viewport()->mapToGlobal(scrollRect.topLeft()); + QRect globalScrollRect(scrollTopLeft, scrollRect.size()); + + const int scrollMargin = 50; + int scrollSpeed = 0; + + if (lastDragPos.y() < globalScrollRect.top() + scrollMargin) { + scrollSpeed = -15; + } else if (lastDragPos.y() > globalScrollRect.bottom() - scrollMargin) { + scrollSpeed = 15; + } + + if (scrollSpeed != 0) { + int newValue = scrollBar->value() + scrollSpeed; + newValue = qBound(scrollBar->minimum(), newValue, scrollBar->maximum()); + scrollBar->setValue(newValue); + } +} + +void ResizablePanel::showDropIndicator(double y) +{ + bool before = (y < height() / 2); + dropIndicator->setGeometry(0, before ? 0 : height() - 3, width(), 3); + dropIndicator->show(); +} + +void ResizablePanel::hideDropIndicator() +{ + dropIndicator->hide(); +} diff --git a/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h new file mode 100644 index 000000000..d958ec64d --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_analytics/resizable_panel.h @@ -0,0 +1,79 @@ +#ifndef COCKATRICE_RESIZABLE_PANEL_H +#define COCKATRICE_RESIZABLE_PANEL_H + +#include "abstract_analytics_panel_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ResizablePanel : public QWidget +{ + Q_OBJECT +public: + explicit ResizablePanel(const QString &typeId, + AbstractAnalyticsPanelWidget *analyticsPanel, + QWidget *parent = nullptr); + + void setSelected(bool selected); + void setHeightFromSaved(int h); + int getCurrentHeight() const; + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + QString getTypeId() const + { + return typeId; + } + + AbstractAnalyticsPanelWidget *panel; + +signals: + void dragStarted(ResizablePanel *panel); + void dropRequested(ResizablePanel *dragged, ResizablePanel *target, bool insertBefore); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private: + int getMinimumAllowedHeight() const; + void updateSizeConstraints(); + void startDrag(); + void performAutoScroll(); + void showDropIndicator(double y); + void hideDropIndicator(); + + QString typeId; + + QFrame *frame; + QFrame *selectionOverlay; + QFrame *dropIndicator; + QPushButton *dragButton; + QWidget *resizeHandle; + + int currentHeight; + bool isResizing = false; + bool isDraggingPanel = false; + double resizeStartY = 0; + int resizeStartHeight = 0; + + QPoint dragStartPos; + QPoint lastDragPos; + QTimer *autoScrollTimer; +}; + +#endif // COCKATRICE_RESIZABLE_PANEL_H diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp new file mode 100644 index 000000000..f7ca669f3 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.cpp @@ -0,0 +1,43 @@ +#include "bar_chart_background_widget.h" + +BarChartBackgroundWidget::BarChartBackgroundWidget(QWidget *parent) : QWidget(parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +QSize BarChartBackgroundWidget::sizeHint() const +{ + return QSize(100, 150); +} + +void BarChartBackgroundWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + + int left = 46; // axis space + internal padding + int right = width() - PAD; + int top = PAD; + int bottom = height() - PAD - LABEL_H; + + int barAreaHeight = bottom - top; + int barAreaWidth = right - left; + + p.fillRect(QRect(left, top, barAreaWidth, barAreaHeight), QColor(250, 250, 250)); + + int ticks = 5; + for (int i = 0; i <= ticks; i++) { + float r = float(i) / ticks; + int y = bottom - r * barAreaHeight; + + p.setPen(QPen(QColor(180, 180, 180, 120), 1)); + p.drawLine(left, y, right, y); + + p.setPen(Qt::black); + p.drawText(left - 35, y - 6, 32, 12, Qt::AlignRight | Qt::AlignVCenter, QString::number(int(r * highest))); + } +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h new file mode 100644 index 000000000..06a17c7c6 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_background_widget.h @@ -0,0 +1,23 @@ +#ifndef COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H +#define COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H + +#include +#include + +class BarChartBackgroundWidget : public QWidget +{ + Q_OBJECT +public: + int highest = 0; // global maximum (shared across bars) + int barCount = 0; // number of CMC columns + int labelHeight = 20; // reserved for CMC numbers + + explicit BarChartBackgroundWidget(QWidget *parent); +public slots: + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +#endif // COCKATRICE_BAR_CHART_BACKGROUND_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp new file mode 100644 index 000000000..998808307 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.cpp @@ -0,0 +1,215 @@ +#include "bar_chart_widget.h" + +#include +#include +#include +#include + +BarChartWidget::BarChartWidget(QWidget *parent) : QWidget(parent) +{ + setMouseTracking(true); +} + +void BarChartWidget::setBars(const QVector &newBars) +{ + bars = newBars; + update(); +} + +void BarChartWidget::setHighest(int h) +{ + highest = qMax(1, h); + update(); +} + +QSize BarChartWidget::sizeHint() const +{ + return QSize(300, 200); +} + +QSize BarChartWidget::minimumSizeHint() const +{ + return QSize(300, 50); +} + +void BarChartWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + + int w = width(); + int h = height(); + + int left = 46; + int right = w - PAD; + int top = PAD; + int bottom = h - PAD - LABEL_H; + + int barAreaHeight = bottom - top; + int barAreaWidth = right - left; + + int barCount = bars.size(); + if (barCount == 0) + return; + + int spacing = 6; + int barWidth = (barAreaWidth - (barCount - 1) * spacing) / barCount; + + // background + p.fillRect(QRect(left, top, barAreaWidth, barAreaHeight), QColor(250, 250, 250)); + + // y-axis ticks + int ticks = 5; + // qInfo() << "Tick Positions "; + for (int i = 0; i <= ticks; i++) { + float r = float(i) / ticks; + int tickVal = i * highest / ticks; // integer value of tick + int y = bottom - (tickVal * barAreaHeight / highest); + + // qInfo() << "Tick" << i << "value" << int(r * highest) << "y" << y; + + p.setPen(QPen(QColor(180, 180, 180, 120), 1)); + p.drawLine(left, y, right, y); + + p.setPen(Qt::black); + p.drawText(left - 35, y - 6, 32, 12, Qt::AlignRight | Qt::AlignVCenter, QString::number(int(r * highest))); + } + + // draw bars + // qInfo() << "Bar Segments"; + int drawWidth = barWidth / 4; // 1/4 of allocated width + int xOffset = (barWidth - drawWidth) / 2; // center the narrow bar + + for (int i = 0; i < barCount; i++) { + const BarData &bar = bars[i]; + int x = left + i * (barWidth + spacing) + xOffset; // shift to center + int yCurrent = bottom; + + for (int j = 0; j < bar.segments.size(); j++) { + const auto &seg = bar.segments[j]; + int segHeight = (seg.value * barAreaHeight / highest); + if (segHeight < 2 && seg.value > 0) + segHeight = 2; + + int topY = yCurrent - segHeight; + + QRect r(x, topY, drawWidth, segHeight); // use drawWidth instead of barWidth + bool isTop = (j == bar.segments.size() - 1); + + QLinearGradient g(r.topLeft(), r.bottomLeft()); + g.setColorAt(0, seg.color.lighter(120)); + g.setColorAt(1, seg.color.darker(110)); + p.setBrush(g); + p.setPen(Qt::NoPen); + + if (isTop) { + QPainterPath path; + int radius = 6; + + int bx = r.x(); + int by = r.y(); + int bw = r.width(); + int bh = r.height(); + + path.moveTo(bx, by + bh); + path.lineTo(bx, by + radius); + path.quadTo(bx, by, bx + radius, by); + path.lineTo(bx + bw - radius, by); + path.quadTo(bx + bw, by, bx + bw, by + radius); + path.lineTo(bx + bw, by + bh); + path.lineTo(bx, by + bh); + path.closeSubpath(); + + p.drawPath(path); + } else { + p.drawRect(r); + } + + yCurrent -= segHeight; + } + + // draw label below bar + QRect labelRect(left + i * (barWidth + spacing), bottom, barWidth, LABEL_H); + QFont f = p.font(); + f.setBold(true); + p.setFont(f); + p.setPen(Qt::black); + p.drawText(labelRect, Qt::AlignCenter, bar.label); + } +} + +void BarChartWidget::leaveEvent(QEvent *) +{ + hoveredBar = -1; + hoveredSegment = -1; + QToolTip::hideText(); +} + +void BarChartWidget::mouseMoveEvent(QMouseEvent *e) +{ + if (bars.isEmpty()) { + return; + } + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + int w = width(); + int h = height(); + int left = 46; + int right = w - PAD; + int top = PAD; + int bottom = h - PAD - LABEL_H; + int barAreaHeight = bottom - top; + + int barCount = bars.size(); + int spacing = 6; + int barWidth = (right - left - (barCount - 1) * spacing) / barCount; + + // find hovered bar + int mx = e->pos().x(); + hoveredBar = -1; + for (int i = 0; i < barCount; i++) { + int x0 = left + i * (barWidth + spacing); + if (mx >= x0 && mx <= x0 + barWidth) { + hoveredBar = i; + break; + } + } + if (hoveredBar < 0) { + return; + } + + // find hovered segment + int yCurrent = bottom; + const auto &segments = bars[hoveredBar].segments; + hoveredSegment = -1; + for (int i = 0; i < segments.size(); i++) { + const auto &seg = segments[i]; + int segHeight = (seg.value * barAreaHeight / highest); + if (segHeight < 2 && seg.value > 0) + segHeight = 2; + + int topY = yCurrent - segHeight; + int bottomY = yCurrent; + if (e->pos().y() >= topY && e->pos().y() <= bottomY) { + hoveredSegment = i; + break; + } + yCurrent -= segHeight; + } + + if (hoveredSegment >= 0) { + const auto &s = segments[hoveredSegment]; + QString text = QString("%1: %2 cards\n\n%3").arg(s.category).arg(s.value).arg(s.cards.join("\n")); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QToolTip::showText(e->globalPosition().toPoint(), text, this); +#else + QToolTip::showText(e->globalPos(), text, this); +#endif + } else { + QToolTip::hideText(); + } +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h new file mode 100644 index 000000000..e80a3f8e8 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_chart_widget.h @@ -0,0 +1,52 @@ +#ifndef COCKATRICE_BAR_CHART_WIDGET_H +#define COCKATRICE_BAR_CHART_WIDGET_H + +#include +#include +#include +#include + +struct BarSegment +{ + QString category; + int value; + QStringList cards; + QColor color; +}; + +struct BarData +{ + QString label; + QVector segments; +}; + +class BarChartWidget : public QWidget +{ + Q_OBJECT +public: + explicit BarChartWidget(QWidget *parent = nullptr); + + void setBars(const QVector &bars); + void setHighest(int h); // global max for scaling + int barCount() const + { + return bars.size(); + } + +protected: + void paintEvent(QPaintEvent *event) override; + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + void mouseMoveEvent(QMouseEvent *event) override; + void leaveEvent(QEvent *event) override; + +private: + QVector bars; + int highest = 1; // global maximum value + + int hoveredBar = -1; + int hoveredSegment = -1; +}; + +#endif // COCKATRICE_BAR_CHART_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.cpp similarity index 100% rename from cockatrice/src/interface/widgets/general/display/bar_widget.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.cpp diff --git a/cockatrice/src/interface/widgets/general/display/bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/bar_widget.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/bar_widget.h diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp similarity index 99% rename from cockatrice/src/interface/widgets/general/display/color_bar.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp index d1eb7ef4c..94e2420b5 100644 --- a/cockatrice/src/interface/widgets/general/display/color_bar.cpp +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.cpp @@ -1,4 +1,3 @@ - #include "color_bar.h" #include diff --git a/cockatrice/src/interface/widgets/general/display/color_bar.h b/cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/color_bar.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/color_bar.h diff --git a/cockatrice/src/interface/widgets/general/display/percent_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp similarity index 100% rename from cockatrice/src/interface/widgets/general/display/percent_bar_widget.cpp rename to cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.cpp diff --git a/cockatrice/src/interface/widgets/general/display/percent_bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.h similarity index 100% rename from cockatrice/src/interface/widgets/general/display/percent_bar_widget.h rename to cockatrice/src/interface/widgets/general/display/charts/bars/percent_bar_widget.h diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp new file mode 100644 index 000000000..e027aabdd --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp @@ -0,0 +1,140 @@ +#include "segmented_bar_widget.h" + +#include +#include +#include +#include + +SegmentedBarWidget::SegmentedBarWidget(QString label, QVector segments, int total, QWidget *parent) + : QWidget(parent), label(std::move(label)), segments(std::move(segments)), total(total) +{ + setMouseTracking(true); + setMinimumWidth(36); + setMaximumWidth(50); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); +} + +QSize SegmentedBarWidget::sizeHint() const +{ + return QSize(50, 150); +} + +void SegmentedBarWidget::paintEvent(QPaintEvent *) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + constexpr int PAD = 4; + constexpr int LABEL_H = 20; + + int w = width(); + int h = height(); + + int barX = PAD; + int barWidth = w - PAD * 2; + + int barTop = PAD; + int barBottom = h - PAD - LABEL_H; + int barHeight = barBottom - barTop; + + int yCurrent = barBottom; + + // draw stacked segments + for (int i = 0; i < segments.size(); i++) { + const auto &seg = segments[i]; + + int segHeight = total > 0 ? (seg.value * barHeight / total) : 0; + if (segHeight < 2) + segHeight = 2; + + QRect r(barX, yCurrent - segHeight, barWidth, segHeight); + bool isTop = (i == segments.size() - 1); + + QLinearGradient g(r.topLeft(), r.bottomLeft()); + g.setColorAt(0, seg.color.lighter(120)); + g.setColorAt(1, seg.color.darker(110)); + p.setBrush(g); + p.setPen(Qt::NoPen); + + if (isTop) { + QPainterPath path; + int radius = 6; + + int x = r.x(); + int y = r.y(); + int w = r.width(); + int h = r.height(); + + path.moveTo(x, y + h); + path.lineTo(x, y + radius); + path.quadTo(x, y, x + radius, y); + path.lineTo(x + w - radius, y); + path.quadTo(x + w, y, x + w, y + radius); + path.lineTo(x + w, y + h); + path.lineTo(x, y + h); + path.closeSubpath(); + + p.drawPath(path); + } else { + p.drawRect(r); + } + + yCurrent -= segHeight; + } + + // draw label + QRect labelRect(0, h - LABEL_H, w, LABEL_H); + QFont f = p.font(); + f.setBold(true); + p.setFont(f); + p.setPen(Qt::black); + p.drawText(labelRect, Qt::AlignCenter, label); +} + +int SegmentedBarWidget::segmentAt(int y) const +{ + int padding = 4; + int labelHeight = 20; + int barHeight = height() - padding * 2 - labelHeight; + int barTop = padding; + int barBottom = barTop + barHeight; + + int currentTop = barBottom; + + for (int i = 0; i < segments.size(); i++) { + int segHeight = total > 0 ? (segments[i].value * barHeight / total) : 0; + if (segHeight < 1) { + segHeight = 1; + } + + int top = currentTop - segHeight; + int bottom = currentTop; + + if (y >= top && y <= bottom) + return i; + + currentTop -= segHeight; + } + return -1; +} + +void SegmentedBarWidget::mouseMoveEvent(QMouseEvent *e) +{ + if (!hovered) { + return; + } + + int idx = segmentAt(e->pos().y()); + if (idx < 0) { + return; + } + + const Segment &s = segments[idx]; + QString text = QString("%1: %2 cards\n%3").arg(s.category).arg(s.value).arg(s.cards.join(", ")); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QToolTip::showText(e->globalPosition().toPoint(), text, this); +#else + QToolTip::showText(e->globalPos(), text, this); +#endif +} diff --git a/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h new file mode 100644 index 000000000..4df54e42e --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/bars/segmented_bar_widget.h @@ -0,0 +1,38 @@ +#ifndef COCKATRICE_SEGMENTED_BAR_WIDGET_H +#define COCKATRICE_SEGMENTED_BAR_WIDGET_H + +#include +#include +#include + +class SegmentedBarWidget : public QWidget +{ + Q_OBJECT + +public: + struct Segment + { + QString category; + int value = 0; + QStringList cards; + QColor color; + }; + + QString label; + QVector segments; + float total = 1.0; + + explicit SegmentedBarWidget(QString label, QVector segments, int total, QWidget *parent = nullptr); + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + void mouseMoveEvent(QMouseEvent *e) override; + + int segmentAt(int y) const; + +private: + bool hovered = true; +}; + +#endif // COCKATRICE_SEGMENTED_BAR_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp new file mode 100644 index 000000000..e86793083 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.cpp @@ -0,0 +1,205 @@ +#include "color_pie.h" + +#include +#include +#include +#include + +ColorPie::ColorPie(const QMap &_colors, QWidget *parent) : QWidget(parent), colors(_colors) +{ + setMouseTracking(true); +} + +void ColorPie::setColors(const QMap &_colors) +{ + colors = _colors; + update(); +} + +QSize ColorPie::minimumSizeHint() const +{ + return QSize(200, 200); +} + +void ColorPie::paintEvent(QPaintEvent *) +{ + if (colors.isEmpty()) { + return; + } + + int total = 0; + for (int v : colors.values()) { + total += v; + } + + if (total == 0) { + return; + } + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + int w = width(); + int h = height(); + int size = qMin(w, h) - 40; // leave space for labels + QRectF rect((w - size) / 2.0, (h - size) / 2.0, size, size); + + // Draw border + p.setPen(QPen(Qt::black, 1)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(rect); + + // Sorted keys for predictable order + QList sortedKeys = colors.keys(); + std::sort(sortedKeys.begin(), sortedKeys.end()); + + double startAngle = 0.0; + + for (const QString &key : sortedKeys) { + int value = colors[key]; + double ratio = double(value) / total; + + if (ratio <= minRatioThreshold) { + continue; + } + + double spanAngle = ratio * 360.0; + + QColor base = colorFromName(key); + + // Gradient + QRadialGradient grad(rect.center(), size / 2); + grad.setColorAt(0, base.lighter(130)); + grad.setColorAt(1, base.darker(130)); + p.setBrush(grad); + p.setPen(Qt::NoPen); + + // Draw slice + p.drawPie(rect, int(startAngle * 16), int(spanAngle * 16)); + + // Draw percent label + double midAngle = startAngle + spanAngle / 2; + double rad = qDegreesToRadians(midAngle); + double labelRadius = size / 2 + 15; // slightly outside the pie + QPointF center = rect.center(); + QPointF labelPos(center.x() + labelRadius * qCos(rad), center.y() - labelRadius * qSin(rad)); + + QString label = QString("%1%").arg(int(ratio * 100 + 0.5)); + + QFontMetrics fm(p.font()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) + int labelWidth = fm.horizontalAdvance(label); +#else + int labelWidth = fm.width(label); +#endif + QRectF textRect(labelPos.x() - labelWidth / 2.0, labelPos.y() - fm.height() / 2.0, labelWidth, fm.height()); + + p.setPen(Qt::black); + p.drawText(textRect, Qt::AlignCenter, label); + + startAngle += spanAngle; + } +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void ColorPie::enterEvent(QEnterEvent *event) +{ + Q_UNUSED(event); + isHovered = true; +} +#else +void ColorPie::enterEvent(QEvent *event) +{ + Q_UNUSED(event); + isHovered = true; +} +#endif + +void ColorPie::leaveEvent(QEvent *) +{ + isHovered = false; +} + +void ColorPie::mouseMoveEvent(QMouseEvent *event) +{ + if (!isHovered || colors.isEmpty()) { + return; + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPoint p = event->position().toPoint(); + QPoint gp = event->globalPosition().toPoint(); +#else + QPoint p = event->pos(); + QPoint gp = event->globalPos(); +#endif + + QString text = tooltipForPoint(p); + if (!text.isEmpty()) { + QToolTip::showText(gp, text, this); + } +} + +QString ColorPie::tooltipForPoint(const QPoint &pt) const +{ + if (colors.isEmpty()) { + return {}; + } + + int total = 0; + for (int v : colors.values()) + total += v; + if (total == 0) + return {}; + + int w = width(); + int h = height(); + int size = qMin(w, h) - 40; + QPointF center(w / 2.0, h / 2.0); + + QPointF v = pt - center; + double distance = std::sqrt(v.x() * v.x() + v.y() * v.y()); + if (distance > size / 2.0) + return {}; // outside pie + + double angle = std::atan2(-v.y(), v.x()) * 180.0 / M_PI; + if (angle < 0) { + angle += 360.0; + } + + double acc = 0.0; + + QList keys = colors.keys(); + std::sort(keys.begin(), keys.end()); + + for (const QString &key : keys) { + double span = (double(colors[key]) / total) * 360.0; + + if (angle >= acc && angle < acc + span) { + double percent = (100.0 * colors[key]) / total; + return QString("%1: %2 cards (%3%)").arg(key).arg(colors[key]).arg(QString::number(percent, 'f', 1)); + } + acc += span; + } + + return {}; +} + +QColor ColorPie::colorFromName(const QString &name) const +{ + static QMap map = { + {"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)}, + {"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)}, + }; + + if (map.contains(name)) { + return map[name]; + } + + QColor c(name); + if (!c.isValid()) { + c = Qt::gray; + } + + return c; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h new file mode 100644 index 000000000..a8fe784a3 --- /dev/null +++ b/cockatrice/src/interface/widgets/general/display/charts/pies/color_pie.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_COLOR_PIE_H +#define COCKATRICE_COLOR_PIE_H + +#ifndef COLOR_PIE_H +#define COLOR_PIE_H + +#include +#include +#include + +class ColorPie : public QWidget +{ + Q_OBJECT + +public: + explicit ColorPie(const QMap &_colors = {}, QWidget *parent = nullptr); + + void setColors(const QMap &_colors); + + QSize minimumSizeHint() const override; + +protected: + void paintEvent(QPaintEvent *) override; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *event) override; +#else + void enterEvent(QEvent *event) override; +#endif + void leaveEvent(QEvent *) override; + void mouseMoveEvent(QMouseEvent *event) override; + +private: + QMap colors; + bool isHovered = false; + const double minRatioThreshold = 0.01; // skip tiny slices + + QColor colorFromName(const QString &name) const; + QString tooltipForPoint(const QPoint &pt) const; +}; + +#endif // COLOR_PIE_H + +#endif // COCKATRICE_COLOR_PIE_H diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp index 2998a03bc..736b69ea2 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp @@ -3,7 +3,7 @@ #include "../../../../../card_picture_loader/card_picture_loader.h" #include "../../../../cards/card_info_picture_with_text_overlay_widget.h" #include "../../../../general/display/background_plate_widget.h" -#include "../../../../general/display/color_bar.h" +#include "../../../../general/display/charts/bars/color_bar.h" #include "archidekt_deck_preview_image_display_widget.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h index 0174016f7..43baddb4c 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_inclusion_display_widget.h @@ -7,7 +7,7 @@ #ifndef EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H #define EDHREC_API_RESPONSE_CARD_INCLUSION_DISPLAY_WIDGET_H -#include "../../../../../general/display/percent_bar_widget.h" +#include "../../../../../general/display/charts/bars/percent_bar_widget.h" #include "../../api_response/cards/edhrec_api_response_card_details.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h index 39d26a409..c2e1c018c 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_synergy_display_widget.h @@ -7,7 +7,7 @@ #ifndef EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H #define EDHREC_API_RESPONSE_CARD_SYNERGY_DISPLAY_WIDGET_H -#include "../../../../../general/display/percent_bar_widget.h" +#include "../../../../../general/display/charts/bars/percent_bar_widget.h" #include "../../api_response/cards/edhrec_api_response_card_details.h" #include 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 f3d573d27..82f96860d 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 @@ -85,7 +85,7 @@ void TabDeckEditorVisual::onDeckChanged() { AbstractTabDeckEditor::onDeckModified(); tabContainer->visualDeckView->constructZoneWidgetsFromDeckListModel(); - tabContainer->deckAnalytics->refreshDisplays(); + tabContainer->deckAnalytics->updateDisplays(); tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel()); } 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 98b6aff00..82aeb05a6 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 @@ -45,10 +45,13 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, connect(visualDatabaseDisplay, &VisualDatabaseDisplayWidget::cardClickedDatabaseDisplay, this, &TabDeckEditorVisualTabWidget::onCardClickedDatabaseDisplay); - deckAnalytics = new DeckAnalyticsWidget(this, deckModel); + statsAnalyzer = new DeckListStatisticsAnalyzer(this, deckModel); + statsAnalyzer->analyze(); + + deckAnalytics = new DeckAnalyticsWidget(this, statsAnalyzer); deckAnalytics->setObjectName("deckAnalytics"); - sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel); + sampleHandWidget = new VisualDeckEditorSampleHandWidget(this, deckModel, statsAnalyzer); this->addNewTab(visualDeckView, tr("Visual Deck View")); this->addNewTab(visualDatabaseDisplay, tr("Visual Database Display")); 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 59c577024..9468df425 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 @@ -78,7 +78,8 @@ public: /// Get the total number of tabs. [[nodiscard]] int getTabCount() const; - VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget. + VisualDeckEditorWidget *visualDeckView; ///< Visual deck editor widget. + DeckListStatisticsAnalyzer *statsAnalyzer; DeckAnalyticsWidget *deckAnalytics; ///< Deck analytics widget. VisualDatabaseDisplayWidget *visualDatabaseDisplay; ///< Database display widget. PrintingSelector *printingSelector; ///< Printing selector widget. diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 0badb76ff..24f521760 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -3,12 +3,16 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" #include "../cards/card_info_picture_widget.h" +#include "../deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h" +#include "../deck_analytics/deck_list_statistics_analyzer.h" #include #include -VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *_deckListModel) - : QWidget(parent), deckListModel(_deckListModel) +VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *parent, + DeckListModel *_deckListModel, + DeckListStatisticsAnalyzer *_statsAnalyzer) + : QWidget(parent), deckListModel(_deckListModel), statsAnalyzer(_statsAnalyzer) { layout = new QVBoxLayout(this); setLayout(layout); @@ -35,6 +39,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); + drawProbabilityWidget = new DrawProbabilityWidget(this, statsAnalyzer); + layout->addWidget(drawProbabilityWidget); + cardSizeWidget = new CardSizeWidget(this, flowWidget); layout->addWidget(cardSizeWidget); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h index 751e16a3c..c63c74a4d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.h @@ -8,6 +8,7 @@ #define VISUAL_DECK_EDITOR_SAMPLE_HAND_WIDGET_H #include "../cards/card_size_widget.h" +#include "../deck_analytics/deck_list_statistics_analyzer.h" #include "../general/layout_containers/flow_widget.h" #include @@ -15,11 +16,14 @@ #include #include +class DrawProbabilityWidget; class VisualDeckEditorSampleHandWidget : public QWidget { Q_OBJECT public: - VisualDeckEditorSampleHandWidget(QWidget *parent, DeckListModel *deckListModel); + VisualDeckEditorSampleHandWidget(QWidget *parent, + DeckListModel *deckListModel, + DeckListStatisticsAnalyzer *statsAnalyzer); QList getRandomCards(int amountToGet); public slots: @@ -29,12 +33,14 @@ public slots: private: DeckListModel *deckListModel; + DeckListStatisticsAnalyzer *statsAnalyzer; QVBoxLayout *layout; QWidget *resetAndHandSizeContainerWidget; QHBoxLayout *resetAndHandSizeLayout; QPushButton *resetButton; QSpinBox *handSizeSpinBox; FlowWidget *flowWidget; + DrawProbabilityWidget *drawProbabilityWidget; CardSizeWidget *cardSizeWidget; }; diff --git a/libcockatrice_utility/libcockatrice/utility/color.h b/libcockatrice_utility/libcockatrice/utility/color.h index 164c6ffd7..bf4759565 100644 --- a/libcockatrice_utility/libcockatrice/utility/color.h +++ b/libcockatrice_utility/libcockatrice/utility/color.h @@ -21,6 +21,43 @@ inline color convertQColorToColor(const QColor &c) result.set_b(c.blue()); return result; } + +namespace GameSpecificColors +{ +namespace MTG +{ +inline QColor colorHelper(const QString &name) +{ + static const QMap colorMap = { + {"W", QColor(245, 245, 220)}, + {"U", QColor(80, 140, 255)}, + {"B", QColor(60, 60, 60)}, + {"R", QColor(220, 60, 50)}, + {"G", QColor(70, 160, 70)}, + {"Creature", QColor(70, 130, 180)}, + {"Instant", QColor(138, 43, 226)}, + {"Sorcery", QColor(199, 21, 133)}, + {"Enchantment", QColor(218, 165, 32)}, + {"Artifact", QColor(169, 169, 169)}, + {"Planeswalker", QColor(210, 105, 30)}, + {"Land", QColor(110, 80, 50)}, + }; + + if (colorMap.contains(name)) + return colorMap[name]; + + if (name.length() == 1 && colorMap.contains(name.toUpper())) + return colorMap[name.toUpper()]; + + uint h = qHash(name); + int r = 100 + (h % 120); + int g = 100 + ((h >> 8) % 120); + int b = 100 + ((h >> 16) % 120); + + return QColor(r, g, b); +} +} // namespace MTG +} // namespace GameSpecificColors #endif inline color makeColor(int r, int g, int b) diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h index 855cc8b18..606947143 100644 --- a/libcockatrice_utility/libcockatrice/utility/qt_utils.h +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -15,6 +15,20 @@ template T *findParentOfType(const QObject *obj) } return nullptr; } + +static inline void clearLayoutRec(QLayout *l) +{ + if (!l) + return; + QLayoutItem *it; + while ((it = l->takeAt(0)) != nullptr) { + if (QWidget *w = it->widget()) + w->deleteLater(); + if (QLayout *sub = it->layout()) + clearLayoutRec(sub); + delete it; + } +} } // namespace QtUtils #endif // COCKATRICE_QT_UTILS_H From 987fe9c9e2c29c290458f9a3835b99d6c69e0f75 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:35:43 -0800 Subject: [PATCH 28/56] [DeckDockWidget] clean up grouping and format sync (#6467) * [DeckDockWidget] clean up grouping and format sync * refresh legalities in rebuildTree * extract applyActiveGroupCriteria * Fix build failure --- .../deck_editor_deck_dock_widget.cpp | 31 ++++++++++--------- .../deck_editor_deck_dock_widget.h | 2 +- .../models/deck_list/deck_list_model.cpp | 1 + 3 files changed, 19 insertions(+), 15 deletions(-) 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 62195974b..24268e28e 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 @@ -64,6 +64,8 @@ void DeckEditorDeckDockWidget::createDeckDock() connect(deckStateManager, &DeckStateManager::focusIndexChanged, this, &DeckEditorDeckDockWidget::setSelectedIndex); connect(deckStateManager, &DeckStateManager::deckReplaced, this, &DeckEditorDeckDockWidget::syncDisplayWidgetsToModel); + connect(deckStateManager, &DeckStateManager::deckReplaced, this, + &DeckEditorDeckDockWidget::applyActiveGroupCriteria); deckView = new QTreeView(); deckView->setObjectName("deckView"); @@ -174,11 +176,8 @@ void DeckEditorDeckDockWidget::createDeckDock() activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE); activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST); activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR); - connect(activeGroupCriteriaComboBox, QOverload::of(&QComboBox::currentIndexChanged), [this]() { - getModel()->setActiveGroupCriteria(static_cast( - activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); - getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - }); + connect(activeGroupCriteriaComboBox, qOverload(&QComboBox::currentIndexChanged), this, + &DeckEditorDeckDockWidget::applyActiveGroupCriteria); aIncrement = new QAction(QString(), this); aIncrement->setIcon(QPixmap("theme:icons/increment")); @@ -297,7 +296,6 @@ void DeckEditorDeckDockWidget::initializeFormats() QString format = deckStateManager->getMetadata().gameFormat; if (!format.isEmpty()) { - getModel()->setActiveFormat(format); formatComboBox->setCurrentIndex(formatComboBox->findData(format)); } else { // Ensure no selection is visible initially @@ -429,6 +427,13 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index) deckStateManager->setBannerCard(bannerCard); } +void DeckEditorDeckDockWidget::applyActiveGroupCriteria() +{ + getModel()->setActiveGroupCriteria( + static_cast(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt())); + getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); +} + void DeckEditorDeckDockWidget::updateShowBannerCardComboBox(const bool visible) { bannerCardLabel->setHidden(!visible); @@ -477,16 +482,14 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() updateBannerCardComboBox(); bannerCardComboBox->blockSignals(false); updateHash(); - sortDeckModelToDeckView(); - deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); -} - -void DeckEditorDeckDockWidget::sortDeckModelToDeckView() -{ - getModel()->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); - getModel()->setActiveFormat(deckStateManager->getMetadata().gameFormat); + formatComboBox->blockSignals(true); formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); + formatComboBox->blockSignals(false); + + deckTagsDisplayWidget->blockSignals(true); + deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); + deckTagsDisplayWidget->blockSignals(false); } /** diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 1a82b00d1..c505ac5fb 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -51,7 +51,6 @@ public slots: void selectNextCard(); void updateBannerCardComboBox(); void syncDisplayWidgetsToModel(); - void sortDeckModelToDeckView(); void actAddCard(const ExactCard &card, const QString &zoneName); void actIncrementSelection(); void actDecrementCard(const ExactCard &card, QString zoneName); @@ -100,6 +99,7 @@ private slots: void writeName(); void writeComments(); void writeBannerCard(int); + void applyActiveGroupCriteria(); void setSelectedIndex(const QModelIndex &newCardIndex); void updateHash(); void refreshShortcuts(); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index ece3bc2f8..ce1c79263 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -599,6 +599,7 @@ void DeckListModel::setDeckList(const QSharedPointer &_deck) deckList = _deck; } rebuildTree(); + refreshCardFormatLegalities(); emit deckReplaced(); } From e7a1f86cbb5dc252ebbfa1855be06b520d625c08 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:17:34 +0100 Subject: [PATCH 29/56] [View Zone Widget] Properly calculate titleBar close rect on Windows. (#6468) --- .../src/game/zones/view_zone_widget.cpp | 21 +++++++++++++------ cockatrice/src/game/zones/view_zone_widget.h | 1 - 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cockatrice/src/game/zones/view_zone_widget.cpp b/cockatrice/src/game/zones/view_zone_widget.cpp index e15a466c3..3ea4eb119 100644 --- a/cockatrice/src/game/zones/view_zone_widget.cpp +++ b/cockatrice/src/game/zones/view_zone_widget.cpp @@ -272,10 +272,11 @@ void ZoneViewWidget::startWindowDrag(QGraphicsSceneMouseEvent *event) QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const { const QRectF frameRectF = windowFrameRect(); - const QRect titleBarRect(frameRectF.toRect().x(), frameRectF.toRect().y(), frameRectF.toRect().width(), - static_cast(kTitleBarHeight)); // query the style for the close button position (handles macOS top-left placement) + // Title bar rect MUST be local (0,0-based) for QStyle + const QRect titleBarRect(0, 0, static_cast(frameRectF.width()), static_cast(kTitleBarHeight)); + if (styleWidget) { QStyleOptionTitleBar opt; opt.initFrom(styleWidget); @@ -283,19 +284,26 @@ QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const opt.text = windowTitle(); opt.icon = styleWidget->windowIcon(); opt.titleBarFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint; + opt.subControls = QStyle::SC_TitleBarCloseButton; opt.activeSubControls = QStyle::SC_TitleBarCloseButton; opt.titleBarState = styleWidget->isActiveWindow() ? Qt::WindowActive : Qt::WindowNoState; - if (styleWidget->isActiveWindow()) + + if (styleWidget->isActiveWindow()) { opt.state |= QStyle::State_Active; - const QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton, - styleWidget); + } + + QRect r = styleWidget->style()->subControlRect(QStyle::CC_TitleBar, &opt, QStyle::SC_TitleBarCloseButton, + styleWidget); + if (r.isValid() && !r.isEmpty()) { + // Translate from local-titlebar coords → frame coords + r.translate(frameRectF.topLeft().toPoint()); return QRectF(r); } } - // fallback: square at right end of titlebar (Windows/Linux style) + // Fallback: frame-relative top-right return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight); } @@ -349,6 +357,7 @@ bool ZoneViewWidget::windowFrameEvent(QEvent *event) close(); return true; } + startWindowDrag(me); me->accept(); return true; diff --git a/cockatrice/src/game/zones/view_zone_widget.h b/cockatrice/src/game/zones/view_zone_widget.h index 4ed8f74d8..1246192b8 100644 --- a/cockatrice/src/game/zones/view_zone_widget.h +++ b/cockatrice/src/game/zones/view_zone_widget.h @@ -54,7 +54,6 @@ private: ZoneViewZone *zone; QGraphicsWidget *zoneContainer; - QPushButton *closeButton; QScrollBar *scrollBar; ScrollableGraphicsProxyWidget *scrollBarProxy; From c1f93b37abafd4c4d4ef2dc6dac3931ff3af0fb4 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:49:09 +0100 Subject: [PATCH 30/56] [TabRoom] Add a setting to hide the new filter toolbar (#6469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [TabRoom] Add a setting to hide the new filter toolbar Took 56 minutes Took 4 seconds * Proper macro. Took 5 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/src/client/settings/cache_settings.cpp | 8 ++++++++ cockatrice/src/client/settings/cache_settings.h | 7 +++++++ cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp | 6 ++++++ cockatrice/src/interface/widgets/dialogs/dlg_settings.h | 1 + cockatrice/src/interface/widgets/server/game_selector.cpp | 6 ++++++ 5 files changed, 28 insertions(+) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index fde8e9b34..2ad1f1ece 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -288,6 +288,7 @@ SettingsCache::SettingsCache() focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool(); + showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); displayCardNames = settings->value("cards/displaycardnames", true).toBool(); roundCardCorners = settings->value("cards/roundcardcorners", true).toBool(); overrideAllCardArtWithPersonalPreference = @@ -715,6 +716,13 @@ void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts) settings->setValue("menu/showshortcuts", showShortcuts); } +void SettingsCache::setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar) +{ + showGameSelectorFilterToolbar = static_cast(_showGameSelectorFilterToolbar); + settings->setValue("menu/showgameselectorfiltertoolbar", showGameSelectorFilterToolbar); + emit showGameSelectorFilterToolbarChanged(showGameSelectorFilterToolbar); +} + void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames) { displayCardNames = static_cast(_displayCardNames); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index df68ce0c4..ca9a2a39b 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -145,6 +145,7 @@ signals: void homeTabBackgroundShuffleFrequencyChanged(); void picDownloadChanged(); void showStatusBarChanged(bool state); + void showGameSelectorFilterToolbarChanged(bool state); void displayCardNamesChanged(); void overrideAllCardArtWithPersonalPreferenceChanged(bool _overrideAllCardArtWithPersonalPreference); void bumpSetsWithCardsInDeckToTopChanged(); @@ -236,6 +237,7 @@ private: bool annotateTokens; QByteArray tabGameSplitterSizes; bool showShortcuts; + bool showGameSelectorFilterToolbar; bool displayCardNames; bool overrideAllCardArtWithPersonalPreference; bool bumpSetsWithCardsInDeckToTop; @@ -553,6 +555,10 @@ public: { return showShortcuts; } + [[nodiscard]] bool getShowGameSelectorFilterToolbar() const + { + return showGameSelectorFilterToolbar; + } [[nodiscard]] bool getDisplayCardNames() const { return displayCardNames; @@ -1017,6 +1023,7 @@ public slots: void setAnnotateTokens(QT_STATE_CHANGED_T _annotateTokens); void setTabGameSplitterSizes(const QByteArray &_tabGameSplitterSizes); void setShowShortcuts(QT_STATE_CHANGED_T _showShortcuts); + void setShowGameSelectorFilterToolbar(QT_STATE_CHANGED_T _showGameSelectorFilterToolbar); void setDisplayCardNames(QT_STATE_CHANGED_T _displayCardNames); void setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T _overrideAllCardArt); void setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T _bumpSetsWithCardsInDeckToTop); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 191cc5d0b..fe8840a41 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -462,8 +462,13 @@ AppearanceSettingsPage::AppearanceSettingsPage() showShortcutsCheckBox.setChecked(settings.getShowShortcuts()); connect(&showShortcutsCheckBox, &QCheckBox::QT_STATE_CHANGED, this, &AppearanceSettingsPage::showShortcutsChanged); + showGameSelectorFilterToolbarCheckBox.setChecked(settings.getShowGameSelectorFilterToolbar()); + connect(&showGameSelectorFilterToolbarCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings, + &SettingsCache::setShowGameSelectorFilterToolbar); + auto *menuGrid = new QGridLayout; menuGrid->addWidget(&showShortcutsCheckBox, 0, 0); + menuGrid->addWidget(&showGameSelectorFilterToolbarCheckBox, 1, 0); menuGroupBox = new QGroupBox; menuGroupBox->setLayout(menuGrid); @@ -727,6 +732,7 @@ void AppearanceSettingsPage::retranslateUi() 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")); cardsGroupBox->setTitle(tr("Card rendering")); displayCardNamesCheckBox.setText(tr("Display card names on cards having a picture")); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 10134f3a8..06ad3601c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -120,6 +120,7 @@ private: QLabel minPlayersForMultiColumnLayoutLabel; QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; + QCheckBox showGameSelectorFilterToolbarCheckBox; QCheckBox displayCardNamesCheckBox; QCheckBox autoRotateSidewaysLayoutCardsCheckBox; QCheckBox overrideAllCardArtWithPersonalPreferenceCheckBox; diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 48a12d5b3..f14cc6d82 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -73,6 +73,12 @@ GameSelector::GameSelector(AbstractClient *_client, if (showFilters && restoresettings) { quickFilterToolBar = new GameSelectorQuickFilterToolBar(this, tabSupervisor, gameListProxyModel, gameTypeMap); + quickFilterToolBar->setVisible(showFilters && restoresettings && + SettingsCache::instance().getShowGameSelectorFilterToolbar()); + + connect(&SettingsCache::instance(), &SettingsCache::showGameSelectorFilterToolbarChanged, this, [this] { + quickFilterToolBar->setVisible(SettingsCache::instance().getShowGameSelectorFilterToolbar()); + }); } else { quickFilterToolBar = nullptr; } From 93a4647b04ce2b3fe48dd931b5ac260f96433234 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:24:47 -0800 Subject: [PATCH 31/56] [DeckList] move SideboardPlan into separate file (#6474) --- libcockatrice_deck_list/CMakeLists.txt | 4 +- .../libcockatrice/deck_list/deck_list.cpp | 56 --------------- .../libcockatrice/deck_list/deck_list.h | 65 +---------------- .../deck_list/sideboard_plan.cpp | 59 +++++++++++++++ .../libcockatrice/deck_list/sideboard_plan.h | 71 +++++++++++++++++++ 5 files changed, 135 insertions(+), 120 deletions(-) create mode 100644 libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp create mode 100644 libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index a7dd01702..5ccdb5f66 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -9,7 +9,9 @@ set(HEADERS libcockatrice/deck_list/tree/inner_deck_list_node.h libcockatrice/deck_list/deck_list.h libcockatrice/deck_list/deck_list_history_manager.h + libcockatrice/deck_list/deck_list_node_tree.h libcockatrice/deck_list/deck_list_memento.h + libcockatrice/deck_list/sideboard_plan.h ) if(Qt6_FOUND) @@ -28,7 +30,7 @@ add_library( libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/deck_list_history_manager.cpp libcockatrice/deck_list/deck_list_node_tree.cpp - libcockatrice/deck_list/deck_list_node_tree.h + libcockatrice/deck_list/sideboard_plan.cpp ) add_dependencies(libcockatrice_deck_list libcockatrice_protocol) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index b453a57ef..14da1eb28 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -21,62 +21,6 @@ uint qHash(const QRegularExpression &key, uint seed) noexcept } #endif -SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) - : name(_name), moveList(_moveList) -{ -} - -void SideboardPlan::setMoveList(const QList &_moveList) -{ - moveList = _moveList; -} - -bool SideboardPlan::readElement(QXmlStreamReader *xml) -{ - while (!xml->atEnd()) { - xml->readNext(); - const QString childName = xml->name().toString(); - if (xml->isStartElement()) { - if (childName == "name") - name = xml->readElementText(); - else if (childName == "move_card_to_zone") { - MoveCard_ToZone m; - while (!xml->atEnd()) { - xml->readNext(); - const QString childName2 = xml->name().toString(); - if (xml->isStartElement()) { - if (childName2 == "card_name") - m.set_card_name(xml->readElementText().toStdString()); - else if (childName2 == "start_zone") - m.set_start_zone(xml->readElementText().toStdString()); - else if (childName2 == "target_zone") - m.set_target_zone(xml->readElementText().toStdString()); - } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { - moveList.append(m); - break; - } - } - } - } else if (xml->isEndElement() && (childName == "sideboard_plan")) - return true; - } - return false; -} - -void SideboardPlan::write(QXmlStreamWriter *xml) -{ - xml->writeStartElement("sideboard_plan"); - xml->writeTextElement("name", name); - for (auto &i : moveList) { - xml->writeStartElement("move_card_to_zone"); - xml->writeTextElement("card_name", QString::fromStdString(i.card_name())); - xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone())); - xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone())); - xml->writeEndElement(); - } - xml->writeEndElement(); -} - bool DeckList::Metadata::isEmpty() const { return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty(); diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index e8c57be67..ffbfebcf3 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -1,6 +1,6 @@ /** * @file deck_list.h - * @brief Defines the DeckList class and supporting types for managing a full + * @brief Defines the DeckList class, which manages a full * deck structure including cards, zones, sideboard plans, and * serialization to/from multiple formats. This is a logic class which * does not care about Qt or user facing views. @@ -12,12 +12,12 @@ #include "deck_list_memento.h" #include "deck_list_node_tree.h" +#include "sideboard_plan.h" #include "tree/inner_deck_list_node.h" #include #include #include -#include #include class AbstractDecklistNode; @@ -27,67 +27,6 @@ class QIODevice; class QTextStream; class InnerDecklistNode; -/** - * @class SideboardPlan - * @ingroup Decks - * @brief Represents a predefined sideboarding strategy for a deck. - * - * Sideboard plans store a named list of card movements that should be applied - * between the mainboard and sideboard for a specific matchup. Each movement - * is expressed using a `MoveCard_ToZone` protobuf message. - * - * ### Responsibilities: - * - Store the plan name and list of moves. - * - Support XML serialization/deserialization. - * - * ### Typical usage: - * A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"), - * each describing how to transform the main deck into its intended configuration. - */ -class SideboardPlan -{ -private: - QString name; ///< Human-readable name of this plan. - QList moveList; ///< List of move instructions for this plan. - -public: - /** - * @brief Construct a new SideboardPlan. - * @param _name The plan name. - * @param _moveList Initial list of card move instructions. - */ - explicit SideboardPlan(const QString &_name = QString(), - const QList &_moveList = QList()); - - /** - * @brief Read a SideboardPlan from an XML stream. - * @param xml XML reader positioned at the plan element. - * @return true if parsing succeeded. - */ - bool readElement(QXmlStreamReader *xml); - - /** - * @brief Write this SideboardPlan to XML. - * @param xml Stream to append the serialized element to. - */ - void write(QXmlStreamWriter *xml); - - /// @return The plan name. - [[nodiscard]] QString getName() const - { - return name; - } - - /// @return Const reference to the move list. - [[nodiscard]] const QList &getMoveList() const - { - return moveList; - } - - /// @brief Replace the move list with a new one. - void setMoveList(const QList &_moveList); -}; - /** * @class DeckList * @ingroup Decks diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp new file mode 100644 index 000000000..a78f8adb3 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp @@ -0,0 +1,59 @@ +#include "sideboard_plan.h" + +#include + +SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) + : name(_name), moveList(_moveList) +{ +} + +void SideboardPlan::setMoveList(const QList &_moveList) +{ + moveList = _moveList; +} + +bool SideboardPlan::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "name") + name = xml->readElementText(); + else if (childName == "move_card_to_zone") { + MoveCard_ToZone m; + while (!xml->atEnd()) { + xml->readNext(); + const QString childName2 = xml->name().toString(); + if (xml->isStartElement()) { + if (childName2 == "card_name") + m.set_card_name(xml->readElementText().toStdString()); + else if (childName2 == "start_zone") + m.set_start_zone(xml->readElementText().toStdString()); + else if (childName2 == "target_zone") + m.set_target_zone(xml->readElementText().toStdString()); + } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { + moveList.append(m); + break; + } + } + } + } else if (xml->isEndElement() && (childName == "sideboard_plan")) + return true; + } + return false; +} + +void SideboardPlan::write(QXmlStreamWriter *xml) +{ + xml->writeStartElement("sideboard_plan"); + xml->writeTextElement("name", name); + for (auto &i : moveList) { + xml->writeStartElement("move_card_to_zone"); + xml->writeTextElement("card_name", QString::fromStdString(i.card_name())); + xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone())); + xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone())); + xml->writeEndElement(); + } + xml->writeEndElement(); +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h new file mode 100644 index 000000000..2ca7845b9 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h @@ -0,0 +1,71 @@ +#ifndef COCKATRICE_SIDEBOARD_PLAN_H +#define COCKATRICE_SIDEBOARD_PLAN_H + +#include +#include + +class QXmlStreamWriter; +class QXmlStreamReader; + +/** + * @class SideboardPlan + * @ingroup Decks + * @brief Represents a predefined sideboarding strategy for a deck. + * + * Sideboard plans store a named list of card movements that should be applied + * between the mainboard and sideboard for a specific matchup. Each movement + * is expressed using a `MoveCard_ToZone` protobuf message. + * + * ### Responsibilities: + * - Store the plan name and list of moves. + * - Support XML serialization/deserialization. + * + * ### Typical usage: + * A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"), + * each describing how to transform the main deck into its intended configuration. + */ +class SideboardPlan +{ +private: + QString name; ///< Human-readable name of this plan. + QList moveList; ///< List of move instructions for this plan. + +public: + /** + * @brief Construct a new SideboardPlan. + * @param _name The plan name. + * @param _moveList Initial list of card move instructions. + */ + explicit SideboardPlan(const QString &_name = QString(), + const QList &_moveList = QList()); + + /** + * @brief Read a SideboardPlan from an XML stream. + * @param xml XML reader positioned at the plan element. + * @return true if parsing succeeded. + */ + bool readElement(QXmlStreamReader *xml); + + /** + * @brief Write this SideboardPlan to XML. + * @param xml Stream to append the serialized element to. + */ + void write(QXmlStreamWriter *xml); + + /// @return The plan name. + [[nodiscard]] QString getName() const + { + return name; + } + + /// @return Const reference to the move list. + [[nodiscard]] const QList &getMoveList() const + { + return moveList; + } + + /// @brief Replace the move list with a new one. + void setMoveList(const QList &_moveList); +}; + +#endif // COCKATRICE_SIDEBOARD_PLAN_H From 84e6907fa98d1e5572be2e56befd393ac93bcef3 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 2 Jan 2026 00:10:41 -0800 Subject: [PATCH 32/56] [DeckList] Store sideboardPlans by value to fix crash (#6475) --- .../libcockatrice/deck_list/deck_list.cpp | 42 +++++++------------ .../libcockatrice/deck_list/deck_list.h | 13 +++--- .../deck_list/sideboard_plan.cpp | 2 +- .../libcockatrice/deck_list/sideboard_plan.h | 5 +-- 4 files changed, 23 insertions(+), 39 deletions(-) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 14da1eb28..a294601fb 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -21,6 +21,8 @@ uint qHash(const QRegularExpression &key, uint seed) noexcept } #endif +static const QString CURRENT_SIDEBOARD_PLAN_KEY = ""; + bool DeckList::Metadata::isEmpty() const { return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty(); @@ -37,36 +39,23 @@ DeckList::DeckList(const QString &nativeString) DeckList::DeckList(const Metadata &metadata, const DecklistNodeTree &tree, - const QMap &sideboardPlans) + const QMap &sideboardPlans) : metadata(metadata), sideboardPlans(sideboardPlans), tree(tree) { } -DeckList::~DeckList() +QList DeckList::getCurrentSideboardPlan() const { - QMapIterator i(sideboardPlans); - while (i.hasNext()) - delete i.next().value(); -} + if (!sideboardPlans.contains(CURRENT_SIDEBOARD_PLAN_KEY)) { + return {}; + } -QList DeckList::getCurrentSideboardPlan() -{ - SideboardPlan *current = sideboardPlans.value(QString(), 0); - if (!current) - return QList(); - else - return current->getMoveList(); + return sideboardPlans.value(CURRENT_SIDEBOARD_PLAN_KEY).getMoveList(); } void DeckList::setCurrentSideboardPlan(const QList &plan) { - SideboardPlan *current = sideboardPlans.value(QString(), 0); - if (!current) { - current = new SideboardPlan; - sideboardPlans.insert(QString(), current); - } - - current->setMoveList(plan); + sideboardPlans[CURRENT_SIDEBOARD_PLAN_KEY].setMoveList(plan); } bool DeckList::readElement(QXmlStreamReader *xml) @@ -95,11 +84,9 @@ bool DeckList::readElement(QXmlStreamReader *xml) } else if (childName == "zone") { tree.readZoneElement(xml); } else if (childName == "sideboard_plan") { - SideboardPlan *newSideboardPlan = new SideboardPlan; - if (newSideboardPlan->readElement(xml)) { - sideboardPlans.insert(newSideboardPlan->getName(), newSideboardPlan); - } else { - delete newSideboardPlan; + SideboardPlan newSideboardPlan; + if (newSideboardPlan.readElement(xml)) { + sideboardPlans.insert(newSideboardPlan.getName(), newSideboardPlan); } } } else if (xml->isEndElement() && (childName == "cockatrice_deck")) { @@ -138,9 +125,8 @@ void DeckList::write(QXmlStreamWriter *xml) const tree.write(xml); // Write sideboard plans - QMapIterator i(sideboardPlans); - while (i.hasNext()) { - i.next().value()->write(xml); + for (auto &sideboardPlan : sideboardPlans.values()) { + sideboardPlan.write(xml); } xml->writeEndElement(); // Close "cockatrice_deck" diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index ffbfebcf3..d0ca55342 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -78,9 +78,9 @@ public: }; private: - Metadata metadata; ///< Deck metadata that is stored in the deck file - QMap sideboardPlans; ///< Named sideboard plans. - DecklistNodeTree tree; ///< The deck tree (zones + cards). + Metadata metadata; ///< Deck metadata that is stored in the deck file + QMap sideboardPlans; ///< Named sideboard plans. + DecklistNodeTree tree; ///< The deck tree (zones + cards). /** * @brief Cached deck hash, recalculated lazily. @@ -132,8 +132,7 @@ public: /// @brief Construct from components DeckList(const Metadata &metadata, const DecklistNodeTree &tree, - const QMap &sideboardPlans = {}); - virtual ~DeckList(); + const QMap &sideboardPlans = {}); /** * @brief Gets a pointer to the underlying node tree. @@ -186,9 +185,9 @@ public: /// @name Sideboard plans ///@{ - QList getCurrentSideboardPlan(); + QList getCurrentSideboardPlan() const; void setCurrentSideboardPlan(const QList &plan); - const QMap &getSideboardPlans() const + const QMap &getSideboardPlans() const { return sideboardPlans; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp index a78f8adb3..d991ec98e 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp @@ -44,7 +44,7 @@ bool SideboardPlan::readElement(QXmlStreamReader *xml) return false; } -void SideboardPlan::write(QXmlStreamWriter *xml) +void SideboardPlan::write(QXmlStreamWriter *xml) const { xml->writeStartElement("sideboard_plan"); xml->writeTextElement("name", name); diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h index 2ca7845b9..524217c2d 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h @@ -36,8 +36,7 @@ public: * @param _name The plan name. * @param _moveList Initial list of card move instructions. */ - explicit SideboardPlan(const QString &_name = QString(), - const QList &_moveList = QList()); + explicit SideboardPlan(const QString &_name = "", const QList &_moveList = {}); /** * @brief Read a SideboardPlan from an XML stream. @@ -50,7 +49,7 @@ public: * @brief Write this SideboardPlan to XML. * @param xml Stream to append the serialized element to. */ - void write(QXmlStreamWriter *xml); + void write(QXmlStreamWriter *xml) const; /// @return The plan name. [[nodiscard]] QString getName() const From bbd8671e6e9f12980d3cc5e79f477086133038e3 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:32:22 -0800 Subject: [PATCH 33/56] [DeckDockWidget] Fix VDE crash due to not mapping proxy index (#6479) --- .../deck_editor_deck_dock_widget.cpp | 45 +++++++++---------- .../deck_editor_deck_dock_widget.h | 2 +- 2 files changed, 23 insertions(+), 24 deletions(-) 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 24268e28e..014f121e4 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 @@ -578,18 +578,19 @@ void DeckEditorDeckDockWidget::expandAll() } /** - * Gets the index of all the currently selected card nodes in the decklist table. + * Gets the source index of all the currently selected card nodes in the decklist table. * The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them. * - * @return A model index list containing all selected card nodes + * @return A list containing the source indices of all selected card nodes */ -QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodes() const +QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodeSourceIndices() const { auto selectedRows = deckView->selectionModel()->selectedRows(); - const auto notLeafNode = [this](const QModelIndex &index) { - return getModel()->hasChildren(proxy->mapToSource(index)); - }; + const auto mapToSource = [this](const QModelIndex &index) { return proxy->mapToSource(index); }; + std::transform(selectedRows.begin(), selectedRows.end(), selectedRows.begin(), mapToSource); + + const auto notLeafNode = [this](const QModelIndex &sourceIndex) { return getModel()->hasChildren(sourceIndex); }; selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end()); std::reverse(selectedRows.begin(), selectedRows.end()); @@ -608,10 +609,10 @@ void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString & void DeckEditorDeckDockWidget::actIncrementSelection() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); - for (const auto &index : selectedRows) { - offsetCountAtIndex(index, true); + for (const auto &sourceIndex : selectedRows) { + offsetCountAtIndex(sourceIndex, true); } } @@ -630,7 +631,7 @@ void DeckEditorDeckDockWidget::actSwapCard(const ExactCard &card, const QString void DeckEditorDeckDockWidget::actSwapSelection() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted @@ -638,8 +639,8 @@ void DeckEditorDeckDockWidget::actSwapSelection() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - for (const auto ¤tIndex : selectedRows) { - deckStateManager->swapCardAtIndex(currentIndex); + for (const auto &sourceIndex : selectedRows) { + deckStateManager->swapCardAtIndex(sourceIndex); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -659,7 +660,7 @@ void DeckEditorDeckDockWidget::actDecrementCard(const ExactCard &card, QString z void DeckEditorDeckDockWidget::actDecrementSelection() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted @@ -667,8 +668,8 @@ void DeckEditorDeckDockWidget::actDecrementSelection() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - for (const auto &index : selectedRows) { - offsetCountAtIndex(index, false); + for (const auto &sourceIndex : selectedRows) { + offsetCountAtIndex(sourceIndex, false); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -676,7 +677,7 @@ void DeckEditorDeckDockWidget::actDecrementSelection() void DeckEditorDeckDockWidget::actRemoveCard() { - auto selectedRows = getSelectedCardNodes(); + auto selectedRows = getSelectedCardNodeSourceIndices(); // hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted // TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted @@ -684,8 +685,8 @@ void DeckEditorDeckDockWidget::actRemoveCard() deckView->setSelectionMode(QAbstractItemView::SingleSelection); } - for (const auto &row : selectedRows) { - deckStateManager->removeCardAtIndex(row); + for (const auto &sourceIndex : selectedRows) { + deckStateManager->removeCardAtIndex(sourceIndex); } deckView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -693,7 +694,7 @@ void DeckEditorDeckDockWidget::actRemoveCard() /** * @brief Increments or decrements the amount of the card node at the index by 1. - * @param idx The proxy index + * @param idx The source index * @param isIncrement If true, increments the count. If false, decrements the count */ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool isIncrement) @@ -702,12 +703,10 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i return; } - QModelIndex sourceIndex = proxy->mapToSource(idx); - if (isIncrement) { - deckStateManager->incrementCountAtIndex(sourceIndex); + deckStateManager->incrementCountAtIndex(idx); } else { - deckStateManager->decrementCountAtIndex(sourceIndex); + deckStateManager->decrementCountAtIndex(idx); } } diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index c505ac5fb..39b298550 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -90,7 +90,7 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; DeckListModel *getModel() const; - [[nodiscard]] QModelIndexList getSelectedCardNodes() const; + [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); private slots: From 2e6f1128bb95165804be707d76bb7bb28dd5718e Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 2 Jan 2026 14:38:25 +0100 Subject: [PATCH 34/56] Docs: Fix search help rendering and open external link in new tab (#6440) * Use HTML link to open webpage in new tab * Fix rendering for doxygen --- cockatrice/resources/help/deck_search.md | 4 +++- cockatrice/resources/help/search.md | 4 +++- doc/doxygen/extra-pages/index.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cockatrice/resources/help/deck_search.md b/cockatrice/resources/help/deck_search.md index c42e41c42..4e1c5c557 100644 --- a/cockatrice/resources/help/deck_search.md +++ b/cockatrice/resources/help/deck_search.md @@ -1,11 +1,13 @@ @page deck_search_syntax_help Deck Search Syntax Help ## Deck Search Syntax Help ------ + The search bar recognizes a set of special commands.
    In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive. +
    +
    Display Name (The deck name, or the filename if the deck name isn't set):
    [red deck wins](#red deck wins) (Any deck with a display name containing the words red, deck, and wins)
    ["red deck wins"](#%22red deck wins%22) (Any deck with a display name containing the exact phrase "red deck wins")
    diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md index 3b3f0cc93..6f25e4013 100644 --- a/cockatrice/resources/help/search.md +++ b/cockatrice/resources/help/search.md @@ -1,10 +1,12 @@ @page search_syntax_help Search Syntax Help ## Search Syntax Help ------ + The search bar recognizes a set of special commands similar to some other card databases.
    In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all searches are case insensitive. +
    +
    Name:
    [birds of paradise](#birds of paradise) (Any card name containing the words birds, of, and paradise)
    ["birds of paradise"](#%22birds of paradise%22) (Any card name containing the exact phrase "birds of paradise")
    diff --git a/doc/doxygen/extra-pages/index.md b/doc/doxygen/extra-pages/index.md index c370a6fc2..b5c2b61e5 100644 --- a/doc/doxygen/extra-pages/index.md +++ b/doc/doxygen/extra-pages/index.md @@ -7,4 +7,4 @@ This is the **main landing page** of the Cockatrice documentation. - Go to the @subpage user_reference page - Review the @subpage developer_reference -Or check out the [Cockatrice Webpage](https://cockatrice.github.io/). +Or check out the Cockatrice Webpage. From bcf3939feef3c9f01515842acefd557d06ede600 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:26:16 +0100 Subject: [PATCH 35/56] Translate cockatrice/cockatrice_en@source.ts in fr (#6480) --- cockatrice/translations/cockatrice_fr.ts | 466 +++++++++++------------ 1 file changed, 233 insertions(+), 233 deletions(-) diff --git a/cockatrice/translations/cockatrice_fr.ts b/cockatrice/translations/cockatrice_fr.ts index 5cd205a48..3bd264358 100644 --- a/cockatrice/translations/cockatrice_fr.ts +++ b/cockatrice/translations/cockatrice_fr.ts @@ -133,7 +133,7 @@ Vérifiez que le répertoire ne soit pas en lecture seule et réessayez. seconds - + secondes @@ -154,7 +154,7 @@ You will not be able to manage printing preferences on a per-deck basis, or see You will have to use the Set Manager, available through Card Database -> Manage Sets. Are you sure you would like to enable this feature? - + L'activation de cette fonction désactivera l'utilisation du sélecteur d'impression.Vous ne pourrez plus gérer les préférences d'impression pour chaque deck ni voir les impressions sélectionnées par d'autres joueurs.Vous devrez utiliser le Gestionnaire d'extensions, accessible via Base de données de cartes -> Gérer les extensions.Êtes-vous sûr de vouloir activer cette fonction ? @@ -165,12 +165,12 @@ You can now choose printings on a per-deck basis in the Deck Editor and configur You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). Are you sure you would like to disable this feature? - + Désactiver cette fonction activera le sélecteur d'impression.Vous pouvez désormais choisir les impressions pour chaque deck dans l'éditeur de deck et configurer l'impression ajoutée par défaut en l'épinglant dans le sélecteur d'impression.Vous pouvez également utiliser le gestionnaire de d'extension pour personnaliser l'ordre de tri des impressions dans le sélecteur d'impression (d'autres ordres de tri, comme l'ordre alphabétique ou la date de parution, sont disponibles).Êtes-vous sûr de vouloir désactiver cette fonction ? Confirm Change - + Confirmer le changement @@ -190,17 +190,17 @@ Are you sure you would like to disable this feature? Home tab background source: - + Source d'arrière-plan de l'onglet Accueil: Home tab background shuffle frequency: - + Fréquence de mélange de l'arrière-plan de l'onglet Accueil: Disabled - + Désactivé @@ -230,7 +230,7 @@ Are you sure you would like to disable this feature? Override all card art with personal set preference (Pre-ProviderID change behavior) - + Remplacer toutes les illustrations de cartes par les préférences personnelles (comportement avant modification du ProviderID) @@ -319,17 +319,17 @@ Are you sure you would like to disable this feature? Theme - + Thème Art crop of random card - + Recadrage d'image d'une carte aléatoire Art crop of background.cod deck file - + Recadrage d'image du fichier background.cod @@ -667,124 +667,124 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Re&veal to... - + Révéler à... &All players - + &Tous les joueurs View related cards - + Voir les cartes associées Token: - + Jeton : All tokens - + Tout les jetons &Select All - + &Sélectionner Tout S&elect Row - + Sél&ectionne une ligne S&elect Column - + Sél&ectionne une colonne &Play - + &Jouer &Hide - + &Cacher Play &Face Down - + Jouer &Face Cachée &Tap / Untap Turn sideways or back again - + &Engager / Dégager Toggle &normal untapping - + Activer dégagement &normal T&urn Over Turn face up/face down - + Reto&urner &Peek at card face - + &Regarder furtivement la carte face cachée &Clone - + &Cloner Attac&h to card... - + Attac&her à une carte... Unattac&h - + Détac&her &Draw arrow... - + &Tracer une flèche... &Set annotation... - + &Annoter... Ca&rd counters - + Ma&rqueurs sur la carte &Add counter (%1) - + &Ajouter un marqueur (%1) &Remove counter (%1) - + &Retirer un marqueur (%1) &Set counters (%1)... - + &Changer le nombre de marqueurs (%1)... @@ -801,133 +801,133 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa their hand nominative - + sa main %1's hand nominative - + main de %1 their library look at zone - + sa bibliothèque %1's library look at zone - + bibliothèque de %1 of their library top cards of zone, - + de sa bibliothèque of %1's library top cards of zone - + de la bibliothèque de %1 their library reveal zone - + sa bibliothèque %1's library reveal zone - + bibliothèque de %1 their library shuffle - + sa bibliothèque %1's library shuffle - + bibliothèque de %1 their library nominative - + sa bibliothèque %1's library nominative - + bibliothèque de %1 their graveyard nominative - + son cimetière %1's graveyard nominative - + le cimetière de %1 their exile nominative - + sa zone d'exil %1's exile nominative - + la zone d'exil de %1 their sideboard look at zone - + sa réserve %1's sideboard look at zone - + la réserve de %1 their sideboard nominative - + sa réserve %1's sideboard nominative - + la réserve de %1 their custom zone '%1' nominative - + sa zone personnalisée '%1' %1's custom zone '%2' nominative - + La zone personnalisée '%2' de %1 @@ -951,13 +951,13 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa C&ustom Zones - + Zones pe&rsonnalisés View custom zone '%1' - + Voir la zone personnalisée '%1' @@ -993,12 +993,12 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Show on EDHRec (Commander) - + Afficher sur EDHRec (Commandant) Show on EDHRec (Card) - + Afficher sur EDHRec (carte) @@ -1204,7 +1204,7 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Load deck from online service... - + Charger un deck depuis un service en ligne... @@ -1671,7 +1671,7 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Load from website... - + Charger depuis un site web... @@ -1983,7 +1983,7 @@ Voulez-vous convertir le deck au format .cod ? Open decklists in lobby - + Ouvrir les listes de deck dans le lobby @@ -2455,7 +2455,7 @@ Assurez-vous d'activer l'édition « Fausse édition contenant les je Hide games with forced open decklists - + Masquer les parties avec les listes de decks ouvertes forcées @@ -2743,7 +2743,7 @@ Assurez-vous d'activer l'édition « Fausse édition contenant les je Paste a link to a decklist site here to import it. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - + Collez ici un lien vers un site de liste de deck pour l'importer.(Archidekt, Deckstats, Moxfield et TappedOut sont supportés.) @@ -2752,28 +2752,28 @@ Assurez-vous d'activer l'édition « Fausse édition contenant les je Load Deck from Website - + Charger un deck depuis un site web No parser available for this deck provider. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - + Aucun analyseur disponible pour ce fournisseur de deck.(Archidekt, Deckstats, Moxfield et TappedOut sont supportés.) Network error: %1 - + Erreur réseau : %1 Received empty deck data. - + Données de deck vide reçues. Failed to parse deck data: %1 - + Échec de l'analyse des données du deck : %1 @@ -2784,7 +2784,7 @@ https://archidekt.com/decks/9999999 https://deckstats.net/decks/99999/9999999-your-deck-name/en https://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzz https://tappedout.net/mtg-decks/your-deck-name/ - + L'URL fournie n'est pas reconnue comme une URL de deck valide.Les URL de deck valides ressemblent à ceci :https://archidekt.com/decks/9999999https://deckstats.net/decks/99999/9999999-your-deck-name/enhttps://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzzhttps://tappedout.net/mtg-decks/your-deck-name/ @@ -3495,22 +3495,22 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. kicked by game host or moderator - + Éjecté par l'hôte de la partie ou un modérateur player left the game - + joueur a quitté la partie player disconnected from server - + joueur déconnecté du serveur reason unknown - + raison inconnue @@ -3676,7 +3676,7 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. open decklists - + Ouvrir les listes de deck @@ -3889,47 +3889,47 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. &Graveyard - + &Cimetière &View graveyard - + &Voir le cimetière &Move graveyard to... - + &Déplacer le cimetière vers... &Top of library - + &Dessus de la bibliothèque &Bottom of library - + &Dessous de la bibliothèque &All players - + &Tous les joueurs &Hand - + &Main &Exile - + &Exil Reveal random card to... - + Révéler une carte au hasard à... @@ -3937,63 +3937,63 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. &Hand - + &Main &View hand - + &Voir la main &Sort hand - + &Trier la main Take &mulligan - + Faire un &mulligan &Move hand to... - + &Déplacer la main vers… &Top of library - + &Dessus de la bibliothèque &Bottom of library - + &Dessous de la bibliothèque &Graveyard - + &Cimetière &Exile - + &Exil &Reveal hand to... - + &Révéler la main à... Reveal r&andom card to... - + Révéler une c&arte au hasard à... &All players - + &Tous les joueurs @@ -4001,47 +4001,47 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. Create New Deck - + Créer un Nouveau Deck Browse Decks - + Parcourir les Decks Browse Card Database - + Parcourir la Base de Carte Browse EDHRec - + Parcourir EDHRec View Replays - + Voir les replays Quit - + Quitter Connecting... - + Connection... Connect - + Se connecter Play - + Jouer @@ -4049,193 +4049,193 @@ Vous allez peut-être devoir télécharger manuellement la nouvelle version. &Library - + &Bibliothèque &View library - + &Voir la bibliothèque View &top cards of library... - + Voir les cartes du &dessus de la bibliothèque... View bottom cards of library... - + Voir les cartes du dessous de la bibliothèque... Reveal &library to... - + Révéler la &bibliothèque à... Lend library to... - + Prêter la bibliothèque à... Reveal &top cards to... - + Révéler les cartes du &dessus à... &Top of library... - + &Dessus de la bibliothèque... &Bottom of library... - + &Dessous de la bibliothèque... &Always reveal top card - + &Toujours révéler la carte du dessus &Always look at top card - + &Toujours voir la carte du dessus &Open deck in deck editor - + &Ouvrir le deck dans l'éditeur &Draw card - + &Piocher une carte D&raw cards... - + P&iocher plusieurs cartes... &Undo last draw - + &Annuler la dernière pioche Shuffle - + Mélanger &Play top card - + &Jouer la carte du dessus Play top card &face down - + Jouer la carte du dessus &face cachée Put top card on &bottom - + Placer la carte du dessus au-&dessous Move top card to grave&yard - + Mettre la carte du dessus dans le cime&tière Move top card to e&xile - + E&xiler la carte du dessus Move top cards to &graveyard... - + Mettre les cartes du dessus dans le &cimetière... Move top cards to &exile... - + &Exiler les cartes du dessus... Put top cards on stack &until... - + Placer les cartes du dessus sur la pile &jusqu'à... Shuffle top cards... - + Mélanger les cartes du dessus... &Draw bottom card - + &Piocher la carte du dessous D&raw bottom cards... - + P&iocher les cartes du dessous... &Play bottom card - + &Jouer la carte du dessous Play bottom card &face down - + Jouer la carte du dessous &face cachée Move bottom card to grave&yard - + Mettre la carte du dessous dans le cime&tière Move bottom card to e&xile - + E&xiler la carte du dessous Move bottom cards to &graveyard... - + Mettre les cartes du dessus dans le &cimetière... Move bottom cards to &exile... - + &Exiler les cartes du dessous... Put bottom card on &top - + Placer la carte du dessous au-&dessus Shuffle bottom cards... - + Mélanger les cartes du dessous... &All players - + &Tous les joueurs Reveal top cards of library - + Révéler les cartes du dessus de la bibliothèque Number of cards: (max. %1) - + Nombre de cartes : (max. %1) @@ -5418,7 +5418,7 @@ Cockactrice va maintenant recharger la base de données de cartes. You are flooding the game. Please wait a couple of seconds. - + Vous floodez la partie. Veuillez patienter quelques secondes. @@ -5804,37 +5804,37 @@ Cockactrice va maintenant recharger la base de données de cartes. Move to - + Déplacer vers &Top of library in random order - + &Dessus de la bibliothèque dans un ordre aléatoire X cards from the top of library... - + X cartes du dessus de la bibliothèque... &Bottom of library in random order - + &Dessous de la bibliothèque dans un ordre aléatoire &Hand - + &Main &Graveyard - + &Cimetière &Exile - + &Exil @@ -6043,7 +6043,7 @@ Cockactrice va maintenant recharger la base de données de cartes. View top cards of library - + Voir les cartes du dessus de la bibliothèque @@ -6058,119 +6058,119 @@ Cockactrice va maintenant recharger la base de données de cartes. Number of cards: (max. %1) - + Nombre de cartes : (max. %1) View bottom cards of library - + Voir les cartes du dessous de la bibliothèque Shuffle top cards of library - + Mélanger les cartes du dessus de la bibliothèque Shuffle bottom cards of library - + Mélanger les cartes du dessous de la bibliothèque Draw hand - + Piocher une main 0 and lower are in comparison to current hand size - + 0 et moins sont en comparaison avec la taille actuelle de la main Draw cards - + Piocher des cartes Move top cards to grave - + Mettre les cartes du dessus dans le cimetière Move top cards to exile - + Exiler les cartes du dessus Move bottom cards to grave - + Mettre les cartes du dessous dans le cimetière Move bottom cards to exile - + Exiler les cartes du dessous Draw bottom cards - + Piocher les cartes du dessous C&reate another %1 token - + C&réer un autre jeton %1 Create tokens - + Créer des jetons Number: - + Nombre : Place card X cards from top of library - + Placer les X cartes depuis le dessus de la bibliothèque Which position should this card be placed: - + Dans quelle position cette carte doit-elle être placée : (max. %1) - + (max. %1) Change power/toughness - + Changer la force/l'endurance Change stats to: - + Changer les stats pour : Set annotation - + Annoter Please enter the new annotation: - + Veuillez entrer la nouvelle annotation : Set counters - + Définir les marqueurs @@ -6178,17 +6178,17 @@ Cockactrice va maintenant recharger la base de données de cartes. Player "%1" - + Joueur "%1" &Counters - + &Marqueurs S&ay - + D&ire @@ -6282,57 +6282,57 @@ Cockactrice va maintenant recharger la base de données de cartes. Power / toughness - + Force / Endurance &Increase power - + &Augmenter la force &Decrease power - + &Diminuer la force I&ncrease toughness - + A&ugmenter l'endurance D&ecrease toughness - + D&iminuer l'endurance In&crease power and toughness - + Au&gmenter la force et l'endurance Dec&rease power and toughness - + Dim&inuer la force et l'endurance Increase power and decrease toughness - + Augmenter la force et diminuer l'endurance Decrease power and increase toughness - + Diminuer la force et augmenter l'endurance Set &power and toughness... - + Changer la &force et l'endurance... Reset p&ower and toughness - + Réinitialiser la f&orce et l'endurance @@ -6565,37 +6565,37 @@ Cockactrice va maintenant recharger la base de données de cartes. &Exile - + &Exil &View exile - + &Voir la zone d'exil &Move exile to... - + &Déplacer la zone d'exil vers... &Top of library - + &Dessus de la bibliothèque &Bottom of library - + &Dessous de la bibliothèque &Hand - + &Main &Graveyard - + &Cimetière @@ -6803,12 +6803,12 @@ Please check your shortcut settings! &Sideboard - + &Réserve &View sideboard - + &Voir la réserve @@ -7382,7 +7382,7 @@ Veuillez entrer un nom: EDHRec: - + EDHRec : @@ -7509,7 +7509,7 @@ Veuillez entrer un nom: Selected cards - + Cartes sélectionnées @@ -7588,7 +7588,7 @@ Veuillez entrer un nom: Home - + Accueil @@ -7841,13 +7841,13 @@ Plus vous entrez d'informations, meilleurs seront les résultats. Get replay share code - + Obtenir le code de partage du replay Look up replay by share code - + Regarder un replay depuis un code de partage @@ -7897,71 +7897,71 @@ Plus vous entrez d'informations, meilleurs seront les résultats. Failed to get code - + Echec d'obtention du code Either this server does not support replay sharing, or does not permit replay sharing for you. - + Soit ce serveur ne prend pas en charge le partage de replay, soit il ne vous autorise pas à partager des replays. Failed - + Échec Could not get replay code - + Impossible d'obtenir le code de replay Replay Share Code - + Code de Partage de Replay Others can use this code to add the replay to their list of remote replays: %1 - + D'autres peuvent utiliser ce code pour ajouter la replay à leur liste de replay distantes :%1 Copy to clipboard - + Copier vers le presse-papier Replay share code - + Code de partage de replay Replay code found - + Code de replay trouvé Replay was added, or you already had access to it. - + Le replay a été ajoutée, ou vous y aviez déjà accès. Replay code not found - + Code de replay non trouvé Failed to submit code - + Échec de l'envoi du code Unexpected error - + Erreur inattendue @@ -8078,7 +8078,7 @@ Plus vous entrez d'informations, meilleurs seront les résultats. Home - + Accueil @@ -8378,22 +8378,22 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Error - + Erreur This user does not exist. - + Cet utilisateur n'existe pas. You are being ignored by %1 and can't see their games. - + Vous avez été ignoré par %1 et ne pouvez pas voir ses parties. Could not get %1's games. - + Impossible d'obtenir les parties de %1. @@ -8833,32 +8833,32 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Increment all card counters - + Incrémenter tous les marqueurs de la carte &Untap all permanents - + &Dégager tous les permanents R&oll die... - + L&ancer un dé... &Create token... - + &Créer un jeton... C&reate another token - + C&réer un autre jeton Cr&eate predefined token - + C&réer un jeton prédéfini @@ -9019,7 +9019,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Loading database ... - + Chargement de la base de données... @@ -10000,7 +10000,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Increment all card counters - + Incrémenter tous les marqueurs de la carte @@ -10256,7 +10256,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Sort Hand - + Trier la main @@ -10449,7 +10449,7 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Home - + Accueil From 73cc0541f518ba68f77ec64b7dd10003000fea4c Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:03:11 +0100 Subject: [PATCH 36/56] [Game] Add shortcuts for same size and hand size - 1 mulligans (#6483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Took 21 minutes Took 3 seconds Co-authored-by: Lukas Brübach --- .../src/client/settings/shortcuts_settings.h | 6 +++ cockatrice/src/game/player/menu/hand_menu.cpp | 16 ++++++- cockatrice/src/game/player/menu/hand_menu.h | 2 + cockatrice/src/game/player/player_actions.cpp | 42 ++++++++++++++----- cockatrice/src/game/player/player_actions.h | 3 ++ 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 736f0c896..156ee82ea 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -660,6 +660,12 @@ private: {"Player/aMulligan", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan"), parseSequenceString("Ctrl+M"), ShortcutGroup::Drawing)}, + {"Player/aMulliganSame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Same hand size)"), + parseSequenceString("Ctrl+Shift+M"), + ShortcutGroup::Drawing)}, + {"Player/aMulliganMinusOne", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Mulligan (Hand size - 1)"), + parseSequenceString("Ctrl+Shift+Alt+M"), + ShortcutGroup::Drawing)}, {"Player/aDrawCard", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Draw a Card"), parseSequenceString("Ctrl+D"), ShortcutGroup::Drawing)}, diff --git a/cockatrice/src/game/player/menu/hand_menu.cpp b/cockatrice/src/game/player/menu/hand_menu.cpp index e193d2e4a..019ab925c 100644 --- a/cockatrice/src/game/player/menu/hand_menu.cpp +++ b/cockatrice/src/game/player/menu/hand_menu.cpp @@ -60,6 +60,16 @@ HandMenu::HandMenu(Player *_player, PlayerActions *actions, QWidget *parent) : T connect(aMulligan, &QAction::triggered, actions, &PlayerActions::actMulligan); addAction(aMulligan); + // Mulligan same size + aMulliganSame = new QAction(this); + connect(aMulliganSame, &QAction::triggered, actions, &PlayerActions::actMulliganSameSize); + addAction(aMulliganSame); + + // Mulligan -1 + aMulliganMinusOne = new QAction(this); + connect(aMulliganMinusOne, &QAction::triggered, actions, &PlayerActions::actMulliganMinusOne); + addAction(aMulliganMinusOne); + addSeparator(); mMoveHandMenu = addTearOffMenu(QString()); @@ -104,7 +114,9 @@ void HandMenu::retranslateUi() aSortHandByType->setText(tr("Type")); aSortHandByManaValue->setText(tr("Mana Value")); - aMulligan->setText(tr("Take &mulligan")); + aMulligan->setText(tr("Take &mulligan (Choose hand size)")); + aMulliganSame->setText(tr("Take mulligan (Same hand size)")); + aMulliganMinusOne->setText(tr("Take mulligan (Hand size - 1)")); mMoveHandMenu->setTitle(tr("&Move hand to...")); aMoveHandToTopLibrary->setText(tr("&Top of library")); @@ -128,6 +140,8 @@ void HandMenu::setShortcutsActive() aSortHandByType->setShortcuts(shortcuts.getShortcut("Player/aSortHandByType")); aSortHandByManaValue->setShortcuts(shortcuts.getShortcut("Player/aSortHandByManaValue")); aMulligan->setShortcuts(shortcuts.getShortcut("Player/aMulligan")); + aMulliganSame->setShortcuts(shortcuts.getShortcut("Player/aMulliganSame")); + aMulliganMinusOne->setShortcuts(shortcuts.getShortcut("Player/aMulliganMinusOne")); aRevealHandToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealHandToAll")); aRevealRandomHandCardToAll->setShortcuts(shortcuts.getShortcut("Player/aRevealRandomHandCardToAll")); } diff --git a/cockatrice/src/game/player/menu/hand_menu.h b/cockatrice/src/game/player/menu/hand_menu.h index 2c776b857..51e071a62 100644 --- a/cockatrice/src/game/player/menu/hand_menu.h +++ b/cockatrice/src/game/player/menu/hand_menu.h @@ -46,6 +46,8 @@ private: QAction *aViewHand = nullptr; QAction *aMulligan = nullptr; + QAction *aMulliganSame = nullptr; + QAction *aMulliganMinusOne = nullptr; QMenu *mSortHand = nullptr; QAction *aSortHandByName = nullptr; diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index 444eecb0d..ed77808b0 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -310,28 +310,48 @@ void PlayerActions::actMulligan() { int startSize = SettingsCache::instance().getStartingHandSize(); int handSize = player->getHandZone()->getCards().size(); - int deckSize = player->getDeckZone()->getCards().size() + handSize; // hand is shuffled back into the deck + 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); + if (!ok) { return; } - Command_Mulligan cmd; + if (number < 1) { - if (handSize == 0) { - return; - } - cmd.set_number(handSize + number); - } else { - cmd.set_number(number); + number = handSize + number; } + + doMulligan(number); + SettingsCache::instance().setStartingHandSize(number); +} + +void PlayerActions::actMulliganSameSize() +{ + int handSize = player->getHandZone()->getCards().size(); + doMulligan(handSize); +} + +void PlayerActions::actMulliganMinusOne() +{ + int handSize = player->getHandZone()->getCards().size(); + int targetSize = qMax(1, handSize - 1); + doMulligan(targetSize); +} + +void PlayerActions::doMulligan(int number) +{ + if (number < 1) { + return; + } + + Command_Mulligan cmd; + cmd.set_number(number); sendGameCommand(cmd); - if (startSize != number) { - SettingsCache::instance().setStartingHandSize(number); - } } void PlayerActions::actDrawCards() diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index e0f86e4fc..b294b5946 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -85,6 +85,9 @@ public slots: void actDrawCards(); void actUndoDraw(); void actMulligan(); + void actMulliganSameSize(); + void actMulliganMinusOne(); + void doMulligan(int number); void actPlay(); void actPlayFacedown(); From 84aefda486defc2d9b9313f8b2d0279e7d4270b2 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:55:27 -0800 Subject: [PATCH 37/56] [DeckListModel] add getCardNodes method (#6484) * [DeckListModel] add getCardNodes method * Update one usage --- .../draw_probability/draw_probability_widget.cpp | 2 +- .../libcockatrice/models/deck_list/deck_list_model.cpp | 10 ++++++++++ .../libcockatrice/models/deck_list/deck_list_model.h | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp index a8bec834f..92ebea630 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp @@ -168,7 +168,7 @@ void DrawProbabilityWidget::updateFilterOptions() QMap categoryCounts; int totalDeckCards = 0; - const auto nodes = analyzer->getModel()->getDeckList()->getCardNodes(); + const auto nodes = analyzer->getModel()->getCardNodes(); for (auto *node : nodes) { CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr(); if (!info) { diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index ce1c79263..23dbd7fec 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -637,6 +637,16 @@ QList DeckListModel::getCardsForZone(const QString &zoneName) const return cardNodesToExactCards(nodes); } +QList DeckListModel::getCardNodes() const +{ + return deckList->getCardNodes(); +} + +QList DeckListModel::getCardNodesForZone(const QString &zoneName) const +{ + return deckList->getCardNodes({zoneName}); +} + QList DeckListModel::getCardNames() const { auto nodes = deckList->getCardNodes(); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index e6f10c072..e7a79d13e 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -339,6 +339,12 @@ public: [[nodiscard]] QList getCards() const; [[nodiscard]] QList getCardsForZone(const QString &zoneName) const; + /** + * @brief Gets a list of all card nodes in the deck. + */ + [[nodiscard]] QList getCardNodes() const; + [[nodiscard]] QList getCardNodesForZone(const QString &zoneName) const; + /** * @brief Gets a deduplicated list of all card names that appear in the model */ From 4fbb9d968267e9545a5e5dc9cf3194897b8600cc Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:04:56 -0800 Subject: [PATCH 38/56] [PrintingSelector] optimize amount calculation (#6478) --- .../all_zones_card_amount_widget.cpp | 18 ++++++- .../all_zones_card_amount_widget.h | 3 ++ .../printing_selector/card_amount_widget.cpp | 42 ++++++---------- .../printing_selector/card_amount_widget.h | 4 +- .../printing_selector/printing_selector.cpp | 36 ++++++++++++++ .../printing_selector/printing_selector.h | 7 +++ .../printing_selector_card_display_widget.cpp | 8 +++- .../printing_selector_card_display_widget.h | 2 + .../printing_selector_card_overlay_widget.cpp | 48 +++++++++---------- .../printing_selector_card_overlay_widget.h | 4 ++ 10 files changed, 115 insertions(+), 57 deletions(-) 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 b1346e4fd..36bccbcc3 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 @@ -72,6 +72,12 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) repaint(); } +void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount) +{ + buttonBoxMainboard->setAmount(mainboardAmount); + buttonBoxSideboard->setAmount(sideboardAmount); +} + /** * @brief Gets the card count in the mainboard zone. * @@ -79,7 +85,7 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) */ int AllZonesCardAmountWidget::getMainboardAmount() { - return buttonBoxMainboard->countCardsInZone(DECK_ZONE_MAIN); + return buttonBoxMainboard->getAmount(); } /** @@ -89,7 +95,15 @@ int AllZonesCardAmountWidget::getMainboardAmount() */ int AllZonesCardAmountWidget::getSideboardAmount() { - return buttonBoxSideboard->countCardsInZone(DECK_ZONE_SIDE); + return buttonBoxSideboard->getAmount(); +} + +/** + * @brief Checks if the amount is at least one in either the mainboard or sideboard. + */ +bool AllZonesCardAmountWidget::isNonZero() +{ + return getMainboardAmount() > 0 || getSideboardAmount() > 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 5a03c5f4a..d158d257e 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,8 @@ public: const ExactCard &rootCard); int getMainboardAmount(); int getSideboardAmount(); + bool isNonZero(); + #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) void enterEvent(QEnterEvent *event) override; #else @@ -31,6 +33,7 @@ public: public slots: void adjustFontSize(int scalePercentage); + void setAmounts(int mainboardAmount, int sideboardAmount); private: QVBoxLayout *layout; 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 62c8e2d60..f8002eb89 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -45,20 +45,28 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, connect(decrementButton, &QPushButton::clicked, this, &CardAmountWidget::removePrintingSideboard); } - cardCountInZone = new QLabel(QString::number(countCardsInZone(zoneName)), this); + cardCountInZone = new QLabel(QString::number(amount), this); cardCountInZone->setAlignment(Qt::AlignCenter); layout->addWidget(decrementButton); layout->addWidget(cardCountInZone); layout->addWidget(incrementButton); - // React to model changes - connect(deckStateManager, &DeckStateManager::cardModified, this, &CardAmountWidget::updateCardCount); - // Connect slider for dynamic font size adjustment connect(cardSizeSlider, &QSlider::valueChanged, this, &CardAmountWidget::adjustFontSize); } +int CardAmountWidget::getAmount() +{ + return amount; +} + +void CardAmountWidget::setAmount(int _amount) +{ + amount = _amount; + updateCardCount(); +} + /** * @brief Handles the painting of the widget, drawing a semi-transparent background. * @@ -124,7 +132,7 @@ void CardAmountWidget::adjustFontSize(int scalePercentage) */ void CardAmountWidget::updateCardCount() { - cardCountInZone->setText("" + QString::number(countCardsInZone(zoneName)) + ""); + cardCountInZone->setText("" + QString::number(amount) + ""); layout->invalidate(); layout->activate(); } @@ -169,8 +177,8 @@ void CardAmountWidget::addPrinting(const QString &zone) QString foundProviderId = existing.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString(); if (foundProviderId.isEmpty()) { - int amount = existing.data(Qt::DisplayRole).toInt(); - extraCopies = amount - 1; // One less because we *always* add one + int existingAmount = existing.data(Qt::DisplayRole).toInt(); + extraCopies = existingAmount - 1; // One less because we *always* add one replacingProviderless = true; } } @@ -246,23 +254,3 @@ void CardAmountWidget::decrementCardHelper(const QString &zone) return model->offsetCountAtIndex(idx, -1); }); } - -/** - * @brief Counts the number of cards in a specific zone (mainboard or sideboard). - * - * @param deckZone The name of the zone (e.g., DECK_ZONE_MAIN or DECK_ZONE_SIDE). - * @return The number of cards in the zone. - */ -int CardAmountWidget::countCardsInZone(const QString &deckZone) -{ - QString uuid = rootCard.getPrinting().getUuid(); - - if (uuid.isEmpty()) { - return 0; // Cards without uuids/providerIds CANNOT match another card, they are undefined for us. - } - - QList cards = deckStateManager->getModel()->getCardsForZone(deckZone); - - return std::count_if(cards.cbegin(), cards.cend(), - [&uuid](const ExactCard &card) { return card.getPrinting().getUuid() == uuid; }); -} \ No newline at end of file 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 983416782..3051b1691 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -31,9 +31,10 @@ public: QSlider *cardSizeSlider, const ExactCard &rootCard, const QString &zoneName); - int countCardsInZone(const QString &deckZone); + int getAmount(); public slots: + void setAmount(int _amount); void updateCardCount(); void addPrinting(const QString &zone); @@ -52,6 +53,7 @@ private: QLabel *cardCountInZone; bool hovered; + int amount = 0; void decrementCardHelper(const QString &zoneName); diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index 95d6b2cdf..becfa16a3 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -78,6 +78,7 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck // Connect deck model data change signal to update display connect(deckStateManager, &DeckStateManager::uniqueCardsChanged, this, &PrintingSelector::printingsInDeckChanged); + connect(deckStateManager, &DeckStateManager::cardModified, this, &PrintingSelector::updateCardAmounts); retranslateUi(); } @@ -93,6 +94,36 @@ void PrintingSelector::printingsInDeckChanged() QTimer::singleShot(100, this, &PrintingSelector::updateDisplay); } +/** + * @return A map of uuid to amounts (main, side). + */ +static QMap> tallyUuidCounts(const DeckListModel *model, const QString &cardName) +{ + QMap> map; + + auto mainNodes = model->getCardNodesForZone(DECK_ZONE_MAIN); + for (auto &node : mainNodes) { + if (node->getName() == cardName) { + map[node->getCardProviderId()].first += node->getNumber(); + } + } + + auto sideNodes = model->getCardNodesForZone(DECK_ZONE_SIDE); + for (auto &node : sideNodes) { + if (node->getName() == cardName) { + map[node->getCardProviderId()].second += node->getNumber(); + } + } + + return map; +} + +void PrintingSelector::updateCardAmounts() +{ + auto map = tallyUuidCounts(deckStateManager->getModel(), selectedCard->getName()); + emit cardAmountsChanged(map); +} + /** * @brief Updates the display by clearing the layout and loading new sets for the current card. */ @@ -156,6 +187,8 @@ void PrintingSelector::getAllSetsForCurrentCard() } printingsToUse = sortToolBar->prependPinnedPrintings(printingsToUse, selectedCard->getName()); + auto uuidToAmounts = tallyUuidCounts(deckStateManager->getModel(), selectedCard->getName()); + // Defer widget creation currentIndex = 0; @@ -166,8 +199,11 @@ void PrintingSelector::getAllSetsForCurrentCard() cardSizeWidget->getSlider(), card); flowWidget->addWidget(cardDisplayWidget); cardDisplayWidget->clampSetNameToPicture(); + cardDisplayWidget->updateCardAmounts(uuidToAmounts); connect(cardDisplayWidget, &PrintingSelectorCardDisplayWidget::cardPreferenceChanged, this, &PrintingSelector::updateDisplay); + connect(this, &PrintingSelector::cardAmountsChanged, cardDisplayWidget, + &PrintingSelectorCardDisplayWidget::updateCardAmounts); } // Stop timer when done diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h index e1c07addf..e48eb2f2c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.h @@ -44,6 +44,7 @@ public slots: private slots: void printingsInDeckChanged(); + void updateCardAmounts(); signals: /** @@ -55,6 +56,12 @@ signals: */ void nextCardRequested(); + /** + * The amounts of the printings in the deck has changed + * @param uuidToAmounts Map of uuids to the amounts (maindeck, sideboard) in the deck + */ + void cardAmountsChanged(const QMap> &uuidToAmounts); + private: QVBoxLayout *layout; SettingsButtonWidget *displayOptionsWidget; 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 92cf2437c..b95c25cbd 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 @@ -27,7 +27,7 @@ PrintingSelectorCardDisplayWidget::PrintingSelectorCardDisplayWidget(QWidget *pa DeckStateManager *deckStateManager, QSlider *cardSizeSlider, const ExactCard &rootCard) - : QWidget(parent) + : QWidget(parent), rootCard(rootCard) { layout = new QVBoxLayout(this); setLayout(layout); @@ -64,3 +64,9 @@ void PrintingSelectorCardDisplayWidget::clampSetNameToPicture() } update(); } + +void PrintingSelectorCardDisplayWidget::updateCardAmounts(const QMap> &uuidToAmounts) +{ + auto [main, side] = uuidToAmounts.value(rootCard.getPrinting().getUuid()); + overlayWidget->updateCardAmounts(main, side); +} 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 2637b0e57..64bb72e22 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,11 +27,13 @@ public: public slots: void clampSetNameToPicture(); + void updateCardAmounts(const QMap> &uuidToAmounts); signals: void cardPreferenceChanged(); private: + ExactCard rootCard; QVBoxLayout *layout; SetNameAndCollectorsNumberDisplayWidget *setNameAndCollectorsNumberDisplayWidget; PrintingSelectorCardOverlayWidget *overlayWidget; 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 ac36f2cf4..4298b6dc3 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 @@ -59,12 +59,6 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa allZonesCardAmountWidget = new AllZonesCardAmountWidget(this, deckStateManager, cardSizeSlider, _rootCard); allZonesCardAmountWidget->raise(); // Ensure it's on top of the picture - // Set initial visibility based on amounts - if (allZonesCardAmountWidget->getMainboardAmount() > 0 || allZonesCardAmountWidget->getSideboardAmount() > 0) { - allZonesCardAmountWidget->setVisible(true); - } else { - allZonesCardAmountWidget->setVisible(false); - } // Attempt to cast the parent to PrintingSelectorCardDisplayWidget if (const auto *parentWidget = qobject_cast(parent)) { @@ -113,8 +107,7 @@ void PrintingSelectorCardOverlayWidget::resizeEvent(QResizeEvent *event) /** * @brief Handles the mouse enter event when the cursor enters the overlay widget area. * - * When the cursor enters the widget, the card information is updated, and the card amount widget - * is displayed if the amounts are zero for both the mainboard and sideboard. + * When the cursor enters the widget, the card amount widget becomes visible regardless of whether the amounts are zero. * * @param event The event triggered when the mouse enters the widget. */ @@ -126,16 +119,27 @@ void PrintingSelectorCardOverlayWidget::enterEvent(QEvent *event) { QWidget::enterEvent(event); deckEditor->updateCard(rootCard); - - // Check if either mainboard or sideboard amount is greater than 0 - if (allZonesCardAmountWidget->getMainboardAmount() > 0 || allZonesCardAmountWidget->getSideboardAmount() > 0) { - // Don't change visibility if amounts are greater than 0 - return; - } - - // Show the widget if amounts are 0 - allZonesCardAmountWidget->setVisible(true); + updateVisibility(); } + +void PrintingSelectorCardOverlayWidget::updateCardAmounts(int mainboardAmount, int sideboardAmount) +{ + allZonesCardAmountWidget->setAmounts(mainboardAmount, sideboardAmount); + updateVisibility(); +} + +/** + * @brief Sets the visibility of the widgets depending on the amounts and whether the mouse is hovering over. + */ +void PrintingSelectorCardOverlayWidget::updateVisibility() +{ + if (allZonesCardAmountWidget->isNonZero() || underMouse()) { + allZonesCardAmountWidget->setVisible(true); + } else { + allZonesCardAmountWidget->setVisible(false); + } +} + /** * @brief Updates the pin badge visibility and position based on the card's pinned state. * @@ -182,15 +186,7 @@ void PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility() void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event) { QWidget::leaveEvent(event); - - // Check if either mainboard or sideboard amount is greater than 0 - if (allZonesCardAmountWidget->getMainboardAmount() > 0 || allZonesCardAmountWidget->getSideboardAmount() > 0) { - // Don't hide the widget if amounts are greater than 0 - return; - } - - // Hide the widget if amounts are 0 - allZonesCardAmountWidget->setVisible(false); + updateVisibility(); } /** 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 8550612bd..ae2307c45 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 @@ -38,7 +38,11 @@ protected: signals: void cardPreferenceChanged(); +public slots: + void updateCardAmounts(int mainboardAmount, int sideboardAmount); + private slots: + void updateVisibility(); void updatePinBadgeVisibility(); private: From b88a98b09a9d1d34081cfd73e1ca4c641568a21a Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:58:59 +0100 Subject: [PATCH 39/56] Translate cockatrice/cockatrice_en@source.ts in fr (#6488) 100% translated source file: 'cockatrice/cockatrice_en@source.ts' on 'fr'. Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- cockatrice/translations/cockatrice_fr.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cockatrice/translations/cockatrice_fr.ts b/cockatrice/translations/cockatrice_fr.ts index 3bd264358..76a42dcd3 100644 --- a/cockatrice/translations/cockatrice_fr.ts +++ b/cockatrice/translations/cockatrice_fr.ts @@ -154,7 +154,13 @@ You will not be able to manage printing preferences on a per-deck basis, or see You will have to use the Set Manager, available through Card Database -> Manage Sets. Are you sure you would like to enable this feature? - L'activation de cette fonction désactivera l'utilisation du sélecteur d'impression.Vous ne pourrez plus gérer les préférences d'impression pour chaque deck ni voir les impressions sélectionnées par d'autres joueurs.Vous devrez utiliser le Gestionnaire d'extensions, accessible via Base de données de cartes -> Gérer les extensions.Êtes-vous sûr de vouloir activer cette fonction ? + L'activation de cette fonction désactivera l'utilisation du sélecteur d'impression. + +Vous ne pourrez plus gérer les préférences d'impression pour chaque deck ni voir les impressions sélectionnées par d'autres joueurs. + +Vous devrez utiliser le Gestionnaire d'extensions, accessible via Base de données de cartes -> Gérer les extensions. + +Êtes-vous sûr de vouloir activer cette fonction ? @@ -165,7 +171,13 @@ You can now choose printings on a per-deck basis in the Deck Editor and configur You can also use the Set Manager to adjust custom sort order for printings in the Printing Selector (other sort orders like alphabetical or release date are available). Are you sure you would like to disable this feature? - Désactiver cette fonction activera le sélecteur d'impression.Vous pouvez désormais choisir les impressions pour chaque deck dans l'éditeur de deck et configurer l'impression ajoutée par défaut en l'épinglant dans le sélecteur d'impression.Vous pouvez également utiliser le gestionnaire de d'extension pour personnaliser l'ordre de tri des impressions dans le sélecteur d'impression (d'autres ordres de tri, comme l'ordre alphabétique ou la date de parution, sont disponibles).Êtes-vous sûr de vouloir désactiver cette fonction ? + Désactiver cette fonction activera le sélecteur d'impression. + +Vous pouvez désormais choisir les impressions pour chaque deck dans l'éditeur de deck et configurer l'impression ajoutée par défaut en l'épinglant dans le sélecteur d'impression. + +Vous pouvez également utiliser le gestionnaire de d'extension pour personnaliser l'ordre de tri des impressions dans le sélecteur d'impression (d'autres ordres de tri, comme l'ordre alphabétique ou la date de parution, sont disponibles). + +Êtes-vous sûr de vouloir désactiver cette fonction ? @@ -2784,7 +2796,13 @@ https://archidekt.com/decks/9999999 https://deckstats.net/decks/99999/9999999-your-deck-name/en https://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzz https://tappedout.net/mtg-decks/your-deck-name/ - L'URL fournie n'est pas reconnue comme une URL de deck valide.Les URL de deck valides ressemblent à ceci :https://archidekt.com/decks/9999999https://deckstats.net/decks/99999/9999999-your-deck-name/enhttps://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzzhttps://tappedout.net/mtg-decks/your-deck-name/ + L'URL fournie n'est pas reconnue comme une URL de deck valide. +Les URL de deck valides ressemblent à ceci: + +https://archidekt.com/decks/9999999 +https://deckstats.net/decks/99999/9999999-your-deck-name/en +https://moxfield.com/decks/XYZxx-XYZ99Yyy-xyzXzzz +https://tappedout.net/mtg-decks/your-deck-name/ From 72a85b58cf1c3d188aacb92b295d57a4b45a5524 Mon Sep 17 00:00:00 2001 From: Bruno Alexandre Rosa <1791393+brunoalr@users.noreply.github.com> Date: Sun, 4 Jan 2026 01:00:05 +0100 Subject: [PATCH 40/56] ci: make fat qt libs thin (#6281) * ci: strip fat qt binaries * parallelize * cache thin qt * print libs * change qt install dir in the action * move qt install logic to separate job * lookup only * debug: show contents of QTDIR * enableCrossOsArchive also when saving * check one dir up * change install dir * keep debugging * try deleting cache * force delete cache * pass gh_token * pass missing params * use api * change cache key, disable cross os archive * move job directly to steps * add comments * set cache param directly * address comments * fixup * Update .ci/thin_macos_qtlib.sh * resolve qt version * move resolution to separate script * use single line for run: * improve error handling in new scripts --------- Co-authored-by: ebbit1q --- .ci/compile.sh | 12 +++++++ .ci/resolve_latest_aqt_qt_version.sh | 40 +++++++++++++++++++++ .ci/thin_macos_qtlib.sh | 25 +++++++++++++ .github/workflows/desktop-build.yml | 54 +++++++++++++++++++++++----- 4 files changed, 123 insertions(+), 8 deletions(-) create mode 100755 .ci/resolve_latest_aqt_qt_version.sh create mode 100755 .ci/thin_macos_qtlib.sh diff --git a/.ci/compile.sh b/.ci/compile.sh index 0af79868b..424527f96 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -156,6 +156,18 @@ function ccachestatsverbose() { # Compile if [[ $RUNNER_OS == macOS ]]; then + # QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action, + # which sets a few environment variables + if QTDIR=$(find "$GITHUB_WORKSPACE/Qt" -depth -maxdepth 2 -name macos -type d -print -quit); then + echo "found QTDIR at $QTDIR" + else + echo "could not find QTDIR!" + exit 2 + fi + # the qtdir is located at Qt/[qtversion]/macos + # we use find to get the first subfolder with the name "macos" + # this works independent of the qt version as there should be only one version installed on the runner at a time + export QTDIR if [[ $TARGET_MACOS_VERSION ]]; then # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version diff --git a/.ci/resolve_latest_aqt_qt_version.sh b/.ci/resolve_latest_aqt_qt_version.sh new file mode 100755 index 000000000..154c15321 --- /dev/null +++ b/.ci/resolve_latest_aqt_qt_version.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# This script is used to resolve the latest patch version of Qt using aqtinstall. +# It interprets wildcards to get the latest patch version. E.g. "6.6.*" -> "6.6.3". + +# This script is meant to be used by the ci enironment. +# It uses the runner's GITHUB_OUTPUT env variable. + +# Usage example: .ci/resolve_latest_aqt_qt_version.sh "6.6.*" + +qt_spec=$1 +if [[ ! $qt_spec ]]; then + echo "usage: $0 [version]" + exit 2 +fi + +# If version is already specific (no wildcard), use it as-is +if [[ $qt_spec != *"*" ]]; then + echo "version $qt_spec is already resolved" + echo "version=$qt_spec" >> "$GITHUB_OUTPUT" + exit 0 +fi + +if ! hash aqt; then + echo "aqt could not be found, has aqtinstall been installed?" + exit 2 +fi + +# Resolve latest patch +if ! qt_resolved=$(aqt list-qt mac desktop --spec "$qt_spec" --latest-version); then + exit 1 +fi + +echo "resolved $qt_spec to $qt_resolved" +if [[ ! $qt_resolved ]]; then + echo "Error: Could not resolve Qt version for $qt_spec" + exit 1 +fi + +echo "version=$qt_resolved" >> "$GITHUB_OUTPUT" diff --git a/.ci/thin_macos_qtlib.sh b/.ci/thin_macos_qtlib.sh new file mode 100755 index 000000000..6c3cc6720 --- /dev/null +++ b/.ci/thin_macos_qtlib.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# The macos binaries from aqt are fat (universal), so we thin them to the target architecture to reduce the size of +# the packages and caches using lipo. + +# This script is meant to be used by the ci enironment on macos runners only. +# It uses the runner's GITHUB_WORKSPACE env variable. +arch=$(uname -m) +nproc=$(sysctl -n hw.ncpu) + +function thin() { + local libfile=$1 + if [[ $(file -b --mime-type "$libfile") == application/x-mach-binary* ]]; then + echo "Processing $libfile" + lipo "$libfile" -thin "$arch" -output "$libfile" + fi + return 0 +} +export -f thin # export to allow use in xargs +export arch +set -eo pipefail + +echo "::group::Thinning Qt libraries to $arch using $nproc cores" +find "$GITHUB_WORKSPACE/Qt" -type f -print0 | xargs -0 -n1 -P"$nproc" -I{} bash -c "thin '{}'" +echo "::endgroup::" diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 9f4ee4984..5b954d021 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -262,7 +262,6 @@ jobs: qt_version: 6.6.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cache_qt: false # qt caches take too much space for macOS (1.1Gi) cmake_generator: Ninja use_ccache: 1 @@ -278,7 +277,6 @@ jobs: qt_version: 6.6.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cache_qt: false cmake_generator: Ninja use_ccache: 1 @@ -294,7 +292,6 @@ jobs: qt_version: 6.6.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cache_qt: false cmake_generator: Ninja use_ccache: 1 @@ -307,7 +304,6 @@ jobs: qt_version: 6.6.* qt_arch: clang_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cache_qt: false cmake_generator: Ninja use_ccache: 1 @@ -320,7 +316,6 @@ jobs: artifact_name: Windows7-installer qt_version: 5.15.* qt_arch: win64_msvc2019_64 - cache_qt: true cmake_generator: "Visual Studio 17 2022" cmake_generator_platform: x64 @@ -334,7 +329,6 @@ jobs: qt_version: 6.6.* qt_arch: win64_msvc2019_64 qt_modules: qtimageformats qtmultimedia qtwebsockets - cache_qt: true cmake_generator: "Visual Studio 17 2022" cmake_generator_platform: x64 @@ -375,13 +369,57 @@ jobs: key: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}-${{env.BRANCH_NAME}} restore-keys: ccache-${{matrix.runner}}-${{matrix.soc}}-${{matrix.type}}- - - name: Install Qt ${{matrix.qt_version}} + - name: Install aqtinstall + if: matrix.os == 'macOS' + run: pipx install aqtinstall + + # Checking if there's a newer, uncached version of Qt available to install via aqtinstall + - name: Resolve latest Qt patch version + if: matrix.os == 'macOS' + id: resolve_qt_version + shell: bash + # Ouputs the version of Qt to install via aqtinstall + 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) + if: matrix.os == 'macOS' + id: restore_qt + uses: actions/cache/restore@v4 + with: + path: ${{ github.workspace }}/Qt + key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + + # 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) + if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' + uses: jurplel/install-qt-action@v4 + with: + cache: false + version: ${{ steps.resolve_qt_version.outputs.version }} + arch: ${{matrix.qt_arch}} + modules: ${{matrix.qt_modules}} + dir: ${{github.workspace}} + + - name: Thin Qt libraries (${{ matrix.soc }} macOS) + 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) + if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/Qt + key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} + + - name: Install Qt ${{matrix.qt_version}} (Windows) + if: matrix.os == 'Windows' uses: jurplel/install-qt-action@v4 with: version: ${{matrix.qt_version}} arch: ${{matrix.qt_arch}} modules: ${{matrix.qt_modules}} - cache: ${{matrix.cache_qt}} + cache: true - name: Setup vcpkg cache id: vcpkg-cache From f16c552d970649e47ace5572f72e75ed3bc05c69 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sat, 3 Jan 2026 16:08:39 -0800 Subject: [PATCH 41/56] [PrintingSelector] Don't refresh display if "bump cards to top" is off (#6486) --- .../widgets/printing_selector/printing_selector.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index becfa16a3..94deaf135 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -90,8 +90,10 @@ void PrintingSelector::retranslateUi() void PrintingSelector::printingsInDeckChanged() { - // Delay the update to avoid race conditions - QTimer::singleShot(100, this, &PrintingSelector::updateDisplay); + if (SettingsCache::instance().getBumpSetsWithCardsInDeckToTop()) { + // Delay the update to avoid race conditions + QTimer::singleShot(100, this, &PrintingSelector::updateDisplay); + } } /** From 746f2af044cd495380f34332112539ce0d45509d Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sat, 3 Jan 2026 19:19:04 -0800 Subject: [PATCH 42/56] [DeckListModel] optimize by iterating over cardNodes instead of ExactCards (#6485) * [DeckListModel] optimize by iterating over cardNodes instead of ExactCards * fix build failure * another optimization * fix build failure --- .../cards/deck_card_zone_display_widget.cpp | 10 ++- .../draw_probability_widget.cpp | 2 +- .../deck_list_statistics_analyzer.cpp | 73 +++++++++++-------- .../visual_deck_editor_sample_hand_widget.cpp | 19 ++++- .../models/deck_list/deck_list_model.cpp | 29 -------- .../models/deck_list/deck_list_model.h | 10 --- 6 files changed, 67 insertions(+), 76 deletions(-) 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 4742467b1..7d45d9cc9 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 @@ -2,6 +2,7 @@ #include "card_group_display_widgets/flat_card_group_display_widget.h" #include "card_group_display_widgets/overlapped_card_group_display_widget.h" +#include "libcockatrice/card/database/card_database_manager.h" #include #include @@ -230,10 +231,13 @@ QList DeckCardZoneDisplayWidget::getGroupCriteriaValueList() { QList groupCriteriaValues; - QList cardsInZone = deckListModel->getCardsForZone(zoneName); + QList nodes = deckListModel->getCardNodesForZone(zoneName); - for (const ExactCard &cardInZone : cardsInZone) { - groupCriteriaValues.append(cardInZone.getInfo().getProperty(activeGroupCriteria)); + for (auto node : nodes) { + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName()); + if (info) { + groupCriteriaValues.append(info->getProperty(activeGroupCriteria)); + } } groupCriteriaValues.removeDuplicates(); diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp index 92ebea630..146ab6f1c 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp @@ -170,7 +170,7 @@ void DrawProbabilityWidget::updateFilterOptions() const auto nodes = analyzer->getModel()->getCardNodes(); for (auto *node : nodes) { - CardInfoPtr info = CardDatabaseManager::query()->getCard({node->getName()}).getCardPtr(); + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName()); if (!info) { continue; } diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp index 7d0259c72..a1dc1ab55 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp @@ -19,82 +19,91 @@ void DeckListStatisticsAnalyzer::analyze() { clearData(); - QList cards = model->getCards(); + QList nodes = model->getCardNodes(); - for (auto card : cards) { - auto info = card.getInfo(); - const int cmc = info.getCmc().toInt(); + for (auto node : nodes) { + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(node->getName()); + if (!info) { + continue; + } + + const int amount = node->getNumber(); + QStringList copiesOfName; + for (int i = 0; i < amount; i++) { + copiesOfName.append(node->getName()); + } // Convert once - QStringList types = info.getMainCardType().split(' '); - QStringList subtypes = info.getCardType().split('-').last().split(" "); - QString colors = info.getColors(); - int power = info.getPowTough().split("/").first().toInt(); - int toughness = info.getPowTough().split("/").last().toInt(); + const int cmc = info->getCmc().toInt(); + QStringList types = info->getMainCardType().split(' '); + QStringList subtypes = info->getCardType().split('-').last().split(" "); + QString colors = info->getColors(); + int power = info->getPowTough().split("/").first().toInt(); + int toughness = info->getPowTough().split("/").last().toInt(); // For each copy of card // ---------------- Mana Curve ---------------- if (config.computeManaCurve) { - manaCurveMap[cmc]++; + manaCurveMap[cmc] += amount; } // per-type curve for (auto &t : types) { - manaCurveByType[t][cmc]++; - manaCurveCardsByType[t][cmc].append(info.getName()); + manaCurveByType[t][cmc] += amount; + manaCurveCardsByType[t][cmc] << copiesOfName; } // Per-subtype curve for (auto &st : subtypes) { - manaCurveBySubtype[st][cmc]++; - manaCurveCardsBySubtype[st][cmc].append(info.getName()); + manaCurveBySubtype[st][cmc] += amount; + manaCurveCardsBySubtype[st][cmc] << copiesOfName; } // per-color curve for (auto &c : colors) { - manaCurveByColor[c][cmc]++; - manaCurveCardsByColor[c][cmc].append(info.getName()); + manaCurveByColor[c][cmc] += amount; + manaCurveCardsByColor[c][cmc] << copiesOfName; } // Power/toughness - manaCurveByPower[QString::number(power)][cmc]++; - manaCurveCardsByPower[QString::number(power)][cmc].append(info.getName()); - manaCurveByToughness[QString::number(toughness)][cmc]++; - manaCurveCardsByToughness[QString::number(toughness)][cmc].append(info.getName()); + manaCurveByPower[QString::number(power)][cmc] += amount; + manaCurveCardsByPower[QString::number(power)][cmc] << copiesOfName; + manaCurveByToughness[QString::number(toughness)][cmc] += amount; + manaCurveCardsByToughness[QString::number(toughness)][cmc] << copiesOfName; // ========== Category Counts =========== for (auto &t : types) { - typeCount[t]++; + typeCount[t] += amount; } for (auto &st : subtypes) { - subtypeCount[st]++; + subtypeCount[st] += amount; } for (auto &c : colors) { - colorCount[c]++; + colorCount[c] += amount; } - manaValueCount[cmc]++; + manaValueCount[cmc] += amount; // ---------------- Mana Base ---------------- if (config.computeManaBase) { - auto prod = determineManaProduction(info.getText()); + auto prod = determineManaProduction(info->getText()); for (auto it = prod.begin(); it != prod.end(); ++it) { if (it.value() > 0) { - productionPipCount[it.key()] += it.value(); - productionCardCount[it.key()]++; + productionPipCount[it.key()] += it.value() * amount; + productionCardCount[it.key()] += amount; } - manaBaseMap[it.key()] += it.value(); + manaBaseMap[it.key()] += it.value() * amount; } } // ---------------- Devotion ---------------- if (config.computeDevotion) { - auto devo = countManaSymbols(info.getManaCost()); + auto devo = countManaSymbols(info->getManaCost()); for (auto &d : devo) { if (d.second > 0) { - devotionPipCount[QString(d.first)] += d.second; - devotionCardCount[QString(d.first)]++; + devotionPipCount[QString(d.first)] += d.second * amount; + devotionCardCount[QString(d.first)] += amount; } - manaDevotionMap[d.first] += d.second; + manaDevotionMap[d.first] += d.second * amount; } } } diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 24f521760..fb5ae780c 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -81,13 +81,30 @@ void VisualDeckEditorSampleHandWidget::updateDisplay() } } +static QList cardNodesToExactCards(QList nodes) +{ + QList cards; + for (auto node : nodes) { + ExactCard card = CardDatabaseManager::query()->getCard(node->toCardRef()); + if (card) { + for (int k = 0; k < node->getNumber(); ++k) { + cards.append(card); + } + } else { + qDebug() << "Card not found in database!"; + } + } + + return cards; +} + QList VisualDeckEditorSampleHandWidget::getRandomCards(int amountToGet) { QList randomCards; if (!deckListModel) return randomCards; - QList mainDeckCards = deckListModel->getCardsForZone(DECK_ZONE_MAIN); + QList mainDeckCards = cardNodesToExactCards(deckListModel->getCardNodesForZone(DECK_ZONE_MAIN)); if (mainDeckCards.isEmpty()) return randomCards; diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 23dbd7fec..499208a6d 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -608,35 +608,6 @@ void DeckListModel::forEachCard(const std::functionforEachCard(func); } -static QList cardNodesToExactCards(QList nodes) -{ - QList cards; - for (auto node : nodes) { - ExactCard card = CardDatabaseManager::query()->getCard(node->toCardRef()); - if (card) { - for (int k = 0; k < node->getNumber(); ++k) { - cards.append(card); - } - } else { - qDebug() << "Card not found in database!"; - } - } - - return cards; -} - -QList DeckListModel::getCards() const -{ - auto nodes = deckList->getCardNodes(); - return cardNodesToExactCards(nodes); -} - -QList DeckListModel::getCardsForZone(const QString &zoneName) const -{ - auto nodes = deckList->getCardNodes({zoneName}); - return cardNodesToExactCards(nodes); -} - QList DeckListModel::getCardNodes() const { return deckList->getCardNodes(); diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index e7a79d13e..133e7a947 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -329,16 +329,6 @@ public: */ void forEachCard(const std::function &func); - /** - * @brief Creates a list consisting of the entries of the model mapped into ExactCards (with each entry looked up - * in the card database). - * If a card node has number > 1, it will be added that many times to the list. - * If an entry's card is not found in the card database, that entry will be left out of the list. - * @return An ordered list of ExactCards - */ - [[nodiscard]] QList getCards() const; - [[nodiscard]] QList getCardsForZone(const QString &zoneName) const; - /** * @brief Gets a list of all card nodes in the deck. */ From 2d5e8deb75b3bd3b6a32b4c5aef2feeb615391ec Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 4 Jan 2026 21:34:32 -0800 Subject: [PATCH 43/56] [Server_AbstractParticipant] Rename bool getters (#6492) * [Server_AbstractParticipant] Rename bool getters * reformat --- .../remote/game/server_abstract_participant.h | 4 +-- .../remote/game/server_abstract_player.cpp | 2 +- .../server/remote/game/server_game.cpp | 27 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h index f60d56dcf..a24fa5799 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -89,11 +89,11 @@ public: { return playerId; } - bool getSpectator() const + bool isSpectator() const { return spectator; } - bool getJudge() const + bool isJudge() const { return judge; } 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 aaf7f1332..a5bb16d13 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 @@ -1516,7 +1516,7 @@ Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseCo zone->addWritePermission(cmd.player_id()); } - if (getJudge()) { + if (isJudge()) { ges.setOverwriteOwnership(true); } 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 5cd6c8bf8..d8574db99 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -189,7 +189,7 @@ void Server_Game::pingClockTimeout() if (participant == nullptr) continue; - if (!participant->getSpectator()) { + if (!participant->isSpectator()) { ++playerCount; } @@ -200,7 +200,7 @@ void Server_Game::pingClockTimeout() } if ((participant->getPingTime() != -1) && - (!participant->getSpectator() || participant->getPlayerId() == hostId)) { + (!participant->isSpectator() || participant->getPlayerId() == hostId)) { allPlayersInactive = false; } } @@ -222,7 +222,7 @@ QMap Server_Game::getPlayers() const // copies poi QMutexLocker locker(&gameMutex); for (int id : participants.keys()) { auto *participant = participants[id]; - if (!participant->getSpectator()) { + if (!participant->isSpectator()) { players[id] = static_cast(participant); } } @@ -232,7 +232,7 @@ QMap Server_Game::getPlayers() const // copies poi Server_AbstractPlayer *Server_Game::getPlayer(int id) const { auto *participant = participants.value(id); - if (!participant->getSpectator()) { + if (!participant->isSpectator()) { return static_cast(participant); } else { return nullptr; @@ -250,7 +250,7 @@ int Server_Game::getSpectatorCount() const int result = 0; for (Server_AbstractParticipant *participant : participants.values()) { - if (participant->getSpectator()) + if (participant->isSpectator()) ++result; } return result; @@ -295,8 +295,8 @@ void Server_Game::sendGameStateToPlayers() // send game state info to clients according to their role in the game for (auto *participant : participants.values()) { GameEventContainer *gec; - if (participant->getSpectator()) { - if (spectatorsSeeEverything || participant->getJudge()) { + if (participant->isSpectator()) { + if (spectatorsSeeEverything || participant->isJudge()) { gec = prepareGameEvent(omniscientEvent, -1); } else { gec = prepareGameEvent(spectatorNormalEvent, -1); @@ -527,7 +527,7 @@ void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Eve gameId, participant->getPlayerId()); participants.remove(participant->getPlayerId()); - bool spectator = participant->getSpectator(); + bool spectator = participant->isSpectator(); GameEventStorage ges; if (!spectator) { auto *player = static_cast(participant); @@ -728,8 +728,8 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti getInfo(*event1.mutable_game_info()); event1.set_host_id(hostId); event1.set_player_id(joiningParticipant->getPlayerId()); - event1.set_spectator(joiningParticipant->getSpectator()); - event1.set_judge(joiningParticipant->getJudge()); + event1.set_spectator(joiningParticipant->isSpectator()); + event1.set_judge(joiningParticipant->isJudge()); event1.set_resuming(resuming); if (resuming) { const QStringList &allGameTypes = room->getGameTypes(); @@ -747,7 +747,7 @@ void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParti event2.set_active_player_id(activePlayer); event2.set_active_phase(activePhase); - bool omniscient = joiningParticipant->getSpectator() && (spectatorsSeeEverything || joiningParticipant->getJudge()); + bool omniscient = joiningParticipant->isSpectator() && (spectatorsSeeEverything || joiningParticipant->isJudge()); for (auto *participant : participants.values()) { participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true); } @@ -763,9 +763,8 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont, cont->set_game_id(gameId); for (auto *participant : participants.values()) { - const bool playerPrivate = - (participant->getPlayerId() == privatePlayerId) || - (participant->getSpectator() && (spectatorsSeeEverything || participant->getJudge())); + const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || + (participant->isSpectator() && (spectatorsSeeEverything || participant->isJudge())); if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) || (recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) participant->sendGameEvent(*cont); From 731c487ccb92830b5431a8a8529b6be7f3fee224 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:43:40 -0800 Subject: [PATCH 44/56] [ServerGame] null check participant in getPlayer (#6493) --- .../libcockatrice/network/server/remote/game/server_game.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d8574db99..c04876696 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -232,7 +232,7 @@ QMap Server_Game::getPlayers() const // copies poi Server_AbstractPlayer *Server_Game::getPlayer(int id) const { auto *participant = participants.value(id); - if (!participant->isSpectator()) { + if (participant && !participant->isSpectator()) { return static_cast(participant); } else { return nullptr; From 489ce416c33e800496eff29e2f33247dbcd8551a Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:31:10 -0800 Subject: [PATCH 45/56] [VDS] Add search query option for comments (#6477) --- cockatrice/resources/help/deck_search.md | 5 +++++ cockatrice/src/filters/deck_filter_string.cpp | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cockatrice/resources/help/deck_search.md b/cockatrice/resources/help/deck_search.md index 4e1c5c557..2ddad9e66 100644 --- a/cockatrice/resources/help/deck_search.md +++ b/cockatrice/resources/help/deck_search.md @@ -29,6 +29,11 @@ searches are case insensitive.
    Format:
    [f:standard](#f:standard) (Any deck with format set to standard)
    +
    Comments:
    +
    [c:good](#c:good) (Any deck with comments containing the word good)
    +
    [c:good c:deck](#c:good c:deck) (Any deck with comments containing the words good and deck)
    +
    [c:"good deck"](#c:%22good deck%22) (Any deck with comments containing the exact phrase "good deck")
    +
    Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):
    [[plains]] (Any deck that contains at least one card with "plains" in its name)
    [[t:legendary]] (Any deck that contains at least one legendary)
    diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp index 5cce5d323..42a77fe5a 100644 --- a/cockatrice/src/filters/deck_filter_string.cpp +++ b/cockatrice/src/filters/deck_filter_string.cpp @@ -13,7 +13,7 @@ QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws* ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart -QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / GenericQuery +QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / FormatQuery / CommentQuery / GenericQuery NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart @@ -25,6 +25,7 @@ DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String FileNameQuery <- [Ff] ([Nn] / 'ile' ([Nn] 'ame')?) [:] String PathQuery <- [Pp] 'ath'? [:] String FormatQuery <- [Ff] 'ormat'? [:] String +CommentQuery <- [Cc] ('omment' 's'?)? [:] String GenericQuery <- String @@ -166,6 +167,14 @@ static void setupParserRules() }; }; + search["CommentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { + auto value = std::any_cast(sv[0]); + return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { + auto comments = deck->deckLoader->getDeck().deckList.getComments(); + return comments.contains(value, Qt::CaseInsensitive); + }; + }; + search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { auto name = std::any_cast(sv[0]); return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) { From d50297bbe65b65dca95348c8156f7c6b1d8cb030 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:03:22 -0800 Subject: [PATCH 46/56] [AnalyticsPanel] Use cogwheel icon for configure button (#6494) --- .../widgets/deck_analytics/abstract_analytics_panel_widget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp index bad883d27..089abc5c8 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/abstract_analytics_panel_widget.cpp @@ -19,7 +19,8 @@ AbstractAnalyticsPanelWidget::AbstractAnalyticsPanelWidget(QWidget *parent, Deck bannerAndSettingsLayout->addWidget(bannerWidget, 1); // config button - configureButton = new QPushButton(tr("Configure"), this); + configureButton = new QPushButton(this); + configureButton->setIcon(QPixmap("theme:icons/cogwheel")); configureButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); connect(configureButton, &QPushButton::clicked, this, &AbstractAnalyticsPanelWidget::applyConfigFromDialog); bannerAndSettingsLayout->addWidget(configureButton, 0); From ee2699413ca4a48dcd7e72f9ded81b760b05d111 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:06:22 -0800 Subject: [PATCH 47/56] [TabDeckEditor] Make card database a dock widget (#6472) * [TabDeckEditor] Make card database a dock widget * delete eventFilter implementation in abstract --- cockatrice/CMakeLists.txt | 1 + .../deck_editor_card_database_dock_widget.cpp | 65 +++++++++++++++++++ .../deck_editor_card_database_dock_widget.h | 32 +++++++++ .../deck_editor_database_display_widget.cpp | 6 +- .../deck_editor_database_display_widget.h | 2 +- .../widgets/tabs/abstract_tab_deck_editor.cpp | 63 ++---------------- .../widgets/tabs/abstract_tab_deck_editor.h | 18 ++--- .../widgets/tabs/tab_deck_editor.cpp | 36 +++++++++- .../tabs/tab_visual_database_display.cpp | 6 +- .../tab_deck_editor_visual.cpp | 8 +-- .../visual_database_display_widget.cpp | 20 +++--- 11 files changed, 171 insertions(+), 86 deletions(-) create mode 100644 cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp create mode 100644 cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 8ebd177db..85e1cd00f 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -170,6 +170,7 @@ set(cockatrice_SOURCES 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/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 src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp 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 new file mode 100644 index 000000000..5abc6e619 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -0,0 +1,65 @@ +#include "deck_editor_card_database_dock_widget.h" + +DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent) +{ + setObjectName("databaseDisplayDock"); + setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable); + + createDatabaseDisplayDock(parent); + + retranslateUi(); +} + +void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor) +{ + databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor); + + auto *frame = new QVBoxLayout; + frame->setObjectName("databaseDisplayFrame"); + frame->addWidget(databaseDisplayWidget); + + auto *dockContents = new QWidget(); + dockContents->setObjectName("databaseDisplayDockContents"); + dockContents->setLayout(frame); + setWidget(dockContents); + + installEventFilter(deckEditor); + connect(this, &QDockWidget::topLevelChanged, deckEditor, &AbstractTabDeckEditor::dockTopLevelChanged); + + // 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); +} + +CardDatabase *DeckEditorCardDatabaseDockWidget::getDatabase() const +{ + return databaseDisplayWidget->databaseModel->getDatabase(); +} + +void DeckEditorCardDatabaseDockWidget::retranslateUi() +{ + setWindowTitle(tr("Card Database")); +} + +void DeckEditorCardDatabaseDockWidget::setFilterTree(FilterTree *filterTree) +{ + databaseDisplayWidget->setFilterTree(filterTree); +} + +void DeckEditorCardDatabaseDockWidget::clearAllDatabaseFilters() +{ + databaseDisplayWidget->clearAllDatabaseFilters(); +} +void DeckEditorCardDatabaseDockWidget::highlightAllSearchEdit() +{ + databaseDisplayWidget->searchEdit->setSelection(0, databaseDisplayWidget->searchEdit->text().length()); +} 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 new file mode 100644 index 000000000..6ad442075 --- /dev/null +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_DECK_EDITOR_CARD_DATABASE_DOCK_WIDGET_H +#define COCKATRICE_DECK_EDITOR_CARD_DATABASE_DOCK_WIDGET_H + +#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" + +#include + +class AbstractTabDeckEditor; +class CardDatabase; +class DeckEditorDatabaseDisplayWidget; +class FilterTree; + +class DeckEditorCardDatabaseDockWidget : public QDockWidget +{ +public: + explicit DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent); + + DeckEditorDatabaseDisplayWidget *databaseDisplayWidget; + + CardDatabase *getDatabase() const; + void setFilterTree(FilterTree *filterTree); + +public slots: + void retranslateUi(); + void clearAllDatabaseFilters(); + void highlightAllSearchEdit(); + +private: + void createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor); +}; + +#endif // COCKATRICE_DECK_EDITOR_CARD_DATABASE_DOCK_WIDGET_H 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 f5549a98c..590fd1d1b 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,10 +21,10 @@ static bool canBeCommander(const CardInfo &cardInfo) cardInfo.getText().contains("can be your commander", Qt::CaseInsensitive); } -DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(AbstractTabDeckEditor *parent) - : QWidget(parent), deckEditor(parent) +DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor) + : QWidget(parent), deckEditor(deckEditor) { - setObjectName("centralWidget"); + setObjectName("databaseDisplayWidget"); centralFrame = new QVBoxLayout(this); centralFrame->setObjectName("centralFrame"); 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 593be7825..16ae6e255 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,7 +23,7 @@ class DeckEditorDatabaseDisplayWidget : public QWidget Q_OBJECT public: - explicit DeckEditorDatabaseDisplayWidget(AbstractTabDeckEditor *parent); + explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor); AbstractTabDeckEditor *deckEditor; SearchLineEdit *searchEdit; CardDatabaseModel *databaseModel; 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 29bcc1ab1..53540b6f9 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -55,7 +55,7 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta deckStateManager = new DeckStateManager(this); - databaseDisplayDockWidget = new DeckEditorDatabaseDisplayWidget(this); + cardDatabaseDockWidget = new DeckEditorCardDatabaseDockWidget(this); deckDockWidget = new DeckEditorDeckDockWidget(this); cardInfoDockWidget = new DeckEditorCardInfoDockWidget(this); filterDockWidget = new DeckEditorFilterDockWidget(this); @@ -68,21 +68,9 @@ AbstractTabDeckEditor::AbstractTabDeckEditor(TabSupervisor *_tabSupervisor) : Ta connect(deckStateManager, &DeckStateManager::isModifiedChanged, this, &AbstractTabDeckEditor::onDeckModified); connect(deckDockWidget, &DeckEditorDeckDockWidget::selectedCardChanged, this, &AbstractTabDeckEditor::updateCard); - // Connect database display signals to this tab - connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::cardChanged, this, - &AbstractTabDeckEditor::updateCard); - connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this, - &AbstractTabDeckEditor::actAddCard); - connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this, - &AbstractTabDeckEditor::actAddCardToSideboard); - connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromMainDeck, this, - &AbstractTabDeckEditor::actDecrementCard); - connect(databaseDisplayDockWidget, &DeckEditorDatabaseDisplayWidget::decrementCardFromSideboard, this, - &AbstractTabDeckEditor::actDecrementCardFromSideboard); - // Connect filter signals - connect(filterDockWidget, &DeckEditorFilterDockWidget::clearAllDatabaseFilters, databaseDisplayDockWidget, - &DeckEditorDatabaseDisplayWidget::clearAllDatabaseFilters); + connect(filterDockWidget, &DeckEditorFilterDockWidget::clearAllDatabaseFilters, cardDatabaseDockWidget, + &DeckEditorCardDatabaseDockWidget::clearAllDatabaseFilters); // Connect shortcut changes connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, @@ -122,7 +110,7 @@ void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString & { deckStateManager->addCard(card, zoneName); - databaseDisplayDockWidget->searchEdit->setSelection(0, databaseDisplayDockWidget->searchEdit->text().length()); + cardDatabaseDockWidget->highlightAllSearchEdit(); } /** @@ -544,21 +532,21 @@ void AbstractTabDeckEditor::actExportDeckDecklistXyz() /** @brief Analyzes the deck using DeckStats. */ void AbstractTabDeckEditor::actAnalyzeDeckDeckstats() { - auto *interface = new DeckStatsInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(), this); + auto *interface = new DeckStatsInterface(*cardDatabaseDockWidget->getDatabase(), this); interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Analyzes the deck using TappedOut. */ void AbstractTabDeckEditor::actAnalyzeDeckTappedout() { - auto *interface = new TappedOutInterface(*databaseDisplayDockWidget->databaseModel->getDatabase(), this); + auto *interface = new TappedOutInterface(*cardDatabaseDockWidget->getDatabase(), this); interface->analyzeDeck(deckStateManager->getDeckList()); } /** @brief Applies a new filter tree to the database display. */ void AbstractTabDeckEditor::filterTreeChanged(FilterTree *filterTree) { - databaseDisplayDockWidget->setFilterTree(filterTree); + cardDatabaseDockWidget->setFilterTree(filterTree); } /** @@ -571,43 +559,6 @@ void AbstractTabDeckEditor::closeEvent(QCloseEvent *event) event->accept(); } -/** - * @brief Event filter for dock visibility and geometry changes. - * @param o Object sending the event. - * @param e Event. - * @return False always. - */ -bool AbstractTabDeckEditor::eventFilter(QObject *o, QEvent *e) -{ - if (e->type() == QEvent::Close) { - if (o == cardInfoDockWidget) { - aCardInfoDockVisible->setChecked(false); - aCardInfoDockFloating->setEnabled(false); - } else if (o == deckDockWidget) { - aDeckDockVisible->setChecked(false); - aDeckDockFloating->setEnabled(false); - } else if (o == filterDockWidget) { - aFilterDockVisible->setChecked(false); - aFilterDockFloating->setEnabled(false); - } else if (o == printingSelectorDockWidget) { - aPrintingSelectorDockVisible->setChecked(false); - aPrintingSelectorDockFloating->setEnabled(false); - } - } - - if (o == this && e->type() == QEvent::Hide) { - LayoutsSettings &layouts = SettingsCache::instance().layouts(); - layouts.setDeckEditorLayoutState(saveState()); - layouts.setDeckEditorGeometry(saveGeometry()); - layouts.setDeckEditorCardSize(cardInfoDockWidget->size()); - layouts.setDeckEditorFilterSize(filterDockWidget->size()); - layouts.setDeckEditorDeckSize(deckDockWidget->size()); - layouts.setDeckEditorPrintingSelectorSize(printingSelectorDockWidget->size()); - } - - return false; -} - /** @brief Shows a confirmation dialog before closing. */ bool AbstractTabDeckEditor::confirmClose() { 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 69b2b4a1c..025a65cad 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -8,6 +8,7 @@ #ifndef TAB_GENERIC_DECK_EDITOR_H #define TAB_GENERIC_DECK_EDITOR_H +#include "../interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h" #include "../interface/widgets/deck_editor/deck_editor_card_info_dock_widget.h" #include "../interface/widgets/deck_editor/deck_editor_database_display_widget.h" #include "../interface/widgets/deck_editor/deck_editor_deck_dock_widget.h" @@ -27,7 +28,7 @@ class CardInfoFrameWidget; class DeckLoader; class DeckEditorMenu; class DeckEditorCardInfoDockWidget; -class DeckEditorDatabaseDisplayWidget; +class DeckEditorCardDatabaseDockWidget; class DeckEditorDeckDockWidget; class DeckEditorFilterDockWidget; class DeckEditorPrintingSelectorDockWidget; @@ -126,7 +127,7 @@ public: // UI Elements DeckStateManager *deckStateManager; DeckEditorMenu *deckMenu; ///< Menu for deck operations - DeckEditorDatabaseDisplayWidget *databaseDisplayDockWidget; ///< Database dock + DeckEditorCardDatabaseDockWidget *cardDatabaseDockWidget; ///< Database dock DeckEditorCardInfoDockWidget *cardInfoDockWidget; ///< Card info dock DeckEditorDeckDockWidget *deckDockWidget; ///< Deck dock DeckEditorFilterDockWidget *filterDockWidget; ///< Filter dock @@ -245,9 +246,6 @@ protected slots: /** @brief Handles dock close events. */ void closeEvent(QCloseEvent *event) override; - /** @brief Event filter for dock state changes. */ - bool eventFilter(QObject *o, QEvent *e) override; - /** @brief Slot triggered when a dock visibility changes. Pure virtual. */ virtual void dockVisibleTriggered() = 0; @@ -295,11 +293,15 @@ protected: virtual void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation); // UI Menu Elements - QMenu *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *printingSelectorDockMenu; + QMenu *viewMenu, *cardInfoDockMenu, *cardDatabaseDockMenu, *deckDockMenu, *filterDockMenu, + *printingSelectorDockMenu; QAction *aResetLayout; - QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating; - QAction *aFilterDockVisible, *aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating; + QAction *aCardInfoDockVisible, *aCardInfoDockFloating; + QAction *aCardDatabaseDockVisible, *aCardDatabaseDockFloating; + QAction *aDeckDockVisible, *aDeckDockFloating; + QAction *aFilterDockVisible, *aFilterDockFloating; + QAction *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating; }; #endif // TAB_GENERIC_DECK_EDITOR_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 286252e13..defd7827a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -55,6 +55,7 @@ void TabDeckEditor::createMenus() viewMenu = new QMenu(this); cardInfoDockMenu = viewMenu->addMenu(QString()); + cardDatabaseDockMenu = viewMenu->addMenu(QString()); deckDockMenu = viewMenu->addMenu(QString()); filterDockMenu = viewMenu->addMenu(QString()); printingSelectorDockMenu = viewMenu->addMenu(QString()); @@ -67,6 +68,14 @@ void TabDeckEditor::createMenus() aCardInfoDockFloating->setCheckable(true); connect(aCardInfoDockFloating, &QAction::triggered, this, &TabDeckEditor::dockFloatingTriggered); + // Card Database dock + aCardDatabaseDockVisible = cardDatabaseDockMenu->addAction(QString()); + aCardDatabaseDockVisible->setCheckable(true); + connect(aCardDatabaseDockVisible, &QAction::triggered, this, &TabDeckEditor::dockVisibleTriggered); + aCardDatabaseDockFloating = cardDatabaseDockMenu->addAction(QString()); + aCardDatabaseDockFloating->setCheckable(true); + connect(aCardDatabaseDockFloating, &QAction::triggered, this, &TabDeckEditor::dockFloatingTriggered); + // Deck dock aDeckDockVisible = deckDockMenu->addAction(QString()); aDeckDockVisible->setCheckable(true); @@ -126,18 +135,22 @@ void TabDeckEditor::retranslateUi() { deckMenu->retranslateUi(); cardInfoDockWidget->retranslateUi(); + cardDatabaseDockWidget->retranslateUi(); deckDockWidget->retranslateUi(); filterDockWidget->retranslateUi(); printingSelectorDockWidget->retranslateUi(); viewMenu->setTitle(tr("&View")); cardInfoDockMenu->setTitle(tr("Card Info")); + cardDatabaseDockMenu->setTitle(tr("Card Database")); deckDockMenu->setTitle(tr("Deck")); filterDockMenu->setTitle(tr("Filters")); printingSelectorDockMenu->setTitle(tr("Printing")); aCardInfoDockVisible->setText(tr("Visible")); aCardInfoDockFloating->setText(tr("Floating")); + aCardDatabaseDockVisible->setText(tr("Visible")); + aCardDatabaseDockFloating->setText(tr("Floating")); aDeckDockVisible->setText(tr("Visible")); aDeckDockFloating->setText(tr("Floating")); aFilterDockVisible->setText(tr("Visible")); @@ -171,7 +184,6 @@ void TabDeckEditor::showPrintingSelector() void TabDeckEditor::loadLayout() { LayoutsSettings &layouts = SettingsCache::instance().layouts(); - setCentralWidget(databaseDisplayDockWidget); auto &layoutState = layouts.getDeckEditorLayoutState(); if (layoutState.isNull()) @@ -189,16 +201,19 @@ void TabDeckEditor::loadLayout() } aCardInfoDockVisible->setChecked(!cardInfoDockWidget->isHidden()); + aCardDatabaseDockVisible->setChecked(!cardDatabaseDockWidget->isHidden()); aFilterDockVisible->setChecked(!filterDockWidget->isHidden()); aDeckDockVisible->setChecked(!deckDockWidget->isHidden()); aPrintingSelectorDockVisible->setChecked(!printingSelectorDockWidget->isHidden()); aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked()); + aCardDatabaseDockFloating->setChecked(aCardDatabaseDockVisible->isChecked()); aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked()); aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked()); aPrintingSelectorDockFloating->setEnabled(aPrintingSelectorDockVisible->isChecked()); aCardInfoDockFloating->setChecked(cardInfoDockWidget->isFloating()); + aCardDatabaseDockFloating->setChecked(cardDatabaseDockWidget->isFloating()); aFilterDockFloating->setChecked(filterDockWidget->isFloating()); aDeckDockFloating->setChecked(deckDockWidget->isFloating()); aPrintingSelectorDockFloating->setChecked(printingSelectorDockWidget->isFloating()); @@ -226,27 +241,31 @@ void TabDeckEditor::restartLayout() // Update menu checkboxes aCardInfoDockVisible->setChecked(true); + aCardDatabaseDockVisible->setChecked(true); aDeckDockVisible->setChecked(true); aFilterDockVisible->setChecked(true); aPrintingSelectorDockVisible->setChecked(!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()); aCardInfoDockFloating->setChecked(false); + aCardDatabaseDockFloating->setChecked(false); aDeckDockFloating->setChecked(false); aFilterDockFloating->setChecked(false); aPrintingSelectorDockFloating->setChecked(false); - setCentralWidget(databaseDisplayDockWidget); + addDockWidget(Qt::LeftDockWidgetArea, cardDatabaseDockWidget); addDockWidget(Qt::RightDockWidgetArea, deckDockWidget); addDockWidget(Qt::RightDockWidgetArea, cardInfoDockWidget); addDockWidget(Qt::RightDockWidgetArea, filterDockWidget); addDockWidget(Qt::RightDockWidgetArea, printingSelectorDockWidget); // Show/hide and reset floating + cardDatabaseDockWidget->setFloating(false); deckDockWidget->setFloating(false); cardInfoDockWidget->setFloating(false); filterDockWidget->setFloating(false); printingSelectorDockWidget->setFloating(false); + cardDatabaseDockWidget->setVisible(true); deckDockWidget->setVisible(true); cardInfoDockWidget->setVisible(true); filterDockWidget->setVisible(true); @@ -269,6 +288,9 @@ void TabDeckEditor::freeDocksSize() deckDockWidget->setMinimumSize(minSize); deckDockWidget->setMaximumSize(maxSize); + cardDatabaseDockWidget->setMinimumSize(minSize); + cardDatabaseDockWidget->setMaximumSize(maxSize); + cardInfoDockWidget->setMinimumSize(minSize); cardInfoDockWidget->setMaximumSize(maxSize); @@ -286,6 +308,9 @@ void TabDeckEditor::dockVisibleTriggered() if (o == aCardInfoDockVisible) { cardInfoDockWidget->setHidden(!aCardInfoDockVisible->isChecked()); aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked()); + } else if (o == aCardDatabaseDockVisible) { + cardDatabaseDockWidget->setHidden(!aCardDatabaseDockVisible->isChecked()); + aCardDatabaseDockFloating->setEnabled(aCardDatabaseDockVisible->isChecked()); } else if (o == aDeckDockVisible) { deckDockWidget->setHidden(!aDeckDockVisible->isChecked()); aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked()); @@ -304,6 +329,8 @@ void TabDeckEditor::dockFloatingTriggered() QObject *o = sender(); if (o == aCardInfoDockFloating) cardInfoDockWidget->setFloating(aCardInfoDockFloating->isChecked()); + else if (o == aCardDatabaseDockFloating) + cardDatabaseDockWidget->setFloating(aCardDatabaseDockFloating->isChecked()); else if (o == aDeckDockFloating) deckDockWidget->setFloating(aDeckDockFloating->isChecked()); else if (o == aFilterDockFloating) @@ -318,6 +345,8 @@ void TabDeckEditor::dockTopLevelChanged(bool topLevel) QObject *o = sender(); if (o == cardInfoDockWidget) aCardInfoDockFloating->setChecked(topLevel); + else if (o == aCardDatabaseDockFloating) + aCardDatabaseDockFloating->setChecked(topLevel); else if (o == deckDockWidget) aDeckDockFloating->setChecked(topLevel); else if (o == filterDockWidget) @@ -338,6 +367,9 @@ bool TabDeckEditor::eventFilter(QObject *o, QEvent *e) if (o == cardInfoDockWidget) { aCardInfoDockVisible->setChecked(false); aCardInfoDockFloating->setEnabled(false); + } else if (o == cardDatabaseDockWidget) { + aCardDatabaseDockVisible->setChecked(false); + aCardDatabaseDockFloating->setEnabled(false); } else if (o == deckDockWidget) { aDeckDockVisible->setChecked(false); aDeckDockFloating->setEnabled(false); 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 d45cdded0..5e8fb8670 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.cpp @@ -6,9 +6,9 @@ TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor { deckEditor = new TabDeckEditor(_tabSupervisor); deckEditor->setHidden(true); - visualDatabaseDisplayWidget = - new VisualDatabaseDisplayWidget(this, deckEditor, deckEditor->databaseDisplayDockWidget->databaseModel, - deckEditor->databaseDisplayDockWidget->databaseDisplayModel); + visualDatabaseDisplayWidget = new VisualDatabaseDisplayWidget( + this, deckEditor, deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseModel, + deckEditor->cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel); setCentralWidget(visualDatabaseDisplayWidget); 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 82f96860d..b24013319 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 @@ -50,7 +50,7 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra refreshShortcuts(); loadLayout(); - databaseDisplayDockWidget->setHidden(true); + cardDatabaseDockWidget->setHidden(true); } /** @brief Creates the central frame containing the tab container. */ @@ -62,9 +62,9 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame = new QVBoxLayout; centralWidget->setLayout(centralFrame); - tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), - databaseDisplayDockWidget->databaseModel, - databaseDisplayDockWidget->databaseDisplayModel); + tabContainer = new TabDeckEditorVisualTabWidget( + centralWidget, this, deckStateManager->getModel(), cardDatabaseDockWidget->databaseDisplayWidget->databaseModel, + cardDatabaseDockWidget->databaseDisplayWidget->databaseDisplayModel); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this, &TabDeckEditorVisual::changeModelIndexAndCardInfo); 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 c5eaa171a..6112faedf 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 @@ -74,25 +74,27 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, searchKeySignals.setObjectName("searchKeySignals"); connect(searchEdit, &SearchLineEdit::textChanged, this, &VisualDatabaseDisplayWidget::updateSearch); - connect(&searchKeySignals, &KeySignals::onEnter, deckEditor->databaseDisplayDockWidget, + + DeckEditorDatabaseDisplayWidget *databaseDisplayWidget = deckEditor->cardDatabaseDockWidget->databaseDisplayWidget; + connect(&searchKeySignals, &KeySignals::onEnter, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, deckEditor->databaseDisplayDockWidget, + connect(&searchKeySignals, &KeySignals::onCtrlAltEqual, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, deckEditor->databaseDisplayDockWidget, + connect(&searchKeySignals, &KeySignals::onCtrlAltRBracket, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, deckEditor->databaseDisplayDockWidget, + connect(&searchKeySignals, &KeySignals::onCtrlAltMinus, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::actDecrementCardFromMainDeck); - connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, deckEditor->databaseDisplayDockWidget, + connect(&searchKeySignals, &KeySignals::onCtrlAltLBracket, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::actDecrementCardFromSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, deckEditor->databaseDisplayDockWidget, + connect(&searchKeySignals, &KeySignals::onCtrlAltEnter, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlEnter, deckEditor->databaseDisplayDockWidget, + connect(&searchKeySignals, &KeySignals::onCtrlEnter, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::actAddCardToSideboard); - connect(&searchKeySignals, &KeySignals::onCtrlC, deckEditor->databaseDisplayDockWidget, + connect(&searchKeySignals, &KeySignals::onCtrlC, databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); - databaseView = deckEditor->databaseDisplayDockWidget->getDatabaseView(); + databaseView = databaseDisplayWidget->getDatabaseView(); databaseView->setFocusProxy(searchEdit); databaseView->setItemDelegate(nullptr); databaseView->setVisible(false); From 85c9d8a9ff5dd6db13a9c45d3bfe74c406c3b01d Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:18:38 -0800 Subject: [PATCH 48/56] [DeckEditor] Fix tokens being added to maindeck (#6495) --- .../widgets/deck_editor/deck_editor_deck_dock_widget.cpp | 3 +-- .../interface/widgets/deck_editor/deck_state_manager.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) 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 014f121e4..afc1f41b2 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 @@ -597,13 +597,12 @@ QModelIndexList DeckEditorDeckDockWidget::getSelectedCardNodeSourceIndices() con return selectedRows; } -void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString &_zoneName) +void DeckEditorDeckDockWidget::actAddCard(const ExactCard &card, const QString &zoneName) { if (!card) { return; } - QString zoneName = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : _zoneName; deckStateManager->addCard(card, zoneName); } 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 628d33d51..7b99e8c6a 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -174,11 +174,13 @@ QModelIndex DeckStateManager::addCard(const ExactCard &card, const QString &zone return {}; } + QString zone = card.getInfo().getIsToken() ? DECK_ZONE_TOKENS : zoneName; + QString reason = tr("Added (%1): %2 (%3) %4") - .arg(zoneName, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), + .arg(zone, card.getName(), card.getPrinting().getSet()->getCorrectedShortName(), card.getPrinting().getProperty("num")); - QModelIndex idx = modifyDeck(reason, [&card, &zoneName](auto model) { return model->addCard(card, zoneName); }); + QModelIndex idx = modifyDeck(reason, [&card, &zone](auto model) { return model->addCard(card, zone); }); if (idx.isValid()) { emit focusIndexChanged(idx); From 192dac0396b4851b6ba74ab7c9f7a148d7b3a977 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:28:59 -0800 Subject: [PATCH 49/56] [DeckListModel] Consolidate methods and signals for card change (#6466) --- .../deck_list_statistics_analyzer.cpp | 2 +- .../deck_editor_deck_dock_widget.cpp | 2 +- .../deck_editor/deck_state_manager.cpp | 3 +- .../dialogs/dlg_select_set_for_cards.cpp | 2 +- .../printing_selector/card_amount_widget.cpp | 2 +- .../models/deck_list/deck_list_model.cpp | 41 +++++++++++++++++++ .../models/deck_list/deck_list_model.h | 40 ++++++++++++++++++ 7 files changed, 86 insertions(+), 6 deletions(-) diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp index a1dc1ab55..ad8afb766 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_list_statistics_analyzer.cpp @@ -12,7 +12,7 @@ DeckListStatisticsAnalyzer::DeckListStatisticsAnalyzer(QObject *parent, DeckListStatisticsAnalyzerConfig _config) : QObject(parent), model(_model), config(_config) { - connect(model, &DeckListModel::dataChanged, this, &DeckListStatisticsAnalyzer::analyze); + connect(model, &DeckListModel::cardsChanged, this, &DeckListStatisticsAnalyzer::analyze); } void DeckListStatisticsAnalyzer::analyze() 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 afc1f41b2..bf7688bb0 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 @@ -154,7 +154,7 @@ void DeckEditorDeckDockWidget::createDeckDock() bannerCardLabel->setText(tr("Banner Card")); bannerCardLabel->setHidden(!SettingsCache::instance().getDeckEditorBannerCardComboBoxVisible()); bannerCardComboBox = new QComboBox(this); - connect(getModel(), &DeckListModel::dataChanged, this, [this]() { + connect(getModel(), &DeckListModel::cardNodesChanged, this, [this]() { // Delay the update to avoid race conditions QTimer::singleShot(100, this, &DeckEditorDeckDockWidget::updateBannerCardComboBox); }); 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 7b99e8c6a..d45416b17 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -11,8 +11,7 @@ DeckStateManager::DeckStateManager(QObject *parent) setModified(true); emit historyChanged(); }); - connect(deckListModel, &DeckListModel::rowsInserted, this, &DeckStateManager::uniqueCardsChanged); - connect(deckListModel, &DeckListModel::rowsRemoved, this, &DeckStateManager::uniqueCardsChanged); + connect(deckListModel, &DeckListModel::cardNodesChanged, this, &DeckStateManager::uniqueCardsChanged); } const DeckList &DeckStateManager::getDeckList() const diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp index 6ed6b67a4..5d9e2679c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp @@ -152,7 +152,7 @@ static bool swapPrinting(DeckListModel *model, const QString &modifiedSet, const return false; } int amount = model->data(idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT), Qt::DisplayRole).toInt(); - model->removeRow(idx.row(), idx.parent()); + model->removeCardAtIndex(idx); CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(cardName); PrintingInfo printing = CardDatabaseManager::query()->getSpecificPrinting(cardName, modifiedSet, ""); for (int i = 0; i < amount; i++) { 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 f8002eb89..1c09a42c1 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -152,7 +152,7 @@ static QModelIndex addAndReplacePrintings(DeckListModel *model, // Check if a card without a providerId already exists in the deckModel and replace it, if so. if (existing.isValid() && existing != newCardIndex && replaceProviderless) { model->offsetCountAtIndex(newCardIndex, extraCopies); - model->removeRow(existing.row(), existing.parent()); + model->removeCardAtIndex(existing); } // Set Index and Focus as if the user had just clicked the new card and modify the deckEditor saveState diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 499208a6d..447d868a8 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -12,6 +12,15 @@ DeckListModel::DeckListModel(QObject *parent) DeckListModel::DeckListModel(QObject *parent, const QSharedPointer &deckList) : DeckListModel(parent) { setDeckList(deckList); + + // forward change signals + connect(this, &DeckListModel::cardAddedAt, this, &DeckListModel::cardsChanged); + connect(this, &DeckListModel::cardRemoved, this, &DeckListModel::cardsChanged); + connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardsChanged); + + connect(this, &DeckListModel::cardNodeAddedAt, this, &DeckListModel::cardNodesChanged); + connect(this, &DeckListModel::cardNodeRemoved, this, &DeckListModel::cardNodesChanged); + connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardNodesChanged); } DeckListModel::~DeckListModel() @@ -421,6 +430,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num"))); const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName(); + bool cardNodeAdded = false; if (!cardNode) { // Determine the correct index int insertRow = findSortedInsertRow(groupNode, cardInfo); @@ -432,6 +442,8 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam beginInsertRows(parentIndex, insertRow, insertRow); cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); endInsertRows(); + + cardNodeAdded = true; } else { cardNode->setNumber(cardNode->getNumber() + 1); cardNode->setCardSetShortName(cardSetName); @@ -444,6 +456,10 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam emitRecursiveUpdates(parentIndex); auto index = nodeToIndex(cardNode); + if (cardNodeAdded) { + emit cardNodeAddedAt(index); + } + emit cardAddedAt(index); return index; @@ -468,17 +484,42 @@ bool DeckListModel::offsetCountAtIndex(const QModelIndex &idx, int offset) if (newCount <= 0) { removeRow(idx.row(), idx.parent()); + emit cardNodeRemoved(); } else { setData(numberIndex, newCount, Qt::EditRole); } if (offset > 0) { emit cardAddedAt(idx); + } else if (offset < 0) { + emit cardRemoved(); } return true; } +bool DeckListModel::removeCardAtIndex(const QModelIndex &idx) +{ + if (!idx.isValid()) { + return false; + } + + auto *node = static_cast(idx.internalPointer()); + auto *card = dynamic_cast(node); + + if (!card) { + return false; + } + + bool success = removeRow(idx.row(), idx.parent()); + + if (success) { + emit cardRemoved(); + } + + return success; +} + int DeckListModel::findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const { if (!cardInfo) { diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 133e7a947..86d36b7f9 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -197,6 +197,12 @@ public: * InnerDecklistNode containers and supports grouping, sorting, adding/removing * cards, and printing decklists. * + * Outside code should refrain from modifying the model with methods inherited from QAbstractItemModel, such as with + * `setData` or `removeRow`. + * Instead, use the custom methods on this class to modify the model, such as `addCard`, `offsetCountAtIndex`, or + * `removeCardAtIndex`. + * This ensures the custom signals for this class are correctly emitted. + * * Signals: * - deckHashChanged(): emitted when the deck contents change in a way that * affects its hash. @@ -231,6 +237,11 @@ signals: */ void deckHashChanged(); + /** + * @brief Emitted whenever the cards in the deck changes. This includes when the deck is replaced. + */ + void cardsChanged(); + /** * @brief Emitted whenever a card is added to the deck, regardless of whether it's an entirely new card or an * existing card that got incremented. @@ -238,6 +249,28 @@ signals: */ void cardAddedAt(const QModelIndex &index); + /** + * @brief Emitted whenever a card is removed from the deck, regardless of whether a card node was removed or an + * existing card got decremented. + */ + void cardRemoved(); + + /** + * @brief Emitted whenever a card node is added or removed. This includes when the deck is replaced. + */ + void cardNodesChanged(); + + /** + * @brief Emitted whenever a new card node is added. + * @param index The index of the card node that got added. + */ + void cardNodeAddedAt(const QModelIndex &index); + + /** + * @brief Emitted whenever a card node is removed. + */ + void cardNodeRemoved(); + /** * @brief Emitted whenever the deck in the model has been replaced with a new one */ @@ -311,6 +344,13 @@ public: */ bool offsetCountAtIndex(const QModelIndex &idx, int offset); + /** + * @brief Removes the card node at the index + * @param idx The index of a card node. No-ops if the index is invalid or not a card node. + * @return Whether the node was removed. + */ + bool removeCardAtIndex(const QModelIndex &idx); + /** * @brief Removes all cards and resets the model. */ From b86853b65ccdd40ded9997b5ec47793bfb922043 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:18:21 +0100 Subject: [PATCH 50/56] Bump actions/cache from 4 to 5 (#6496) --- .github/workflows/desktop-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 5b954d021..684b375cc 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -384,7 +384,7 @@ jobs: - name: Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries (${{ matrix.soc }} macOS) if: matrix.os == 'macOS' id: restore_qt - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ${{ github.workspace }}/Qt key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} @@ -407,7 +407,7 @@ jobs: - name: Cache thin Qt libraries (${{ matrix.soc }} macOS) if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: ${{ github.workspace }}/Qt key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} From 0a2fdb05ad010e42efbf45fe90002b61cc476085 Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 6 Jan 2026 02:42:35 -0800 Subject: [PATCH 51/56] [VDS] Try to fix memory leak by properly parenting widgets (#6498) * [VDS] Try to fix memory leak by properly parenting widgets * format --- .../src/interface/widgets/cards/card_info_picture_widget.cpp | 2 +- .../visual_deck_storage/deck_preview/deck_preview_widget.cpp | 2 +- .../visual_deck_storage/deck_preview/deck_preview_widget.h | 5 +++++ 3 files changed, 7 insertions(+), 2 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 85d210e1a..7206c05c9 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -47,7 +47,7 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT originalPos = this->pos(); // Create the animation - animation = new QPropertyAnimation(this, "pos"); + animation = new QPropertyAnimation(this, "pos", this); animation->setDuration(200); // 200ms animation duration animation->setEasingCurve(QEasingCurve::OutQuad); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 4c782c989..f98e37e16 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -92,7 +92,7 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess) bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); bannerCardComboBox->setObjectName("bannerCardComboBox"); bannerCardComboBox->setCurrentText(deckLoader->getDeck().deckList.getBannerCard().name); - bannerCardComboBox->installEventFilter(new NoScrollFilter()); + bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox)); connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &DeckPreviewWidget::setBannerCard); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index e5975db71..85f0ce76e 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -82,6 +82,11 @@ private slots: class NoScrollFilter : public QObject { Q_OBJECT +public: + explicit NoScrollFilter(QObject *parent = nullptr) : QObject(parent) + { + } + protected: bool eventFilter(QObject *obj, QEvent *event) override { From 6340c4a6b712fb409c06e1e915a0cc2048b3b319 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:35:53 +0100 Subject: [PATCH 52/56] Update translation source strings (#6465) --- cockatrice/cockatrice_en@source.ts | 4692 ++++++++++++++++------------ oracle/oracle_en@source.ts | 214 +- 2 files changed, 2861 insertions(+), 2045 deletions(-) diff --git a/cockatrice/cockatrice_en@source.ts b/cockatrice/cockatrice_en@source.ts index 53e6a7a96..9a2f56f07 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': @@ -25,12 +25,12 @@ AbstractDlgDeckTextEdit - + &Refresh - + Parse Set Name and Number (if available) @@ -38,79 +38,81 @@ 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 + + + AddAnalyticsPanelDialog - - No deck was selected to be exported. + + Add Analytics Panel AdminNotesDialog - + Update Notes - + Admin Notes for %1 @@ -118,12 +120,12 @@ Please check that the directory is writable and try again. AllZonesCardAmountWidget - + Mainboard - + Sideboard @@ -131,22 +133,22 @@ Please check that the directory is writable and try again. AppearanceSettingsPage - + seconds - + Error - + Could not create themes directory at '%1'. - + Enabling this feature will disable the use of the Printing Selector. You will not be able to manage printing preferences on a per-deck basis, or see printings other people have selected for their decks. @@ -157,7 +159,7 @@ Are you sure you would like to enable this feature? - + Disabling this feature will enable the Printing Selector. You can now choose printings on a per-deck basis in the Deck Editor and configure which printing gets added to a deck by default by pinning it in the Printing Selector. @@ -168,152 +170,165 @@ Are you sure you would like to disable this feature? - + Confirm Change - + Theme settings - + Current theme: - + Open themes folder - + Home tab background source: - + Home tab background shuffle frequency: - + Disabled - + 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) - + Bump sets that the deck contains cards from to the top in the printing selector - + Scale cards on mouse over - + Use rounded card corners - + 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: + + ArchidektApiResponseDeckDisplayWidget + + + Open Deck in Deck Editor + + + BackgroundSources @@ -335,111 +350,111 @@ Are you sure you would like to disable this feature? 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. @@ -470,32 +485,32 @@ This is only saved for moderators and cannot be seen by the banned person. CardDatabaseModel - + Name - + Sets - + Mana cost - + Card type - + P/T - + Color(s) @@ -503,96 +518,101 @@ This is only saved for moderators and cannot be seen by the banned person. CardFilter - + AND Logical conjunction operator used in card filter - + OR Logical disjunction operator used in card filter - + AND NOT Negated logical conjunction operator used in card filter - + OR NOT Negated logical disjunction operator used in card filter - + Name - + + Name (Exact) + + + + Type - + Color - + Text - + Set - + Mana Cost - + Mana Value - + Rarity - + Power - + Toughness - + Loyalty - + Format - + Main Type - + Sub Type @@ -600,22 +620,22 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoFrameWidget - + Image - + Description - + Both - + View transformation @@ -623,22 +643,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 @@ -651,12 +671,22 @@ This is only saved for moderators and cannot be seen by the banned person. - + + Set: + + + + + Collector Number: + + + + Related cards: - + Unknown card: @@ -664,124 +694,124 @@ 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 - + Toggle &normal untapping - + T&urn Over Turn face up/face down - + &Peek at card face - + &Clone - + Attac&h to card... - + Unattac&h - + &Draw arrow... - + &Set annotation... - + Ca&rd counters - + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... @@ -789,7 +819,7 @@ This is only saved for moderators and cannot be seen by the banned person. CardSizeWidget - + Card Size @@ -797,133 +827,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 + 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 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' + their sideboard nominative + %1's sideboard + nominative + + + + + their custom zone '%1' + nominative + + + + %1's custom zone '%2' nominative @@ -932,7 +962,7 @@ This is only saved for moderators and cannot be seen by the banned person. CockatriceXml3Parser - + Parse error at line %1 col %2: @@ -940,7 +970,7 @@ This is only saved for moderators and cannot be seen by the banned person. CockatriceXml4Parser - + Parse error at line %1 col %2: @@ -959,6 +989,37 @@ This is only saved for moderators and cannot be seen by the banned person. + + DeckAnalyticsWidget + + + Add Panel + + + + + Remove Panel + + + + + Save Layout + + + + + Load Layout + + + + + DeckEditorCardDatabaseDockWidget + + + Card Database + + + DeckEditorCardInfoDockWidget @@ -970,47 +1031,47 @@ 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 @@ -1018,87 +1079,97 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorDeckDockWidget - + + Loading Database... + + + + Banner Card - + Main Type - + Mana Cost - + Colors - + 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 @@ -1106,17 +1177,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorFilterDockWidget - + Filters - + &Clear all filters - + Delete selected @@ -1124,114 +1195,114 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorMenu - + &Deck Editor - + &New deck - + &Load deck... - + Load recent deck... - + Clear - + &Save deck - + Save deck &as... - + Load deck from cl&ipboard... - + Edit deck in clipboard - - + + Annotated - - + + Not Annotated - + Save deck to clipboard - + Annotated (No set info) - + Not Annotated (No set info) - + &Print deck... - + Load deck from online service... - + &Send deck to online service - + Create decklist (decklist.org) - + Create decklist (decklist.xyz) - + Analyze deck (deckstats.net) - + Analyze deck (tappedout.net) - + &Close @@ -1239,7 +1310,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorPrintingSelectorDockWidget - + Printing Selector @@ -1247,194 +1318,227 @@ 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 + + + Undo + + + + + Redo + + + + + Undo/Redo history + + + + + Click on an entry to revert to that point in the history. + + + + + [redo] + + + + + [undo] + + + DeckListModel - + Count - + Set - + Number - + Provider ID - + Card @@ -1442,12 +1546,12 @@ This is only saved for moderators and cannot be seen by the banned person. DeckLoader - + Common deck formats (%1) - + All files (*.*) @@ -1455,17 +1559,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckPreviewColorIdentityFilterWidget - + Mode: Exact Match - + Mode: Includes - + Color identity filter mode (AND/OR/NOT conjunctions of filters) @@ -1473,7 +1577,7 @@ This is only saved for moderators and cannot be seen by the banned person. DeckPreviewDeckTagsDisplayWidget - + Edit tags ... @@ -1481,62 +1585,62 @@ This is only saved for moderators and cannot be seen by the banned person. DeckPreviewTagDialog - + Deck Tags Manager - + Manage your deck tags. Check or uncheck tags as needed, or add new ones: - + Add a new tag (e.g., Aggro️) - + Add Tag - + Filter tags... - + OK - + Edit default tags - + Cancel - + Invalid Input - + Tag name cannot be empty! - + Duplicate Tag - + This tag already exists. @@ -1549,93 +1653,151 @@ This is only saved for moderators and cannot be seen by the banned person. - + Open in deck editor - + Edit Tags - + Rename Deck - + Save Deck to Clipboard - + Annotated - + Annotated (No set info) - + Not Annotated - + Not Annotated (No set info) - + Rename File - + Delete File - + Set Banner Card - - + + New name: - - + + Error - + Rename failed - + Delete file - + Are you sure you want to delete the selected file? - + Delete failed + + DeckStateManager + + + Rename deck to "%1" from "%2" + + + + + Updated comments (was %1 chars, now %2 chars) + + + + + Set banner card to %1 (%2) + + + + + Tags changed + + + + + Set format to %1 + + + + + Added (%1): %2 (%3) %4 + + + + + Moved to %1 1 × "%2" (%3) + + + + + Removed "%1" (all copies) + + + + + %1 1 × "%2" (%3) + + + + + Added + + + + + Removed + + + DeckStatsInterface @@ -1653,74 +1815,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 @@ -1753,143 +1915,143 @@ This will kick all non-ready players from the game. - + Known Hosts - + Delete the currently selected saved server - + Refresh the server list with known public servers - + New Host - + Name: - + &Host: - + &Port: - + Player &name: - + P&assword: - + &Save password - + A&uto connect - + Automatically connect to the most recent login when Cockatrice opens - + If you have any trouble connecting or registering then contact the server staff for help! - - + + Webpage - + Reset Password - + Forgot password? - + &Connect - + Server - + Login - + Server Contact - + Connect to Server - + 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. @@ -1897,117 +2059,122 @@ This will kick all non-ready players from the game. DlgCreateGame - + Re&member settings - + &Description: - + P&layers: - + General - + Game type - + &Password: - + Only &buddies can join - + Only &registered users can join - + Joining restrictions - + &Spectators can watch - + Spectators &need a password to watch - + Spectators can &chat - + Spectators can see &hands - + Create game as spectator - + Spectators - + Starting life total: - + Open decklists in lobby - + + Create game as judge + + + + Game setup options - + &Clear - + Create game - + Game information - + Error - + Server error. @@ -2015,97 +2182,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 @@ -2113,53 +2280,53 @@ This will kick all non-ready players from the game. DlgDefaultTagsEditor - + Edit Tags - + Add - + Confirm - + Cancel - + Enter a tag and press Enter - - + + - + Invalid Input - + Tag name cannot be empty! - + Duplicate Tag - + This tag already exists. @@ -2167,39 +2334,39 @@ This will kick all non-ready players from the game. DlgEditAvatar - - + + No image chosen. - + To change your avatar, choose a new image. To remove your current avatar, confirm without choosing a new image. - + Browse... - + Change avatar - + Open Image - + Image Files (*.png *.jpg *.bmp) - + Invalid image chosen. @@ -2207,17 +2374,17 @@ To remove your current avatar, confirm without choosing a new image. DlgEditDeckInClipboard - + Edit deck in clipboard - + Error - + Invalid deck list. @@ -2225,38 +2392,38 @@ To remove your current avatar, confirm without choosing a new image. DlgEditPassword - + Old password: - + New password: - + Confirm new password: - + Change password - - + + Error - + Your password is too short. - + The new passwords don't match. @@ -2264,93 +2431,93 @@ To remove your current avatar, confirm without choosing a new image. DlgEditTokens - + &Name: - + C&olor: - + white - + blue - + black - + red - + green - + multicolor - + colorless - + &P/T: - + &Annotation: - + Token data - - + + Add token - + Remove token - + Edit custom tokens - + Please enter the name of the token: - + 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. @@ -2443,7 +2610,8 @@ Make sure to enable the 'Token' set in the "Manage sets" dia - Hide games not created by buddy + Hide games not created by buddies + Hide games not created by buddy @@ -2530,52 +2698,52 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgForgotPasswordChallenge - + Reset Password Challenge Warning - + A problem has occurred. Please try to request a new password again. - + Enter the information of the server and the account you'd like to request a new password for. - + &Host: - + &Port: - + Player &name: - + Email: - + Reset Password Challenge - + Reset Password Challenge Error - + The email address can't be empty. @@ -2583,37 +2751,37 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgForgotPasswordRequest - + Enter the information of the server you'd like to request a new password for. - + &Host: - + &Port: - + Player &name: - + Reset Password Request - + Reset Password Error - + The player name can't be empty. @@ -2621,86 +2789,86 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgForgotPasswordReset - + Reset Password Warning - + A problem has occurred. Please try to request a new password again. - + Enter the received token and the new password in order to set your new password. - + &Host: - + &Port: - + Player &name: - + Token: - - + + New Password: - + Reset Password - + The player name can't be empty. - - - - + + + + Reset Password Error - + The token can't be empty. - + The new password can't be empty. - + Error - + Your password is too short. - + The passwords do not match. @@ -2716,17 +2884,17 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgLoadDeckFromClipboard - + Load deck from clipboard - + Error - + Invalid deck list. @@ -2734,43 +2902,43 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgLoadDeckFromWebsite - + Paste a link to a decklist site here to import it. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - - - - - + + + + + Load Deck from Website - + No parser available for this deck provider. (Archidekt, Deckstats, Moxfield, and TappedOut are supported.) - + Network error: %1 - + Received empty deck data. - + Failed to parse deck data: %1 - + The provided URL is not recognized as a valid deck URL. Valid deck URLs look like this: @@ -2784,7 +2952,7 @@ https://tappedout.net/mtg-decks/your-deck-name/ DlgLoadRemoteDeck - + Load deck @@ -2792,37 +2960,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 @@ -2830,91 +2998,91 @@ https://tappedout.net/mtg-decks/your-deck-name/ DlgRegister - + Enter your information and the information of the server you'd like to register to. Your email will be used to verify your account. - + &Host: - + &Port: - + Player &name: - + P&assword: - + Password (again): - + Email: - + Email (again): - + Country: - + Undefined - + 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. @@ -2940,40 +3108,55 @@ Your email will be used to verify your account. DlgSelectSetForCards - + Unmodified Cards: - + Modified Cards: - + Check Sets to enable them. Drag-and-Drop to reorder them and change their priority. Cards will use the printing of the highest priority enabled set. - + Clear all set information - + Set all to preferred + + + Bulk modified printings. + + + + + Cleared all printing information. + + + + + Set all printings to preferred. + + DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -2984,7 +3167,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 @@ -2995,7 +3178,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 @@ -3004,21 +3187,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 @@ -3027,59 +3210,59 @@ 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 - + Card Sources - + Chat - + Sound - + Shortcuts @@ -3087,39 +3270,39 @@ Would you like to change your database location setting? DlgStartupCardCheck - + Card Update Check - + It has been more than %2 days since you last checked your card database for updates. Choose how you would like to run the card database updater. You can always change this behavior in the 'General' settings tab. - + Run in foreground - + Run in background - + Run in background and always from now on - + Don't prompt again and don't run - + Don't run this time @@ -3127,17 +3310,17 @@ You can always change this behavior in the 'General' settings tab. DlgTipOfTheDay - + Next - + Previous - + Tip of the Day @@ -3145,164 +3328,164 @@ You can always change this behavior in the 'General' settings tab. DlgUpdate - + Current release channel - + Reinstall - + Cancel Download - + Open Download Page - + Check for Client Updates - - - - + + + + Error - + Cockatrice was not built with SSL support, therefore you cannot download updates automatically! Please visit the download page to update manually. - + Downloading update: %1 - + Checking for updates... - + Finished checking for updates - + No Update Available - + Cockatrice is up to date! - + You are already running the latest version available in the chosen release channel. - + Current version - + Selected release channel + + + + Update Available + + + + + + A new version of Cockatrice is available! + + + + + + New version + + - Update Available - - - - - - A new version of Cockatrice is available! - - - - - - New version - - - - - Released - - + + Changelog - + Do you want to update now? - + 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 @@ -3310,21 +3493,153 @@ You may have to manually download the new version. DlgViewLog - + Clear log when closing - + Copy to clipboard - + Debug Log + + DrawProbabilityConfigDialog + + + Draw Probability Settings + + + + + Criteria: + + + + + Card Name + + + + + Type + + + + + Subtype + + + + + Mana Value + + + + + Exactness: + + + + + At least + + + + + Exactly + + + + + Quantity (N): + + + + + Cards drawn (M): + + + + + + cards + + + + + 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 (%) + + + EdhrecApiResponseCardInclusionDisplayWidget @@ -3394,33 +3709,39 @@ You may have to manually download the new version. + + EdhrecCommanderApiResponseBracketNavigationWidget + + + Game Changers + + + + + EdhrecCommanderApiResponseBudgetNavigationWidget + + + Budget + + + EdhrecCommanderApiResponseNavigationWidget - + Combos - + Average Deck - - - Game Changers - - - - - Budget - - EdhrecCommanderResponseCommanderDetailsDisplayWidget - + Salt: @@ -3436,22 +3757,22 @@ You may have to manually download the new version. FilterDisplayWidget - + Confirm Delete - + Are you sure you want to delete the filter '%1'? - + Delete Failed - + Failed to delete filter '%1'. @@ -3459,22 +3780,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 @@ -3482,133 +3803,186 @@ 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 + + GameSelectorQuickFilterToolBar + + + All types + + + + + Filter by game name... + + + + + Filter by game type/format + + + + + Hide games not created by buddies + + + + + Hide full games + + + + + Hide started games + + + GamesModel - + >1 day - + %1%2 hr short age in hours @@ -3617,12 +3991,12 @@ You may have to manually download the new version. - + new - + %1%2 min short age in minutes @@ -3631,83 +4005,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 @@ -3715,143 +4089,143 @@ You may have to manually download the new version. GeneralSettingsPage - + Reset all paths - + All paths have been reset - - - - - - - + + + + + + + Choose path - + Personal settings - + Language: - + 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) @@ -3859,47 +4233,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... @@ -3907,111 +4281,141 @@ You may have to manually download the new version. HandMenu - + &Hand - + &View hand - - &Sort hand + + Sort hand by... - - Take &mulligan + + 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... - - Reveal r&andom card to... + + + All players - - - &All players + + Reveal r&andom card to... HomeWidget - + Create New Deck - + Browse Decks - + Browse Card Database - + Browse EDHRec - + + Browse Archidekt + + + + View Replays - + Quit - + Connecting... - + Connect - + Play @@ -4213,60 +4617,60 @@ 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 @@ -4276,602 +4680,602 @@ Reason for shutdown: %1 - + Scheduled server shutdown - - + + Success - + Registration accepted. Will now login. - + Account activation accepted. Will now login. - + Number of players - + Please enter the number of players. - - + + 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? @@ -4881,81 +5285,81 @@ Do you want to enable it/them? - + 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. @@ -4963,359 +5367,529 @@ 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 + + + ManaBaseWidget - - + Mana Base + + ManaCurveConfigDialog + + + Group By: + + + + + type + + + + + color + + + + + subtype + + + + + power + + + + + toughness + + + + + Filters (optional): + + + + + Show main bar row + + + + + Show per-category rows + + + ManaCurveWidget - - + Mana Curve + + ManaDevotionConfigDialog + + + Display type: + + + + + pie + + + + + bar + + + + + combinedBar + + + + + Filter Colors (optional): + + + ManaDevotionWidget - - + Mana Devotion + + ManaDistributionConfigDialog + + + Top display type: + + + + + pie + + + + + bar + + + + + Colors: + + + + + Show per-color rows + + + + + ManaDistributionSingleDisplayWidget + + + %1 pips (%2 cards) + + + + + %1 mana (%2 cards) + + + + + ManaDistributionWidget + + + Mana Production + Devotion + + + + + Mana Distribution Settings + + + 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. - + %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. - + %1 moves %2%3 to custom zone '%4'. - + %1 tries to draw from an empty library - + %1 draws %2 card(s). @@ -5323,12 +5897,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 @@ -5337,72 +5911,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). @@ -5410,28 +5984,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 @@ -5440,107 +6014,107 @@ 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). @@ -5548,7 +6122,7 @@ Cockatrice will now reload the card database. - + %1 removes %2 "%3" counter(s) from %4 (now %5). @@ -5556,97 +6130,97 @@ Cockatrice will now reload the card database. - + %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). @@ -5654,110 +6228,110 @@ 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 - - + + 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 @@ -5874,62 +6448,62 @@ Cockatrice will now reload the card database. Phase - + Unknown Phase - + Untap - + Upkeep - + Draw - + First Main - + Beginning of Combat - + Declare Attackers - + Declare Blockers - + Combat Damage - + End of Combat - + Second Main - + End/Cleanup @@ -5995,7 +6569,7 @@ Cockatrice will now reload the card database. PictureLoader - + en code for scryfall's language property, not available for all languages @@ -6004,134 +6578,134 @@ 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 - + Move top cards to grave - + Move top cards to exile - + Move bottom cards to grave - + Move bottom cards to exile - + 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 @@ -6139,17 +6713,17 @@ Cockatrice will now reload the card database. PlayerMenu - + Player "%1" - + &Counters - + S&ay @@ -6157,7 +6731,7 @@ Cockatrice will now reload the card database. PrintingSelector - + Display Navigation Buttons @@ -6165,22 +6739,22 @@ Cockatrice will now reload the card database. PrintingSelectorCardOverlayWidget - + Preference - + Pin Printing - + Unpin Printing - + Show Related cards @@ -6196,17 +6770,17 @@ Cockatrice will now reload the card database. PrintingSelectorCardSelectionWidget - + Previous Card in Deck - + Bulk Selection - + Next Card in Deck @@ -6214,28 +6788,28 @@ Cockatrice will now reload the card database. PrintingSelectorCardSortingWidget - + Alphabetical - + Preference - + Release Date - - + + Descending - + Ascending @@ -6301,37 +6875,37 @@ Cockatrice will now reload the card database. QMenuBar - + Services - + Hide %1 - + Hide Others - + Show All - + Preferences... - + Quit %1 - + About %1 @@ -6339,42 +6913,42 @@ Cockatrice will now reload the card database. QObject - + Cockatrice card database (*.xml) - + All files (*.*) - + Cockatrice replays (*.cor) - + Maindeck - + Sideboard - + Tokens - + Overwrite Existing File? - + A .cod version of this deck already exists. Overwrite it? @@ -6382,92 +6956,92 @@ Cockatrice will now reload the card database. 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 @@ -6564,37 +7138,37 @@ Cockatrice will now reload the card database. RoomSelector - + Rooms - + Joi&n - + Room - + Description - + Permissions - + Players - + Games @@ -6635,27 +7209,27 @@ Cockatrice will now reload the card database. SetsModel - + Enabled - + Set type - + Set code - + Long name - + Release date @@ -6663,53 +7237,53 @@ Cockatrice will now reload the card database. 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 @@ -6717,12 +7291,12 @@ Cockatrice will now reload the card database. ShortcutTreeView - + Action - + Shortcut @@ -6730,13 +7304,13 @@ Cockatrice will now reload the card database. ShortcutsSettings - + Your configuration file contained invalid shortcuts. Please check your shortcut settings! - + The following shortcuts have been set to default: @@ -6763,12 +7337,12 @@ Please check your shortcut settings! SideboardMenu - + &Sideboard - + &View sideboard @@ -6776,27 +7350,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -6804,48 +7378,48 @@ Please check your shortcut settings! SpoilerBackgroundUpdater - + Spoilers season has ended - + Deleting spoiler.xml. Please run Oracle - - + + Spoilers download failed - + No internet connection - + Error - + Spoilers already up to date - + No new spoilers added - + Spoilers have been updated! - + Last change: @@ -7009,56 +7583,123 @@ Please check your shortcut settings! + + TabArchidekt + + + + Desc. + + + + + Asc. + + + + + + Any Bracket + + + + + Deck name contains... + + + + + Owner name contains... + + + + + Disabled + + + + + Search + + + + + Formats + + + + + Min. # of Cards: + + + + + Page: + + + + + Archidekt: + + + TabDeckEditor - + Card Info - + Deck - + Filters - + &View - + + Card Database + + + + Printing - - - - + + + + + Visible - - - - + + + + + Floating - + Reset layout - + Deck: %1 @@ -7066,61 +7707,61 @@ Please check your shortcut settings! TabDeckEditorVisual - + Visual Deck: %1 - + &Visual Deck Editor - - + + Card Info - - + + Deck - - + + Filters - + &View - + Printing - - - - + + + + Visible - - - - + + + + Floating - + Reset layout @@ -7128,22 +7769,22 @@ Please check your shortcut settings! TabDeckEditorVisualTabWidget - + Visual Deck View - + Visual Database Display - + Deck Analytics - + Sample Hand @@ -7151,133 +7792,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: @@ -7290,17 +7931,17 @@ Please enter a name: TabDeckStorageVisual - + Visual Deck Storage - + Error - + Could not open deck at %1 @@ -7341,7 +7982,7 @@ Please enter a name: - + EDHRec: @@ -7349,197 +7990,197 @@ 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. @@ -7547,7 +8188,7 @@ Please enter a name: TabHome - + Home @@ -7555,157 +8196,157 @@ Please enter a name: TabLog - + Logs - - - + + + Time;SenderName;SenderIP;Message;TargetID;TargetName - + Room Logs - + Game Logs - + Chat Logs - - + + Error - + You must select at least one filter. - + You have to select a valid number of days to locate. - + 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. @@ -7751,180 +8392,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 @@ -7990,30 +8631,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. @@ -8021,92 +8662,97 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Deck Editor - + Visual Deck Editor - + EDHRec - - - Home - - - - - &Visual Deck Storage - - - - - Visual Database Display - - - Server + Archidekt - Account + Home - Deck Storage + &Visual Deck Storage - Game Replays + Visual Database Display - Administration + 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. @@ -8114,38 +8760,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) @@ -8154,7 +8800,12 @@ Please refrain from engaging in this activity or further actions may be taken ag TabVisualDatabaseDisplay - + + Database Display + + + + Visual Database Display @@ -8234,7 +8885,7 @@ Please refrain from engaging in this activity or further actions may be taken ag UpdateDownloader - + Could not open the file for reading. @@ -8242,206 +8893,206 @@ Please refrain from engaging in this activity or further actions may be taken ag UserContextMenu - + User &details - + Private &chat - + Show this user's &games - + Add to &buddy list - + Remove from &buddy list - + Add to &ignore list - + Remove from &ignore list - + Kick from &game - + Warn user - + View user's war&n history - + Ban from &server - + View user's &ban history - + &Promote user to moderator - + Dem&ote user from moderator - + Promote user to &judge - + Demote user from judge - + View admin notes - - - + + + Error - + This user does not exist. - + You are being ignored by %1 and can't see their games. - + Could not get %1's games. - + %1's games - - - + + + Ban History - + Ban Time;Moderator;Ban Length;Ban Reason;Visible Reason - + User has never been banned. - + 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. - + + - Failed - + Failed to promote user. - + Failed to demote user. - + Copy hash to clipboard - + Remove this user's messages @@ -8629,137 +9280,142 @@ Please refrain from engaging in this activity or further actions may be taken ag 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 - - - Close card view window when last card is removed - - - - - Auto focus search bar when card view window is opened - - - - - Annotate card text on tokens - - - - - Use tear-off menus, allowing right click menus to persist on screen - - - - - 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 + Do not delete &arrows inside of subphases - Open deck in new tab by default + Close card view window when last card is removed - Use visual deck storage in game lobby + Auto focus search bar when card view window is opened - Use selection animation for Visual Deck Storage + Annotate card text on tokens + + + + + Use tear-off menus, allowing right click menus to persist on screen - When adding a tag in the visual deck storage to a .txt deck: + Notifications settings + + + + + Enable notifications in taskbar - do nothing + Notify in the taskbar for game events while you are spectating + + + + + Notify in the taskbar when users in your buddy list connect - ask to convert to .cod + Animation settings + + + + + &Tap/untap animation - always convert to .cod + Deck editor/storage settings - Default deck editor type + Open deck in new tab by default - Classic Deck Editor + Use visual deck storage in game lobby - Visual Deck Editor - - - - - Replay settings + 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: @@ -8767,22 +9423,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 @@ -8790,32 +9446,32 @@ 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... - + &Create token... - + C&reate another token - + Cr&eate predefined token @@ -8823,22 +9479,22 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayColorFilterWidget - + Mode: Exact Match - + Mode: Includes - + Mode: Include/Exclude - + Filter mode (AND/OR/NOT conjunctions of filters) @@ -8846,21 +9502,49 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayFilterSaveLoadWidget - + + Search filter... + + + + Save Filter - + Save all currently applied filters to a file - + Enter filename... + + VisualDatabaseDisplayFormatLegalityFilterWidget + + + Do not display formats with less than this amount of cards in the database + + + + + Filter mode (AND/OR/NOT conjunctions of filters) + + + + + Mode: Exact Match + + + + + Mode: Includes + + + VisualDatabaseDisplayMainTypeFilterWidget @@ -8886,28 +9570,28 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayNameFilterWidget - - - Filter by name... - - - Load from Deck + Filter by name... (Exact match) - Apply all card names in currently loaded deck as exact match name filters + Load from Deck - Load from Clipboard + 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 @@ -8971,50 +9655,115 @@ 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 - + + Sort by: + + + + + Filter by: + + + + Save and load filters - + Filter by exact card name - + Filter by card sub-type - + Filter by set + + + Table + + + + + VisualDeckDisplayOptionsWidget + + + Group by: + + + + + Change how cards are divided into categories/groups. + + + + + Sort by: + + + + + Click and drag to change the sort order within the groups + + + + + Configure how cards are sorted within their groups + + + + + + Toggle Layout: Overlap + + + + + Change how cards are displayed within zones (i.e. overlapped or fully visible.) + + + + + Toggle Layout: Flat + + VisualDeckEditorSampleHandWidget - + Draw a new sample hand - + Sample hand size @@ -9022,46 +9771,25 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckEditorWidget - - Click and drag to change the sort order within the groups + + 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 - - - Configure how cards are sorted within their groups - - - - - - Overlap Layout - - - - - Change how cards are displayed within zones (i.e. overlapped or fully visible.) - - - - - Flat Layout - - VisualDeckStorageFolderDisplayWidget - + Deck Storage @@ -9117,7 +9845,7 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckStorageSearchWidget - + Search by filename (or search expression) @@ -9125,22 +9853,22 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckStorageSortWidget - + Sort Alphabetically (Deck Name) - + Sort Alphabetically (Filename) - + Sort by Last Modified - + Sort by Last Loaded @@ -9148,17 +9876,17 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckStorageWidget - + Loading database ... - + Refresh loaded files - + Visual Deck Storage Settings @@ -9166,43 +9894,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. @@ -9210,133 +9938,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 @@ -9344,72 +10072,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 @@ -9417,7 +10145,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English @@ -9425,12 +10153,12 @@ Please refrain from engaging in this activity or further actions may be taken ag main - + Connect on startup - + Debug to file @@ -9438,1005 +10166,1041 @@ Please refrain from engaging in this activity or further actions may be taken ag shortcutsTab - + Main Window - - + + Deck Editor - + Game Lobby - + Card Counters - + Player Counters - + Power and Toughness - + Game Phases - + Playing Area - + Move Selected Card - + View - + Move Top Card - + Move Bottom Card - + Gameplay - + Drawing - + Chat Room - + Game Window - + Load Deck from Clipboard - - + + Replays - + Tabs - + Check for Card Updates... - + Connect... - + Disconnect - + Exit - + Full screen - + Register... - + Settings... - + Start a Local Game... - + Watch Replay... - + Analyze Deck (deckstats.net) - + Analyze Deck (tappedout.net) - + Clear All Filters - + Clear Selected Filter - + Close - + Remove Card - + Manage Sets... - + Edit Custom Tokens... - + Export Deck (decklist.org) - + Export Deck (decklist.xyz) - + Add Card - + Load Deck... - - + + 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 - + Show Status Bar - + 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 Untap - + Turn Card Over - + Peek Card - + Play Card - + Attach Card... - + Unattach Card - + Clone Card - + Create Token... - + Create All Related Tokens - + Create Another Token - + Set Annotation... - + 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 - - Sort Hand - - - - + Library - + Sideboard - + Top Cards of Library - + Bottom Cards of Library - + Close Recent View - - + + Stack - - + + Graveyard (Multiple) - - + + Exile (Multiple) - + Stack Until Found - + Draw Bottom Card - + Draw Multiple Cards from Bottom... - + Draw Arrow... - + Remove Local Arrows - + Leave Game - + Concede - + Roll Dice... - + 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 3bdd8bef8..53ad27b4d 100644 --- a/oracle/oracle_en@source.ts +++ b/oracle/oracle_en@source.ts @@ -4,22 +4,22 @@ IntroPage - + Introduction - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: - + Version: @@ -27,134 +27,134 @@ LoadSetsPage - + Source selection - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: - + Local file: - + Restore default URL - + Choose file... - + Load sets file - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error - + The provided URL is not valid. - + Downloading (0MB) - + Please choose a file. - + Cannot open file '%1'. - + Downloading (%1MB) - + Network error: %1. - + Parsing file - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. - + Zip extraction failed: %1. - + Sorry, this version of Oracle does not support zipped files. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. @@ -162,42 +162,57 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -205,42 +220,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -248,7 +278,7 @@ OracleImporter - + Dummy set containing tokens @@ -256,7 +286,7 @@ OracleWizard - + Oracle Importer @@ -264,22 +294,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -287,73 +317,73 @@ SaveSetsPage - - + + Error - + No set has been imported. - + Sets imported - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. - + %1: %2 cards imported - + Save card database - + XML; card database (*.xml) - + The file could not be saved to %1 @@ -361,34 +391,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -537,7 +589,7 @@ i18n - + English From 7c7755b61d3f4cdaa4c5bb128b02d78cb16aa90c Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:41:40 -0800 Subject: [PATCH 53/56] [VDE] Fix crash vy adding null check for card in PrintingSelector (#6500) --- .../interface/widgets/printing_selector/printing_selector.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp index 94deaf135..506395789 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector.cpp @@ -122,6 +122,10 @@ static QMap> tallyUuidCounts(const DeckListModel *model void PrintingSelector::updateCardAmounts() { + if (selectedCard.isNull()) { + return; + } + auto map = tallyUuidCounts(deckStateManager->getModel(), selectedCard->getName()); emit cardAmountsChanged(map); } From 0deaa9d9b4ab4e1dc1fe0cb90285b5aea5204bbe Mon Sep 17 00:00:00 2001 From: RickyRister <42636155+RickyRister@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:27:54 -0800 Subject: [PATCH 54/56] [DeckEditor] Don't change widget focus when adding card (#6503) --- .../widgets/deck_editor/deck_editor_deck_dock_widget.cpp | 7 +++++-- .../widgets/deck_editor/deck_editor_deck_dock_widget.h | 2 +- .../interface/widgets/deck_editor/deck_state_manager.cpp | 4 ++-- .../src/interface/widgets/deck_editor/deck_state_manager.h | 3 ++- .../widgets/printing_selector/card_amount_widget.cpp | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) 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 bf7688bb0..73bafe54f 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 @@ -459,12 +459,15 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() } } -void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex) +void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus) { deckView->clearSelection(); deckView->setCurrentIndex(newCardIndex); recursiveExpand(newCardIndex); - deckView->setFocus(Qt::FocusReason::MouseFocusReason); + + if (!preserveWidgetFocus) { + deckView->setFocus(Qt::FocusReason::MouseFocusReason); + } } void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index 39b298550..8dddf5882 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -100,7 +100,7 @@ private slots: void writeComments(); void writeBannerCard(int); void applyActiveGroupCriteria(); - void setSelectedIndex(const QModelIndex &newCardIndex); + void setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus); void updateHash(); void refreshShortcuts(); void updateShowBannerCardComboBox(bool visible); 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 d45416b17..412954bb8 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -182,7 +182,7 @@ QModelIndex DeckStateManager::addCard(const ExactCard &card, const QString &zone QModelIndex idx = modifyDeck(reason, [&card, &zone](auto model) { return model->addCard(card, zone); }); if (idx.isValid()) { - emit focusIndexChanged(idx); + emit focusIndexChanged(idx, true); } return idx; @@ -208,7 +208,7 @@ QModelIndex DeckStateManager::decrementCard(const ExactCard &card, const QString } if (idx.isValid()) { - emit focusIndexChanged(idx); + emit focusIndexChanged(idx, true); } return idx; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 0f3ba3255..4f1ec7e04 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -290,8 +290,9 @@ signals: /** * The selected card on any views connected to this deck should be changed to this index. * @param index The model index + * @param preserveWidgetFocus Whether to keep the widget focus unchanged */ - void focusIndexChanged(QModelIndex index); + void focusIndexChanged(QModelIndex index, bool preserveWidgetFocus); }; #endif // COCKATRICE_DECK_STATE_MANAGER_H \ No newline at end of file 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 1c09a42c1..25222f437 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -198,7 +198,7 @@ void CardAmountWidget::addPrinting(const QString &zone) }); if (newCardIndex.isValid()) { - emit deckStateManager->focusIndexChanged(newCardIndex); + emit deckStateManager->focusIndexChanged(newCardIndex, false); } } From 9ab398f08d5eca5a18335800acffaaa86d4ccda9 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 11 Jan 2026 12:48:31 +0100 Subject: [PATCH 55/56] [Doxygen] Add documentation for beta channel. (#6510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Doxygen] Add documentation for beta channel. Took 36 minutes * [Doxygen] Use default theme. Took 13 minutes --------- Co-authored-by: Lukas Brübach --- .../extra-pages/user_documentation/index.md | 14 ++++- .../user_documentation/settings/beta.md | 54 ++++++++++++++++++ doc/doxygen/images/release_channel_beta.png | Bin 0 -> 93843 bytes 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 doc/doxygen/extra-pages/user_documentation/settings/beta.md create mode 100644 doc/doxygen/images/release_channel_beta.png diff --git a/doc/doxygen/extra-pages/user_documentation/index.md b/doc/doxygen/extra-pages/user_documentation/index.md index 33104991a..29e76eff6 100644 --- a/doc/doxygen/extra-pages/user_documentation/index.md +++ b/doc/doxygen/extra-pages/user_documentation/index.md @@ -1,9 +1,17 @@ @page user_reference User Reference - -- @subpage search_syntax_help -- @subpage deck_search_syntax_help +# Deck Management + - @subpage creating_decks - @subpage importing_decks - @subpage editing_decks - @subpage exporting_decks + +# Release Channels + +- @subpage beta_release + +# Syntax Help + +- @subpage search_syntax_help +- @subpage deck_search_syntax_help \ No newline at end of file diff --git a/doc/doxygen/extra-pages/user_documentation/settings/beta.md b/doc/doxygen/extra-pages/user_documentation/settings/beta.md new file mode 100644 index 000000000..733af79f5 --- /dev/null +++ b/doc/doxygen/extra-pages/user_documentation/settings/beta.md @@ -0,0 +1,54 @@ +@page beta_release Beta Release + +The beta version of Cockatrice features all the newest features before they make their way into the latest stable +release. +It allows us to test these features ahead of time and you to give feedback on them. +We release new betas very frequently and usually release a follow-up beta with a fix within a day or two if we break +anything. In contrast, stable releases are released much less frequently but ensured to be stable. + +# What to Expect from the Beta + +Using the beta and giving feedback on it helps us improve Cockatrice a lot. +We do not perform any automatic data collection so your voluntary feedback is the only way for us to get a sense of what +you enjoy, expect, or would like to change. + +We highly appreciate your active participation and feedback. + +That being said, some words of warning should be issued: + +The beta release may contain unfinished features, bugs, or performance issues. +It is intended for testing and feedback and may be less stable than the default release. + +We recommend using the beta only if you are comfortable encountering issues and helping us improve Cockatrice. + +# Switching to the Beta + +To switch to the Beta, navigate to your settings, using the Tab "Cockatrice → Settings" or Ctrl + Shift + P (default +shortcut). + +Within the settings, in the first tab 'General', the first section 'Personal Settings' contains the setting 'Release +Channel'. + +\image html release_channel_beta.png + +Switch this to 'Beta'. + +Afterwards, close your settings, and use the 'Help' tab to search for a client update. You should be prompted to install +the beta version. + +# Switching back to Stable + +Follow the same steps as above, but set Release Channel to 'Default'. + +# Giving Feedback + +If you encounter a bug or unexpected behavior while using the beta, please report it on: + +- GitHub: https://github.com/Cockatrice/Cockatrice/issues +- Discord: https://discord.gg/4hNswvHeFd in #beta-testing + +When reporting issues, please include: + +- Your operating system +- The beta version number (found in 'Help → About Cockatrice') +- Steps to reproduce the problem, if possible diff --git a/doc/doxygen/images/release_channel_beta.png b/doc/doxygen/images/release_channel_beta.png new file mode 100644 index 0000000000000000000000000000000000000000..00869a9cf2ac24caadeafa0209a6a0968394f70b GIT binary patch literal 93843 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%0AvLK06l0PwF*pFV(^b07*qoM6N<$g3B%0+5i9m literal 0 HcmV?d00001 From 52547bbfe8ee1d9829b890252bd599b8f7e9a844 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:41:16 +0100 Subject: [PATCH 56/56] Updates for project Cockatrice and language de (#6508) * Translate oracle/oracle_en@source.ts in de 100% translated source file: 'oracle/oracle_en@source.ts' on 'de'. * Translate oracle/oracle_en@source.ts in de 100% translated source file: 'oracle/oracle_en@source.ts' on 'de'. --------- Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- oracle/translations/oracle_de.ts | 216 +++++++++++++++++++------------ 1 file changed, 134 insertions(+), 82 deletions(-) diff --git a/oracle/translations/oracle_de.ts b/oracle/translations/oracle_de.ts index b3f1d9259..bdf344048 100644 --- a/oracle/translations/oracle_de.ts +++ b/oracle/translations/oracle_de.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Einführung - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Dieser Assistent wird eine Liste aller Editionen, Karten und Spielsteine importieren, die von Cockatrice genutzt werden. - + Interface language: Interfacesprache: - + Version: Version: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Quellenauswahl - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Bitte geben Sie eine kompatible Quelle für die Liste der Editionen und Karten an. Sie können eine URL-Adresse zum Herunterladen angeben oder eine existierende Datei von Ihrem Computer verwenden. - + Download URL: Download URL: - + Local file: Lokale Datei: - + Restore default URL Standard-URL wiederherstellen - + Choose file... Datei auswählen... - + Load sets file Editionsdatei wird geladen - + Sets file (%1) Sets JSON file (%1) Sets Datei (%1) - - - - - - - + + + + + + + Error Fehler - + The provided URL is not valid. Die eingegebene URL ist nicht gültig. - + Downloading (0MB) Herunterladen (0MB) - + Please choose a file. - Bitte wählen Sie eine Datei. + Bitte wähle eine Datei aus. - + Cannot open file '%1'. Datei '%1' kann nicht geöffnet werden. - + Downloading (%1MB) Herunterladen (%1MB) - + Network error: %1. Netzwerkfehler: %1. - + Parsing file Datei wird verarbeitet - + Xz extraction failed. Fehler beim Extrahieren der xz Datei. - + Sorry, this version of Oracle does not support xz compressed files. Es tut uns Leid, diese Version von Oracle unterstützt keine xz komprimierten Dateien. - + Failed to open Zip archive: %1. Fehler beim Öffnen des Zip Archivs: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Fehler beim Extrahieren: Das Zip Archiv enthält mehr als eine Datei. - + Zip extraction failed: %1. Fehler beim Extrahieren: %1. - + Sorry, this version of Oracle does not support zipped files. Es tut uns Leid, diese Version von Oracle unterstützt keine Zip Archive. - + Failed to interpret downloaded data. Interpretation der heruntergeladenen Daten fehlgeschlagen. - + Do you want to download the uncompressed file instead? Möchten Sie stattdessen die unkomprimierte Datei herunterladen? - + The file was retrieved successfully, but it does not contain any sets data. Die Datei wurde erfolgreich abgerufen, sie enthält aber keine Editionsdaten. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Speichere Spoilerdatenbank - + XML; spoiler database (*.xml) XML; Spoilerdatenbank (*.xml) - + + spoiler + Spoiler + + + Spoilers import Spoilerimport - + Please specify a compatible source for spoiler data. Bitte geben Sie eine kompatible Quelle für Spoilerdaten an. - + Download URL: Download URL: - + + Local file: + Lokale Datei: + + + Restore default URL Standard-URL wiederherstellen - + + Choose file... + Datei auswählen... + + + The spoiler database will be saved at the following location: Die Spoilerdatenbank wird in folgendem Pfad gespeichert: - + Save to a custom path (not recommended) Speichere in benutzerdefiniertem Pfad (nicht empfohlen) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Speichere Spielsteindatenbank - + XML; token database (*.xml) XML; Spielsteindatenbank (*.xml) - + + tokens + Spielsteine + + + Tokens import Spielsteinimport - + Please specify a compatible source for token data. Bitte geben Sie eine kompatible Quelle für Spielsteindaten an. - + Download URL: Download URL: - + + Local file: + Lokale Datei: + + + Restore default URL Standard-URL wiederherstellen - + + Choose file... + Datei auswählen... + + + The token database will be saved at the following location: Die Spielsteindatenbank wird in folgendem Pfad gespeichert: - + Save to a custom path (not recommended) Speichere in benutzerdefiniertem Pfad (nicht empfohlen) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Platzhalter Edition mit Spielsteinen @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle Importer @@ -262,22 +292,22 @@ OutroPage - + Finished Fertig - + The wizard has finished. Der Wizard ist fertig. - + You can now start using Cockatrice with the newly updated cards. Sie können nun Cockatrice mit den aktuellen Karten verwenden. - + If the card databases don't reload automatically, restart the Cockatrice client. Falls die Datenbanken nicht automatisch neu geladen werden, starten Sie bitte Cockatrice neu. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Fehler - + No set has been imported. Es wurden keine Editionen importiert. - + Sets imported Editionen wurden importiert - + A cockatrice database file of %1 MB has been downloaded. Eine Cockatrice-Datenbankdatei der Größe %1 MB wurde heruntergeladen. - + The following sets have been found: Die folgenden Sets wurden gefunden: - + Press "Save" to store the imported cards in the Cockatrice database. Drücken Sie "Speichern", um die importierten Karten in der Datenbank zu speichern. - + The card database will be saved at the following location: Die Kartendatenbank wird in folgendem Pfad gespeichert: - + Save to a custom path (not recommended) Speichere in benutzerdefiniertem Pfad (nicht empfohlen) - + &Save &Speichern - + Import finished: %1 cards. Importieren abgeschlossen: %1 Karten. - + %1: %2 cards imported %1: %2 Karten importiert. - + Save card database Kartendatenbank speichern - + XML; card database (*.xml) XML; Kartendatenbank (*.xml) - + The file could not be saved to %1 Die Datei konnte nicht gespeichert werden: %1 @@ -360,34 +390,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + Lade %1 Datei + + + + %1 file (%1) + %1 Datei (%1) + + + + + + + Error Fehler - + The provided URL is not valid: Die bereitgestellte URL ist nicht gültig: - + Downloading (0MB) Herunterladen (0MB) - + + Please choose a file. + Bitte wähle eine Datei aus. + + + + Cannot open file '%1'. + Datei '%1' kann nicht geöffnet werden. + + + Downloading (%1MB) Herunterladen (%1MB) - + Network error: %1. Netzwerkfehler: %1. - + The file could not be saved to %1 Die Datei konnte nicht in %1 gespeichert werden @@ -536,7 +588,7 @@ i18n - + English Deutsch (German)