diff --git a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index 586989822..e43206f36 100644 --- a/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/client/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -24,6 +24,8 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, &TabDeckEditorVisualTabWidget::onCardChanged); connect(visualDeckView, &VisualDeckEditorWidget::cardClicked, this, &TabDeckEditorVisualTabWidget::onCardClickedDeckEditor); + connect(visualDeckView, &VisualDeckEditorWidget::cardAdditionRequested, deckEditor, + &AbstractTabDeckEditor::actAddCard); visualDatabaseDisplay = new VisualDatabaseDisplayWidget(this, deckEditor, cardDatabaseModel, cardDatabaseDisplayModel); diff --git a/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 8e96eb6d5..41bf33a3d 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -2,8 +2,11 @@ #include "../../../../deck/deck_list_model.h" #include "../../../../deck/deck_loader.h" +#include "../../../../game/cards/card_completer_proxy_model.h" #include "../../../../game/cards/card_database.h" #include "../../../../game/cards/card_database_manager.h" +#include "../../../../game/cards/card_database_model.h" +#include "../../../../game/cards/card_search_model.h" #include "../../../../main.h" #include "../../../../utility/card_info_comparator.h" #include "../../layouts/overlap_layout.h" @@ -12,7 +15,10 @@ #include "../general/layout_containers/flow_widget.h" #include "../general/layout_containers/overlap_control_widget.h" +#include #include +#include +#include #include #include @@ -22,10 +28,85 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, DeckListModel *_ connect(deckListModel, &DeckListModel::dataChanged, this, &VisualDeckEditorWidget::decklistDataChanged); // The Main Widget and Main Layout, which contain a single Widget: The Scroll Area - this->setMinimumSize(0, 0); - this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - mainLayout = new QVBoxLayout(); - this->setLayout(mainLayout); + setMinimumSize(0, 0); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + mainLayout = new QVBoxLayout(this); + setLayout(mainLayout); + mainLayout->setContentsMargins(0, 0, 0, 0); + + searchContainer = new QWidget(this); + searchLayout = new QHBoxLayout(searchContainer); + searchContainer->setLayout(searchLayout); + + searchBar = new QLineEdit(this); + connect(searchBar, &QLineEdit::returnPressed, this, [=, this]() { + CardInfoPtr card = CardDatabaseManager::getInstance()->getCard(searchBar->text()); + if (card) { + emit cardAdditionRequested(card); + } + }); + cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); + cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); + CardSearchModel *searchModel = new CardSearchModel(cardDatabaseDisplayModel, this); + + proxyModel = new CardCompleterProxyModel(this); + proxyModel->setSourceModel(searchModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterRole(Qt::DisplayRole); + + completer = new QCompleter(proxyModel, this); + completer->setCompletionRole(Qt::DisplayRole); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setMaxVisibleItems(15); + searchBar->setCompleter(completer); + + // Update suggestions dynamically + 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) { + // Prevent the text from changing automatically when navigating with arrow keys + if (searchBar->text() != completion) { + searchBar->setText(completion); // Set the completion explicitly + searchBar->setCursorPosition(searchBar->text().length()); // Move cursor to the end + } + }); + + // Ensure that the text stays consistent during selection + connect(searchBar, &QLineEdit::textEdited, this, [=, this](const QString &text) { + if (searchBar->hasFocus() && !searchBar->completer()->popup()->isVisible()) { + // Allow text to change when typing, but not when navigating the completer + QString pattern = ".*" + QRegularExpression::escape(text) + ".*"; + proxyModel->setFilterRegularExpression( + QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); + } + }); + + // Search button functionality + searchPushButton = new QPushButton(this); + connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { + CardInfoPtr card = CardDatabaseManager::getInstance()->getCard(searchBar->text()); + if (card) { + emit cardAdditionRequested(card); + } + }); + + searchLayout->addWidget(searchBar); + searchLayout->addWidget(searchPushButton); + + mainLayout->addWidget(searchContainer); groupAndSortContainer = new QWidget(this); groupAndSortLayout = new QHBoxLayout(groupAndSortContainer); @@ -100,6 +181,7 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, DeckListModel *_ void VisualDeckEditorWidget::retranslateUi() { sortLabel->setText(tr("Click and drag to change\nthe sort order within the groups")); + searchPushButton->setText(tr("Quick search and add card")); } void VisualDeckEditorWidget::updateZoneWidgets() diff --git a/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h index ff44e005c..77bd5c45c 100644 --- a/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h +++ b/cockatrice/src/client/ui/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -2,13 +2,16 @@ #define VISUAL_DECK_EDITOR_H #include "../../../../deck/deck_list_model.h" +#include "../../../../game/cards/card_completer_proxy_model.h" #include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_model.h" #include "../cards/card_info_picture_with_text_overlay_widget.h" #include "../general/layout_containers/flow_widget.h" #include "../general/layout_containers/overlap_control_widget.h" #include "../quick_settings/settings_button_widget.h" #include +#include #include #include @@ -34,6 +37,7 @@ signals: void activeGroupCriteriaChanged(QString activeGroupCriteria); void activeSortCriteriaChanged(QStringList activeSortCriteria); void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); + void cardAdditionRequested(CardInfoPtr card); protected slots: void onHover(CardInfoPtr hoveredCard); @@ -44,6 +48,14 @@ protected slots: private: DeckListModel *deckListModel; QVBoxLayout *mainLayout; + QWidget *searchContainer; + QHBoxLayout *searchLayout; + QLineEdit *searchBar; + CardDatabaseModel *cardDatabaseModel; + CardDatabaseDisplayModel *cardDatabaseDisplayModel; + CardCompleterProxyModel *proxyModel; + QCompleter *completer; + QPushButton *searchPushButton; QWidget *groupAndSortContainer; QHBoxLayout *groupAndSortLayout; QComboBox *groupByComboBox; diff --git a/cockatrice/src/game/cards/card_completer_proxy_model.cpp b/cockatrice/src/game/cards/card_completer_proxy_model.cpp new file mode 100644 index 000000000..387eb454f --- /dev/null +++ b/cockatrice/src/game/cards/card_completer_proxy_model.cpp @@ -0,0 +1,18 @@ +#include "card_completer_proxy_model.h" + +CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ +} + +bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (filterRegularExpression().pattern().isEmpty()) { + return true; + } + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + QString data = index.data(Qt::DisplayRole).toString(); + + // Ensure substring matching + return data.contains(filterRegularExpression()); +} diff --git a/cockatrice/src/game/cards/card_completer_proxy_model.h b/cockatrice/src/game/cards/card_completer_proxy_model.h new file mode 100644 index 000000000..73cf223ed --- /dev/null +++ b/cockatrice/src/game/cards/card_completer_proxy_model.h @@ -0,0 +1,16 @@ +#ifndef CARD_COMPLETER_PROXY_MODEL_H +#define CARD_COMPLETER_PROXY_MODEL_H + +#include + +class CardCompleterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit CardCompleterProxyModel(QObject *parent = nullptr); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +}; + +#endif // CARD_COMPLETER_PROXY_MODEL_H diff --git a/cockatrice/src/game/cards/card_search_model.cpp b/cockatrice/src/game/cards/card_search_model.cpp new file mode 100644 index 000000000..8c256b3ee --- /dev/null +++ b/cockatrice/src/game/cards/card_search_model.cpp @@ -0,0 +1,73 @@ +#include "card_search_model.h" + +#include "../../utility/levenshtein.h" + +#include + +CardSearchModel::CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent) + : QAbstractListModel(parent), sourceModel(sourceModel) +{ +} + +int CardSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return searchResults.size(); +} + +QVariant CardSearchModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= searchResults.size()) + return QVariant(); + + if (role == Qt::DisplayRole) { + return searchResults.at(index.row()).card->getName(); + } + + return QVariant(); +} + +void CardSearchModel::updateSearchResults(const QString &query) +{ + beginResetModel(); + searchResults.clear(); + + if (query.isEmpty() || !sourceModel) + return; + + // Set the filter for the display model + sourceModel->setCardName(query); + + // 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()); + + if (!sourceDbModel || !sourceIndex.isValid()) + return; + + CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row()); + + if (!card) + continue; + + int distance = levenshteinDistance(query.toLower(), card->getName().toLower()); + searchResults.append({card, distance}); + } + + // 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; }); + + // 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(); + for (SearchResult &result : searchResults) { + qDebug() << result.card->getName(); + } + endResetModel(); +} diff --git a/cockatrice/src/game/cards/card_search_model.h b/cockatrice/src/game/cards/card_search_model.h new file mode 100644 index 000000000..c0575cb61 --- /dev/null +++ b/cockatrice/src/game/cards/card_search_model.h @@ -0,0 +1,30 @@ +#ifndef CARD_SEARCH_MODEL_H +#define CARD_SEARCH_MODEL_H + +#include "card_database_model.h" + +#include + +class CardSearchModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + void updateSearchResults(const QString &query); // Update results based on input + +private: + struct SearchResult + { + CardInfoPtr card; + int distance; + }; + + CardDatabaseDisplayModel *sourceModel; + QList searchResults; +}; + +#endif // CARD_SEARCH_MODEL_H diff --git a/cockatrice/src/utility/levenshtein.cpp b/cockatrice/src/utility/levenshtein.cpp new file mode 100644 index 000000000..cfb972f91 --- /dev/null +++ b/cockatrice/src/utility/levenshtein.cpp @@ -0,0 +1,25 @@ +#include "levenshtein.h" + +#include +#include + +int levenshteinDistance(const QString &s1, const QString &s2) +{ + int len1 = s1.size(); + int len2 = s2.size(); + std::vector> dp(len1 + 1, std::vector(len2 + 1)); + + for (int i = 0; i <= len1; i++) + dp[i][0] = i; + for (int j = 0; j <= len2; j++) + dp[0][j] = j; + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + dp[i][j] = std::min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost}); + } + } + + return dp[len1][len2]; +} diff --git a/cockatrice/src/utility/levenshtein.h b/cockatrice/src/utility/levenshtein.h new file mode 100644 index 000000000..b9b05a13c --- /dev/null +++ b/cockatrice/src/utility/levenshtein.h @@ -0,0 +1,8 @@ +#ifndef LEVENSHTEIN_H +#define LEVENSHTEIN_H + +#include + +int levenshteinDistance(const QString &s1, const QString &s2); + +#endif // LEVENSHTEIN_H