diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 1c2755f71..9ea463eef 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -301,10 +301,8 @@ set(cockatrice_SOURCES src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp - src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp - src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index 509390611..8b5309603 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -1,7 +1,5 @@ #include "url_parser.h" -#include "../widgets/tabs/tab_room.h" -#include "../widgets/tabs/tab_supervisor.h" #include "../window_main.h" #include "contexts/context_join_game.h" #include "intent_join_server_game.h" @@ -11,7 +9,6 @@ #include #include #include -#include #include IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) @@ -74,15 +71,6 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query) return; } - const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded); - const QString message = generateJoinGameMessage(*ctx, gameDescription); - - const QMessageBox::StandardButton answer = QMessageBox::question( - mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - if (answer != QMessageBox::Yes) { - return; - } - // The join game intent owns the context and the credential lookup; once the // chain finishes (or fails) it deletes the whole tree. ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; @@ -99,38 +87,3 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query) getLoginCredentialsIntent->execute(); } - -QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription) -{ - const QString hostname = context.roomContext.serverContext.hostname; - const QString port = context.roomContext.serverContext.port; - const int roomId = context.roomContext.roomId; - const int gameId = context.gameId; - const QString server = QStringLiteral("%1:%2").arg(hostname, port); - - // Prefer the room name over the raw numeric id: it means something to the - // user. The name is only known when we are already connected to the same - // server and sitting in that room — otherwise fall back to a plain prompt. - AbstractClient *client = mainWindow->getTabSupervisor()->getClient(); - const bool sameServer = client != nullptr && client->getStatus() == StatusLoggedIn && - hostname.compare(client->serverName(), Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == port; - TabRoom *roomTab = sameServer ? mainWindow->getTabSupervisor()->getRoomTabs().value(roomId) : nullptr; - - const QString gameIdStr = QString::number(gameId); - // Links built by newer clients embed the game description ("game" item); - // restate it in the confirm so it matches what the chat anchor showed. - // Unknown query items are ignored, so old links without it keep working. - // The multi-arg .arg() overloads replace in a single pass, so a description - // containing "%…" cannot corrupt later placeholders. - // FullyDecoded undoes every %XX escape and must match the chat anchor's - // decode mode, so a description containing "%" reads identically in both. - if (gameDescription.isEmpty()) { - return roomTab ? tr("Join game #%1 in \"%2\" on %3?").arg(gameIdStr, roomTab->getRoomName(), server) - : tr("Join game #%1 on %2?").arg(gameIdStr, server); - } - - return roomTab ? tr("Join game \"%1\" (#%2) in \"%3\" on %4?") - .arg(gameDescription, gameIdStr, roomTab->getRoomName(), server) - : tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server); -} diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index 6d705e013..bac0e3d25 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -4,7 +4,6 @@ #include class MainWindow; -struct ContextJoinGame; class IntentUrlParser : public QObject { Q_OBJECT @@ -15,8 +14,6 @@ public: void handleJoinGame(const QUrlQuery &query); private: - QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription); - MainWindow *mainWindow; }; diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index 372dbfc19..5c4a88974 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -355,7 +355,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, { QAction *aCopyToClipBoard = nullptr, *aRemoveMessages = nullptr; aUserName->setText(userName); - const bool anotherUser = userName != userListProxy->getOwnUsername(); auto *menu = new QMenu(static_cast(parent())); menu->addAction(aUserName); @@ -367,17 +366,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addAction(aDetails); menu->addAction(aShowGames); menu->addAction(aChat); - const QList inviteOptions = inviteOptionsForUser(userName); - if (!inviteOptions.isEmpty()) { - auto *inviteMenu = new QMenu(tr("&Invite to Game"), menu); - for (const GameInviteOption &option : inviteOptions) { - QAction *inviteAction = inviteMenu->addAction(option.label); - inviteAction->setEnabled(anotherUser && online); - connect(inviteAction, &QAction::triggered, this, - [this, userName, option] { execInvite(userName, option); }); - } - menu->addMenu(inviteMenu); - } if (userLevel.testFlag(ServerInfo_User::IsRegistered) && userListProxy->isOwnUserRegistered()) { menu->addSeparator(); if (userListProxy->isUserBuddy(userName)) { @@ -428,6 +416,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos, menu->addAction(aPromoteToJudge); } } + bool anotherUser = userName != userListProxy->getOwnUsername(); aDetails->setEnabled(true); aChat->setEnabled(anotherUser && online); aShowGames->setEnabled(online); @@ -491,60 +480,6 @@ void UserContextMenu::execChat(const QString &userName) emit openMessageDialog(userName, true); } -QList UserContextMenu::inviteOptionsForUser(const QString &userName) const -{ - if (!gameInviteLinkProvider) { - return {}; - } - const QList options = gameInviteLinkProvider(); - QList result; - for (const GameInviteOption &option : options) { - // Buddy-only games accept invites only from their creator, and only to - // users on the creator's buddy list. - if (option.onlyBuddies && - (option.creatorName != userListProxy->getOwnUsername() || !userListProxy->isUserBuddy(userName))) { - continue; - } - result.append(option); - } - return result; -} - -void UserContextMenu::execInvite(const QString &userName) -{ - const QList options = inviteOptionsForUser(userName); - if (options.isEmpty()) { - return; - } - - if (options.size() == 1) { - execInvite(userName, options.first()); - return; - } - - // More than one game in the room — let the user pick which one to invite to. - auto *menu = new QMenu(static_cast(parent())); - for (const GameInviteOption &option : options) { - QAction *action = menu->addAction(option.label); - connect(action, &QAction::triggered, this, [this, userName, option] { execInvite(userName, option); }); - } - menu->setAttribute(Qt::WA_DeleteOnClose); - menu->popup(QCursor::pos()); -} - -void UserContextMenu::execInvite(const QString &userName, const GameInviteOption &option) -{ - // Name the game by description first, then its id — "Join my game 'Magic' - // (#123)" — so a description-less fallback still identifies the game. - // The multi-arg .arg() overloads replace in a single pass, so a description - // containing "%…" cannot corrupt later placeholders. - const QString prefix = - option.description.isEmpty() - ? tr("Join my game (#%1):").arg(option.gameId) - : tr("Join my game \"%1\" (#%2):").arg(option.description, QString::number(option.gameId)); - tabSupervisor->sendInviteToUser(userName, prefix + " " + option.url); -} - void UserContextMenu::execDetails(const QString &userName) { auto *w = new UserInfoBox(client, false, static_cast(parent()), diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h index 70bbff977..00fdc51fe 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h @@ -7,12 +7,9 @@ #ifndef USER_CONTEXT_MENU_H #define USER_CONTEXT_MENU_H -#include "../../interface/widgets/server/game_link.h" - -#include #include -#include #include + class AbstractGame; class UserListProxy; class AbstractClient; @@ -46,7 +43,6 @@ private: QAction *aPromoteToJudge, *aDemoteFromJudge; QAction *aWarnUser, *aWarnHistory; QAction *aGetAdminNotes; - std::function()> gameInviteLinkProvider; signals: void openMessageDialog(const QString &userName, bool focus); private slots: @@ -84,28 +80,9 @@ public: return userListProxy; } - void setGameInviteLinkProvider(std::function()> provider) - { - gameInviteLinkProvider = std::move(provider); - } - - /** - * The games currently inviteable for @p userName, honoring the room's - * buddy-only setting (the inviter must be the game's creator and the - * target a buddy of theirs). Empty when there is no live provider. - */ - QList inviteOptionsForUser(const QString &userName) const; - - /** Whether at least one invite link is currently available for @p userName. */ - bool hasGameInviteLink(const QString &userName) const - { - return !inviteOptionsForUser(userName).isEmpty(); - } - // Individual action entry points — used by UserInfoPopup to trigger // actions without re-running the full context menu flow. void execChat(const QString &userName); - void execInvite(const QString &userName); void execDetails(const QString &userName); void execShowGames(const QString &userName); void execAddToBuddy(const QString &userName); @@ -120,9 +97,6 @@ public: void execAdminNotes(const QString &userName); void execAdjustMod(const QString &userName, bool shouldBeMod); void execAdjustJudge(const QString &userName, bool shouldBeJudge); - -private: - void execInvite(const QString &userName, const GameInviteOption &option); }; #endif 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 b63457169..be52b9871 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -1838,8 +1838,3 @@ void UserListWidget::finishSectionedMutation() applyFilter(); userTree->viewport()->update(); } - -void UserListWidget::setGameInviteLinkProvider(std::function()> provider) -{ - userContextMenu->setGameInviteLinkProvider(std::move(provider)); -} 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 d97843264..e048c7fb7 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -8,7 +8,6 @@ #define USERLIST_H #include "../../cards/card_info_picture_art_crop_widget.h" -#include "../../interface/widgets/server/game_link.h" #include "user_avatar_provider.h" #include "user_card_art_provider.h" #include "user_info_popup.h" @@ -23,7 +22,6 @@ #include #include #include -#include #include #include @@ -273,7 +271,6 @@ public: } void showContextMenu(const QPoint &pos, const QModelIndex &index); void sortItems(); - void setGameInviteLinkProvider(std::function()> provider); protected: void hideEvent(QHideEvent *e) override; diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index d482d3dd7..9eccea7a2 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_message.cpp @@ -96,18 +96,6 @@ void TabMessage::closeEvent(QCloseEvent *event) event->accept(); } -void TabMessage::sendPrivateMessage(const QString &text) -{ - Command_Message cmd; - cmd.set_user_name(otherUserInfo->name()); - cmd.set_message(text.toStdString()); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - pend->setExtraData(text); - connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent); - client->sendCommand(pend); -} - void TabMessage::sendMessage() { if (sayEdit->text().isEmpty()) { @@ -115,17 +103,22 @@ void TabMessage::sendMessage() } if (!userOnline) { + // Keep the draft: the user may be back momentarily, and the typed text + // should not be lost to a transient offline spell. notifyUserOffline(); return; } - sendPrivateMessage(sayEdit->text()); - sayEdit->clear(); -} + Command_Message cmd; + cmd.set_user_name(otherUserInfo->name()); + cmd.set_message(sayEdit->text().toStdString()); -bool TabMessage::isUserOnline() const -{ - return userOnline; + PendingCommand *pend = client->prepareSessionCommand(cmd); + pend->setExtraData(sayEdit->text()); + connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent); + client->sendCommand(pend); + + sayEdit->clear(); } void TabMessage::messageSent(const Response &response, diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.h b/cockatrice/src/interface/widgets/tabs/tab_message.h index e9b987ce2..f7d15b4f6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.h +++ b/cockatrice/src/interface/widgets/tabs/tab_message.h @@ -64,9 +64,6 @@ public: void processUserLeft(); void processUserJoined(const ServerInfo_User &_userInfo); - [[nodiscard]] bool isUserOnline() const; - void sendPrivateMessage(const QString &text); - private: bool shouldShowSystemPopup(const Event_UserMessage &event); void showSystemPopup(const Event_UserMessage &event); diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 6245b5301..508d5a048 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -4,7 +4,6 @@ #include "../../../client/settings/shortcuts_settings.h" #include "../interface/widgets/dialogs/dlg_settings.h" #include "../interface/widgets/server/chat_view/chat_view.h" -#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/game_selector.h" #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_panel_widget.h" @@ -31,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -68,9 +66,6 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, userList = userListPanel->getUserList(); connect(userListPanel, &UserListPanelWidget::openMessageDialog, this, &TabRoom::openMessageDialog); - const auto gameInviteLinkProvider = [this]() { return tabSupervisor->getGameInviteLinksForRoom(roomId); }; - userList->setGameInviteLinkProvider(gameInviteLinkProvider); - chatView = new ChatView(tabSupervisor, nullptr, true, this); connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup); connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 77b93802a..1ab812c54 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -3,7 +3,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" #include "../interface/pixel_map_generator.h" -#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_widget.h" #include "../main.h" @@ -951,53 +950,6 @@ void TabSupervisor::talkLeft(TabMessage *tab) removeTab(indexOf(tab)); } -QList TabSupervisor::getGameInviteLinksForRoom(int roomId) const -{ - QList options; - if (isLocalGame) { - return options; - } - - // 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 (TabGame *tab : gameTabs) { - GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo(); - if (metaInfo->proto().room_id() != roomId) { - continue; - } - // A closed game is a dead end — drop it. Started/full games stay - // listed: an invite to them is a legitimate "come spectate" offer. - if (metaInfo->proto().closed()) { - continue; - } - - const int gameId = metaInfo->gameId(); - const QString description = QString::fromStdString(metaInfo->proto().description()); - - GameInviteOption option{ - .gameId = gameId, - .label = - description.isEmpty() ? tr("Game #%1").arg(gameId) : tr("Game #%1 — %2").arg(gameId).arg(description), - .url = makeGameJoinLink(client->serverName(), client->serverPort(), roomId, gameId, description), - .description = description, - .onlyBuddies = metaInfo->proto().only_buddies(), - .creatorName = QString::fromStdString(metaInfo->proto().creator_info().name()), - }; - options.append(option); - } - - return options; -} - -void TabSupervisor::sendInviteToUser(const QString &userName, const QString &inviteText) -{ - TabMessage *tab = addMessageTab(userName, true); - if (tab && tab->isUserOnline()) { - tab->sendPrivateMessage(inviteText); - } -} - /** * Creates a new deck editor tab and loads the deck into it. * Creates either a classic or visual deck editor tab depending on settings diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 81ad22f54..5ac3eb365 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -9,7 +9,6 @@ #define TAB_SUPERVISOR_H #include "../../deck_loader/deck_loader.h" -#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_proxy.h" #include "abstract_tab_deck_editor.h" #include "api/archidekt/tab_archidekt.h" @@ -161,8 +160,6 @@ public: { return deckEditorTabs; } - [[nodiscard]] QList getGameInviteLinksForRoom(int roomId) const; - void sendInviteToUser(const QString &userName, const QString &inviteText); [[nodiscard]] bool getAdminLocked() const; void closeEvent(QCloseEvent *event) override; bool switchToGameTabIfAlreadyExists(const int gameId); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp deleted file mode 100644 index bf2c49604..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ /dev/null @@ -1,503 +0,0 @@ -#include "visual_deck_storage_model.h" - -#include "../../deck_loader/deck_loader.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -/** - * @brief The result of a background directory scan: the deck rows in scan order - * plus the sorted list of subfolder paths. - */ -struct DeckScanResult -{ - QList decks; ///< Deck rows in scan order. - QStringList folderPaths; ///< Sorted list of subfolder paths. -}; - -/** - * @brief The result of a background deck file load: the parsed deck plus the - * file's modification time, so the disk stat happens off the UI thread. - */ -struct DeckLoadResult -{ - LoadedDeck deck; ///< The parsed deck. - QDateTime lastModified; ///< File modification time at load. -}; - -/** - * @brief The path of \a path relative to the deck root, or empty if \a path - * is not below it. - */ -QString relativePathFromDeckRoot(const QString &path, const QString &deckPath) -{ - if (!path.startsWith(deckPath)) { - return {}; - } - QString relativePath = path.mid(deckPath.length()); - if (relativePath.startsWith('/')) { - relativePath.remove(0, 1); - } - return relativePath; -} - -/** - * @brief The path of \a filePath relative to \a deckPath, or the bare file name - * if \a filePath is not below \a deckPath. - */ -QString relativeFilePathFor(const QString &filePath, const QString &deckPath) -{ - if (filePath.startsWith(deckPath)) { - return filePath.mid(deckPath.length()); - } - - return QFileInfo(filePath).fileName(); -} - -/** - * @brief The directory of \a filePath relative to \a deckPath, or empty if the - * file sits directly in the deck root. - */ -QString folderPathFor(const QString &filePath, const QString &deckPath) -{ - return relativePathFromDeckRoot(QFileInfo(filePath).absolutePath(), deckPath); -} - -/** - * @brief Scans a deck directory on a worker thread, returning discovered deck - * files and subfolder paths. - */ -DeckScanResult scanDeckDirectory(const QString &deckPath) -{ - DeckScanResult result; - - QDirIterator fileIt(deckPath, DeckLoader::ACCEPTED_FILE_EXTENSIONS, QDir::Files, - QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); - while (fileIt.hasNext()) { - const QString filePath = fileIt.next(); - DeckPreviewData data; - data.filePath = filePath; - data.relativeFilePath = relativeFilePathFor(filePath, deckPath); - data.folderPath = folderPathFor(filePath, deckPath); - data.lastModified = QFileInfo(filePath).lastModified(); - result.decks.append(std::move(data)); - } - - QSet seenFolders; - QDirIterator folderIt(deckPath, QDir::Dirs | QDir::NoDotAndDotDot, - QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); - while (folderIt.hasNext()) { - const QString folderPath = relativePathFromDeckRoot(folderIt.next(), deckPath); - if (!folderPath.isEmpty() && !seenFolders.contains(folderPath)) { - seenFolders.insert(folderPath); - result.folderPaths.append(folderPath); - } - } - result.folderPaths.sort(); - - return result; -} -} // namespace - -VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListModel(parent) -{ -} - -int VisualDeckStorageModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : decks.size(); -} - -QVariant VisualDeckStorageModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() < 0 || index.row() >= decks.size()) { - return {}; - } - - const DeckPreviewData &data = decks.at(index.row()); - switch (role) { - case Qt::DisplayRole: - case VisualDeckStorageRoles::DisplayNameRole: - return data.displayName; - case VisualDeckStorageRoles::FilePathRole: - return data.filePath; - case VisualDeckStorageRoles::RelativeFilePathRole: - return data.relativeFilePath; - case VisualDeckStorageRoles::FolderPathRole: - return data.folderPath; - case VisualDeckStorageRoles::TagsRole: - return data.tags; - case VisualDeckStorageRoles::ColorIdentityRole: - return data.colorIdentity; - case VisualDeckStorageRoles::LastModifiedRole: - return data.lastModified; - case VisualDeckStorageRoles::LastLoadedRole: - return data.lastLoaded; - case VisualDeckStorageRoles::BannerCardNameRole: - return data.bannerCard.name; - case VisualDeckStorageRoles::BannerCardProviderIdRole: - return data.bannerCard.providerId; - default: - return {}; - } -} - -void VisualDeckStorageModel::setDeckPath(const QString &path) -{ - QString cleanedPath = QDir::cleanPath(path); - if (cleanedPath == ".") { - cleanedPath.clear(); - } - deckPath = cleanedPath; - startScan(); -} - -void VisualDeckStorageModel::refresh() -{ - startScan(); -} - -const DeckPreviewData &VisualDeckStorageModel::dataForRow(int row) const -{ - static const DeckPreviewData emptyData; - if (row < 0 || row >= decks.size()) { - return emptyData; - } - return decks.at(row); -} - -const LoadedDeck &VisualDeckStorageModel::deckForRow(int row) const -{ - return dataForRow(row).deck; -} - -int VisualDeckStorageModel::rowForFilePath(const QString &filePath) const -{ - for (int i = 0; i < decks.size(); ++i) { - if (decks.at(i).filePath == filePath) { - return i; - } - } - return -1; -} - -void VisualDeckStorageModel::startScan() -{ - ++scanGeneration; - beginResetModel(); - decks.clear(); - folderPaths.clear(); - endResetModel(); - - if (deckPath.isEmpty()) { - return; - } - - const QString currentDeckPath = deckPath; - const int generation = scanGeneration; - - // The scan (directory walk + one stat per file) runs on a worker thread so that - // constructing the widget never stalls the UI thread on a large deck folder. - auto *watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [this, watcher, generation] { - watcher->deleteLater(); - - if (generation != scanGeneration) { - return; // A newer scan started while this one was running; drop the stale result. - } - - const DeckScanResult result = watcher->result(); - folderPaths = result.folderPaths; - - if (result.decks.isEmpty()) { - return; - } - - beginInsertRows(QModelIndex(), 0, result.decks.size() - 1); - decks = result.decks; - endInsertRows(); - - for (int row = 0; row < decks.size(); ++row) { - beginLoad(row); - } - }); - - watcher->setFuture( - QtConcurrent::run([currentDeckPath]() -> DeckScanResult { return scanDeckDirectory(currentDeckPath); })); -} - -void VisualDeckStorageModel::beginLoad(int row) -{ - if (row < 0 || row >= decks.size() || decks.at(row).loadInProgress) { - return; - } - - DeckPreviewData &data = decks[row]; - data.loadInProgress = true; - - const QString filePath = data.filePath; - const DeckFileFormat::Format fmt = DeckFileFormat::getFormatFromName(filePath); - const int generation = scanGeneration; - - auto *watcher = new QFutureWatcher>(this); - connect(watcher, &QFutureWatcher>::finished, this, - [this, watcher, filePath, generation] { - watcher->deleteLater(); - - if (generation != scanGeneration) { - return; // The deck list was re-scanned while this load was running; drop the stale result. - } - - const int row = rowForFilePath(filePath); - if (row == -1) { - return; - } - - DeckPreviewData &data = decks[row]; - data.loadInProgress = false; - - std::optional result = watcher->result(); - if (!result) { - return; // Leave the row unloaded; it stays visible but without deck data. - } - - data.deck = std::move(result->deck); - data.loadSucceeded = true; - data.lastModified = result->lastModified; - recomputeDeckMetadata(data); - - emit dataChanged(index(row), index(row)); - emit deckLoaded(row); - }); - - watcher->setFuture(QtConcurrent::run([filePath, fmt]() -> std::optional { - std::optional deck = DeckLoader::loadFromFile(filePath, fmt, false); - if (!deck) { - return std::nullopt; - } - return DeckLoadResult{*deck, QFileInfo(filePath).lastModified()}; - })); -} - -/** - * @brief Computes the color identity of a deck in WUBRG order. - */ -static QString computeColorIdentity(const LoadedDeck &deck) -{ - QStringList cardList = deck.deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE}); - if (cardList.isEmpty()) { - return {}; - } - - QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G) - - for (const QString &cardName : cardList) { - CardInfoPtr currentCard = CardDatabaseManager::query()->getCardInfo(cardName); - if (currentCard) { - const QString colors = currentCard->getColors(); // Something like "WUB" - for (const QChar &color : colors) { - colorSet.insert(color); - } - } - } - - // Ensure the color identity is in WUBRG order - QString colorIdentity; - const QString wubrgOrder = "WUBRG"; - for (const QChar &color : wubrgOrder) { - if (colorSet.contains(color)) { - colorIdentity.append(color); - } - } - - return colorIdentity; -} - -/** - * @brief Recomputes all derived metadata of a row from its loaded deck. - */ -void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data) -{ - const DeckList &deckList = data.deck.deckList; - - data.deckName = deckList.getName(); - data.displayName = !data.deckName.isEmpty() ? data.deckName : QFileInfo(data.deck.lastLoadInfo.fileName).fileName(); - data.tags = deckList.getTags(); - data.lastLoaded = QDateTime::fromString(deckList.getLastLoadedTimestamp()); - data.bannerCard = deckList.getBannerCard(); - data.colorIdentity = computeColorIdentity(data.deck); -} - -void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePath) -{ - if (row < 0 || row >= decks.size()) { - return; - } - - DeckPreviewData &data = decks[row]; - data.filePath = newFilePath; - data.relativeFilePath = relativeFilePathFor(newFilePath, deckPath); - data.folderPath = folderPathFor(newFilePath, deckPath); -} - -bool VisualDeckStorageModel::renameDeck(int row, const QString &newName) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - data.deck.deckList.setName(newName); - if (!DeckLoader::saveToFile(data.deck)) { - return false; - } - - recomputeDeckMetadata(data); - emit dataChanged(index(row), index(row), {VisualDeckStorageRoles::DisplayNameRole}); - return true; -} - -bool VisualDeckStorageModel::renameFile(int row, const QString &newBaseName) -{ - if (row < 0 || row >= decks.size() || newBaseName.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - const QFileInfo info(data.filePath); - if (newBaseName == info.baseName()) { - return false; - } - - QString newFileName = newBaseName; - if (!info.suffix().isEmpty()) { - newFileName += "." + info.suffix(); - } - - const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath(); - if (!QFile::rename(info.filePath(), newFilePath)) { - return false; - } - - const QString oldFilePath = data.filePath; - data.deck.lastLoadInfo.fileName = newFilePath; - setFilePathForRow(row, newFilePath); - data.lastModified = QFileInfo(newFilePath).lastModified(); - - emit dataChanged(index(row), index(row)); - emit deckFilePathChanged(oldFilePath, newFilePath); - return true; -} - -bool VisualDeckStorageModel::deleteFile(int row) -{ - if (row < 0 || row >= decks.size()) { - return false; - } - - const QString filePath = decks.at(row).filePath; - if (!QFile::remove(QFileInfo(filePath).filePath())) { - return false; - } - - beginRemoveRows(QModelIndex(), row, row); - decks.removeAt(row); - endRemoveRows(); - return true; -} - -bool VisualDeckStorageModel::setTags(int row, const QStringList &tags) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - data.deck.deckList.setTags(tags); - if (!DeckLoader::saveToFile(data.deck)) { - return false; - } - - data.tags = tags; - emit dataChanged(index(row), index(row), {VisualDeckStorageRoles::TagsRole}); - return true; -} - -bool VisualDeckStorageModel::setBannerCard(int row, const CardRef &cardRef) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - data.deck.deckList.setBannerCard(cardRef); - if (!DeckLoader::saveToFile(data.deck)) { - return false; - } - - data.bannerCard = cardRef; - emit dataChanged(index(row), index(row), - {VisualDeckStorageRoles::BannerCardNameRole, VisualDeckStorageRoles::BannerCardProviderIdRole}); - return true; -} - -bool VisualDeckStorageModel::convertToCockatriceFormat(int row) -{ - if (row < 0 || row >= decks.size() || decks.at(row).deck.isEmpty()) { - return false; - } - - DeckPreviewData &data = decks[row]; - const QString oldFilePath = data.filePath; - if (!DeckLoader::convertToCockatriceFormat(data.deck)) { - return false; - } - - setFilePathForRow(row, data.deck.lastLoadInfo.fileName); - data.lastModified = QFileInfo(data.filePath).lastModified(); - recomputeDeckMetadata(data); - - emit dataChanged(index(row), index(row)); - if (oldFilePath != data.filePath) { - emit deckFilePathChanged(oldFilePath, data.filePath); - } - return true; -} - -bool VisualDeckStorageModel::reloadIfModified(int row) -{ - if (row < 0 || row >= decks.size()) { - return false; - } - - DeckPreviewData &data = decks[row]; - QFileInfo fileInfo(data.filePath); - const QDateTime newLastModified = fileInfo.lastModified(); - if (!newLastModified.isValid() || newLastModified <= data.lastModified) { - return false; - } - - std::optional result = - DeckLoader::loadFromFile(data.filePath, DeckFileFormat::getFormatFromName(data.filePath), false); - if (!result) { - return false; - } - - data.deck = *result; - data.loadSucceeded = true; - data.lastModified = fileInfo.lastModified(); - recomputeDeckMetadata(data); - - emit dataChanged(index(row), index(row)); - emit deckLoaded(row); - return true; -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h deleted file mode 100644 index a44e7412d..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @file visual_deck_storage_model.h - * @ingroup VisualDeckStorageWidgets - * @brief Source model for the Visual Deck Storage: the deck files on disk. - * - * The model owns the deck metadata (name, tags, color identity, banner card, - * modification times) and the parsed deck list, loading each deck file in the - * background. Views read through the roles or the direct accessors, and all - * mutations (rename, tags, banner card, delete, conversion) go through this - * class so the view layer never touches the filesystem directly. - */ - -#ifndef VISUAL_DECK_STORAGE_MODEL_H -#define VISUAL_DECK_STORAGE_MODEL_H - -#include "../../deck_loader/loaded_deck.h" - -#include -#include -#include -#include -#include -#include - -namespace VisualDeckStorageRoles -{ -/** - * @brief Custom roles exposed by the VisualDeckStorageModel. - */ -enum -{ - FilePathRole = Qt::UserRole + 1, /**< Absolute file path of the deck. */ - RelativeFilePathRole, /**< File path relative to the deck folder. */ - FolderPathRole, /**< Directory of the deck relative to the deck folder ("" for root). */ - DisplayNameRole, /**< Deck name, or the file name if the deck has no name. */ - TagsRole, /**< The deck's tags. */ - ColorIdentityRole, /**< The deck's color identity (WUBRG order). */ - LastModifiedRole, /**< QDateTime of the deck file's last modification. */ - LastLoadedRole, /**< QDateTime when the deck was last loaded from the file. */ - BannerCardNameRole, /**< Name of the deck's banner card. */ - BannerCardProviderIdRole /**< Provider id of the deck's banner card. */ -}; -} // namespace VisualDeckStorageRoles - -/** - * @brief One deck file as seen by the Visual Deck Storage. - * - * The metadata is computed once when the deck loads and refreshed on reloads - * and mutations, so filters and sorts never re-read the file from disk. - */ -struct DeckPreviewData -{ - QString filePath; ///< Absolute file path. - QString relativeFilePath; ///< File path relative to the deck folder. - QString folderPath; ///< Directory relative to the deck folder ("" for the deck folder itself). - QString deckName; ///< The deck name as stored in the file (may be empty). - QString displayName; ///< Deck name, or the file name if the deck has no name. - QStringList tags; ///< The deck's tags. - QString colorIdentity; ///< The deck's color identity in WUBRG order. - QDateTime lastModified; ///< File modification time at last check. - QDateTime lastLoaded; ///< When the deck was last loaded from the file. - CardRef bannerCard; ///< The deck's banner card (name + provider id). - LoadedDeck deck; ///< The parsed deck; empty until the file has been loaded. - bool loadSucceeded = false; ///< Whether the deck file finished loading successfully. - bool loadInProgress = false; ///< Whether the deck file is currently being loaded. -}; - -/** - * @brief The list model backing the Visual Deck Storage widget tree. - * - * Rows are in filesystem scan order; ordering and filtering are handled by - * VisualDeckStorageSortFilterProxyModel on top of this model. - */ -class VisualDeckStorageModel : public QAbstractListModel -{ - Q_OBJECT -public: - explicit VisualDeckStorageModel(QObject *parent = nullptr); - - /// @name Qt model overrides - ///@{ - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - ///@} - - /** - * @brief Sets the folder to scan for deck files and starts (re)loading. - * Clears the model immediately (modelReset), then populates it asynchronously - * as the background scan discovers deck files. - */ - void setDeckPath(const QString &path); - - /** - * @brief Re-scans the current deck folder, reloading every deck file. - */ - void refresh(); - - [[nodiscard]] QString getDeckPath() const - { - return deckPath; - } - - /** - * @brief The relative paths of all subdirectories of the deck folder, one level at a time. - * Used by the view to build the folder tree. Sorted for deterministic order. - */ - [[nodiscard]] QStringList getFolderPaths() const - { - return folderPaths; - } - - /// @name Data accessors - ///@{ - [[nodiscard]] const DeckPreviewData &dataForRow(int row) const; - [[nodiscard]] const LoadedDeck &deckForRow(int row) const; - [[nodiscard]] int rowForFilePath(const QString &filePath) const; - ///@} - - /// @name Mutations (persist to disk and update the row) - ///@{ - bool renameDeck(int row, const QString &newName); - bool renameFile(int row, const QString &newBaseName); - bool deleteFile(int row); - bool setTags(int row, const QStringList &tags); - bool setBannerCard(int row, const CardRef &cardRef); - bool convertToCockatriceFormat(int row); - bool reloadIfModified(int row); - ///@} - -signals: - /** - * @brief Emitted when a deck file finishes loading. - * @param row The row of the deck that finished loading. - */ - void deckLoaded(int row); - - /** - * @brief Emitted when a deck's file path changes (rename file, conversion). - * @param oldFilePath The previous file path. - * @param newFilePath The new file path. - */ - void deckFilePathChanged(const QString &oldFilePath, const QString &newFilePath); - -private: - void startScan(); - void beginLoad(int row); - static void recomputeDeckMetadata(DeckPreviewData &data); - void setFilePathForRow(int row, const QString &newFilePath); - - QString deckPath; - QList decks; - QStringList folderPaths; ///< All subdirectories of the deck folder, sorted. - int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored. -}; - -#endif // VISUAL_DECK_STORAGE_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp deleted file mode 100644 index 8968f6cb3..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ /dev/null @@ -1,275 +0,0 @@ -#include "visual_deck_storage_sort_filter_proxy_model.h" - -#include "../../filters/deck_filter_string.h" - -#include -#include - -VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent) - : QSortFilterProxyModel(parent) -{ - setDynamicSortFilter(false); -} - -void VisualDeckStorageSortFilterProxyModel::setSourceModel(QAbstractItemModel *model) -{ - if (QAbstractItemModel *oldModel = sourceModel()) { - disconnect(oldModel, &QAbstractItemModel::modelReset, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - disconnect(oldModel, &QAbstractItemModel::rowsInserted, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - disconnect(oldModel, &QAbstractItemModel::rowsRemoved, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - } - - QSortFilterProxyModel::setSourceModel(model); - - if (model) { - connect(model, &QAbstractItemModel::modelReset, this, &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - connect(model, &QAbstractItemModel::rowsInserted, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - connect(model, &QAbstractItemModel::rowsRemoved, this, - &VisualDeckStorageSortFilterProxyModel::resizeMatchLists); - } - - resizeMatchLists(); -} - -void VisualDeckStorageSortFilterProxyModel::setSearchText(const QString &text) -{ - if (searchText == text) { - return; - } - - searchText = text; - updateSearchMatches(); - invalidate(); -} - -void VisualDeckStorageSortFilterProxyModel::setTagFilter(const QSet &newSelectedTags, - const QSet &newExcludedTags) -{ - if (selectedTags == newSelectedTags && excludedTags == newExcludedTags) { - return; - } - - selectedTags = newSelectedTags; - excludedTags = newExcludedTags; - updateTagMatches(); - invalidate(); -} - -void VisualDeckStorageSortFilterProxyModel::setColorFilter(FilterMode mode, const QSet &colors) -{ - if (colorFilterMode == mode && activeColors == colors) { - return; - } - - colorFilterMode = mode; - activeColors = colors; - updateColorMatches(); - invalidate(); -} - -void VisualDeckStorageSortFilterProxyModel::setSortOrder(SortOrder order) -{ - // No equality guard: the initial reapply (with the default order) must still - // trigger sort(0), since without a sort the proxy would show scan order. - sortOrder = order; - sort(0); -} - -void VisualDeckStorageSortFilterProxyModel::reapplyFilters() -{ - const QList oldSearchMatches = searchMatches; - const QList oldTagMatches = tagMatches; - const QList oldColorMatches = colorMatches; - - updateSearchMatches(); - updateTagMatches(); - updateColorMatches(); - - if (searchMatches != oldSearchMatches || tagMatches != oldTagMatches || colorMatches != oldColorMatches) { - invalidate(); - } - - if (sortOrder == ByName || sortOrder == ByLastLoaded) { - // These orders depend on data that only becomes available when a deck finishes loading. - sort(0); - } -} - -void VisualDeckStorageSortFilterProxyModel::resort() -{ - sort(0); -} - -bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const -{ - if (sourceParent.isValid()) { - return true; - } - - // If the match lists aren't sized to the current model yet, don't hide anything. - if (sourceRow < 0 || sourceRow >= searchMatches.size() || sourceRow >= tagMatches.size() || - sourceRow >= colorMatches.size()) { - return true; - } - - return searchMatches.at(sourceRow) && tagMatches.at(sourceRow) && colorMatches.at(sourceRow); -} - -bool VisualDeckStorageSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const -{ - const auto *source = deckSourceModel(); - if (!source) { - return false; - } - - const DeckPreviewData &leftData = source->dataForRow(left.row()); - const DeckPreviewData &rightData = source->dataForRow(right.row()); - - switch (sortOrder) { - case ByName: - return leftData.deckName < rightData.deckName; - case Alphabetical: - return QString::localeAwareCompare(QFileInfo(leftData.filePath).fileName(), - QFileInfo(rightData.filePath).fileName()) < 0; - case ByLastModified: - return leftData.lastModified > rightData.lastModified; - case ByLastLoaded: - return leftData.lastLoaded > rightData.lastLoaded; - } - - return false; -} - -void VisualDeckStorageSortFilterProxyModel::resizeMatchLists() -{ - const int count = sourceModel() ? sourceModel()->rowCount() : 0; - searchMatches.resize(count); - searchMatches.fill(true); - tagMatches.resize(count); - tagMatches.fill(true); - colorMatches.resize(count); - colorMatches.fill(true); -} - -void VisualDeckStorageSortFilterProxyModel::updateSearchMatches() -{ - const auto *source = deckSourceModel(); - if (!source) { - searchMatches.clear(); - return; - } - - const int count = source->rowCount(); - searchMatches.resize(count); - if (searchText.isEmpty()) { - searchMatches.fill(true); - return; - } - - DeckFilterString filterString(searchText); - for (int row = 0; row < count; ++row) { - const DeckPreviewData &data = source->dataForRow(row); - - // isEmpty() is intentional: if a deck fails to load, loadInProgress becomes false - // but the deck remains empty. Using loadInProgress alone would pass failed decks - // to DeckFilterString::check, which requires a non-empty deck. - if (data.deck.isEmpty()) { - searchMatches[row] = true; - continue; - } - - DeckSearchData searchData{ - .deck = &data.deck, - .filePath = data.filePath, - .displayName = data.displayName, - .relativeFilePath = data.relativeFilePath, - }; - searchMatches[row] = filterString.check(searchData); - } -} - -void VisualDeckStorageSortFilterProxyModel::updateTagMatches() -{ - const auto *source = deckSourceModel(); - if (!source) { - tagMatches.clear(); - return; - } - - const int count = source->rowCount(); - tagMatches.resize(count); - - if (selectedTags.isEmpty() && excludedTags.isEmpty()) { - tagMatches.fill(true); - return; - } - - for (int row = 0; row < count; ++row) { - const QStringList deckTags = source->dataForRow(row).tags; - - const bool hasAllSelected = std::all_of(selectedTags.begin(), selectedTags.end(), - [&deckTags](const QString &tag) { return deckTags.contains(tag); }); - const bool hasAnyExcluded = std::any_of(excludedTags.begin(), excludedTags.end(), - [&deckTags](const QString &tag) { return deckTags.contains(tag); }); - - tagMatches[row] = hasAllSelected && !hasAnyExcluded; - } -} - -void VisualDeckStorageSortFilterProxyModel::updateColorMatches() -{ - const auto *source = deckSourceModel(); - if (!source) { - colorMatches.clear(); - return; - } - - const int count = source->rowCount(); - colorMatches.resize(count); - - if (activeColors.isEmpty()) { - colorMatches.fill(true); - return; - } - - for (int row = 0; row < count; ++row) { - const QString colorIdentity = source->dataForRow(row).colorIdentity; - - bool matches = true; - switch (colorFilterMode) { - case ExactMatch: { - QSet activeColorSet; - for (const QChar &color : activeColors) { - activeColorSet.insert(color.toUpper()); - } - - QSet colorIdentitySet; - for (const QChar &color : colorIdentity) { - colorIdentitySet.insert(color.toUpper()); - } - - matches = activeColorSet == colorIdentitySet; - break; - } - case Includes: - matches = std::all_of(activeColors.begin(), activeColors.end(), - [&colorIdentity](const QChar &color) { return colorIdentity.contains(color); }); - break; - case Excludes: - matches = std::none_of(activeColors.begin(), activeColors.end(), - [&colorIdentity](const QChar &color) { return colorIdentity.contains(color); }); - break; - } - - colorMatches[row] = matches; - } -} - -const VisualDeckStorageModel *VisualDeckStorageSortFilterProxyModel::deckSourceModel() const -{ - return qobject_cast(sourceModel()); -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h deleted file mode 100644 index d2842a02f..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @file visual_deck_storage_sort_filter_proxy_model.h - * @ingroup VisualDeckStorageWidgets - * @brief Sorting and filtering proxy on top of VisualDeckStorageModel. - * - * Owns all search / tag / color filter state and the sort order. Filtering is - * evaluated against the model's data (never against widgets), so it can run - * before any view exists and re-evaluate whenever deck data finishes loading. - */ - -#ifndef VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H -#define VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H - -#include "visual_deck_storage_model.h" - -#include -#include -#include - -class VisualDeckStorageSortFilterProxyModel : public QSortFilterProxyModel -{ - Q_OBJECT -public: - /** - * @brief The order in which decks are sorted. Values must match the - * entries of the sort widget's combo box and the stored settings value. - */ - enum SortOrder - { - ByName, - Alphabetical, - ByLastModified, - ByLastLoaded, - }; - Q_ENUM(SortOrder) - - /** - * @brief How the color identity filter is applied. - */ - enum FilterMode - { - ExactMatch, - Includes, - Excludes - }; - Q_ENUM(FilterMode) - - explicit VisualDeckStorageSortFilterProxyModel(QObject *parent = nullptr); - - void setSourceModel(QAbstractItemModel *model) override; - - /// @name Filter input setters (each re-evaluates the affected matches) - ///@{ - void setSearchText(const QString &text); - void setTagFilter(const QSet &newSelectedTags, const QSet &newExcludedTags); - void setColorFilter(FilterMode mode, const QSet &colors); - ///@} - - /** - * @brief Sets the sort order and applies it immediately. - */ - void setSortOrder(SortOrder order); - - /** - * @brief Re-evaluates all matches against the current model data and - * re-applies filtering and sorting. Called after deck data changes. - */ - void reapplyFilters(); - - /** - * @brief Re-applies the current sort order without touching the filters. - */ - void resort(); - -protected: - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; - bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - -private: - void resizeMatchLists(); - void updateSearchMatches(); - void updateTagMatches(); - void updateColorMatches(); - [[nodiscard]] const VisualDeckStorageModel *deckSourceModel() const; - - QString searchText; - QSet selectedTags; - QSet excludedTags; - FilterMode colorFilterMode = ExactMatch; - QSet activeColors; - SortOrder sortOrder = Alphabetical; - - QList searchMatches; ///< Per-row search match, sized like the source model. - QList tagMatches; ///< Per-row tag match. - QList colorMatches; ///< Per-row color identity match. -}; - -#endif // VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index fb4fd3155..80a80e1ae 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -11,7 +11,6 @@ set(HEADERS game/server_cardzone.h game/server_counter.h game/game_config.h - game/server_deck_validation_strategy.h game/server_game.h game/server_player.h game/server_spectator.h diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_deck_validation_strategy.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_deck_validation_strategy.h deleted file mode 100644 index 8298214b4..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_deck_validation_strategy.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef SERVER_DECK_VALIDATION_STRATEGY_H -#define SERVER_DECK_VALIDATION_STRATEGY_H - -#include - -class DeckList; -class Server_Game; -class Server_Player; -class ResponseContainer; - -/** - * @brief Strategy for validating a player's deck before it is loaded into a game. - * - * Subclasses decide whether a deck may be accepted; the default implementation - * accepts every deck. - */ -class Server_DeckValidationStrategy -{ -public: - virtual ~Server_DeckValidationStrategy() = default; - - /** - * @brief Validate @p deck for @p player in @p game. - * - * @p rc is an out parameter used to attach the response details for a rejected - * deck (e.g. an error response extension via ResponseContainer::setResponseExtension). - * @return Response::RespOk when the deck is accepted, an error code otherwise. - */ - virtual Response::ResponseCode - validate(Server_Game *game, Server_Player *player, DeckList *deck, ResponseContainer &rc) = 0; -}; - -/** - * @brief Default deck validation strategy that accepts every deck. - */ -class Server_DefaultDeckValidationStrategy : public Server_DeckValidationStrategy -{ -public: - Response::ResponseCode validate(Server_Game *, Server_Player *, DeckList *, ResponseContainer &) override - { - return Response::RespOk; - } -}; - -#endif 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 069a10463..60d11ead1 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -62,8 +62,7 @@ Server_Game::Server_Game(const GameConfig &config, Server_Room *_room) spectatorsCanTalk(config.spectatorsCanTalk), spectatorsSeeEverything(config.spectatorsSeeEverything), startingLifeTotal(config.startingLifeTotal), shareDecklistsOnLoad(config.shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), - turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), - deckValidationStrategy(new Server_DefaultDeckValidationStrategy), gameMutex() + turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), gameMutex() { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); @@ -887,8 +886,3 @@ void Server_Game::returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPl } } } - -void Server_Game::setDeckValidationStrategy(Server_DeckValidationStrategy *strategy) -{ - deckValidationStrategy.reset(strategy); -} 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 da316975d..60b5398f2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -22,13 +22,11 @@ #include "../server_response_containers.h" #include "game_config.h" -#include "server_deck_validation_strategy.h" #include #include #include #include -#include #include #include #include @@ -81,8 +79,6 @@ private: QList replayList; GameReplay *currentReplay; - QScopedPointer deckValidationStrategy; - void createGameStateChangedEvent(Event_GameStateChanged *event, Server_AbstractParticipant *recipient, bool omniscient, @@ -212,14 +208,6 @@ public: GameEventStorageItem::SendToOthers, int privatePlayerId = -1); void returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); - - /** @brief Get the current deck validation strategy (non-owning). */ - Server_DeckValidationStrategy *getDeckValidationStrategy() const - { - return deckValidationStrategy.data(); - } - /** @brief Replace the deck validation strategy; takes ownership of @p strategy. */ - void setDeckValidationStrategy(Server_DeckValidationStrategy *strategy); }; #endif