diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 9fd05ae01..ed8e49f2d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -143,11 +143,7 @@ set(cockatrice_SOURCES src/interface/intents/intent_wait_for_database_load.h src/interface/layouts/flow_layout.cpp src/interface/layouts/overlap_layout.cpp - src/interface/widgets/utility/card_completer_delegate.cpp - src/interface/widgets/utility/card_completer_styler.cpp - src/interface/widgets/utility/completer_utils.cpp src/interface/widgets/utility/line_edit_completer.cpp - src/interface/widgets/utility/reversed_completer_model.cpp src/interface/pixel_map_generator.cpp src/interface/theme_config.cpp src/interface/theme_manager.cpp diff --git a/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h index 8bd2e9bc0..b2f6b62c0 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h +++ b/cockatrice/src/interface/widgets/cards/additional_info/mana_cost_widget.h @@ -17,7 +17,7 @@ class ManaCostWidget : public QWidget public: explicit ManaCostWidget(QWidget *parent, CardInfoPtr card); - static QStringList parseManaCost(const QString &manaString); + QStringList parseManaCost(const QString &manaString); public slots: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp index 40bef3df3..f442fe425 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_enlarged_widget.cpp @@ -52,29 +52,12 @@ void CardInfoPictureEnlargedWidget::loadPixmap(const QSize &size) * @param size The desired size for the pixmap. * * Sets the widget's pixmap to the card image and resizes the widget to match the specified size. Triggers a repaint. - * - * When the image is not yet cached, the pixmap is cleared (instead of showing a stale previous card) and the widget - * refreshes automatically once the card image finishes loading. */ void CardInfoPictureEnlargedWidget::setCardPixmap(const ExactCard &_card, const QSize size) { - if (card.getCardPtr()) { - disconnect(card.getCardPtr().data(), nullptr, this, nullptr); - } - card = _card; - - // Clear any previous card's art so we never paint a stale pixmap while the new image loads - enlargedPixmap = QPixmap(); loadPixmap(size); - if (card.getCardPtr()) { - connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this]() { - loadPixmap(this->size()); - update(); - }); - } - setFixedSize(size); // Set the widget size to the enlarged size update(); // Trigger a repaint diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 108338332..56c9600a0 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -1,7 +1,8 @@ #include "user_card_settings_dialog.h" #include "../../../card_picture_loader/card_picture_loader.h" -#include "../../utility/completer_utils.h" +#include "card/card_completer_proxy_model.h" +#include "card/card_search_model.h" #include "card_database_display_model.h" #include "card_database_model.h" #include "user_card_art_provider.h" @@ -18,6 +19,7 @@ #include #include #include +#include #include #include @@ -131,14 +133,29 @@ void UserCardArtSettingsDialog::initializeSearchBar() cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); + searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15); - searchModel = cardSetup.searchModel; - proxyModel = cardSetup.proxyModel; - completer = cardSetup.completer; + proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(15); searchBar->setCompleter(completer); - connectCardCompleterSearch(searchBar, cardSetup); + connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textEdited, this, [this](const QString &text) { + const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + completer->complete(); + } + }); connect(completer, static_cast(&QCompleter::activated), this, [this](const QString &completion) { diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index 98b21d0f1..374d35cdf 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -2,12 +2,12 @@ #include "../../../../../client/settings/cache_settings.h" #include "../../../cards/additional_info/mana_symbol_widget.h" -#include "../../../utility/completer_utils.h" #include "../../tab_supervisor.h" #include "api_response/archidekt_deck_listing_api_response.h" #include "display/archidekt_api_response_deck_display_widget.h" #include "display/archidekt_api_response_deck_listings_display_widget.h" +#include #include #include #include @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -278,14 +279,41 @@ void TabArchidekt::setupFilterWidgets() auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto displayModel = new CardDatabaseDisplayModel(this); displayModel->setSourceModel(cardDatabaseModel); + auto *searchModel = new CardSearchModel(displayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this); - cardsField->setCompleter(cardSetup.completer); - commandersField->setCompleter(cardSetup.completer); + auto *proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + auto *completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(10); + + cardsField->setCompleter(completer); + commandersField->setCompleter(completer); // Keep autocomplete working for both fields - connectCardCompleterSearch(cardsField, cardSetup); - connectCardCompleterSearch(commandersField, cardSetup); + connect(cardsField, &QLineEdit::textChanged, this, [=](const QString &text) { + searchModel->updateSearchResults(text); + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + completer->complete(); + } + }); + + connect(commandersField, &QLineEdit::textChanged, this, [=](const QString &text) { + searchModel->updateSearchResults(text); + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + completer->complete(); + } + }); // Assemble secondary toolbar secondaryToolbarLayout->addWidget(bracketLabel); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp index 500eab13a..42a689898 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp @@ -1,7 +1,6 @@ #include "tab_edhrec_main.h" #include "../../../../../client/settings/cache_settings.h" -#include "../../../utility/completer_utils.h" #include "../../tab_supervisor.h" #include "api_response/average_deck/edhrec_average_deck_api_response.h" #include "api_response/commander/edhrec_commander_api_response.h" @@ -13,6 +12,7 @@ #include "display/top_commander/edhrec_top_commanders_api_response_display_widget.h" #include "display/top_tags/edhrec_top_tags_api_response_display_widget.h" +#include #include #include #include @@ -63,12 +63,32 @@ TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor auto cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); auto displayModel = new CardDatabaseDisplayModel(this); displayModel->setSourceModel(cardDatabaseModel); + auto *searchModel = new CardSearchModel(displayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, this); - searchBar->setCompleter(cardSetup.completer); + auto *proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + auto *completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(10); + searchBar->setCompleter(completer); // Update suggestions dynamically - connectCardCompleterSearch(searchBar, cardSetup); + connect(searchBar, &QLineEdit::textChanged, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textChanged, this, [=](const QString &text) { + // Ensure substring matching + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + + if (!text.isEmpty()) { + completer->complete(); // Force the dropdown to appear + } + }); searchPushButton = new QPushButton(navigationContainer); connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { doSearch(); }); diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp index 999bc3db0..3dc4de15f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -1,6 +1,5 @@ #include "tab_card_art_rules.h" -#include "../utility/completer_utils.h" #include "libcockatrice/card/database/card_database_manager.h" #include @@ -195,14 +194,29 @@ void TabCardArtRules::initSearchBar() cardDbModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDbDisplayModel = new CardDatabaseDisplayModel(this); cardDbDisplayModel->setSourceModel(cardDbModel); + cardSearchModel = new CardSearchModel(cardDbDisplayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(cardDbDisplayModel, this, 15); - cardSearchModel = cardSetup.searchModel; - cardProxyModel = cardSetup.proxyModel; - searchCompleter = cardSetup.completer; + cardProxyModel = new CardCompleterProxyModel(this); + cardProxyModel->setSourceModel(cardSearchModel); + cardProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + searchCompleter = new QCompleter(cardProxyModel, this); + searchCompleter->setCompletionRole(Qt::DisplayRole); + searchCompleter->setCompletionMode(QCompleter::PopupCompletion); + searchCompleter->setCaseSensitivity(Qt::CaseInsensitive); + searchCompleter->setFilterMode(Qt::MatchContains); + searchCompleter->setMaxVisibleItems(15); searchEdit->setCompleter(searchCompleter); - connectCardCompleterSearch(searchEdit, cardSetup); + connect(searchEdit, &QLineEdit::textEdited, cardSearchModel, &CardSearchModel::updateSearchResults); + connect(searchEdit, &QLineEdit::textEdited, this, [this](const QString &text) { + const QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + cardProxyModel->setFilterRegularExpression( + QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + if (!text.isEmpty()) { + searchCompleter->complete(); + } + }); connect(searchCompleter, static_cast(&QCompleter::activated), this, [this](const QString &name) { searchEdit->setText(name); }); connect(searchEdit, &QLineEdit::editingFinished, this, diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 82d99b605..7ffcd8a9b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -21,15 +21,10 @@ #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/server/user/user_list_manager.h" -#include "../interface/widgets/utility/completer_utils.h" #include "../interface/widgets/utility/line_edit_completer.h" #include "../interface/window_main.h" #include "../main.h" #include "../utility/visibility_change_listener.h" -#include "card/card_completer_proxy_model.h" -#include "card/card_search_model.h" -#include "card_database_display_model.h" -#include "card_database_model.h" #include "tab_supervisor.h" #include @@ -40,9 +35,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -545,8 +538,7 @@ bool TabGame::leaveGame() void TabGame::actSay() { - if (sayEdit->hasVisibleCompleterPopup()) { - sayEdit->hideCompleterPopups(); + if (completer->popup()->isVisible()) { return; } @@ -566,14 +558,14 @@ void TabGame::addPlayerToAutoCompleteList(QString playerName) { if (sayEdit && !autocompleteUserList.contains(playerName)) { autocompleteUserList << playerName; - mentionModel->setStringList(autocompleteUserList); + sayEdit->setCompletionList(autocompleteUserList); } } void TabGame::removePlayerFromAutoCompleteList(QString playerName) { if (sayEdit && autocompleteUserList.removeOne(playerName)) { - mentionModel->setStringList(autocompleteUserList); + sayEdit->setCompletionList(autocompleteUserList); } } @@ -636,8 +628,8 @@ void TabGame::actRotateViewCCW() void TabGame::actCompleterChanged() { - SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2) - : mentionCompleter->setCompletionRole(1); + SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) + : completer->setCompletionRole(1); } void TabGame::notifyPlayerJoin(QString playerName) @@ -1289,25 +1281,12 @@ void TabGame::createMessageDock(bool bReplay) sayEdit->setMaxLength(MAX_TEXT_LENGTH); sayLabel->setBuddy(sayEdit); connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent); - mentionModel = new QStringListModel(autocompleteUserList, sayEdit); - mentionCompleter = createMentionCompleter(mentionModel, sayEdit); - sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention); - - auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit); - auto *displayModel = new CardDatabaseDisplayModel(sayEdit); - displayModel->setSourceModel(cardDatabaseModel); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit); - sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card); - - connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) { - cardSetup.searchModel->updateSearchResults(text); - cardSetup.proxyModel->setFilterRegularExpression( - QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); - if (sayEdit->hasFocus()) { - cardSetup.completer->complete(); - } - }); + completer = new QCompleter(autocompleteUserList, sayEdit); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setMaxVisibleItems(5); + completer->setFilterMode(Qt::MatchStartsWith); + sayEdit->setCompleter(completer); actCompleterChanged(); if (game->getPlayerManager()->isSpectator()) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index fc51817c6..9c1406b56 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -19,7 +19,6 @@ #include #include #include -#include class CardMenu; class ServerInfo_PlayerProperties; @@ -62,8 +61,7 @@ private: const UserListProxy *userListProxy; ReplayWidget *replayWidget = nullptr; QStringList gameTypes; - QCompleter *mentionCompleter; - QStringListModel *mentionModel; + QCompleter *completer; QStringList autocompleteUserList; QStackedWidget *mainWidget; diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 705266b1d..5705c184e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -8,11 +8,6 @@ #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/server/user/user_list_widget.h" #include "../main.h" -#include "../utility/completer_utils.h" -#include "card/card_completer_proxy_model.h" -#include "card/card_search_model.h" -#include "card_database_display_model.h" -#include "card_database_model.h" #include "tab_account.h" #include "tab_supervisor.h" @@ -22,14 +17,11 @@ #include #include #include -#include #include -#include #include #include #include #include -#include #include #include #include @@ -145,27 +137,13 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, gameSelector->processGameInfo(info.game_list(i)); } - mentionModel = new QStringListModel(autocompleteUserList, sayEdit); - mentionCompleter = createMentionCompleter(mentionModel, sayEdit); - sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention); - - auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit); - auto *displayModel = new CardDatabaseDisplayModel(sayEdit); - displayModel->setSourceModel(cardDatabaseModel); - const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit); - sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card); - - connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) { - cardSetup.searchModel->updateSearchResults(text); - cardSetup.proxyModel->setFilterRegularExpression( - QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); - if (sayEdit->hasFocus()) { - cardSetup.completer->complete(); - } - }); + completer = new QCompleter(autocompleteUserList, sayEdit); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setMaxVisibleItems(5); + completer->setFilterMode(Qt::MatchStartsWith); + sayEdit->setCompleter(completer); actCompleterChanged(); - connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &TabRoom::refreshShortcuts); refreshShortcuts(); @@ -235,8 +213,8 @@ void TabRoom::sendMessage() { if (sayEdit->text().isEmpty()) { return; - } else if (sayEdit->hasVisibleCompleterPopup()) { - sayEdit->hideCompleterPopups(); + } else if (completer->popup()->isVisible()) { + completer->popup()->hide(); return; } else { Command_RoomSay cmd; @@ -270,8 +248,8 @@ void TabRoom::actOpenChatSettings() void TabRoom::actCompleterChanged() { - SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2) - : mentionCompleter->setCompletionRole(1); + SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2) + : completer->setCompletionRole(1); } void TabRoom::processRoomEvent(const RoomEvent &event) @@ -307,18 +285,16 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event) void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event) { - QString mention = "@" + QString::fromStdString(event.user_info().name()); - if (!autocompleteUserList.contains(mention)) { - autocompleteUserList << mention; - mentionModel->setStringList(autocompleteUserList); + if (!autocompleteUserList.contains("@" + QString::fromStdString(event.user_info().name()))) { + autocompleteUserList << "@" + QString::fromStdString(event.user_info().name()); + sayEdit->setCompletionList(autocompleteUserList); } } void TabRoom::processLeaveRoomEvent(const Event_LeaveRoom &event) { - QString mention = "@" + QString::fromStdString(event.name()); - autocompleteUserList.removeOne(mention); - mentionModel->setStringList(autocompleteUserList); + autocompleteUserList.removeOne("@" + QString::fromStdString(event.name())); + sayEdit->setCompletionList(autocompleteUserList); } void TabRoom::processRoomSayEvent(const Event_RoomSay &event) diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index dc58b8bf6..2881c25f4 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -14,7 +14,6 @@ #include #include #include -#include class UserListProxy; class UserListManager; @@ -64,7 +63,6 @@ private: ChatView *chatView; QLabel *sayLabel; LineEditCompleter *sayEdit; - QStringListModel *mentionModel; QGroupBox *chatGroupBox; QMenu *roomMenu; @@ -74,7 +72,7 @@ private: [[nodiscard]] QString sanitizeHtml(QString dirty) const; QStringList autocompleteUserList; - QCompleter *mentionCompleter; + QCompleter *completer; signals: void roomClosing(TabRoom *tab); void openMessageDialog(const QString &userName, bool focus); diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp deleted file mode 100644 index 6cf096cd4..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp +++ /dev/null @@ -1,439 +0,0 @@ -#include "card_completer_delegate.h" - -#include "../cards/additional_info/mana_cost_widget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// Internal colour helpers -// --------------------------------------------------------------------------- - -namespace -{ - -struct ManaColor -{ - QColor fill; - QColor rim; - QColor text; -}; - -ManaColor manaColor(QChar symbol) -{ - switch (symbol.unicode()) { - case 'W': - return {QColor(248, 248, 246), QColor(190, 180, 160), QColor(80, 70, 50)}; - case 'U': - return {QColor(55, 130, 210), QColor(30, 90, 160), QColor(255, 255, 255)}; - case 'B': - return {QColor(90, 65, 160), QColor(55, 38, 110), QColor(220, 200, 255)}; - case 'R': - return {QColor(210, 55, 55), QColor(150, 30, 30), QColor(255, 255, 255)}; - case 'G': - return {QColor(45, 148, 90), QColor(28, 95, 58), QColor(255, 255, 255)}; - default: - return {QColor(100, 115, 135), QColor(65, 78, 95), QColor(230, 235, 240)}; - } -} - -QColor blend(QColor a, QColor b, qreal t) -{ - return QColor::fromRgbF(a.redF() + (b.redF() - a.redF()) * t, a.greenF() + (b.greenF() - a.greenF()) * t, - a.blueF() + (b.blueF() - a.blueF()) * t, a.alphaF() + (b.alphaF() - a.alphaF()) * t); -} - -} // namespace - -// --------------------------------------------------------------------------- - -QColor CardCompleterDelegate::accentForColors(const QString &colors) -{ - if (colors.isEmpty()) { - return QColor(100, 115, 135); - } - - QSet seen; - for (const QChar c : colors) { - if (QString("WUBRG").contains(c)) { - seen.insert(c); - } - } - - if (seen.size() > 1) { - return QColor(205, 145, 25); - } - - if (seen.isEmpty()) { - return QColor(100, 115, 135); - } - - return manaColor(*seen.begin()).fill; -} - -// --------------------------------------------------------------------------- - -CardCompleterDelegate::CardCompleterDelegate(QObject *parent) : QStyledItemDelegate(parent) -{ - symbolCache.setMaxCost(64); - setCodeCache.setMaxCost(64); -} - -// --------------------------------------------------------------------------- -// sizeHint -// --------------------------------------------------------------------------- - -QSize CardCompleterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - Q_UNUSED(option) - - if (!index.isValid()) { - return QStyledItemDelegate::sizeHint(option, index); - } - - // Fixed wide rows so the popup has room for name, type line, set and mana - return {480, CardRowHeight}; -} - -// --------------------------------------------------------------------------- -// Mana symbol painting -// --------------------------------------------------------------------------- - -const QPixmap *CardCompleterDelegate::cachedSymbolPixmap(const QString &symbol, int size) const -{ - const QString key = symbol + QString::number(size); - - if (symbolCache.contains(key)) { - return symbolCache[key]; - } - - QPixmap src(QString("theme:icons/mana/%1").arg(symbol)); - - if (!src.isNull()) { - auto *pm = new QPixmap(src.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - - symbolCache.insert(key, pm); - return pm; - } - - return nullptr; -} - -// --------------------------------------------------------------------------- - -void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const -{ - const QRect pip(centre.x() - radius, centre.y() - radius, radius * 2, radius * 2); - - const QPixmap *px = cachedSymbolPixmap(symbol, radius * 2); - - if (px && !px->isNull()) { - p->drawPixmap(pip, *px); - return; - } - - bool isNumeric = false; - const int numVal = symbol.toInt(&isNumeric); - - const QString label = isNumeric ? QString::number(numVal) : symbol; - - const ManaColor mc = - (symbol.length() == 1 && QString("WUBRG").contains(symbol)) ? manaColor(symbol[0]) : manaColor(QChar('X')); - - QPainterPath circle; - circle.addEllipse(pip); - - p->save(); - p->setClipPath(circle); - p->fillPath(circle, mc.fill); - p->restore(); - - p->setPen(QPen(mc.rim, 1.2)); - p->setBrush(Qt::NoBrush); - p->drawEllipse(pip.adjusted(1, 1, -1, -1)); - - QFont f = p->font(); - f.setPixelSize(qMax(radius - 1, 7)); - f.setBold(true); - - p->setFont(f); - p->setPen(mc.text); - p->drawText(pip, Qt::AlignCenter, label); -} - -// --------------------------------------------------------------------------- - -int CardCompleterDelegate::drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const -{ - if (manaCost.isEmpty()) { - return row.right(); - } - - const int diam = radius * 2; - - // Split, adventure, aftermath and prepare cards store both halves of the - // cost joined by "//" (e.g. "1W // W"); draw each half as its own group. - static const QRegularExpression splitRegex("\\s*//\\s*"); - - QList parts; - - for (const QString &part : manaCost.split(splitRegex, Qt::SkipEmptyParts)) { - const QStringList symbols = ManaCostWidget::parseManaCost(part); - - if (!symbols.isEmpty()) { - parts.append(symbols); - } - } - - int totalW = 0; - - for (int i = 0; i < parts.size(); ++i) { - if (i > 0) { - totalW += PartGap; - } - - totalW += parts.at(i).size() * diam + qMax(0, parts.at(i).size() - 1) * SymbolSpacing; - } - - const int rightPad = 14; - - int x = row.right() - rightPad - totalW + radius; - - const int cy = row.center().y(); - - for (int i = 0; i < parts.size(); ++i) { - const QStringList &symbols = parts.at(i); - - for (const QString &sym : symbols) { - drawManaSymbol(p, {x, cy}, sym, radius); - x += diam + SymbolSpacing; - } - - if (i < parts.size() - 1) { - x += PartGap - SymbolSpacing; - } - } - - return row.right() - rightPad - totalW - 10; -} - -// --------------------------------------------------------------------------- - -QString CardCompleterDelegate::setCodeForCard(const QSharedPointer &card) const -{ - if (!card) { - return QString(); - } - - const QString name = card->getName(); - - if (setCodeCache.contains(name)) { - return *setCodeCache[name]; - } - - QString code; - - const PrintingInfo printing = CardDatabaseManager::query()->getPreferredPrinting(card); - - if (auto set = printing.getSet()) { - code = set->getShortName(); - } - - auto *cached = new QString(code); - setCodeCache.insert(name, cached); - return code; -} - -// --------------------------------------------------------------------------- -// paint -// --------------------------------------------------------------------------- - -void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - if (!index.isValid()) { - QStyledItemDelegate::paint(painter, option, index); - return; - } - - const QRect rect = option.rect; - const QPalette &pal = option.palette; - - const bool selected = option.state & QStyle::State_Selected; - const bool hovered = option.state & QStyle::State_MouseOver; - - painter->save(); - painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); - - auto card = index.data(CardSearchModel::CardInfoRole).value>(); - - if (!card) { - painter->fillRect(rect, pal.color(QPalette::Base)); - painter->restore(); - return; - } - - const QString cardName = card->getName(); - const QString manaCost = card->getManaCost(); - const QString typeLine = card->getCardType(); - const QString setCode = setCodeForCard(card); - - const QColor accent = accentForColors(card->getColors()); - - const QColor base = pal.color(QPalette::Base); - const QColor textColor = pal.color(QPalette::Text); - const QColor secondaryColor = pal.color(QPalette::PlaceholderText); - - QColor tinted = blend(base, accent, 0.40); - - if (hovered) { - tinted = blend(tinted, Qt::white, 0.05); - } - - const QRectF cardRect = rect.adjusted(3, 2, -3, -2); - - // ----------------------------------------------------------------------- - // Main card body - // ----------------------------------------------------------------------- - - QPainterPath path; - path.addRoundedRect(cardRect, 7, 7); - - painter->save(); - painter->setClipPath(path); - - QLinearGradient bodyGrad(cardRect.topLeft(), cardRect.bottomLeft()); - - bodyGrad.setColorAt(0.0, blend(tinted, Qt::white, 0.10)); - bodyGrad.setColorAt(1.0, blend(tinted, Qt::black, 0.18)); - - painter->fillPath(path, bodyGrad); - - // ----------------------------------------------------------------------- - // Accent strip - // ----------------------------------------------------------------------- - - QRectF accentRect(cardRect.left(), cardRect.top(), AccentBarWidth, cardRect.height()); - - QLinearGradient accentGrad(accentRect.topLeft(), accentRect.bottomLeft()); - - accentGrad.setColorAt(0.0, blend(accent, Qt::white, 0.20)); - accentGrad.setColorAt(1.0, blend(accent, Qt::black, 0.25)); - - painter->fillRect(accentRect, accentGrad); - - // ----------------------------------------------------------------------- - // Right mana zone - // ----------------------------------------------------------------------- - - const QRectF manaZone(cardRect.right() - 110, cardRect.top(), 110, cardRect.height()); - - QLinearGradient manaGrad(manaZone.topLeft(), manaZone.bottomLeft()); - - manaGrad.setColorAt(0, QColor(0, 0, 0, 18)); - manaGrad.setColorAt(1, QColor(0, 0, 0, 42)); - - painter->fillRect(manaZone, manaGrad); - - painter->restore(); - - // ----------------------------------------------------------------------- - // Border - // ----------------------------------------------------------------------- - - QColor border = blend(accent, Qt::black, 0.45); - - if (hovered) { - border = blend(border, Qt::white, 0.18); - } - - painter->setPen(QPen(border, 1.2)); - painter->drawPath(path); - - // ----------------------------------------------------------------------- - // Selection glow - // ----------------------------------------------------------------------- - - if (selected) { - QColor glow = pal.color(QPalette::Highlight); - glow.setAlpha(30); - - painter->fillPath(path, glow); - - painter->setPen(QPen(pal.color(QPalette::Highlight), 2)); - painter->drawPath(path); - } - - // ----------------------------------------------------------------------- - // Mana cost - // ----------------------------------------------------------------------- - - const int costLeft = drawManaCost(painter, cardRect.toRect(), manaCost, SymbolRadius); - - // ----------------------------------------------------------------------- - // Card name + type line + set code - // ----------------------------------------------------------------------- - - const int textLeft = cardRect.left() + AccentBarWidth + 12; - const int textRight = costLeft - 8; - const int textWidth = qMax(0, textRight - textLeft); - - // ----------------------------------------------------------------------- - // Card name (top band) - // ----------------------------------------------------------------------- - - { - const QRect nameRect(textLeft, rect.top() + 2, textWidth, 20); - - QFont f = option.font; - f.setPixelSize(13); - f.setBold(true); - - painter->setFont(f); - - const QString nameText = QFontMetrics(f).elidedText(cardName, Qt::ElideRight, nameRect.width()); - - painter->setPen(QColor(0, 0, 0, 140)); - painter->drawText(nameRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, nameText); - - painter->setPen(textColor); - painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, nameText); - } - - // ----------------------------------------------------------------------- - // Type line + set code (bottom band) - // ----------------------------------------------------------------------- - - { - const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24); - - QFont f = option.font; - f.setPixelSize(10); - - painter->setFont(f); - - QString infoLine = typeLine; - - if (!setCode.isEmpty()) { - infoLine += " \u00b7 " + setCode; - } - - const QString infoText = QFontMetrics(f).elidedText(infoLine, Qt::ElideRight, infoRect.width()); - - painter->setPen(QColor(0, 0, 0, 120)); - painter->drawText(infoRect.translated(0, 1), Qt::AlignLeft | Qt::AlignVCenter, infoText); - - painter->setPen(secondaryColor); - painter->drawText(infoRect, Qt::AlignLeft | Qt::AlignVCenter, infoText); - } - - painter->restore(); -} diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.h b/cockatrice/src/interface/widgets/utility/card_completer_delegate.h deleted file mode 100644 index 18661a21b..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @file card_completer_delegate.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef CARD_COMPLETER_DELEGATE_H -#define CARD_COMPLETER_DELEGATE_H - -#include -#include -#include -#include - -class CardInfo; - -/** - * @brief Paints styled card completer popup rows. - * - * Each row shows the card name, type line, set code and mana cost pips, - * color-coded by the card's color identity. Card data is read directly from - * the CardSearchModel::CardInfoRole so no extra database lookups are needed. - */ -class CardCompleterDelegate : public QStyledItemDelegate -{ - Q_OBJECT -public: - explicit CardCompleterDelegate(QObject *parent = nullptr); - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -private: - // Mana symbol pixmaps, loaded once and cached - mutable QCache symbolCache; - - // Set short codes, resolved once per card name and cached - mutable QCache setCodeCache; - - // Resolve the card's color string ("RG", "W", "", ...) → accent QColor - static QColor accentForColors(const QString &colors); - - // Draw a single mana symbol pip at centre point - void drawManaSymbol(QPainter *p, QPoint centre, const QString &symbol, int radius) const; - - // Draw all mana pips for a cost string like "2RG" or "{2}{R}{G}"; split and - // adventure costs ("1W // W") are drawn as separate groups. Returns the left-most x used - int drawManaCost(QPainter *p, const QRect &row, const QString &manaCost, int radius) const; - - // Load (or return cached) a mana icon pixmap; falls back to painted circle - const QPixmap *cachedSymbolPixmap(const QString &symbol, int size) const; - - // Resolve the preferred printing's set short code for a card - QString setCodeForCard(const QSharedPointer &card) const; - - static constexpr int CardRowHeight = 40; - static constexpr int AccentBarWidth = 5; - static constexpr int SymbolRadius = 9; - static constexpr int SymbolSpacing = 2; - static constexpr int PartGap = 14; -}; - -#endif // CARD_COMPLETER_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp b/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp deleted file mode 100644 index 6794ccbb4..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_styler.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include "card_completer_styler.h" - -#include "../cards/card_info_picture_enlarged_widget.h" -#include "card_completer_delegate.h" -#include "reversed_completer_model.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -const QSize PreviewSize(300, 419); -const int PreviewMargin = 16; -const int FadeDuration = 120; -} // namespace - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::apply(QCompleter *completer) -{ - if (!completer) { - return; - } - - // The styler lives as long as the completer - new CardCompleterStyler(completer, completer); -} - -// --------------------------------------------------------------------------- - -CardCompleterStyler::CardCompleterStyler(QCompleter *completer, QObject *parent) - : QObject(parent), completer(completer), reversedModel(nullptr), preview(nullptr), above(false) -{ - auto *popup = completer->popup(); - - // Wrap the completer's model so its row order can be reversed when the - // popup is shown above the text edit - QAbstractItemModel *sourceModel = completer->model(); - reversedModel = new ReversedCompleterModel(completer); - reversedModel->setSourceModel(sourceModel); - completer->setModel(reversedModel); - - popup->setItemDelegate(new CardCompleterDelegate(popup)); - - popup->viewport()->setMouseTracking(true); - - popup->installEventFilter(this); - popup->viewport()->installEventFilter(this); - - connect(popup->selectionModel(), &QItemSelectionModel::currentRowChanged, this, - &CardCompleterStyler::updatePreview); - connect(completer, qOverload(&QCompleter::activated), this, &CardCompleterStyler::hidePreview); - connect(completer->completionModel(), &QAbstractItemModel::modelReset, this, - &CardCompleterStyler::onCompletionReset); -} - -CardCompleterStyler::~CardCompleterStyler() -{ - if (preview) { - preview->deleteLater(); - } -} - -// --------------------------------------------------------------------------- - -bool CardCompleterStyler::eventFilter(QObject *obj, QEvent *ev) -{ - auto *popup = completer->popup(); - - if (obj == popup->viewport()) { - if (ev->type() == QEvent::MouseMove) { - updatePreviewFromHover(static_cast(ev)->pos()); - } - } else if (obj == popup) { - switch (ev->type()) { - case QEvent::Show: - case QEvent::Move: - case QEvent::Resize: - updateOrientation(); - reposition(); - break; - case QEvent::Hide: - hidePreview(); - break; - case QEvent::KeyPress: - if (handlePopupKeyPress(static_cast(ev))) { - return true; - } - break; - default: - break; - } - } - - return QObject::eventFilter(obj, ev); -} - -// --------------------------------------------------------------------------- - -bool CardCompleterStyler::handlePopupKeyPress(QKeyEvent *event) -{ - const int key = event->key(); - if (key != Qt::Key_Up && key != Qt::Key_Down) { - return false; - } - - if (!above) { - return false; - } - - auto *popup = completer->popup(); - const int rowCount = completer->completionModel()->rowCount(); - if (rowCount == 0) { - return true; - } - - // With the popup above the text edit the rows are shown in reverse order and - // the closest match is in the bottom-most row. The up arrow then advances - // through the list (towards its end at the top of the popup). - const int currentRow = popup->currentIndex().row(); - const int step = (key == Qt::Key_Up) ? -1 : 1; - const int newRow = qBound(0, currentRow + step, rowCount - 1); - - if (newRow != currentRow) { - popup->setCurrentIndex(completer->completionModel()->index(newRow, completer->completionColumn())); - } - - return true; -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::updateOrientation() -{ - above = isPopupAboveWidget(); - reversedModel->setEnabled(above); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::ensureClosestSelected() -{ - auto *popup = completer->popup(); - auto *completionModel = completer->completionModel(); - const int rowCount = completionModel->rowCount(); - if (rowCount == 0) { - return; - } - - const int currentRow = popup->currentIndex().row(); - const int closestRow = rowCount - 1; - - if (currentRow != closestRow) { - popup->setCurrentIndex(completionModel->index(closestRow, completer->completionColumn())); - } -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::onCompletionReset() -{ - if (above) { - ensureClosestSelected(); - } -} - -// --------------------------------------------------------------------------- - -bool CardCompleterStyler::isPopupAboveWidget() const -{ - auto *popup = completer->popup(); - QWidget *widget = completer->widget(); - - if (!widget || !popup->isVisible()) { - return false; - } - - const QPoint popupBottom = popup->mapToGlobal(QPoint(0, popup->height())); - const QPoint fieldTop = widget->mapToGlobal(QPoint(0, 0)); - - return popupBottom.y() <= fieldTop.y(); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::updatePreviewFromHover(const QPoint &pos) -{ - const QModelIndex index = completer->popup()->indexAt(pos); - - // Hovering updates the preview but must not change the current completion - if (index.isValid() && index != previewedIndex) { - updatePreview(index); - } -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::updatePreview(const QModelIndex &index) -{ - previewedIndex = index; - - if (!index.isValid()) { - hidePreview(); - return; - } - - auto card = index.data(CardSearchModel::CardInfoRole).value>(); - - if (!card) { - hidePreview(); - return; - } - - if (!preview) { - QWidget *contextWindow = completer->widget() ? completer->widget()->window() : nullptr; - - preview = new CardInfoPictureEnlargedWidget(contextWindow); - preview->hide(); - preview->setWindowOpacity(0.0); - } - - const ExactCard exact = CardDatabaseManager::query()->getCard({card->getName()}); - - if (!exact) { - hidePreview(); - return; - } - - preview->setCardPixmap(exact, PreviewSize); - - reposition(); - showPreview(); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::showPreview() -{ - if (!preview) { - return; - } - - stopPreviewFade(); - - preview->show(); - preview->raise(); - - auto *fade = new QPropertyAnimation(preview, "windowOpacity", preview); - - fade->setDuration(FadeDuration); - fade->setStartValue(preview->windowOpacity()); - fade->setEndValue(1.0); - fade->start(QAbstractAnimation::DeleteWhenStopped); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::hidePreview() -{ - if (!preview) { - return; - } - - // Stop any in-flight fade first so a stale one cannot keep the preview visible - stopPreviewFade(); - preview->hide(); -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::stopPreviewFade() -{ - if (!preview) { - return; - } - - const auto animations = preview->findChildren(); - for (auto *animation : animations) { - animation->stop(); - } -} - -// --------------------------------------------------------------------------- - -void CardCompleterStyler::reposition() -{ - if (!preview) { - return; - } - - auto *popup = completer->popup(); - - const QRect popupGlobalRect(popup->mapToGlobal(popup->rect().topLeft()), popup->rect().size()); - - int anchorY; - - if (previewedIndex.isValid()) { - const QRect itemRect = popup->visualRect(previewedIndex); - const QPoint itemCenter = popup->viewport()->mapToGlobal(itemRect.center()); - anchorY = itemCenter.y(); - } else { - anchorY = popupGlobalRect.center().y(); - } - - const QScreen *screen = popup->screen(); - const QRect screenGeom = screen ? screen->availableGeometry() : QRect(); - - const int rightX = popupGlobalRect.right() + PreviewMargin; - const int leftX = popupGlobalRect.left() - PreviewMargin - preview->width(); - - int x; - int y = anchorY - preview->height() / 2; - - if (screenGeom.isEmpty()) { - x = rightX; - } else if (rightX + preview->width() <= screenGeom.right()) { - x = rightX; - } else if (leftX >= screenGeom.left()) { - x = leftX; - } else { - x = rightX; - } - - if (!screenGeom.isEmpty()) { - x = qBound(screenGeom.left(), x, qMax(screenGeom.left(), screenGeom.right() - preview->width())); - y = qBound(screenGeom.top(), y, qMax(screenGeom.top(), screenGeom.bottom() - preview->height())); - } - - preview->move(x, y); -} diff --git a/cockatrice/src/interface/widgets/utility/card_completer_styler.h b/cockatrice/src/interface/widgets/utility/card_completer_styler.h deleted file mode 100644 index 616e3fe61..000000000 --- a/cockatrice/src/interface/widgets/utility/card_completer_styler.h +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @file card_completer_styler.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef CARD_COMPLETER_STYLER_H -#define CARD_COMPLETER_STYLER_H - -#include -#include - -class CardInfoPictureEnlargedWidget; -class QCompleter; -class QKeyEvent; -class QPoint; -class ReversedCompleterModel; - -/** - * @brief Applies styled row painting and a card-image preview to a card completer. - * - * The completer popup rows are painted by CardCompleterDelegate and the image of - * the currently selected (or hovered) row is shown beside the popup. - */ -class CardCompleterStyler : public QObject -{ - Q_OBJECT -public: - /** - * @brief Styles @p completer and follows its current selection. - * - * The styler is parented to the completer so it lives exactly as long as the - * completer itself. - */ - static void apply(QCompleter *completer); - -protected: - bool eventFilter(QObject *obj, QEvent *ev) override; - -private slots: - void updatePreview(const QModelIndex &index); - void updatePreviewFromHover(const QPoint &pos); - void onCompletionReset(); - -private: - explicit CardCompleterStyler(QCompleter *completer, QObject *parent = nullptr); - ~CardCompleterStyler() override; - - void showPreview(); - void hidePreview(); - void stopPreviewFade(); - void reposition(); - - void updateOrientation(); - void ensureClosestSelected(); - - bool handlePopupKeyPress(QKeyEvent *event); - bool isPopupAboveWidget() const; - - QCompleter *completer; - ReversedCompleterModel *reversedModel; - CardInfoPictureEnlargedWidget *preview; - QModelIndex previewedIndex; - bool above; -}; - -#endif // CARD_COMPLETER_STYLER_H diff --git a/cockatrice/src/interface/widgets/utility/completer_utils.cpp b/cockatrice/src/interface/widgets/utility/completer_utils.cpp deleted file mode 100644 index 16d5cfd13..000000000 --- a/cockatrice/src/interface/widgets/utility/completer_utils.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "completer_utils.h" - -#include "card_completer_styler.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems) -{ - auto *searchModel = new CardSearchModel(displayModel, parent); - - auto *proxyModel = new CardCompleterProxyModel(parent); - proxyModel->setSourceModel(searchModel); - proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); - - auto *completer = new QCompleter(proxyModel, parent); - completer->setCompletionRole(Qt::DisplayRole); - completer->setCompletionMode(QCompleter::PopupCompletion); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - completer->setMaxVisibleItems(maxVisibleItems); - CardCompleterStyler::apply(completer); - - return {searchModel, proxyModel, completer}; -} - -void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup) -{ - QObject::connect(edit, &QLineEdit::textEdited, setup.searchModel, &CardSearchModel::updateSearchResults); - QObject::connect(edit, &QLineEdit::textEdited, setup.completer, [setup](const QString &text) { - setup.proxyModel->setFilterRegularExpression( - QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption)); - if (!text.isEmpty()) { - setup.completer->complete(); - } - }); -} - -QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent) -{ - auto *completer = new QCompleter(model, parent); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setMaxVisibleItems(5); - completer->setFilterMode(Qt::MatchStartsWith); - return completer; -} diff --git a/cockatrice/src/interface/widgets/utility/completer_utils.h b/cockatrice/src/interface/widgets/utility/completer_utils.h deleted file mode 100644 index 5d2ddfc30..000000000 --- a/cockatrice/src/interface/widgets/utility/completer_utils.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @file completer_utils.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef COMPLETER_UTILS_H -#define COMPLETER_UTILS_H - -class CardCompleterProxyModel; -class CardDatabaseDisplayModel; -class CardSearchModel; -class QCompleter; -class QLineEdit; -class QObject; -class QStringListModel; - -struct CardCompleterSetup -{ - CardSearchModel *searchModel; - CardCompleterProxyModel *proxyModel; - QCompleter *completer; -}; - -CardCompleterSetup -createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems = 10); - -void connectCardCompleterSearch(QLineEdit *edit, const CardCompleterSetup &setup); - -QCompleter *createMentionCompleter(QStringListModel *model, QObject *parent); - -#endif // CARD_COMPLETER_UTILS_H diff --git a/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp b/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp index 3a678029d..13f475a61 100644 --- a/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp +++ b/cockatrice/src/interface/widgets/utility/line_edit_completer.cpp @@ -1,199 +1,135 @@ #include "line_edit_completer.h" #include +#include #include -#include +#include +#include +#include +#include -LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent) +LineEditCompleter::LineEditCompleter(QWidget *parent) : LineEditUnfocusable(parent), c(nullptr) { } -void LineEditCompleter::addCompleter(QCompleter *c, CompleterTrigger trigger) -{ - c->setWidget(this); - c->setCompletionMode(QCompleter::PopupCompletion); - c->setCaseSensitivity(Qt::CaseInsensitive); - connect(c, qOverload(&QCompleter::activated), this, - qOverload(&LineEditCompleter::insertCompletion)); - - completers.append({c, trigger}); -} - -bool LineEditCompleter::hasVisibleCompleterPopup() const -{ - for (const auto &info : completers) { - if (info.completer->popup()->isVisible()) { - return true; - } - } - return false; -} - -void LineEditCompleter::hideCompleterPopups() -{ - for (const auto &info : completers) { - info.completer->popup()->hide(); - } -} - void LineEditCompleter::focusOutEvent(QFocusEvent *e) { LineEditUnfocusable::focusOutEvent(e); - - // Only commit the highlighted completion when focus moves away via Tab. - // Other focus losses (e.g. the unfocus shortcut / Escape) must simply close - // the popup without inserting anything. - if (e->reason() != Qt::TabFocusReason) { - hideCompleterPopups(); - return; + if (c->popup()->isVisible()) { + // Remove Popup + c->popup()->hide(); + // Truncate the line to last space or whole string + QString textValue = text(); + int lastIndex = textValue.length(); + int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; + int leftShift = qMin(lastIndex, lastWordStartIndex); + setText(textValue.left(leftShift)); + // Insert highlighted line from popup + insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); + // Set focus back to the textbox since tab was pressed + setFocus(); } - - for (auto &info : completers) { - if (!info.completer->popup()->isVisible()) { - continue; - } - - const QModelIndex currentIndex = info.completer->popup()->currentIndex(); - if (currentIndex.isValid()) { - insertCompletion(info.completer, currentIndex.data().toString()); - } - } - - hideCompleterPopups(); } void LineEditCompleter::keyPressEvent(QKeyEvent *event) { + switch (event->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Escape: + if (c->popup()->isVisible()) { + event->ignore(); + // Remove Popup + c->popup()->hide(); + // Truncate the line to last space or whole string + QString textValue = text(); + int lastIndexof = qMax(0, textValue.lastIndexOf(" ")); + QString finalString = textValue.left(lastIndexof); + // Add a space if there's a word + if (finalString != "") { + finalString += " "; + } + setText(finalString); + return; + } + break; + case Qt::Key_Space: + if (c->popup()->isVisible()) { + event->ignore(); + // Remove Popup + c->popup()->hide(); + // Truncate the line to last space or whole string + QString textValue = text(); + int lastIndex = textValue.length(); + int lastWordStartIndex = textValue.lastIndexOf(" ") + 1; + int leftShift = qMin(lastIndex, lastWordStartIndex); + setText(textValue.left(leftShift)); + // Insert highlighted line from popup + insert(c->completionModel()->index(c->popup()->currentIndex().row(), 0).data().toString() + " "); + return; + } + break; + default: + break; + } + LineEditUnfocusable::keyPressEvent(event); - - if (event->key() == Qt::Key_Escape) { - hideCompleterPopups(); + // return if the completer is null or if the most recently typed char was '@'. + // Only want the popup AFTER typing the first char of the mention. + if (!c || text().right(1).contains("@")) { + c->popup()->hide(); return; } - QString textValue = text(); - int cursorPos = cursorPosition(); - - CompleterInfo *active = nullptr; - QString prefix; - - for (auto &info : completers) { - bool triggered = false; - switch (info.trigger) { - case CompleterTrigger::Mention: { - int triggerPos = textValue.lastIndexOf("@", cursorPos - 1); - if (triggerPos != -1 && (triggerPos == 0 || textValue[triggerPos - 1].isSpace())) { - triggered = true; - // Keep the "@" so the prefix matches the "@"-prefixed mention model entries. - prefix = textValue.mid(triggerPos, cursorPos - triggerPos); - } - break; - } - case CompleterTrigger::Card: { - int triggerPos = textValue.lastIndexOf("[[", cursorPos - 1); - int closePos = textValue.indexOf("]]", triggerPos + 2); - if (triggerPos != -1 && (closePos == -1 || closePos >= cursorPos)) { - triggered = true; - prefix = textValue.mid(triggerPos + 2, cursorPos - (triggerPos + 2)); - } - break; - } - } - - if (triggered) { - active = &info; - break; - } - } - - if (!active) { - for (auto &info : completers) { - info.completer->popup()->hide(); - } + // Set new completion prefix + c->setCompletionPrefix(cursorWord(text())); + if (c->completionPrefix().length() < 1) { + c->popup()->hide(); return; } - active->completer->setCompletionPrefix(prefix); + // Draw completion box + QRect cr = cursorRect(); + cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); + c->complete(cr); - switch (active->trigger) { - case CompleterTrigger::Card: - emit cardPartialChanged(prefix); - return; - case CompleterTrigger::Mention: - break; - } - - active->completer->complete(); + // Select first item in the completion popup + QItemSelectionModel *sm = new QItemSelectionModel(c->completionModel()); + c->popup()->setSelectionModel(sm); + sm->select(c->completionModel()->index(0, 0), QItemSelectionModel::ClearAndSelect); + sm->setCurrentIndex(c->completionModel()->index(0, 0), QItemSelectionModel::NoUpdate); } -void LineEditCompleter::insertCompletion(const QString &completion) +QString LineEditCompleter::cursorWord(const QString &line) const { - for (auto &info : completers) { - if (info.completer == sender()) { - insertCompletion(info.completer, completion); - return; - } - } + return line.mid(line.left(cursorPosition()).lastIndexOf(" ") + 1, + cursorPosition() - line.left(cursorPosition()).lastIndexOf(" ") - 1); } -void LineEditCompleter::insertCompletion(QCompleter *completer, const QString &completion) +void LineEditCompleter::insertCompletion(QString arg) { - QString t = text(); - int pos = cursorPosition(); - - for (const auto &info : completers) { - if (info.completer != completer) { - continue; - } - - switch (info.trigger) { - case CompleterTrigger::Card: { - int triggerPos = t.lastIndexOf("[[", pos - 1); - if (triggerPos == -1) { - return; - } - - // If an earlier "[[" is still open it also encloses the cursor, so - // replace from its start. Otherwise completing in text such as - // "[[Opt[[Amok" would leave a stray "[[" behind. - int startPos = triggerPos; - for (int searchFrom = triggerPos; searchFrom > 0;) { - const int earlier = t.lastIndexOf("[[", searchFrom - 1); - if (earlier == -1) { - break; - } - const int earlierClose = t.indexOf("]]", earlier + 2); - if (earlierClose != -1 && earlierClose < pos) { - break; - } - startPos = earlier; - searchFrom = earlier; - } - - // If the cursor sits inside an already-closed [[...]] pair, replace - // the whole construct instead of leaving a duplicate closing bracket - // behind. - int insertEnd = pos; - const int closePos = t.indexOf("]]", startPos + 2); - if (closePos != -1 && closePos >= pos) { - insertEnd = closePos + 2; - } - - QString after = t.mid(insertEnd); - QString replaced = t.left(startPos + 2) + completion + "]] "; - setText(replaced + after); - setCursorPosition(replaced.length()); - return; - } - case CompleterTrigger::Mention: { - int triggerPos = t.lastIndexOf("@", pos - 1); - if (triggerPos == -1) { - return; - } - setText(t.replace(triggerPos, pos - triggerPos, completion + " ")); - setCursorPosition(triggerPos + completion.length() + 1); - return; - } - } - } + QString s_arg = arg + " "; + setText(text().replace(text().left(cursorPosition()).lastIndexOf(" ") + 1, + cursorPosition() - text().left(cursorPosition()).lastIndexOf(" ") - 1, s_arg)); +} + +void LineEditCompleter::setCompleter(QCompleter *completer) +{ + c = completer; + c->setWidget(this); + connect(c, qOverload(&QCompleter::activated), this, &LineEditCompleter::insertCompletion); +} + +void LineEditCompleter::setCompletionList(QStringList completionList) +{ + if (!c || c->popup()->isVisible()) { + return; + } + + QStringListModel *model; + model = (QStringListModel *)(c->model()); + if (model == NULL) { + model = new QStringListModel(); + } + model->setStringList(completionList); } diff --git a/cockatrice/src/interface/widgets/utility/line_edit_completer.h b/cockatrice/src/interface/widgets/utility/line_edit_completer.h index 369b7b553..65fa382ac 100644 --- a/cockatrice/src/interface/widgets/utility/line_edit_completer.h +++ b/cockatrice/src/interface/widgets/utility/line_edit_completer.h @@ -1,7 +1,6 @@ /** * @file line_edit_completer.h * @ingroup UI - * @brief Line edit with support for multiple trigger-based completers, e.g., @mention and [[card]]. */ //! \todo Document this file. @@ -10,49 +9,25 @@ #include "custom_line_edit.h" -#include -#include #include -#include -#include -#include - -enum class CompleterTrigger -{ - Mention, //< "@" prefix, replaced in place with a trailing space. - Card //< "[[" prefix, replaced with the completion plus a closing "]] ". -}; - -struct CompleterInfo -{ - QCompleter *completer; - CompleterTrigger trigger; -}; class LineEditCompleter : public LineEditUnfocusable { Q_OBJECT -signals: - void cardPartialChanged(const QString &partial); - private: - QList completers; - - void insertCompletion(QCompleter *completer, const QString &completion); - + QString cursorWord(const QString &line) const; + QCompleter *c; private slots: - void insertCompletion(const QString &text); + void insertCompletion(QString); protected: - void keyPressEvent(QKeyEvent *event) override; - void focusOutEvent(QFocusEvent *e) override; + void keyPressEvent(QKeyEvent *event); + void focusOutEvent(QFocusEvent *e); public: explicit LineEditCompleter(QWidget *parent = nullptr); - void addCompleter(QCompleter *c, CompleterTrigger trigger); - - bool hasVisibleCompleterPopup() const; - void hideCompleterPopups(); + void setCompleter(QCompleter *); + void setCompletionList(QStringList); }; #endif diff --git a/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp b/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp deleted file mode 100644 index 1bc9ba14b..000000000 --- a/cockatrice/src/interface/widgets/utility/reversed_completer_model.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "reversed_completer_model.h" - -#include -#include -#include - -void ReversedCompleterModel::setSourceModel(QAbstractItemModel *sourceModel) -{ - if (sourceModel == this->sourceModel()) { - return; - } - - if (QAbstractItemModel *old = this->sourceModel()) { - disconnect(old, nullptr, this, nullptr); - } - - QAbstractProxyModel::setSourceModel(sourceModel); - - if (sourceModel) { - connect(sourceModel, &QAbstractItemModel::modelReset, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::columnsInserted, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::columnsRemoved, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::dataChanged, this, &ReversedCompleterModel::invalidate); - connect(sourceModel, &QAbstractItemModel::layoutChanged, this, &ReversedCompleterModel::invalidate); - } - - invalidate(); -} - -void ReversedCompleterModel::setEnabled(bool enabled) -{ - if (enabled == isEnabled) { - return; - } - isEnabled = enabled; - invalidate(); -} - -QModelIndex ReversedCompleterModel::mapToSource(const QModelIndex &proxyIndex) const -{ - if (!proxyIndex.isValid() || !sourceModel()) { - return {}; - } - const int sourceRow = isEnabled ? sourceRowCount() - 1 - proxyIndex.row() : proxyIndex.row(); - return sourceModel()->index(sourceRow, proxyIndex.column()); -} - -QModelIndex ReversedCompleterModel::mapFromSource(const QModelIndex &sourceIndex) const -{ - if (!sourceIndex.isValid() || !sourceModel()) { - return {}; - } - const int proxyRow = isEnabled ? sourceRowCount() - 1 - sourceIndex.row() : sourceIndex.row(); - return index(proxyRow, sourceIndex.column()); -} - -QModelIndex ReversedCompleterModel::index(int row, int column, const QModelIndex &parent) const -{ - if (parent.isValid() || !sourceModel() || row < 0 || row >= rowCount() || column < 0 || column >= columnCount()) { - return {}; - } - return createIndex(row, column); -} - -QModelIndex ReversedCompleterModel::parent(const QModelIndex &) const -{ - return {}; -} - -int ReversedCompleterModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() || !sourceModel() ? 0 : sourceModel()->rowCount(); -} - -int ReversedCompleterModel::columnCount(const QModelIndex &parent) const -{ - return parent.isValid() || !sourceModel() ? 0 : sourceModel()->columnCount(); -} - -QVariant ReversedCompleterModel::data(const QModelIndex &proxyIndex, int role) const -{ - return sourceModel() ? sourceModel()->data(mapToSource(proxyIndex), role) : QVariant(); -} - -QVariant ReversedCompleterModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - return sourceModel() ? sourceModel()->headerData(section, orientation, role) : QVariant(); -} - -int ReversedCompleterModel::sourceRowCount() const -{ - return sourceModel() ? sourceModel()->rowCount() : 0; -} - -void ReversedCompleterModel::invalidate() -{ - beginResetModel(); - endResetModel(); -} diff --git a/cockatrice/src/interface/widgets/utility/reversed_completer_model.h b/cockatrice/src/interface/widgets/utility/reversed_completer_model.h deleted file mode 100644 index 64233a1b0..000000000 --- a/cockatrice/src/interface/widgets/utility/reversed_completer_model.h +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @file reversed_completer_model.h - * @ingroup UtilityWidgets - */ -//! \todo Document this file. - -#ifndef REVERSED_COMPLETER_MODEL_H -#define REVERSED_COMPLETER_MODEL_H - -#include - -class QAbstractItemModel; - -/** - * @brief A completer model that can present its rows bottom-to-top. - * - * The original row order is kept intact in the source model (row 0 is always the - * closest match). When enabled, the proxy maps the source rows in reverse so the - * popup shows the closest match in the row nearest to the text edit. Any change - * in the source model is forwarded as a full reset, which is all QCompleter - * needs to rebuild its completion list. - */ -class ReversedCompleterModel : public QAbstractProxyModel -{ -public: - using QAbstractProxyModel::QAbstractProxyModel; - - void setSourceModel(QAbstractItemModel *sourceModel) override; - void setEnabled(bool enabled); - - QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; - QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; - QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; - QModelIndex parent(const QModelIndex &) const override; - int rowCount(const QModelIndex &parent = {}) const override; - int columnCount(const QModelIndex &parent = {}) const override; - QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - -private: - int sourceRowCount() const; - void invalidate(); - - bool isEnabled = false; -}; - -#endif // REVERSED_COMPLETER_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 6a4eaa382..e5bcb2fd3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -10,7 +10,6 @@ #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h" #include "../utility/compact_push_button.h" -#include "../utility/completer_utils.h" #include "visual_deck_display_options_widget.h" #include @@ -22,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -97,14 +98,32 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); + CardSearchModel *searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); - const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15); - proxyModel = cardSetup.proxyModel; - completer = cardSetup.completer; + proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(15); searchBar->setCompleter(completer); // Update suggestions dynamically - connectCardCompleterSearch(searchBar, cardSetup); + connect(searchBar, &QLineEdit::textEdited, searchModel, &CardSearchModel::updateSearchResults); + connect(searchBar, &QLineEdit::textEdited, this, [=, this](const QString &text) { + // Ensure substring matching + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + + if (!text.isEmpty()) { + completer->complete(); // Force the dropdown to appear + } + }); connect(completer, static_cast(&QCompleter::activated), this, [=, this](const QString &completion) { diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp index f31463b50..387eb454f 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp @@ -6,10 +6,6 @@ CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterP bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - if (!sourceModel()) { - return false; - } - if (filterRegularExpression().pattern().isEmpty()) { return true; } @@ -17,5 +13,6 @@ bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QString data = index.data(Qt::DisplayRole).toString(); + // Ensure substring matching return data.contains(filterRegularExpression()); } diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp index 621f28983..d1fbbac2f 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp @@ -23,14 +23,8 @@ QVariant CardSearchModel::data(const QModelIndex &index, int role) const return QVariant(); } - const SearchResult &result = searchResults.at(index.row()); - if (role == Qt::DisplayRole) { - return result.card->getName(); - } - - if (role == CardInfoRole) { - return QVariant::fromValue(result.card); + return searchResults.at(index.row()).card->getName(); } return QVariant(); @@ -42,62 +36,43 @@ void CardSearchModel::updateSearchResults(const QString &query) searchResults.clear(); if (query.isEmpty() || !sourceModel) { - endResetModel(); return; } - CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); - if (!sourceDbModel) { - endResetModel(); - return; - } + // Set the filter for the display model + sourceModel->setCardName(query); - const QString lowerQuery = query.toLower(); + // Collect matching cards and compute Levenshtein distance + for (int i = 0; i < sourceModel->rowCount(); ++i) { + QModelIndex modelIndex = sourceModel->index(i, 0); + QModelIndex sourceIndex = sourceModel->mapToSource(modelIndex); + CardDatabaseModel *sourceDbModel = qobject_cast(sourceModel->sourceModel()); - QList prefixMatches; - QList containsMatches; + if (!sourceDbModel || !sourceIndex.isValid()) { + return; + } + + CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row()); - // Iterate the raw database model directly so results are always complete and fresh - const int rowCount = sourceDbModel->rowCount(); - for (int i = 0; i < rowCount; ++i) { - CardInfoPtr card = sourceDbModel->getCard(i); if (!card) { continue; } - const QString lowerName = card->getName().toLower(); - if (!lowerName.contains(lowerQuery)) { - continue; - } - - const int distance = levenshteinDistance(lowerQuery, lowerName); - - if (lowerName.startsWith(lowerQuery)) { - prefixMatches.append({card, distance}); - } else { - containsMatches.append({card, distance}); - } + int distance = levenshteinDistance(query.toLower(), card->getName().toLower()); + searchResults.append({card, distance}); } - auto sortByDistanceThenLength = [](const SearchResult &a, const SearchResult &b) { - if (a.distance != b.distance) { - return a.distance < b.distance; - } - return a.card->getName().size() < b.card->getName().size(); - }; + // Sort by Levenshtein distance (lower distance = better match) + std::sort(searchResults.begin(), searchResults.end(), + [](const SearchResult &a, const SearchResult &b) { return a.distance < b.distance; }); - std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength); - std::sort(containsMatches.begin(), containsMatches.end(), sortByDistanceThenLength); - - // Prefix matches always come first, then contains-only matches - searchResults.reserve(prefixMatches.size() + containsMatches.size()); - searchResults.append(prefixMatches); - searchResults.append(containsMatches); - - // Keep only the top 10 results + // Keep only the top 5 results if (searchResults.size() > 10) { searchResults = searchResults.mid(0, 10); } + emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); + emit layoutChanged(); + endResetModel(); } diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h index 646bf7e61..bc4be7a0e 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h @@ -15,11 +15,6 @@ class CardSearchModel : public QAbstractListModel { Q_OBJECT public: - enum CardSearchRoles - { - CardInfoRole = Qt::UserRole + 1, - }; - explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr); [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server.h b/libcockatrice_network/libcockatrice/network/server/remote/server.h index 12e71ebff..2fca46593 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.h @@ -91,10 +91,6 @@ public: { return QString(); } - virtual SessionEvent *getLoginSessionEvent() const - { - return nullptr; - } virtual QString getRequiredFeatures() const { return QString(); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 5b893799f..c3686ddfa 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -562,11 +562,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd event.set_message(server->getLoginMessage().toStdString()); rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - SessionEvent *loginEvent = server->getLoginSessionEvent(); - if (loginEvent) { - rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, loginEvent); - } - auto *re = new Response_Login; re->mutable_user_info()->CopyFrom(copyUserInfo(true)); diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 4305f6882..aa50e068a 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -673,27 +673,8 @@ void Servatrice::statusUpdate() } } -SessionEvent *Servatrice::makeShutdownEvent() const -{ - Event_ServerShutdown event; - event.set_reason(shutdownReason.toStdString()); - event.set_minutes(static_cast(shutdownMinutes)); - return Server_ProtocolHandler::prepareSessionEvent(event); -} - -SessionEvent *Servatrice::getLoginSessionEvent() const -{ - // Notify newly logged-in users of a pending server shutdown - QMutexLocker locker(&shutdownStateMutex); - if (shutdownTimer && shutdownMinutes > 0) { - return makeShutdownEvent(); - } - return nullptr; -} - void Servatrice::scheduleShutdown(const QString &reason, int minutes) { - shutdownStateMutex.lock(); shutdownReason = reason; shutdownMinutes = minutes; nextShutdownMessageMinutes = shutdownMinutes; @@ -702,7 +683,6 @@ void Servatrice::scheduleShutdown(const QString &reason, int minutes) connect(shutdownTimer, SIGNAL(timeout()), this, SLOT(shutdownTimeout())); shutdownTimer->start(60000); } - shutdownStateMutex.unlock(); shutdownTimeout(); } @@ -722,7 +702,6 @@ void Servatrice::incRxBytes(quint64 num) void Servatrice::shutdownTimeout() { - QMutexLocker locker(&shutdownStateMutex); // Show every time counter cut in half & every minute for last 5 minutes if (shutdownMinutes <= 5 || shutdownMinutes == nextShutdownMessageMinutes) { if (shutdownMinutes == nextShutdownMessageMinutes) { @@ -731,7 +710,10 @@ void Servatrice::shutdownTimeout() SessionEvent *se; if (shutdownMinutes) { - se = makeShutdownEvent(); + Event_ServerShutdown event; + event.set_reason(shutdownReason.toStdString()); + event.set_minutes(static_cast(shutdownMinutes)); + se = Server_ProtocolHandler::prepareSessionEvent(event); } else { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::SERVER_SHUTDOWN); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 6eb00c165..62fb382cb 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -158,7 +158,6 @@ private: Servatrice_IslServer *islServer; mutable QMutex loginMessageMutex; QString loginMessage; - mutable QMutex shutdownStateMutex; QString dbPrefix; QMap serverRequiredFeatureList; QString officialWarnings; @@ -217,8 +216,6 @@ public: QMutexLocker locker(&loginMessageMutex); return loginMessage; } - SessionEvent *getLoginSessionEvent() const override; - SessionEvent *makeShutdownEvent() const; QString getRequiredFeatures() const override; QString getAuthenticationMethodString() const; QString getDBTypeString() const;