Add a widget to quickly add cards to the deck.

This commit is contained in:
Lukas Brübach 2025-03-13 16:27:59 +01:00
parent a729b749ba
commit a7ee47c098
9 changed files with 270 additions and 4 deletions

View file

@ -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);

View file

@ -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 <QCompleter>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QResizeEvent>
#include <qscrollarea.h>
@ -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<void (QCompleter::*)(const QString &)>(&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()

View file

@ -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 <QListWidget>
#include <QPushButton>
#include <QWidget>
#include <qscrollarea.h>
@ -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;

View file

@ -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());
}

View file

@ -0,0 +1,16 @@
#ifndef CARD_COMPLETER_PROXY_MODEL_H
#define CARD_COMPLETER_PROXY_MODEL_H
#include <QSortFilterProxyModel>
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

View file

@ -0,0 +1,73 @@
#include "card_search_model.h"
#include "../../utility/levenshtein.h"
#include <algorithm>
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<CardDatabaseModel *>(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();
}

View file

@ -0,0 +1,30 @@
#ifndef CARD_SEARCH_MODEL_H
#define CARD_SEARCH_MODEL_H
#include "card_database_model.h"
#include <QAbstractListModel>
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<SearchResult> searchResults;
};
#endif // CARD_SEARCH_MODEL_H

View file

@ -0,0 +1,25 @@
#include "levenshtein.h"
#include <algorithm>
#include <vector>
int levenshteinDistance(const QString &s1, const QString &s2)
{
int len1 = s1.size();
int len2 = s2.size();
std::vector<std::vector<int>> dp(len1 + 1, std::vector<int>(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];
}

View file

@ -0,0 +1,8 @@
#ifndef LEVENSHTEIN_H
#define LEVENSHTEIN_H
#include <QString>
int levenshteinDistance(const QString &s1, const QString &s2);
#endif // LEVENSHTEIN_H