[Card] Add a setting for the language used in card search (#7314)

* [Card] Add a setting for the language used in card search

Localized card names and texts can now be searched too, controlled by a
'Language used in card search' toggle (English, selected card language, or
both) on the general settings page. Untranslated cards always keep matching
in English.

Removed the redundant local copy of the URL templates list in the localized
picture loader while here.

* [Card] Bind search language per FilterString instance

The peg parser rules are set up once per process, so the GenericQuery and
OracleQuery rule actions could not capture per-instance state. Instead of
storing the search language in a process-global that FilterString instance
methods mutate, hand it to the rule actions through a thread-local parse
context and copy it into the filter closures they produce. Card evaluation
in FilterString::check no longer reads any process-global state, and each
instance keeps the language it was built with; constructing one instance no
longer changes what unrelated instances (deck filter, drop-to-hand, zone
views) match against.

The card database display model stores the raw query and rebuilds the
FilterString when the search language changes, since the language is now
bound at parse time.

Add tests for the English/Selected/Both search modes, the English fallback
for untranslated cards, and per-instance language independence.

* [Card] Pass the card search language to deck and zone card searches

Wire the two remaining FilterString consumers to the configured card search
language so card-name matches respect it everywhere:

- DeckFilterString now takes the search language and mode, exposes them to its
  [[card name]] rule action via a thread-local parse context (same pattern as
  FilterString), and the engine's card database uses them for content search.
- ZoneViewZone reads the card language from CardsDisplaySettings when applying
  its search filter, and the reveal-zone widget re-applies the active search
  when the language setting changes.
- The deck-storage search re-runs its filter against the current card language
  setting, including live re-application when the setting changes.

Game-action targeting (DlgMoveTopCardsUntil) intentionally keeps evaluating
against English card names.

* [Card] Rename CardSearchLanguage to SearchLanguageMode

* [Card] Restore displaced namespace doc in card_localization.h

* [Filters] Pass CardSearchLanguage as a single struct

* [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.

* [CreateTokenDialog] Fetch cardsDisplay settings inside the apply lambda

Avoid capturing the raw settings pointer in the lambda: resolve the card
language and card search language from the settings cache at call time so
the values are always current when the search language is re-applied.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-09-21 08:50:48 +02:00 committed by GitHub
parent 12299abcc8
commit ef68a7bdcc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 488 additions and 68 deletions

View file

@ -65,25 +65,31 @@ void CardSearchModel::updateSearchResults(const QString &query)
continue;
}
const QString lowerName = card->getName().toLower();
if (!lowerName.contains(lowerQuery)) {
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);
const int distance = levenshteinDistance(lowerQuery, lowerName);
if (lowerName.startsWith(lowerQuery)) {
prefixMatches.append({card, distance});
} else {
containsMatches.append({card, distance});
if (lowerName.startsWith(lowerQuery)) {
prefixMatches.append({card, distance});
} else {
containsMatches.append({card, distance});
}
break;
}
}
auto sortByDistanceThenLength = [](const SearchResult &a, const SearchResult &b) {
auto sortByDistanceThenLength = [this](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();
return sortableName(a.card).size() < sortableName(b.card).size();
};
std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength);
@ -101,3 +107,25 @@ void CardSearchModel::updateSearchResults(const QString &query)
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);
}

View file

@ -27,6 +27,14 @@ public:
void updateSearchResults(const QString &query); // Update results based on input
void setSearchLanguage(const CardSearchLanguage &searchLang)
{
if (searchLanguage == searchLang) {
return;
}
searchLanguage = searchLang;
}
private:
struct SearchResult
{
@ -34,8 +42,15 @@ private:
int distance;
};
/** @brief The names a card is searched by with the current search language. */
[[nodiscard]] QStringList searchableNames(const CardInfoPtr &card) const;
/** @brief The name used to break distance ties when sorting suggestions. */
[[nodiscard]] QString sortableName(const CardInfoPtr &card) const;
CardDatabaseDisplayModel *sourceModel;
QList<SearchResult> searchResults;
CardSearchLanguage searchLanguage;
};
#endif // CARD_SEARCH_MODEL_H

View file

@ -179,7 +179,7 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex
}
if (filterString != nullptr) {
if (filterTree != nullptr && !filterTree->acceptsCard(info)) {
if (filterTree != nullptr && !filterTree->acceptsCard(info, searchLanguage)) {
return false;
}
return filterString->check(info);
@ -190,8 +190,14 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex
bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
{
if (!cardName.isEmpty() && !info->getName().contains(cardName, Qt::CaseInsensitive)) {
return false;
if (!cardName.isEmpty()) {
const bool matchesEnglish = info->getName().contains(cardName, Qt::CaseInsensitive);
const bool matchesLocalized =
!searchLanguage.isEnglishOnly() &&
info->getLocalizedName(searchLanguage.language).contains(cardName, Qt::CaseInsensitive);
if (!matchesEnglish && !matchesLocalized) {
return false;
}
}
if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) {
@ -199,7 +205,7 @@ bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
}
if (filterTree != nullptr) {
return filterTree->acceptsCard(info);
return filterTree->acceptsCard(info, searchLanguage);
}
return true;
@ -235,6 +241,28 @@ void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree)
invalidate();
}
void CardDatabaseDisplayModel::setStringFilter(const QString &_src)
{
searchText = _src;
delete filterString;
filterString = new FilterString(_src, searchLanguage);
dirty();
}
void CardDatabaseDisplayModel::setSearchLanguage(const CardSearchLanguage &searchLang)
{
if (searchLanguage == searchLang) {
return;
}
searchLanguage = searchLang;
if (filterString != nullptr) {
setStringFilter(searchText);
}
dirty();
}
void CardDatabaseDisplayModel::filterTreeChanged()
{
invalidate();

View file

@ -10,6 +10,7 @@
#include <QSortFilterProxyModel>
#include <QTimer>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/filters/filter_string.h>
class FilterTree;
@ -32,6 +33,8 @@ private:
FilterString *filterString;
int loadedRowCount;
QTimer dirtyTimer;
CardSearchLanguage searchLanguage;
QString searchText;
/** The translation table that will be used for sanitizeCardName. */
static QMap<wchar_t, wchar_t> characterTranslation;
@ -55,17 +58,13 @@ public:
cardName = sanitizeCardName(_cardName, characterTranslation);
dirty();
}
void setStringFilter(const QString &_src)
{
delete filterString;
filterString = new FilterString(_src);
dirty();
}
void setStringFilter(const QString &_src);
void setCardNameSet(const QSet<QString> &_cardNameSet)
{
cardNameSet = _cardNameSet;
dirty();
}
void setSearchLanguage(const CardSearchLanguage &searchLang);
void dirty()
{