Cockatrice/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp
Lukas Brübach a640ada8cc [CardSearchModel] Match English and localized names in Both mode
Card names are stored in both English and localized forms, so search for
matches in both during the 'Both' search mode instead of checking only
the localized name.
2026-09-21 00:10:21 +02:00

131 lines
3.8 KiB
C++

#include "card_search_model.h"
#include "../card_database_display_model.h"
#include "../card_database_model.h"
#include <algorithm>
#include <libcockatrice/utility/levenshtein.h>
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();
}
const SearchResult &result = searchResults.at(index.row());
if (role == Qt::DisplayRole) {
return result.card->getName();
}
if (role == CardInfoRole) {
return QVariant::fromValue(result.card);
}
return QVariant();
}
void CardSearchModel::updateSearchResults(const QString &query)
{
beginResetModel();
searchResults.clear();
if (query.isEmpty() || !sourceModel) {
endResetModel();
return;
}
CardDatabaseModel *sourceDbModel = qobject_cast<CardDatabaseModel *>(sourceModel->sourceModel());
if (!sourceDbModel) {
endResetModel();
return;
}
const QString lowerQuery = query.toLower();
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;
}
// The completer suggestions match against the same languages the card
// search uses, so typing a localized name finds the card. In Both mode
// either language can match.
for (const QString &matchName : searchableNames(card)) {
const QString lowerName = matchName.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});
}
break;
}
}
auto sortByDistanceThenLength = [this](const SearchResult &a, const SearchResult &b) {
if (a.distance != b.distance) {
return a.distance < b.distance;
}
return sortableName(a.card).size() < sortableName(b.card).size();
};
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);
}
endResetModel();
}
QStringList CardSearchModel::searchableNames(const CardInfoPtr &card) const
{
if (searchLanguage.isEnglishOnly()) {
return {card->getName()};
}
const QString localizedName = card->getLocalizedName(searchLanguage.language);
if (searchLanguage.mode == SearchLanguageMode::Selected) {
return {localizedName};
}
return {card->getName(), localizedName};
}
QString CardSearchModel::sortableName(const CardInfoPtr &card) const
{
if (searchLanguage.isEnglishOnly()) {
return card->getName();
}
return card->getLocalizedName(searchLanguage.language);
}