mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[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:
parent
a0e76607b5
commit
83fd65b34b
25 changed files with 1526 additions and 290 deletions
|
|
@ -6,6 +6,10 @@ CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterP
|
|||
|
||||
bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
if (!sourceModel()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterRegularExpression().pattern().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -13,6 +17,5 @@ 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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue