diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 75fbc59f1..e895e2220 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: steps: - name: "Checkout repository" - uses: actions/checkout@v7 + uses: actions/checkout@v6 - name: "Initialize CodeQL" uses: github/codeql-action/init@v4 diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 255e8b045..df4fe233c 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -127,7 +127,7 @@ jobs: steps: - name: "Download digests" - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: path: ${{ runner.temp }}/digests pattern: digest-* diff --git a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp index c51830cf0..1cbe42e48 100644 --- a/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/card_database_view.cpp @@ -151,7 +151,8 @@ void CardDatabaseView::openCustomMenu(QPoint point) if (zoneMenuProvider) { QMenu *addToZoneMenu = menu.addMenu(tr("Add to Zone")); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { + for (const QString &boardName : + {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { QAction *action = addToZoneMenu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); connect(action, &QAction::triggered, this, [this, card, boardName] { emit cardAdded(card->getName(), boardName); }); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index d78758375..d3263a8c8 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -26,17 +26,17 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck [deckEditor]() -> QList> { QList> result; auto *deckListModel = deckEditor->deckStateManager->getModel(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { + for (const QString &boardName : + {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { result.append({boardName, deckListModel->getCustomZoneNames(boardName)}); } return result; }, [this, deckEditor] { QString boardName; - const QString zoneName = - DeckZoneDialog::promptForNewZone(this, {}, &boardName, [deckEditor](const QString &candidate) { - return deckEditor->deckStateManager->validateNewZoneName(candidate); - }); + const QString zoneName = promptForNewZone(this, {}, &boardName, [deckEditor](const QString &candidate) { + return deckEditor->deckStateManager->validateNewZoneName(candidate); + }); if (!zoneName.isEmpty()) { deckEditor->deckStateManager->createCustomZone(boardName, zoneName); } 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 9296f8697..a65efdb5c 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 @@ -793,7 +793,7 @@ void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point) QAction *renameAction = menu.addAction(tr("&Rename zone...")); connect(renameAction, &QAction::triggered, this, [this, zoneName] { - const QString newName = DeckZoneDialog::promptForRename(this, zoneName, [this](const QString &candidate) { + const QString newName = promptForRename(this, zoneName, [this](const QString &candidate) { return deckStateManager->validateNewZoneName(candidate); }); if (!newName.isEmpty() && newName != zoneName) { @@ -843,14 +843,14 @@ void DeckEditorDeckDockWidget::addMoveToZoneMenu(QMenu *menu, const QModelIndex deckStateManager->moveCardToZone(sourceCardIndex, targetZoneName); }; - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { + for (const QString &boardName : {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { QAction *action = menu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); connect(action, &QAction::triggered, this, [moveToZone, boardName] { moveToZone(boardName); }); } const auto tree = deckStateManager->getDeckListShared()->getTree(); bool anyCustomZone = false; - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { + for (const QString &boardName : {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { QList customZones = tree->getCustomZones(boardName); if (customZones.isEmpty()) { continue; @@ -873,7 +873,7 @@ void DeckEditorDeckDockWidget::addMoveToZoneMenu(QMenu *menu, const QModelIndex void DeckEditorDeckDockWidget::addChangeBoardMenu(QMenu *menu, const QString &zoneName) { const auto tree = deckStateManager->getDeckListShared()->getTree(); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { + for (const QString &boardName : {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { QAction *action = menu->addAction(InnerDecklistNode::visibleNameFromName(boardName)); // The board currently holding the zone is marked instead of offered. @@ -902,10 +902,9 @@ void DeckEditorDeckDockWidget::addNewZoneAction(QMenu *menu, const QString &init QAction *newZoneAction = menu->addAction(tr("Create &new zone...")); connect(newZoneAction, &QAction::triggered, this, [this, initialBoardName] { QString boardName; - const QString zoneName = - DeckZoneDialog::promptForNewZone(this, initialBoardName, &boardName, [this](const QString &candidate) { - return deckStateManager->validateNewZoneName(candidate); - }); + const QString zoneName = promptForNewZone(this, initialBoardName, &boardName, [this](const QString &candidate) { + return deckStateManager->validateNewZoneName(candidate); + }); if (!zoneName.isEmpty()) { deckStateManager->createCustomZone(boardName, 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 b60685d20..46850521d 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -409,7 +409,7 @@ bool DeckStateManager::moveCustomZone(const QString &zoneName, const QString &ne // Locate the board currently holding the zone. QString currentBoardName; - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { + for (const QString &boardName : {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { for (const auto *zone : tree->getCustomZones(boardName)) { if (zone->getName() == zoneName) { currentBoardName = boardName; @@ -478,7 +478,7 @@ QString DeckStateManager::validateNewZoneName(const QString &zoneName) const } // Custom zone names are unique across the whole deck. - for (const QString &board : InnerDecklistNode::boardZoneNames()) { + for (const QString &board : {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { for (const auto *customZone : tree->getCustomZones(board)) { if (customZone->getName() == trimmedZoneName) { return tr("A zone with this name already exists."); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp index a14beec0e..a159f411b 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.cpp @@ -20,16 +20,13 @@ DeckZoneDialog::DeckZoneDialog(QWidget *parent, nameEdit->setMaxLength(MAX_NAME_LENGTH); errorLabel = new QLabel(this); + errorLabel->setStyleSheet("color: palette(bright-text);"); errorLabel->hide(); boardLabel = new QLabel(this); boardCombo = new QComboBox(this); - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { - // Use the icon overload explicitly so `boardName` lands in the user data role - // (visible text is applied below in retranslateUi). The two-argument form - // addItem({}, boardName) would be ambiguous and resolve to the icon overload - // with empty user data, yielding empty entries and an empty getBoardName(). - boardCombo->addItem({}, {}, boardName); + for (const QString &boardName : {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { + boardCombo->addItem({}, boardName); } if (!initialBoardName.isEmpty()) { int idx = boardCombo->findData(initialBoardName); @@ -116,10 +113,10 @@ void DeckZoneDialog::validateName() buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error.isEmpty()); } -QString DeckZoneDialog::promptForNewZone(QWidget *parent, - const QString &initialBoardName, - QString *chosenBoardName, - const std::function &nameValidator) +QString promptForNewZone(QWidget *parent, + const QString &initialBoardName, + QString *chosenBoardName, + const std::function &nameValidator) { DeckZoneDialog dialog(parent, initialBoardName, nameValidator); if (dialog.exec() != QDialog::Accepted) { @@ -132,9 +129,9 @@ QString DeckZoneDialog::promptForNewZone(QWidget *parent, return dialog.getZoneName(); } -QString DeckZoneDialog::promptForRename(QWidget *parent, - const QString ¤tZoneName, - const std::function &nameValidator) +QString promptForRename(QWidget *parent, + const QString ¤tZoneName, + const std::function &nameValidator) { DeckZoneDialog dialog(parent, {}, nameValidator, false); dialog.setZoneName(currentZoneName); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h index 6f55617a8..d1a3572ee 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h @@ -62,39 +62,6 @@ public: */ void setZoneName(const QString &zoneName); - /** - * @brief Prompts the user for a new custom zone name and the board zone to nest it under. - * - * Convenience wrapper that runs DeckZoneDialog modally. - * - * @param parent The parent widget for the dialog - * @param initialBoardName The board zone to preselect in the dialog. Unknown names fall - * back to main. - * @param chosenBoardName (out) The internal name of the board zone the user chose - * @param nameValidator Optional validator forwarded to the dialog - * @return The trimmed zone name, or an empty string if the user cancelled - */ - static QString promptForNewZone(QWidget *parent, - const QString &initialBoardName, - QString *chosenBoardName, - const std::function &nameValidator = {}); - - /** - * @brief Prompts the user for a new name for an existing custom zone. - * - * Same inline validation as promptForNewZone, but without a parent-zone picker. - * - * @param parent The parent widget for the dialog - * @param currentZoneName The current name, prefilled for editing - * @param nameValidator Validator deciding whether a candidate name is usable. It sees - * the current name too, so callers wanting to allow unchanged names must - * special-case that themselves. - * @return The trimmed new name, or an empty string if the user cancelled - */ - static QString promptForRename(QWidget *parent, - const QString ¤tZoneName, - const std::function &nameValidator = {}); - protected: void changeEvent(QEvent *event) override; @@ -120,4 +87,37 @@ private: bool allowBoardSelection; }; +/** + * @brief Prompts the user for a new custom zone name and the board zone to nest it under. + * + * Convenience wrapper that runs DeckZoneDialog modally. + * + * @param parent The parent widget for the dialog + * @param initialBoardName The board zone to preselect in the dialog. Unknown names fall + * back to main. + * @param chosenBoardName (out) The internal name of the board zone the user chose + * @param nameValidator Optional validator forwarded to the dialog + * @return The trimmed zone name, or an empty string if the user cancelled + */ +QString promptForNewZone(QWidget *parent, + const QString &initialBoardName, + QString *chosenBoardName, + const std::function &nameValidator = {}); + +/** + * @brief Prompts the user for a new name for an existing custom zone. + * + * Same inline validation as promptForNewZone, but without a parent-zone picker. + * + * @param parent The parent widget for the dialog + * @param currentZoneName The current name, prefilled for editing + * @param nameValidator Validator deciding whether a candidate name is usable. It sees + * the current name too, so callers wanting to allow unchanged names must + * special-case that themselves. + * @return The trimmed new name, or an empty string if the user cancelled + */ +QString promptForRename(QWidget *parent, + const QString ¤tZoneName, + const std::function &nameValidator = {}); + #endif // DECK_ZONE_DIALOG_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp index a4a31d78d..198fa259b 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp @@ -1,17 +1,9 @@ #include "dlg_convert_deck_to_cod_format.h" -#include "../../../client/settings/cache_settings.h" -#include "../../deck_loader/deck_loader.h" - #include #include -#include -#include -#include #include -#include #include -#include DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent) { @@ -46,71 +38,3 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const { return dontAskAgainCheckbox->isChecked(); } - -namespace -{ - -bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) -{ - QFileInfo fileInfo(filePath); - QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); - - if (QFile::exists(newFileName)) { - QMessageBox::StandardButton reply = - QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), - QObject::tr("A .cod version of this deck already exists. Overwrite it?"), - QMessageBox::Yes | QMessageBox::No); - return reply == QMessageBox::Yes; - } - return true; // Safe to proceed -} - -} // namespace - -bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent, - const QString &filePath, - const std::function &convert) -{ - if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) { - return true; - } - - // Retrieve saved preference if the prompt is disabled - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { - return false; - } - - if (!confirmOverwriteIfExists(parent, filePath)) { - return false; - } - - return convert(); - } - - // Show the dialog to the user - DialogConvertDeckToCodFormat conversionDialog(parent); - if (conversionDialog.exec() != QDialog::Accepted) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( - !conversionDialog.dontAskAgain()); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); - - return false; - } - - // Try to convert file - if (!confirmOverwriteIfExists(parent, filePath)) { - return false; - } - - if (!convert()) { - return false; - } - - if (conversionDialog.dontAskAgain()) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); - } - - return true; -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h index 526582135..6642ad8c6 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h @@ -13,9 +13,6 @@ #include #include #include -#include - -class QWidget; class DialogConvertDeckToCodFormat : public QDialog { @@ -27,21 +24,6 @@ public: [[nodiscard]] bool dontAskAgain() const; - /** - * @brief Checks whether the deck file at \a filePath can store tags. - * - * If the file is not a .cod deck, prompts the user for conversion to the - * Cockatrice format, honoring the saved "always convert / don't ask again" - * preference. On acceptance \a convert is called to perform the conversion. - * - * @param parent The widget to parent the prompt to. - * @param filePath The path of the deck file to check. - * @param convert Called to convert the deck once the user agrees. - * @return true if tags can be stored (no conversion needed, or the conversion - * was performed), false if the user declined to convert. - */ - static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function &convert); - private: QVBoxLayout *layout; QLabel *label; diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index fb610e814..f6f34a6a5 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -525,13 +525,6 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); }); add(games); - // ── Invite (only while the inviter has a joinable game for this user) ──── - if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) { - auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme); - connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); }); - add(invite); - } - // ── Buddy / ignore (registered users only) ──────────────────────────────── if (!isSelf && isReg) { if (isBuddy) { diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h index ed7320fba..02cc2b44e 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -150,17 +149,6 @@ public: /** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */ void refreshHeader(); - /** - * Sets a predicate evaluated on every action-button rebuild. It receives - * the name of the user the popup currently shows; when it returns true an - * "Invite" button is shown. The popup itself never resolves the invite - * link, it just forwards the request. - */ - void setGameInviteAvailable(std::function available) - { - gameInviteAvailable = std::move(available); - } - signals: void mouseEnteredPopup(); void mouseLeftPopup(); @@ -171,7 +159,6 @@ signals: // ── Action signals — connect to UserContextMenu::exec*() ────────────────── void chatRequested(const QString &userName); - void inviteRequested(const QString &userName); void detailsRequested(const QString &userName); void showGamesRequested(const QString &userName); void addBuddyRequested(const QString &userName); @@ -213,7 +200,6 @@ private: QString currentUser; ServerInfo_User currentUserInfo; bool currentOnline = false; - std::function gameInviteAvailable; UserInfoHeaderWidget *header; QWidget *actionArea; ///< rebuilt per user diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index a8c99c979..2cacfc4f9 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -345,11 +345,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, &cardArtProvider->cache(), &cardArtParamsMap, window()); // parented to main window so it floats above siblings - // The invite availability is scoped to the room this list belongs to, - // and gated on the room's buddy-only setting for the hovered user. - userInfoPopup->setGameInviteAvailable( - [this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); }); - userInfoPopup->hide(); userInfoPopup->setWindowOpacity(0.0); userInfoPopup->installEventFilter(this); @@ -667,8 +662,6 @@ void UserListWidget::connectPopupSignals() // Wire all action signals to UserContextMenu::exec*() connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat); - connect(userInfoPopup, &UserInfoPopup::inviteRequested, this, - [this](const QString &userName) { userContextMenu->execInvite(userName); }); connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails); connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames); connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 412271160..7531ef925 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -22,7 +22,6 @@ #include #include #include -#include #include class QTreeWidget; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index b0dac3e7c..f96c139b3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -1091,8 +1091,7 @@ QList TabSupervisor::getGameInviteLinksForRoom(int roomId) con // The inviter may be in several games of the same room (hosting one and // spectating another, for example). Return every game so the caller can // let the user choose which one to invite to. - for (auto it = gameTabs.cbegin(); it != gameTabs.cend(); ++it) { - TabGame *tab = it.value(); + for (TabGame *tab : gameTabs) { GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo(); if (metaInfo->proto().room_id() != roomId) { continue; 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 46d1b08b6..a093bca53 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 @@ -275,7 +275,7 @@ bool TabDeckEditorVisual::actSaveDeckAs() void TabDeckEditorVisual::createNewZone() { QString boardName; - const QString zoneName = DeckZoneDialog::promptForNewZone(this, {}, &boardName, [this](const QString &candidate) { + const QString zoneName = promptForNewZone(this, {}, &boardName, [this](const QString &candidate) { return deckStateManager->validateNewZoneName(candidate); }); if (!zoneName.isEmpty()) { 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 2b58142b7..b65987767 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 @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -93,9 +92,10 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, // Without a deck model there is nothing to add cards to, so the zone menu stays hidden. if (deckListModel) { databaseView->setZoneMenuProvider( - [deckListModel]() -> QList> { + [this, deckListModel]() -> QList> { QList> result; - for (const QString &boardName : InnerDecklistNode::boardZoneNames()) { + for (const QString &boardName : + {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), QString(DECK_ZONE_MAYBEBOARD)}) { result.append({boardName, deckListModel->getCustomZoneNames(boardName)}); } return result; 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 876fbf6ad..04dcdf7f2 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 @@ -10,6 +10,8 @@ #include "../visual_deck_storage_widget.h" #include "deck_preview_deck_tags_display_widget.h" +#include +#include #include #include #include @@ -497,6 +499,21 @@ void DeckPreviewWidget::actDeleteFile() // The folder widget removes this preview once the row is gone. } +static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) +{ + QFileInfo fileInfo(filePath); + QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); + + if (QFile::exists(newFileName)) { + QMessageBox::StandardButton reply = + QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), + QObject::tr("A .cod version of this deck already exists. Overwrite it?"), + QMessageBox::Yes | QMessageBox::No); + return reply == QMessageBox::Yes; + } + return true; // Safe to proceed +} + /** * Checks if the deck's file format supports tags. * If not, then prompt the user for file conversion. @@ -504,8 +521,45 @@ void DeckPreviewWidget::actDeleteFile() */ bool DeckPreviewWidget::promptFileConversionIfRequired() { - return DialogConvertDeckToCodFormat::promptIfRequired(this, filePath, [this] { + if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) { + return true; + } + + // Retrieve saved preference if the prompt is disabled + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { + return false; + } + + if (!confirmOverwriteIfExists(this, filePath)) { + return false; + } + model->convertToCockatriceFormat(row()); return true; - }); + } + + // Show the dialog to the user + DialogConvertDeckToCodFormat conversionDialog(this); + if (conversionDialog.exec() != QDialog::Accepted) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( + !conversionDialog.dontAskAgain()); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); + + return false; + } + + // Try to convert file + if (!confirmOverwriteIfExists(this, filePath)) { + return false; + } + + model->convertToCockatriceFormat(row()); + + if (conversionDialog.dontAskAgain()) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); + } + + return true; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp index 5e7ba403b..ec860dc56 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp @@ -43,13 +43,6 @@ void InnerDecklistNode::setSortMethod(DeckSortMethod method) } } -const QList &InnerDecklistNode::boardZoneNames() -{ - static const QList names = {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE), - QString(DECK_ZONE_MAYBEBOARD)}; - return names; -} - QString InnerDecklistNode::getVisibleName() const { return visibleNameFromName(name); diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h index 0d454c11e..906ed6cb5 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h @@ -18,9 +18,6 @@ #include "abstract_deck_list_node.h" -#include -#include - /** @brief Constant for the "main" deck zone name. */ #define DECK_ZONE_MAIN "main" /** @brief Constant for the "sideboard" zone name. */ @@ -121,13 +118,6 @@ public: */ static QString visibleNameFromName(const QString &_name); - /** - * @brief The standard board zone names, in display order. - * - * @return main, side and maybeboard. - */ - static const QList &boardZoneNames(); - /** * @brief Get this node’s display-friendly name. * @return Human-readable name (zone/group name). diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt index a6ab2a204..d4aee3686 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -7,8 +7,7 @@ set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) add_library( - libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_model_custom_zones.cpp - deck_list_sort_filter_proxy_model.cpp + libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp ) target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 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 ad278c8bf..924aebf78 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -66,9 +66,18 @@ void DeckListModel::rebuildTree() for (int j = 0; j < currentZone->size(); j++) { auto *currentCard = dynamic_cast(currentZone->at(j)); - // Non-card children are custom zones; they are mirrored in a single - // pass below so each is mirrored exactly once. + //! \todo Better sanity checking. if (currentCard == nullptr) { + // Custom zones nested under the board zone are mirrored as-is, + // with their cards as direct children (no further grouping). + if (auto *customZone = dynamic_cast(currentZone->at(j))) { + auto *shadowZone = new DecklistModelSubZoneNode(customZone->getName(), node); + for (int k = 0; k < customZone->size(); k++) { + if (auto *customCard = dynamic_cast(customZone->at(k))) { + new DecklistModelCardNode(customCard, shadowZone); + } + } + } continue; } @@ -83,10 +92,6 @@ void DeckListModel::rebuildTree() new DecklistModelCardNode(currentCard, groupNode); } - - // Custom zones nested under the board zone are mirrored as-is, with their - // cards as direct children (no further grouping). - DeckListModelCustomZones::mirrorCustomZones(currentZone, node); } // The shadow tree was built in deck file order. Apply the active sort while @@ -167,7 +172,7 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const return true; case DeckRoles::IsCustomZoneRole: - return DeckListModelCustomZones::isCustomZone(group); + return dynamic_cast(group) != nullptr; default: return {}; @@ -348,7 +353,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) // Custom zone rows are managed through the deck tree, never removed as model rows. for (int i = 0; i < count; i++) { - if (DeckListModelCustomZones::isCustomZone(node->at(row + i))) { + if (dynamic_cast(node->at(row + i))) { return false; } } @@ -364,7 +369,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) endRemoveRows(); // Empty criteria groups get pruned, but custom zones stay until explicitly deleted. - if (node->empty() && (node != root) && !DeckListModelCustomZones::isCustomZone(node)) { + if (node->empty() && (node != root) && !dynamic_cast(node)) { removeRows(parent.row(), 1, parent.parent()); } else { emitRecursiveUpdates(parent); @@ -409,10 +414,7 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, } for (auto *child : *zoneNode) { - if (!DeckListModelCustomZones::isCustomZone(child)) { - continue; - } - auto *customZone = dynamic_cast(child); + auto *customZone = dynamic_cast(child); if (!customZone) { continue; } @@ -424,7 +426,7 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, } // 2. Custom zone lookup by name (custom zone names are deck-unique). - if (auto *customZone = DeckListModelCustomZones::findSubZoneByName(root, zoneName)) { + if (auto *customZone = findSubZoneNodeByName(zoneName)) { return dynamic_cast( customZone->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber)); } @@ -445,6 +447,25 @@ QModelIndex DeckListModel::findCard(const QString &cardName, return nodeToIndex(cardNode); } +DecklistModelSubZoneNode *DeckListModel::findSubZoneNodeByName(const QString &zoneName) const +{ + for (int i = 0; i < root->size(); i++) { + auto *boardZone = dynamic_cast(root->at(i)); + if (!boardZone) { + continue; + } + + for (int j = 0; j < boardZone->size(); j++) { + auto *customZone = dynamic_cast(boardZone->at(j)); + if (customZone && customZone->getName() == zoneName) { + return customZone; + } + } + } + + return nullptr; +} + QModelIndex DeckListModel::addPreferredPrintingCard(const QString &cardName, const QString &zoneName, bool abAddAnyway) { ExactCard card = CardDatabaseManager::query()->getCard({cardName}); @@ -476,7 +497,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam InnerDecklistNode *cardParent = nullptr; auto *boardNode = dynamic_cast(root->findChild(zoneName)); - auto *customZoneNode = boardNode ? nullptr : DeckListModelCustomZones::findSubZoneByName(root, zoneName); + auto *customZoneNode = boardNode ? nullptr : findSubZoneNodeByName(zoneName); if (boardNode) { // Board zone: cards are grouped by the active criteria. @@ -637,18 +658,39 @@ QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const return createIndex(node->getParent()->indexOf(node), 0, node); } +/** + * @brief Moves custom zones of a board zone after the criteria groups, stably. + * + * Sorting would interleave custom zones with criteria groups by name; custom + * zones must always stay after the groups, regardless of their names. + */ +void DeckListModel::shiftCustomZonesToEnd(InnerDecklistNode *boardZone) +{ + QList customZones; + for (int i = boardZone->size() - 1; i >= 0; --i) { + if (dynamic_cast(boardZone->at(i))) { + customZones.prepend(boardZone->takeAt(i)); + } + } + for (auto *customZone : customZones) { + boardZone->append(customZone); + } +} + /** * @brief Sorts a freshly built shadow subtree without emitting model signals. * * Used by rebuildTree while the model reset is still open (emitting layout - * changes during a reset is invalid). Reorders every node just like - * sortHelper does, but ignores the movement mapping because there are no - * persistent indices established yet. + * changes during a reset is invalid). Mirrors the reordering part of + * sortHelper: sort every node, then move board-level custom zones after groups. */ void DeckListModel::sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order) { - // The mapping is not needed: fresh shadow nodes have no persistent indices yet. - (void)DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); + node->sort(order); + + if (node != root && node->getParent() == root) { + shiftCustomZonesToEnd(node); + } for (int i = node->size() - 1; i >= 0; --i) { if (auto *subNode = dynamic_cast(node->at(i))) { @@ -659,19 +701,33 @@ void DeckListModel::sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order) void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order) { - // Sort children (custom zones always sorted after groups within a board) and - // use the movement mapping to update the list of persistent indices. - const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order); + // Sort children of node and save the information needed to + // update the list of persistent indexes. + QVector> sortResult = node->sort(order); + + // For board zones, custom zones must stay after the criteria groups, + // regardless of their names. Shift them to the end (stable). + QHash rowAfterSort; + bool isBoardZone = (node != root) && (node->getParent() == root); + if (isBoardZone) { + for (int i = 0; i < node->size(); ++i) { + rowAfterSort.insert(node->at(i), i); + } + shiftCustomZonesToEnd(node); + } QModelIndexList from, to; int columns = columnCount(); - for (const auto &move : mapping) { - const int preSortRow = move.first; - const int finalRow = move.second; - AbstractDecklistNode *temp = node->at(finalRow); + for (int newRow = 0; newRow < node->size(); ++newRow) { + AbstractDecklistNode *temp = node->at(newRow); + int preSortRow = sortResult.value(newRow).first; + if (isBoardZone) { + const int rowAfterSortOfNode = rowAfterSort.value(temp, newRow); + preSortRow = sortResult.value(rowAfterSortOfNode).first; + } for (int j = 0; j < columns; ++j) { from << createIndex(preSortRow, j, temp); - to << createIndex(finalRow, j, temp); + to << createIndex(newRow, j, temp); } } changePersistentIndexList(from, to); 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 09600ca67..f00b61558 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -1,8 +1,6 @@ #ifndef DECKLISTMODEL_H #define DECKLISTMODEL_H -#include "deck_list_model_custom_zones.h" - #include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h> #include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> #include @@ -193,6 +191,21 @@ public: } }; +/** + * @class DecklistModelSubZoneNode + * @ingroup DeckModels + * @brief Model node representing a custom zone nested under a board zone. + * + * Custom zones group cards by user-defined names (e.g. "Removal", "Utility") + * inside a board zone. They are mirrored from the underlying deck tree so that + * they can be told apart from criteria group nodes. + */ +class DecklistModelSubZoneNode : public InnerDecklistNode +{ +public: + using InnerDecklistNode::InnerDecklistNode; +}; + /** * @class DeckListModel * @ingroup DeckModels @@ -416,6 +429,16 @@ private: const QString &providerId = "", const QString &cardNumber = "") const; + /** + * @brief Finds a custom zone node in the model tree by name. + * + * Custom zone names are unique across the whole deck. + * + * @param zoneName Name of the custom zone. + * @return The custom zone node, or nullptr if not found. + */ + [[nodiscard]] DecklistModelSubZoneNode *findSubZoneNodeByName(const QString &zoneName) const; + /** * @brief Determines the sorted insertion row for a card. * @param parent The parent node where the card will be inserted. @@ -439,6 +462,7 @@ private: void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); void sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order); + void shiftCustomZonesToEnd(InnerDecklistNode *boardZone); template T getNode(const QModelIndex &index) const { diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp deleted file mode 100644 index f10ee1a72..000000000 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include "deck_list_model_custom_zones.h" - -#include "deck_list_model.h" - -#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> -#include -#include - -namespace DeckListModelCustomZones -{ - -bool isCustomZone(const AbstractDecklistNode *node) -{ - return dynamic_cast(node) != nullptr; -} - -void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone) -{ - for (int j = 0; j < deckBoardZone->size(); j++) { - auto *customCard = dynamic_cast(deckBoardZone->at(j)); - if (customCard) { - continue; - } - - auto *customZone = dynamic_cast(deckBoardZone->at(j)); - if (!customZone) { - continue; - } - - auto *shadowZone = new DecklistModelSubZoneNode(customZone->getName(), shadowBoardZone); - for (int k = 0; k < customZone->size(); k++) { - if (auto *zoneCard = dynamic_cast(customZone->at(k))) { - new DecklistModelCardNode(zoneCard, shadowZone); - } - } - } -} - -DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName) -{ - for (int i = 0; i < root->size(); i++) { - auto *boardZone = dynamic_cast(root->at(i)); - if (!boardZone) { - continue; - } - - for (int j = 0; j < boardZone->size(); j++) { - auto *customZone = dynamic_cast(boardZone->at(j)); - if (customZone && customZone->getName() == zoneName) { - return customZone; - } - } - } - - return nullptr; -} - -namespace -{ - -/** - * @brief Sorts a node's children and returns the (preSortRow, finalRow) mapping. - */ -QList> plainSort(InnerDecklistNode *node, Qt::SortOrder order) -{ - const QVector> sortResult = node->sort(order); - - QList> mapping; - mapping.reserve(node->size()); - for (int i = 0; i < node->size(); ++i) { - mapping.append({sortResult[i].first, i}); - } - return mapping; -} - -/** - * @brief Sorts a board zone's children, then stably moves custom zones to the end. - * - * @return The (preSortRow, finalRow) mapping covering both the sort and the shift. - */ -QList> boardSort(InnerDecklistNode *node, Qt::SortOrder order) -{ - const QVector> sortResult = node->sort(order); - - QVector groups; - QVector customZones; - QHash preSortRowOf; - - groups.reserve(node->size()); - customZones.reserve(node->size()); - - for (int i = 0; i < node->size(); ++i) { - AbstractDecklistNode *child = node->at(i); - preSortRowOf.insert(child, sortResult[i].first); - if (isCustomZone(child)) { - customZones.append(child); - } else { - groups.append(child); - } - } - - QVector ordered = groups + customZones; - for (int i = 0; i < ordered.size(); ++i) { - node->replace(i, ordered[i]); - } - - QList> mapping; - mapping.reserve(ordered.size()); - for (int i = 0; i < ordered.size(); ++i) { - mapping.append({preSortRowOf.value(ordered[i]), i}); - } - return mapping; -} - -} // namespace - -QList> sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order) -{ - const bool isBoardZone = (node != root) && (node->getParent() == root); - return isBoardZone ? boardSort(node, order) : plainSort(node, order); -} - -} // namespace DeckListModelCustomZones diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h deleted file mode 100644 index a973b127e..000000000 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model_custom_zones.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef DECK_LIST_MODEL_CUSTOM_ZONES_H -#define DECK_LIST_MODEL_CUSTOM_ZONES_H - -#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h> -#include -#include -#include - -/** - * @class DecklistModelSubZoneNode - * @ingroup DeckModels - * @brief Model node representing a custom zone nested under a board zone. - * - * Custom zones group cards by user-defined names (e.g. "Removal", "Utility") - * inside a board zone. They are mirrored from the underlying deck tree so that - * they can be told apart from criteria group nodes by type. - */ -class DecklistModelSubZoneNode : public InnerDecklistNode -{ -public: - using InnerDecklistNode::InnerDecklistNode; -}; - -/** - * @namespace DeckListModelCustomZones - * @ingroup DeckModels - * @brief Tree-level helpers for the deck list model's custom-zone shadow nodes. - * - * The deck list model keeps a second "shadow" tree of InnerDecklistNode that - * mirrors the canonical deck tree for grouping and sorting. Custom zones add a - * layer of bookkeeping to that shadow tree: they must be mirrored alongside - * criteria groups, always sort after the groups within a board, and be - * resolvable by deck-unique name. - * - * This namespace centralizes every "what is / where is a custom zone" decision - * so the model itself only wires the results into Qt model signals. - */ -namespace DeckListModelCustomZones -{ - -/** - * @brief Whether the given node is a custom zone (as opposed to a criteria group). - */ -[[nodiscard]] bool isCustomZone(const AbstractDecklistNode *node); - -/** - * @brief Mirrors the custom zones of a deck board zone into its shadow board node. - * - * Each custom zone becomes a DecklistModelSubZoneNode under @p shadowBoardZone - * with its cards as direct (un-grouped) children. - * - * @param deckBoardZone The board zone in the canonical deck tree. - * @param shadowBoardZone The matching board zone in the model's shadow tree. - */ -void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone); - -/** - * @brief Finds a custom zone in the shadow tree by deck-unique name. - * @param root Root of the shadow tree. - * @param zoneName The custom zone name to find. - * @return The matching custom zone node, or nullptr if not found. - */ -[[nodiscard]] DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName); - -/** - * @brief Sorts a shadow node's children, keeping a board's custom zones last. - * - * Sorting alone would interleave custom zones with criteria groups by name, but - * custom zones must always stay after the groups within a board, regardless of - * name. This applies the sort and, for board zones, stably moves the custom - * zones to the end. - * - * @param root Root of the shadow tree (used to classify board zones). - * @param node The shadow node whose children are reordered. - * @param order Sort order to apply. - * @return A list of (preSortRow, finalRow) pairs describing how each node moved. - */ -[[nodiscard]] QList> -sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order); - -} // namespace DeckListModelCustomZones - -#endif // DECK_LIST_MODEL_CUSTOM_ZONES_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 18ab60d06..34784538b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -114,7 +114,6 @@ target_link_libraries( add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) -add_subdirectory(deck_list_model) add_subdirectory(deck_list_zones) add_subdirectory(loading_from_clipboard) add_subdirectory(movecard_tests) diff --git a/tests/deck_list_model/CMakeLists.txt b/tests/deck_list_model/CMakeLists.txt deleted file mode 100644 index 8f2cfcc2c..000000000 --- a/tests/deck_list_model/CMakeLists.txt +++ /dev/null @@ -1,33 +0,0 @@ -add_executable(deck_list_model_custom_zones_test deck_list_model_custom_zones_test.cpp) - -if(NOT GTEST_FOUND) - add_dependencies(deck_list_model_custom_zones_test gtest) -endif() - -target_link_libraries( - deck_list_model_custom_zones_test - libcockatrice_models - libcockatrice_card - libcockatrice_deck_list - Threads::Threads - ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) -add_test(NAME deck_list_model_custom_zones_test COMMAND deck_list_model_custom_zones_test) - -add_executable(deck_list_model_zone_integration_test ${VERSION_STRING_CPP} deck_list_model_zone_integration_test.cpp) - -if(NOT GTEST_FOUND) - add_dependencies(deck_list_model_zone_integration_test gtest) -endif() - -target_link_libraries( - deck_list_model_zone_integration_test - libcockatrice_models - libcockatrice_card - libcockatrice_deck_list - Threads::Threads - ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) -add_test(NAME deck_list_model_zone_integration_test COMMAND deck_list_model_zone_integration_test) diff --git a/tests/deck_list_model/deck_list_model_custom_zones_test.cpp b/tests/deck_list_model/deck_list_model_custom_zones_test.cpp deleted file mode 100644 index c51c72d8f..000000000 --- a/tests/deck_list_model/deck_list_model_custom_zones_test.cpp +++ /dev/null @@ -1,225 +0,0 @@ -/** - * @file deck_list_model_custom_zones_test.cpp - * @brief Tests for the deck list model's custom-zone shadow-tree helpers. - * - * DeckListModelCustomZones centralizes every "what is / where is a custom zone" - * decision for the model's shadow tree: type testing, mirroring from the deck - * tree, name lookup, and the sort-with-custom-zones-last ordering. These tests - * exercise that logic directly on hand-built shadow trees, independent of the - * full model and card database machinery. - */ - -#include -#include -#include -#include - -namespace -{ - -DecklistModelCardNode *cardNode(InnerDecklistNode *parent, const QString &name, int number) -{ - // The underlying data node is detached; only the model wrapper is attached to the shadow tree. - auto *data = new DecklistCardNode(name, number, nullptr); - return new DecklistModelCardNode(data, parent); -} - -QStringList childNames(const InnerDecklistNode *node) -{ - QStringList names; - for (int i = 0; i < node->size(); ++i) { - names.append(node->at(i)->getName()); - } - return names; -} - -} // namespace - -// ===================================================================================================================== -// isCustomZone -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, IsCustomZoneDistinguishesZoneFromGroup) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - auto *zone = new DecklistModelSubZoneNode("Removal", board); - - auto *card = cardNode(group, "A", 1); - - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(board)); - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(group)); - EXPECT_FALSE(DeckListModelCustomZones::isCustomZone(card)); - EXPECT_TRUE(DeckListModelCustomZones::isCustomZone(zone)); -} - -// ===================================================================================================================== -// findSubZoneByName -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, FindSubZoneByNameFindsAcrossBoards) -{ - InnerDecklistNode root; - auto *main = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *side = new InnerDecklistNode(DECK_ZONE_SIDE, &root); - new DecklistModelSubZoneNode("Removal", main); - new DecklistModelSubZoneNode("Utility", side); - new InnerDecklistNode("Plain", main); // not a custom zone - - auto *removal = DeckListModelCustomZones::findSubZoneByName(&root, "Removal"); - ASSERT_NE(removal, nullptr); - EXPECT_EQ(removal->getName(), QString("Removal")); - - auto *utility = DeckListModelCustomZones::findSubZoneByName(&root, "Utility"); - ASSERT_NE(utility, nullptr); - EXPECT_EQ(utility->getName(), QString("Utility")); - - // Names are deck-unique; a plain group or built-in board is not matched. - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Plain"), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, DECK_ZONE_MAIN), nullptr); - EXPECT_EQ(DeckListModelCustomZones::findSubZoneByName(&root, "Missing"), nullptr); -} - -// ===================================================================================================================== -// mirrorCustomZones -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, MirrorCustomZonesCopiesCardsFlat) -{ - // Deck-tree board zone: one direct card plus one nested custom zone. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - new DecklistCardNode("Direct", 2, deckBoard); - - auto *deckZone = new InnerDecklistNode("Removal", deckBoard); - auto *deckCard1 = new DecklistCardNode("Bolt", 3, deckZone); - auto *deckCard2 = new DecklistCardNode("Swords", 1, deckZone); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - - // Only the custom zone is mirrored as a sub-zone; the direct card is not. - ASSERT_EQ(shadowBoard->size(), 1); - auto *shadowZone = dynamic_cast(shadowBoard->at(0)); - ASSERT_NE(shadowZone, nullptr); - EXPECT_EQ(shadowZone->getName(), QString("Removal")); - - // Cards live flat (un-grouped) inside the mirrored zone, wrapping the same data nodes. - ASSERT_EQ(shadowZone->size(), 2); - auto *shadowCard1 = dynamic_cast(shadowZone->at(0)); - auto *shadowCard2 = dynamic_cast(shadowZone->at(1)); - ASSERT_NE(shadowCard1, nullptr); - ASSERT_NE(shadowCard2, nullptr); - EXPECT_EQ(shadowCard1->getDataNode(), deckCard1); - EXPECT_EQ(shadowCard2->getDataNode(), deckCard2); -} - -TEST(DeckListModelCustomZones, MirrorCustomZonesWithNoCustomZonesIsNoop) -{ - // A board zone with only direct cards has nothing to mirror. - auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN); - new DecklistCardNode("Direct", 2, deckBoard); - - InnerDecklistNode shadowRoot; - auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot); - - DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard); - EXPECT_EQ(shadowBoard->size(), 0); -} - -// ===================================================================================================================== -// sortWithCustomZonesLast -// ===================================================================================================================== - -TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsAscending) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - new DecklistModelSubZoneNode("Zebra", board); - new InnerDecklistNode("Creature", board); - new InnerDecklistNode("Instant", board); - new DecklistModelSubZoneNode("Alpha", board); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder); - - // Groups sort first (by name), then custom zones (by name), always after groups. - EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"})); - - // Some non-identity movement occurred. - EXPECT_FALSE(mapping.isEmpty()); -} - -TEST(DeckListModelCustomZones, SortBoardKeepsCustomZonesAfterGroupsDescending) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - new DecklistModelSubZoneNode("Zebra", board); - new InnerDecklistNode("Creature", board); - new InnerDecklistNode("Instant", board); - new DecklistModelSubZoneNode("Alpha", board); - - root.setSortMethod(DeckSortMethod::ByName); - - (void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::DescendingOrder); - - // Groups still lead (descending), custom zones still last. - EXPECT_EQ(childNames(board), (QStringList{"Instant", "Creature", "Zebra", "Alpha"})); -} - -TEST(DeckListModelCustomZones, SortBoardMappingIsConsistent) -{ - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - - QList originalOrder; - auto *g0 = new InnerDecklistNode("Creature", board); - originalOrder.append(g0); - auto *z0 = new DecklistModelSubZoneNode("Zebra", board); - originalOrder.append(z0); - auto *g1 = new InnerDecklistNode("Instant", board); - originalOrder.append(g1); - auto *z1 = new DecklistModelSubZoneNode("Alpha", board); - originalOrder.append(z1); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder); - - // The mapping reports, for each final row, the original row of the node now sitting there. - ASSERT_EQ(mapping.size(), board->size()); - for (const auto &move : mapping) { - const int preSortRow = move.first; - const int finalRow = move.second; - ASSERT_GE(preSortRow, 0); - ASSERT_LT(preSortRow, originalOrder.size()); - EXPECT_EQ(board->at(finalRow), originalOrder[preSortRow]) << "row " << finalRow; - } - - // Final order sanity: groups first in name order, then custom zones. - EXPECT_EQ(childNames(board), (QStringList{"Creature", "Instant", "Alpha", "Zebra"})); -} - -TEST(DeckListModelCustomZones, SortPlainNodeDoesNotReorderCustomZones) -{ - // A non-board node (e.g. a group whose children are cards) is sorted plainly; - // custom zones are not a special case there. Cards sort by name. - InnerDecklistNode root; - auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root); - auto *group = new InnerDecklistNode("Creature", board); - cardNode(group, "Swords", 1); - cardNode(group, "Bolt", 3); - - root.setSortMethod(DeckSortMethod::ByName); - - auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, group, Qt::AscendingOrder); - EXPECT_EQ(childNames(group), (QStringList{"Bolt", "Swords"})); - ASSERT_EQ(mapping.size(), 2); - EXPECT_EQ(mapping[0].first, 1); // "Bolt" was originally at row 1 - EXPECT_EQ(mapping[0].second, 0); - EXPECT_EQ(mapping[1].first, 0); - EXPECT_EQ(mapping[1].second, 1); -} diff --git a/tests/deck_list_model/deck_list_model_zone_integration_test.cpp b/tests/deck_list_model/deck_list_model_zone_integration_test.cpp deleted file mode 100644 index 530e1c351..000000000 --- a/tests/deck_list_model/deck_list_model_zone_integration_test.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include -#include -#include -#include -#include -#include - -namespace -{ - -int totalCustomZoneRows(const DeckListModel &model) -{ - int count = 0; - const int rootRows = model.rowCount(QModelIndex()); - for (int r = 0; r < rootRows; ++r) { - const QModelIndex board = model.index(r, 0, QModelIndex()); - const int childRows = model.rowCount(board); - for (int c = 0; c < childRows; ++c) { - const QModelIndex child = model.index(c, 0, board); - if (child.data(DeckRoles::IsCustomZoneRole).toBool()) { - ++count; - } - } - } - return count; -} - -} // namespace - -// The "Add to Zone" combobox/submenu lists getCustomZoneNames(), which reads the -// deck tree. These verify the source data a freshly-created zone populates. - -TEST(DeckListModelZoneIntegration, CreateZoneThenReadCustomZoneNames) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal"})); -} - -TEST(DeckListModelZoneIntegration, CreateTwoZonesThenReadBoth) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr); - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"})); -} - -// Mirroring regression: rebuildTree must mirror each custom zone exactly once. -TEST(DeckListModelZoneIntegration, RebuildTreeMirrorsEachZoneOnce) -{ - QSharedPointer deck(new DeckList()); - DeckListModel model(nullptr, deck); - auto *tree = deck->getTree(); - - // One direct mainboard card plus two nested custom zones. - tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr); - tree->addCard("Swords to Plowshares", 1, "Removal", -1); - ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Utility"), nullptr); - - model.rebuildTree(); - - EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"})); - EXPECT_EQ(totalCustomZoneRows(model), 2); -}