[CardSearch] Add card completion popups to chats and search fields (#7089)

* Add card completion popups to chats and search fields

Completes @mention and [[card]] in chat, and card names in the deck
editor, EDHREC, Archidekt, card art rules, and user card settings
searches. Pops up a styled list with mana pips and a card image
preview, flipping the list order when the popup opens above the text
field.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-11 18:50:30 +02:00 committed by GitHub
parent a0e76607b5
commit 83fd65b34b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1526 additions and 290 deletions

View file

@ -23,8 +23,14 @@ QVariant CardSearchModel::data(const QModelIndex &index, int role) const
return QVariant();
}
const SearchResult &result = searchResults.at(index.row());
if (role == Qt::DisplayRole) {
return searchResults.at(index.row()).card->getName();
return result.card->getName();
}
if (role == CardInfoRole) {
return QVariant::fromValue(result.card);
}
return QVariant();
@ -36,43 +42,62 @@ void CardSearchModel::updateSearchResults(const QString &query)
searchResults.clear();
if (query.isEmpty() || !sourceModel) {
endResetModel();
return;
}
// Set the filter for the display model
sourceModel->setCardName(query);
CardDatabaseModel *sourceDbModel = qobject_cast<CardDatabaseModel *>(sourceModel->sourceModel());
if (!sourceDbModel) {
endResetModel();
return;
}
// 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());
const QString lowerQuery = query.toLower();
if (!sourceDbModel || !sourceIndex.isValid()) {
return;
}
CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row());
QList<SearchResult> prefixMatches;
QList<SearchResult> containsMatches;
// 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;
}
int distance = levenshteinDistance(query.toLower(), card->getName().toLower());
searchResults.append({card, distance});
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});
}
}
// 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; });
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();
};
// Keep only the top 5 results
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
if (searchResults.size() > 10) {
searchResults = searchResults.mid(0, 10);
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
emit layoutChanged();
endResetModel();
}