[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

@ -43,6 +43,12 @@ NumericValue <- [0-9]+
static std::once_flag init;
// The peglib parser is a single permanent object, so the rule actions below cannot see
// per-instance state. The card language that the nested [[card name]] search matches
// against is passed through this thread-local context, which is live only while a
// DeckFilterString is being parsed, and copied into the nested FilterString closures.
thread_local CardSearchLanguage deckSearchLanguageContext;
static void setupParserRules()
{
// plumbing
@ -116,7 +122,7 @@ static void setupParserRules()
// actual functionality
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]), deckSearchLanguageContext);
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
return [=](const DeckSearchData &data) -> bool {
@ -186,7 +192,7 @@ DeckFilterString::DeckFilterString()
_error = "Not initialized";
}
DeckFilterString::DeckFilterString(const QString &expr)
DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
{
QByteArray ba = expr.simplified().toUtf8();
@ -199,6 +205,8 @@ DeckFilterString::DeckFilterString(const QString &expr)
return;
}
deckSearchLanguageContext = searchLanguage;
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
});

View file

@ -12,6 +12,7 @@
#include <QLoggingCategory>
#include <QString>
#include <functional>
#include <libcockatrice/card/card_localization.h>
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
@ -35,7 +36,7 @@ class DeckFilterString
{
public:
DeckFilterString();
explicit DeckFilterString(const QString &expr);
explicit DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage = {});
bool check(const DeckSearchData &data) const
{
return filter(data);

View file

@ -16,11 +16,13 @@
#include <QLineEdit>
#include <QRadioButton>
#include <QTreeView>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/models/database/token/token_display_model.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/layouts_settings.h>
#include <libcockatrice/utility/string_limits.h>
@ -88,6 +90,17 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
cardDatabaseDisplayModel = new TokenDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
const auto applyCardSearchLanguage = [this]() {
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
cardDatabaseDisplayModel->setSearchLanguage(CardSearchLanguage{
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
};
applyCardSearchLanguage();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
applyCardSearchLanguage);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
applyCardSearchLanguage);
chooseTokenFromAllRadioButton = new QRadioButton(tr("Show &all tokens"));
connect(chooseTokenFromAllRadioButton, &QRadioButton::toggled, this, &DlgCreateToken::actChooseTokenFromAll);
chooseTokenFromDeckRadioButton = new QRadioButton(tr("Show tokens from this &deck"));

View file

@ -1,5 +1,6 @@
#include "view_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/view_zone_logic.h"
@ -11,11 +12,13 @@
#include <QGraphicsSceneWheelEvent>
#include <QPainter>
#include <QtMath>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/protocol/pb/command_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/response_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/cards_display_settings.h>
/**
* @param parent the parent QGraphicsWidget containing the reveal zone
@ -253,7 +256,10 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca
void ZoneViewZone::setFilterString(const QString &_filterString)
{
filterString = FilterString(_filterString);
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
filterString = FilterString(
_filterString, CardSearchLanguage{cardsDisplay.getCardLang(),
static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
reorganizeCards();
}

View file

@ -21,6 +21,7 @@
#include <QStyle>
#include <QStyleOption>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h>
namespace
@ -168,6 +169,12 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
}
connect(&searchEdit, &QLineEdit::textChanged, zone, &ZoneViewZone::setFilterString);
const auto applyCardSearchLanguage = [this] { zone->setFilterString(searchEdit.text()); };
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
applyCardSearchLanguage);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
applyCardSearchLanguage);
}
setLayout(vbox);

View file

@ -94,8 +94,7 @@ void CardPictureToLoad::populateSetUrls()
}
}
const QStringList orderedTemplates = urlTemplates;
for (const QString &urlTemplate : orderedTemplates) {
for (const QString &urlTemplate : urlTemplates) {
QString transformedUrl = transformUrl(urlTemplate);
if (!transformedUrl.isEmpty()) {

View file

@ -11,8 +11,10 @@
#include <QHeaderView>
#include <QToolButton>
#include <QTreeView>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/settings/cards_display_settings.h>
DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel)
: QWidget(parent)
@ -40,6 +42,17 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
databaseDisplayModel->setSourceModel(databaseModel);
databaseDisplayModel->setFilterKeyColumn(0);
const auto applyCardSearchLanguage = [this]() {
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
databaseDisplayModel->setSearchLanguage(CardSearchLanguage{
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
};
applyCardSearchLanguage();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
applyCardSearchLanguage);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
applyCardSearchLanguage);
databaseView = new CardDatabaseView(this, databaseDisplayModel);
databaseView->setObjectName("databaseView");
databaseView->setFocusProxy(searchEdit);

View file

@ -63,13 +63,26 @@ GeneralSettingsPage::GeneralSettingsPage()
connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::cardLanguageBoxChanged);
// card search language, independent of the card display language
cardSearchLanguageBox.addItem(""); // texts set in retranslateUi
cardSearchLanguageBox.addItem("");
cardSearchLanguageBox.addItem("");
const int cardSearchLanguageIndex = SettingsCache::instance().cardsDisplay().getCardSearchLanguage();
cardSearchLanguageBox.setCurrentIndex(cardSearchLanguageIndex < 0 ? static_cast<int>(SearchLanguageMode::English)
: cardSearchLanguageIndex);
connect(&cardSearchLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::cardSearchLanguageBoxChanged);
auto *languageGrid = new QGridLayout;
languageGrid->addWidget(&languageLabel, 0, 0);
languageGrid->addWidget(&languageBox, 0, 1);
languageGrid->addWidget(&cardLanguageLabel, 1, 0);
languageGrid->addWidget(&cardLanguageBox, 1, 1);
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight);
languageGrid->addWidget(&cardSearchLanguageLabel, 3, 0);
languageGrid->addWidget(&cardSearchLanguageBox, 3, 1);
languageGrid->addWidget(&advertiseTranslationPageLabel, 4, 1, Qt::AlignRight);
cardLanguageNoteLabel.setWordWrap(true);
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
@ -481,6 +494,11 @@ void GeneralSettingsPage::cardLanguageBoxChanged(int index)
}
}
void GeneralSettingsPage::cardSearchLanguageBoxChanged(int index)
{
SettingsCache::instance().cardsDisplay().setCardSearchLanguage(index);
}
void GeneralSettingsPage::updateStartupServerControlsVisibility()
{
const int index = startupTabSelector.currentIndex();
@ -502,6 +520,12 @@ void GeneralSettingsPage::retranslateUi()
cardLanguageLabel.setText(tr("Card text & images language:"));
cardLanguageNoteLabel.setText(
tr("Foreign card names, text and art apply after you update the card database (Oracle)."));
cardSearchLanguageLabel.setText(tr("Language used in card search:"));
cardSearchLanguageBox.setItemText(static_cast<int>(SearchLanguageMode::English), tr("English"));
cardSearchLanguageBox.setItemText(static_cast<int>(SearchLanguageMode::Selected),
tr("Selected card language (untranslated cards still match in English)"));
cardSearchLanguageBox.setItemText(static_cast<int>(SearchLanguageMode::Both),
tr("English and selected card language"));
advertiseTranslationPageLabel.setText(
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));

View file

@ -38,6 +38,7 @@ private slots:
void resetAllPathsClicked();
void languageBoxChanged(int index);
void cardLanguageBoxChanged(int index);
void cardSearchLanguageBoxChanged(int index);
void updateStartupServerControlsVisibility();
private:
@ -55,6 +56,9 @@ private:
QComboBox cardLanguageBox;
QLabel cardLanguageNoteLabel;
QLabel cardSearchLanguageLabel;
QComboBox cardSearchLanguageBox;
QLabel updateReleaseChannelLabel;
QComboBox updateReleaseChannelBox;
QCheckBox startupUpdateCheckCheckBox;

View file

@ -1,5 +1,6 @@
#include "completer_utils.h"
#include "../../../client/settings/cache_settings.h"
#include "card_completer_styler.h"
#include <QCompleter>
@ -7,13 +8,26 @@
#include <QObject>
#include <QRegularExpression>
#include <QStringListModel>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h>
#include <libcockatrice/models/database/card_database_display_model.h>
#include <libcockatrice/settings/cards_display_settings.h>
namespace
{
void applyCardSearchLanguage(CardSearchModel *searchModel)
{
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
searchModel->setSearchLanguage(CardSearchLanguage{
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
}
} // namespace
CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems)
{
auto *searchModel = new CardSearchModel(displayModel, parent);
applyCardSearchLanguage(searchModel);
auto *proxyModel = new CardCompleterProxyModel(parent);
proxyModel->setSourceModel(searchModel);
@ -27,6 +41,12 @@ CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, Q
completer->setMaxVisibleItems(maxVisibleItems);
CardCompleterStyler::apply(completer);
auto *cardsDisplay = &SettingsCache::instance().cardsDisplay();
QObject::connect(cardsDisplay, &CardsDisplaySettings::cardLangChanged, searchModel,
[searchModel] { applyCardSearchLanguage(searchModel); });
QObject::connect(cardsDisplay, &CardsDisplaySettings::cardSearchLanguageChanged, searchModel,
[searchModel] { applyCardSearchLanguage(searchModel); });
return {searchModel, proxyModel, completer};
}

View file

@ -41,6 +41,17 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
databaseDisplayModel->setSourceModel(database_model);
databaseDisplayModel->setFilterKeyColumn(0);
const auto applyCardSearchLanguage = [this]() {
const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay();
databaseDisplayModel->setSearchLanguage(CardSearchLanguage{
cardsDisplay.getCardLang(), static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
};
applyCardSearchLanguage();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
applyCardSearchLanguage);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this,
applyCardSearchLanguage);
cards = new QList<ExactCard>;
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
&VisualDatabaseDisplayWidget::modelDirty);

View file

@ -1,9 +1,12 @@
#include "visual_deck_storage_sort_filter_proxy_model.h"
#include "../../../client/settings/cache_settings.h"
#include "../../filters/deck_filter_string.h"
#include <QFileInfo>
#include <algorithm>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/settings/cards_display_settings.h>
VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
@ -215,7 +218,10 @@ void VisualDeckStorageSortFilterProxyModel::updateSearchMatches()
return;
}
DeckFilterString filterString(searchText);
const auto &cardsDisplay = SettingsCache::instance().cardsDisplay();
DeckFilterString filterString(
searchText, CardSearchLanguage{cardsDisplay.getCardLang(),
static_cast<SearchLanguageMode>(cardsDisplay.getCardSearchLanguage())});
for (int row = 0; row < count; ++row) {
const DeckPreviewData &data = source->dataForRow(row);

View file

@ -15,6 +15,7 @@
#include <QTimer>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
@ -118,6 +119,13 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
&VisualDeckStorageWidget::updateSearchFilter);
// The deck content search matches card names in the configured card language;
// re-run it whenever that setting changes so active searches follow immediately.
CardsDisplaySettings *cardsDisplay = &SettingsCache::instance().cardsDisplay();
const auto reapplySearchForLanguage = [this] { storageProxyModel->reapplyFilters(); };
connect(cardsDisplay, &CardsDisplaySettings::cardLangChanged, this, reapplySearchForLanguage);
connect(cardsDisplay, &CardsDisplaySettings::cardSearchLanguageChanged, this, reapplySearchForLanguage);
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&VisualDeckStorageWidget::createRootFolderWidget);

View file

@ -4,6 +4,42 @@
#include <QString>
#include <QStringList>
/**
* @brief The card languages card search should run against.
*/
enum class SearchLanguageMode
{
English, ///< Only search the English card names and texts.
Selected, ///< Search the selected card language (untranslated cards still match in English).
Both ///< Search both the English and the selected card language names and texts.
};
/**
* @brief The card language and matching mode searches run against.
*
* Bundles the card language code configured in the settings with the
* SearchLanguageMode, so entry points take one value instead of two related
* parameters.
*/
struct CardSearchLanguage
{
QString language; ///< Card language code (e.g. "de"); empty means the English fallback.
SearchLanguageMode mode = SearchLanguageMode::English; ///< How the language participates in the search.
/**
* @brief Whether only the English card data is searched.
*
* @return True when no card language is selected or English itself is selected.
*/
[[nodiscard]] bool isEnglishOnly() const
{
return language.isEmpty() || language == QLatin1String("en");
}
bool operator==(const CardSearchLanguage &) const = default;
bool operator!=(const CardSearchLanguage &) const = default;
};
/**
* @namespace CardLocalization
* @ingroup Cards

View file

@ -74,6 +74,36 @@ NumericValue <- [0-9]+
static std::once_flag init;
// The peglib parser rules (and therefore their rule actions) are set up once per
// process, so a rule action cannot capture per-instance state. The card language
// plain-text name and text queries search in is therefore handed to the GenericQuery
// and OracleQuery rule actions through this thread-local context, which is live only
// while a FilterString is being parsed. The rule actions copy it into the filter
// closures they produce, so card evaluation never reads process-global state.
thread_local CardSearchLanguage searchLanguageContext;
namespace
{
bool matchesInSearchLanguage(const QString &english,
const QString &localized,
const CardSearchLanguage &searchLanguage,
const StringMatcher &matcher)
{
if (searchLanguage.mode == SearchLanguageMode::English) {
return matcher(english);
}
if (searchLanguage.mode == SearchLanguageMode::Both) {
if (!searchLanguage.isEnglishOnly() && matcher(localized)) {
return true;
}
return matcher(english);
}
return searchLanguage.isEnglishOnly() ? matcher(english) : matcher(localized);
}
} // namespace
static void setupParserRules()
{
auto passthru = [](const peg::SemanticValues &sv) -> Filter {
@ -333,7 +363,11 @@ static void setupParserRules()
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) { return matcher(x->getText()); };
const CardSearchLanguage searchLanguage = searchLanguageContext;
return [=](const CardData &x) {
return matchesInSearchLanguage(x->getText(), x->getLocalizedText(searchLanguage.language), searchLanguage,
matcher);
};
};
search["ColorQuery"] = [](const peg::SemanticValues &sv) -> Filter {
@ -410,7 +444,11 @@ static void setupParserRules()
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) { return matcher(x->getName()); };
const CardSearchLanguage searchLanguage = searchLanguageContext;
return [=](const CardData &x) {
return matchesInSearchLanguage(x->getName(), x->getLocalizedName(searchLanguage.language), searchLanguage,
matcher);
};
};
search["Color"] = [](const peg::SemanticValues &sv) -> char { return "WUBRGU"[sv.choice()]; };
@ -425,7 +463,7 @@ FilterString::FilterString()
_error = "Not initialized";
}
FilterString::FilterString(const QString &expr)
FilterString::FilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
{
QByteArray ba = expr.simplified().toUtf8();
@ -438,6 +476,8 @@ FilterString::FilterString(const QString &expr)
return;
}
searchLanguageContext = searchLanguage;
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
});

View file

@ -14,6 +14,7 @@
#include <QString>
#include <functional>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/card_localization.h>
#include <utility>
inline Q_LOGGING_CATEGORY(FilterStringLog, "filter_string");
@ -35,7 +36,7 @@ class FilterString
{
public:
FilterString();
explicit FilterString(const QString &exp);
explicit FilterString(const QString &exp, const CardSearchLanguage &searchLanguage = {});
[[nodiscard]] bool check(const CardData &card) const
{
if (card.isNull()) {

View file

@ -100,14 +100,16 @@ FilterTreeNode *FilterItemList::termNode(const QString &term)
return childNodes.at(i);
}
bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeAnd(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) {
if (!(*i)->isEnabled()) {
continue;
}
if (!(*i)->acceptCardAttr(info, attr)) {
if (!(*i)->acceptCardAttr(info, attr, searchLanguage)) {
return false;
}
}
@ -115,13 +117,17 @@ bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr)
return true;
}
bool FilterItemList::testTypeAndNot(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeAndNot(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
// if any one in the list is true, return false
return !testTypeOr(info, attr);
return !testTypeOr(info, attr, searchLanguage);
}
bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeOr(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
bool noChildEnabledChild = true;
@ -134,7 +140,7 @@ bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) c
noChildEnabledChild = false;
}
if ((*i)->acceptCardAttr(info, attr)) {
if ((*i)->acceptCardAttr(info, attr, searchLanguage)) {
return true;
}
}
@ -142,20 +148,58 @@ bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) c
return noChildEnabledChild;
}
bool FilterItemList::testTypeOrNot(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItemList::testTypeOrNot(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
// if any one in the list is false, return true
return !testTypeAnd(info, attr);
return !testTypeAnd(info, attr, searchLanguage);
}
bool FilterItem::acceptName(const CardInfoPtr info) const
bool FilterItem::acceptName(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
return info->getName().contains(term, Qt::CaseInsensitive);
const QString &englishName = info->getName();
const QString &localizedName = info->getLocalizedName(searchLanguage.language);
switch (searchLanguage.mode) {
case SearchLanguageMode::English:
return englishName.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Both:
if (englishName.contains(term, Qt::CaseInsensitive)) {
return true;
}
return !searchLanguage.isEnglishOnly() && localizedName.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Selected:
if (searchLanguage.isEnglishOnly()) {
return englishName.contains(term, Qt::CaseInsensitive);
}
return localizedName.contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptNameExact(const CardInfoPtr info) const
return false;
}
bool FilterItem::acceptNameExact(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
return info->getName() == term;
const QString &englishName = info->getName();
const QString &localizedName = info->getLocalizedName(searchLanguage.language);
switch (searchLanguage.mode) {
case SearchLanguageMode::English:
return englishName == term;
case SearchLanguageMode::Both:
if (englishName == term) {
return true;
}
return !searchLanguage.isEnglishOnly() && localizedName == term;
case SearchLanguageMode::Selected:
if (searchLanguage.isEnglishOnly()) {
return englishName == term;
}
return localizedName == term;
}
return false;
}
bool FilterItem::acceptType(const CardInfoPtr info) const
@ -213,9 +257,27 @@ bool FilterItem::acceptColor(const CardInfoPtr info) const
return match_count == converted_term.length();
}
bool FilterItem::acceptText(const CardInfoPtr info) const
bool FilterItem::acceptText(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
return info->getText().contains(term, Qt::CaseInsensitive);
const QString &englishText = info->getText();
const QString &localizedText = info->getLocalizedText(searchLanguage.language);
switch (searchLanguage.mode) {
case SearchLanguageMode::English:
return englishText.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Both:
if (englishText.contains(term, Qt::CaseInsensitive)) {
return true;
}
return !searchLanguage.isEnglishOnly() && localizedText.contains(term, Qt::CaseInsensitive);
case SearchLanguageMode::Selected:
if (searchLanguage.isEnglishOnly()) {
return englishText.contains(term, Qt::CaseInsensitive);
}
return localizedText.contains(term, Qt::CaseInsensitive);
}
return false;
}
bool FilterItem::acceptSet(const CardInfoPtr info) const
@ -402,19 +464,21 @@ bool FilterItem::relationCheck(int cardInfo) const
return result;
}
bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) const
bool FilterItem::acceptCardAttr(const CardInfoPtr info,
CardFilter::Attr attr,
const CardSearchLanguage &searchLanguage) const
{
switch (attr) {
case CardFilter::AttrName:
return acceptName(info);
return acceptName(info, searchLanguage);
case CardFilter::AttrNameExact:
return acceptNameExact(info);
return acceptNameExact(info, searchLanguage);
case CardFilter::AttrType:
return acceptType(info);
case CardFilter::AttrColor:
return acceptColor(info);
case CardFilter::AttrText:
return acceptText(info);
return acceptText(info, searchLanguage);
case CardFilter::AttrSet:
return acceptSet(info);
case CardFilter::AttrManaCost:
@ -484,18 +548,18 @@ FilterTreeNode *FilterTree::termNode(const CardFilter *f)
return termNode(f->attr(), f->type(), f->term());
}
bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const
bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const
{
const FilterItemList *fil;
bool status = true;
fil = lm->findTypeList(CardFilter::TypeAnd);
if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr)) {
if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr, searchLanguage)) {
return false;
}
fil = lm->findTypeList(CardFilter::TypeAndNot);
if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr)) {
if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr, searchLanguage)) {
return false;
}
@ -504,23 +568,23 @@ bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const
status = false;
// if this is true we can return because it is OR'd with the OrNot list
if (fil->testTypeOr(info, lm->attr)) {
if (fil->testTypeOr(info, lm->attr, searchLanguage)) {
return true;
}
}
fil = lm->findTypeList(CardFilter::TypeOrNot);
if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr)) {
if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr, searchLanguage)) {
return true;
}
return status;
}
bool FilterTree::acceptsCard(const CardInfoPtr info) const
bool FilterTree::acceptsCard(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const
{
for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) {
if ((*i)->isEnabled() && !testAttr(info, *i)) {
if ((*i)->isEnabled() && !testAttr(info, *i, searchLanguage)) {
return false;
}
}

View file

@ -11,6 +11,7 @@
#include <QList>
#include <QObject>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/card/database/card_database.h>
#include <utility>
@ -167,10 +168,14 @@ public:
return CardFilter::typeName(type);
}
[[nodiscard]] bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool
testTypeAnd(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool
testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool
testTypeOr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool
testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
};
class FilterItem : public FilterTreeNode
@ -207,20 +212,21 @@ public:
return true;
}
[[nodiscard]] bool acceptName(CardInfoPtr info) const;
[[nodiscard]] bool acceptNameExact(CardInfoPtr info) const;
[[nodiscard]] bool acceptName(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptNameExact(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptType(CardInfoPtr info) const;
[[nodiscard]] bool acceptMainType(CardInfoPtr info) const;
[[nodiscard]] bool acceptSubType(CardInfoPtr info) const;
[[nodiscard]] bool acceptColor(CardInfoPtr info) const;
[[nodiscard]] bool acceptText(CardInfoPtr info) const;
[[nodiscard]] bool acceptText(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptSet(CardInfoPtr info) const;
[[nodiscard]] bool acceptManaCost(CardInfoPtr info) const;
[[nodiscard]] bool acceptCmc(CardInfoPtr info) const;
[[nodiscard]] bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool acceptLoyalty(CardInfoPtr info) const;
[[nodiscard]] bool acceptRarity(CardInfoPtr info) const;
[[nodiscard]] bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const;
[[nodiscard]] bool
acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr, const CardSearchLanguage &searchLanguage) const;
[[nodiscard]] bool acceptFormat(CardInfoPtr info) const;
[[nodiscard]] bool relationCheck(int cardInfo) const;
};
@ -240,7 +246,7 @@ private:
LogicMap *attrLogicMap(CardFilter::Attr attr);
FilterItemList *attrTypeList(CardFilter::Attr attr, CardFilter::Type type);
bool testAttr(CardInfoPtr info, const LogicMap *lm) const;
bool testAttr(CardInfoPtr info, const LogicMap *lm, const CardSearchLanguage &searchLanguage) const;
void nodeChanged() const override
{
@ -279,7 +285,7 @@ public:
return 0;
}
[[nodiscard]] bool acceptsCard(CardInfoPtr info) const;
[[nodiscard]] bool acceptsCard(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const;
void removeFiltersByAttr(CardFilter::Attr filterType);
void removeFilter(const CardFilter *toRemove);
void clear();

View file

@ -29,6 +29,7 @@ public:
[[nodiscard]] virtual int getArchidektPreviewSize() const = 0;
[[nodiscard]] virtual int getSampleHandSize() const = 0;
[[nodiscard]] virtual QString getCardLang() const = 0;
[[nodiscard]] virtual int getCardSearchLanguage() const = 0;
};
#endif // COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H

View file

@ -65,7 +65,11 @@ void CardSearchModel::updateSearchResults(const QString &query)
continue;
}
const QString lowerName = card->getName().toLower();
// 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;
}
@ -77,13 +81,15 @@ void CardSearchModel::updateSearchResults(const QString &query)
} 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,16 +190,22 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex
bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
{
if (!cardName.isEmpty() && !info->getName().contains(cardName, Qt::CaseInsensitive)) {
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())) {
return false;
}
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()
{

View file

@ -110,6 +110,11 @@ QString CardsDisplaySettings::getCardLang() const
return getValue("cardLang", QString(), QString(), "en").toString();
}
int CardsDisplaySettings::getCardSearchLanguage() const
{
return getValue("cardSearchLanguage", QString(), QString(), static_cast<int>(SearchLanguageMode::English)).toInt();
}
void CardsDisplaySettings::setDisplayCardNames(bool _displayCardNames)
{
setValue(_displayCardNames, "displayCardNames");
@ -242,3 +247,12 @@ void CardsDisplaySettings::setCardLang(const QString &_cardLang)
sync();
emit cardLangChanged(_cardLang);
}
void CardsDisplaySettings::setCardSearchLanguage(int _cardSearchLanguage)
{
if (_cardSearchLanguage == getCardSearchLanguage()) {
return;
}
setValue(_cardSearchLanguage, "cardSearchLanguage");
emit cardSearchLanguageChanged(_cardSearchLanguage);
}

View file

@ -3,6 +3,7 @@
#include "settings_manager.h"
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/interfaces/interface_cards_display_settings_provider.h>
class CardsDisplaySettings : public SettingsManager, public ICardsDisplaySettingsProvider
@ -32,6 +33,7 @@ public:
[[nodiscard]] int getArchidektPreviewSize() const override;
[[nodiscard]] int getSampleHandSize() const override;
[[nodiscard]] QString getCardLang() const override;
[[nodiscard]] int getCardSearchLanguage() const override;
void setDisplayCardNames(bool _displayCardNames);
void setRoundCardCorners(bool _roundCardCorners);
@ -54,6 +56,7 @@ public:
void setArchidektPreviewCardSize(int _archidektPreviewCardSize);
void setSampleHandSize(int _sampleHandSize);
void setCardLang(const QString &_cardLang);
void setCardSearchLanguage(int _cardSearchLanguage);
signals:
void displayCardNamesChanged();
@ -71,6 +74,7 @@ signals:
void archidektPreviewSizeChanged();
void sampleHandSizeChanged(int amount);
void cardLangChanged(const QString &lang);
void cardSearchLanguageChanged(int cardSearchLanguage);
public:
explicit CardsDisplaySettings(const QString &settingPath, QObject *parent = nullptr);

View file

@ -73,6 +73,57 @@ QUERY(Color4, cat, "c!gw", false)
QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true)
CardInfoPtr localizedCat()
{
CardInfoPtr localized = CardInfo::newInstance("Cat", "Meow!", false, {}, {}, {}, {}, {});
localized->setLocalizedName("de", "Kater");
localized->setLocalizedText("de", "miaut");
return localized;
}
TEST_F(CardQuery, SearchLanguageEnglishMatchesOnlyEnglish)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Cat", CardSearchLanguage{"de", SearchLanguageMode::English}).check(localized));
ASSERT_FALSE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::English}).check(localized));
}
TEST_F(CardQuery, SearchLanguageSelectedMatchesLocalizedNameAndText)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::Selected}).check(localized));
ASSERT_TRUE(FilterString("o:miaut", CardSearchLanguage{"de", SearchLanguageMode::Selected}).check(localized));
ASSERT_FALSE(FilterString("Cat", CardSearchLanguage{"de", SearchLanguageMode::Selected}).check(localized));
}
TEST_F(CardQuery, SearchLanguageSelectedFallsBackToEnglishForUntranslatedCards)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Cat", CardSearchLanguage{"fr", SearchLanguageMode::Selected}).check(localized));
ASSERT_FALSE(FilterString("Kater", CardSearchLanguage{"fr", SearchLanguageMode::Selected}).check(localized));
}
TEST_F(CardQuery, SearchLanguageBothMatchesEitherLanguage)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Cat", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
ASSERT_TRUE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
}
TEST_F(CardQuery, SearchLanguageIsBoundPerInstance)
{
const CardData localized = localizedCat();
FilterString germanQuery("Kater", CardSearchLanguage{"de", SearchLanguageMode::Selected});
ASSERT_TRUE(germanQuery.check(localized));
// Constructing an English-bound instance afterwards must not change the
// language the earlier instance searches in.
FilterString englishQuery("Kater", CardSearchLanguage{"", SearchLanguageMode::English});
ASSERT_FALSE(englishQuery.check(localized));
ASSERT_TRUE(germanQuery.check(localized));
}
} // namespace
int main(int argc, char **argv)

View file

@ -575,6 +575,19 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_SetAndGet)
ASSERT_EQ(s.getCardLang(), QString("de"));
}
TEST_F(SettingsDefaultsTest, CardsDisplay_CardSearchLanguage_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getCardSearchLanguage(), static_cast<int>(SearchLanguageMode::English));
}
TEST_F(SettingsDefaultsTest, CardsDisplay_CardSearchLanguage_SetAndGet)
{
CardsDisplaySettings s(settingsPath, nullptr);
s.setCardSearchLanguage(static_cast<int>(SearchLanguageMode::Selected));
ASSERT_EQ(s.getCardSearchLanguage(), static_cast<int>(SearchLanguageMode::Selected));
}
// --- VisualDeckStorageSettings ---
TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)