[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; 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() static void setupParserRules()
{ {
// plumbing // plumbing
@ -116,7 +122,7 @@ static void setupParserRules()
// actual functionality // actual functionality
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter { 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; }; auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
return [=](const DeckSearchData &data) -> bool { return [=](const DeckSearchData &data) -> bool {
@ -186,7 +192,7 @@ DeckFilterString::DeckFilterString()
_error = "Not initialized"; _error = "Not initialized";
} }
DeckFilterString::DeckFilterString(const QString &expr) DeckFilterString::DeckFilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
{ {
QByteArray ba = expr.simplified().toUtf8(); QByteArray ba = expr.simplified().toUtf8();
@ -199,6 +205,8 @@ DeckFilterString::DeckFilterString(const QString &expr)
return; return;
} }
deckSearchLanguageContext = searchLanguage;
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) { 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)); _error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
}); });

View file

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

View file

@ -16,11 +16,13 @@
#include <QLineEdit> #include <QLineEdit>
#include <QRadioButton> #include <QRadioButton>
#include <QTreeView> #include <QTreeView>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h> #include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/models/database/card_database_model.h> #include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/models/database/token/token_display_model.h> #include <libcockatrice/models/database/token/token_display_model.h>
#include <libcockatrice/settings/card_override_settings.h> #include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/layouts_settings.h> #include <libcockatrice/settings/layouts_settings.h>
#include <libcockatrice/utility/string_limits.h> #include <libcockatrice/utility/string_limits.h>
@ -88,6 +90,17 @@ DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *pa
cardDatabaseDisplayModel = new TokenDisplayModel(this); cardDatabaseDisplayModel = new TokenDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); 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")); chooseTokenFromAllRadioButton = new QRadioButton(tr("Show &all tokens"));
connect(chooseTokenFromAllRadioButton, &QRadioButton::toggled, this, &DlgCreateToken::actChooseTokenFromAll); connect(chooseTokenFromAllRadioButton, &QRadioButton::toggled, this, &DlgCreateToken::actChooseTokenFromAll);
chooseTokenFromDeckRadioButton = new QRadioButton(tr("Show tokens from this &deck")); chooseTokenFromDeckRadioButton = new QRadioButton(tr("Show tokens from this &deck"));

View file

@ -1,5 +1,6 @@
#include "view_zone.h" #include "view_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../../game/zones/view_zone_logic.h" #include "../../game/zones/view_zone_logic.h"
@ -11,11 +12,13 @@
#include <QGraphicsSceneWheelEvent> #include <QGraphicsSceneWheelEvent>
#include <QPainter> #include <QPainter>
#include <QtMath> #include <QtMath>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/protocol/pb/command_dump_zone.pb.h> #include <libcockatrice/protocol/pb/command_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h> #include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/response_dump_zone.pb.h> #include <libcockatrice/protocol/pb/response_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h> #include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/cards_display_settings.h>
/** /**
* @param parent the parent QGraphicsWidget containing the reveal zone * @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) 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(); reorganizeCards();
} }

View file

@ -21,6 +21,7 @@
#include <QStyle> #include <QStyle>
#include <QStyleOption> #include <QStyleOption>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h> #include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
namespace namespace
@ -168,6 +169,12 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
} }
connect(&searchEdit, &QLineEdit::textChanged, zone, &ZoneViewZone::setFilterString); 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); setLayout(vbox);

View file

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

View file

@ -11,8 +11,10 @@
#include <QHeaderView> #include <QHeaderView>
#include <QToolButton> #include <QToolButton>
#include <QTreeView> #include <QTreeView>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h> #include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/settings/cards_display_settings.h>
DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel) DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent, CardDatabaseModel *databaseModel)
: QWidget(parent) : QWidget(parent)
@ -40,6 +42,17 @@ DeckEditorDatabaseDisplayWidget::DeckEditorDatabaseDisplayWidget(QWidget *parent
databaseDisplayModel->setSourceModel(databaseModel); databaseDisplayModel->setSourceModel(databaseModel);
databaseDisplayModel->setFilterKeyColumn(0); 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 = new CardDatabaseView(this, databaseDisplayModel);
databaseView->setObjectName("databaseView"); databaseView->setObjectName("databaseView");
databaseView->setFocusProxy(searchEdit); databaseView->setFocusProxy(searchEdit);

View file

@ -63,13 +63,26 @@ GeneralSettingsPage::GeneralSettingsPage()
connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this, connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::cardLanguageBoxChanged); &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; auto *languageGrid = new QGridLayout;
languageGrid->addWidget(&languageLabel, 0, 0); languageGrid->addWidget(&languageLabel, 0, 0);
languageGrid->addWidget(&languageBox, 0, 1); languageGrid->addWidget(&languageBox, 0, 1);
languageGrid->addWidget(&cardLanguageLabel, 1, 0); languageGrid->addWidget(&cardLanguageLabel, 1, 0);
languageGrid->addWidget(&cardLanguageBox, 1, 1); languageGrid->addWidget(&cardLanguageBox, 1, 1);
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 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.setWordWrap(true);
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter); 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() void GeneralSettingsPage::updateStartupServerControlsVisibility()
{ {
const int index = startupTabSelector.currentIndex(); const int index = startupTabSelector.currentIndex();
@ -502,6 +520,12 @@ void GeneralSettingsPage::retranslateUi()
cardLanguageLabel.setText(tr("Card text & images language:")); cardLanguageLabel.setText(tr("Card text & images language:"));
cardLanguageNoteLabel.setText( cardLanguageNoteLabel.setText(
tr("Foreign card names, text and art apply after you update the card database (Oracle).")); 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( advertiseTranslationPageLabel.setText(
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); 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 resetAllPathsClicked();
void languageBoxChanged(int index); void languageBoxChanged(int index);
void cardLanguageBoxChanged(int index); void cardLanguageBoxChanged(int index);
void cardSearchLanguageBoxChanged(int index);
void updateStartupServerControlsVisibility(); void updateStartupServerControlsVisibility();
private: private:
@ -55,6 +56,9 @@ private:
QComboBox cardLanguageBox; QComboBox cardLanguageBox;
QLabel cardLanguageNoteLabel; QLabel cardLanguageNoteLabel;
QLabel cardSearchLanguageLabel;
QComboBox cardSearchLanguageBox;
QLabel updateReleaseChannelLabel; QLabel updateReleaseChannelLabel;
QComboBox updateReleaseChannelBox; QComboBox updateReleaseChannelBox;
QCheckBox startupUpdateCheckCheckBox; QCheckBox startupUpdateCheckCheckBox;

View file

@ -1,5 +1,6 @@
#include "completer_utils.h" #include "completer_utils.h"
#include "../../../client/settings/cache_settings.h"
#include "card_completer_styler.h" #include "card_completer_styler.h"
#include <QCompleter> #include <QCompleter>
@ -7,13 +8,26 @@
#include <QObject> #include <QObject>
#include <QRegularExpression> #include <QRegularExpression>
#include <QStringListModel> #include <QStringListModel>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.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/card_search_model.h>
#include <libcockatrice/models/database/card_database_display_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) CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, QObject *parent, int maxVisibleItems)
{ {
auto *searchModel = new CardSearchModel(displayModel, parent); auto *searchModel = new CardSearchModel(displayModel, parent);
applyCardSearchLanguage(searchModel);
auto *proxyModel = new CardCompleterProxyModel(parent); auto *proxyModel = new CardCompleterProxyModel(parent);
proxyModel->setSourceModel(searchModel); proxyModel->setSourceModel(searchModel);
@ -27,6 +41,12 @@ CardCompleterSetup createCardCompleter(CardDatabaseDisplayModel *displayModel, Q
completer->setMaxVisibleItems(maxVisibleItems); completer->setMaxVisibleItems(maxVisibleItems);
CardCompleterStyler::apply(completer); 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}; return {searchModel, proxyModel, completer};
} }

View file

@ -41,6 +41,17 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
databaseDisplayModel->setSourceModel(database_model); databaseDisplayModel->setSourceModel(database_model);
databaseDisplayModel->setFilterKeyColumn(0); 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>; cards = new QList<ExactCard>;
connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this,
&VisualDatabaseDisplayWidget::modelDirty); &VisualDatabaseDisplayWidget::modelDirty);

View file

@ -1,9 +1,12 @@
#include "visual_deck_storage_sort_filter_proxy_model.h" #include "visual_deck_storage_sort_filter_proxy_model.h"
#include "../../../client/settings/cache_settings.h"
#include "../../filters/deck_filter_string.h" #include "../../filters/deck_filter_string.h"
#include <QFileInfo> #include <QFileInfo>
#include <algorithm> #include <algorithm>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/settings/cards_display_settings.h>
VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent) VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QObject *parent)
: QSortFilterProxyModel(parent) : QSortFilterProxyModel(parent)
@ -215,7 +218,10 @@ void VisualDeckStorageSortFilterProxyModel::updateSearchMatches()
return; 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) { for (int row = 0; row < count; ++row) {
const DeckPreviewData &data = source->dataForRow(row); const DeckPreviewData &data = source->dataForRow(row);

View file

@ -15,6 +15,7 @@
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/visual_deck_storage_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, connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
&VisualDeckStorageWidget::updateSearchFilter); &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, connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&VisualDeckStorageWidget::createRootFolderWidget); &VisualDeckStorageWidget::createRootFolderWidget);

View file

@ -4,6 +4,42 @@
#include <QString> #include <QString>
#include <QStringList> #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 * @namespace CardLocalization
* @ingroup Cards * @ingroup Cards

View file

@ -74,6 +74,36 @@ NumericValue <- [0-9]+
static std::once_flag init; 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() static void setupParserRules()
{ {
auto passthru = [](const peg::SemanticValues &sv) -> Filter { auto passthru = [](const peg::SemanticValues &sv) -> Filter {
@ -333,7 +363,11 @@ static void setupParserRules()
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter { search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]); 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 { search["ColorQuery"] = [](const peg::SemanticValues &sv) -> Filter {
@ -410,7 +444,11 @@ static void setupParserRules()
}; };
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter { search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]); 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()]; }; search["Color"] = [](const peg::SemanticValues &sv) -> char { return "WUBRGU"[sv.choice()]; };
@ -425,7 +463,7 @@ FilterString::FilterString()
_error = "Not initialized"; _error = "Not initialized";
} }
FilterString::FilterString(const QString &expr) FilterString::FilterString(const QString &expr, const CardSearchLanguage &searchLanguage)
{ {
QByteArray ba = expr.simplified().toUtf8(); QByteArray ba = expr.simplified().toUtf8();
@ -438,6 +476,8 @@ FilterString::FilterString(const QString &expr)
return; return;
} }
searchLanguageContext = searchLanguage;
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) { 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)); _error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -65,25 +65,31 @@ void CardSearchModel::updateSearchResults(const QString &query)
continue; continue;
} }
const QString lowerName = card->getName().toLower(); // The completer suggestions match against the same languages the card
if (!lowerName.contains(lowerQuery)) { // search uses, so typing a localized name finds the card. In Both mode
continue; // 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)) { if (lowerName.startsWith(lowerQuery)) {
prefixMatches.append({card, distance}); prefixMatches.append({card, distance});
} else { } else {
containsMatches.append({card, distance}); 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) { if (a.distance != b.distance) {
return 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); std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength);
@ -101,3 +107,25 @@ void CardSearchModel::updateSearchResults(const QString &query)
endResetModel(); 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 updateSearchResults(const QString &query); // Update results based on input
void setSearchLanguage(const CardSearchLanguage &searchLang)
{
if (searchLanguage == searchLang) {
return;
}
searchLanguage = searchLang;
}
private: private:
struct SearchResult struct SearchResult
{ {
@ -34,8 +42,15 @@ private:
int distance; 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; CardDatabaseDisplayModel *sourceModel;
QList<SearchResult> searchResults; QList<SearchResult> searchResults;
CardSearchLanguage searchLanguage;
}; };
#endif // CARD_SEARCH_MODEL_H #endif // CARD_SEARCH_MODEL_H

View file

@ -179,7 +179,7 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex
} }
if (filterString != nullptr) { if (filterString != nullptr) {
if (filterTree != nullptr && !filterTree->acceptsCard(info)) { if (filterTree != nullptr && !filterTree->acceptsCard(info, searchLanguage)) {
return false; return false;
} }
return filterString->check(info); return filterString->check(info);
@ -190,8 +190,14 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex
bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
{ {
if (!cardName.isEmpty() && !info->getName().contains(cardName, Qt::CaseInsensitive)) { if (!cardName.isEmpty()) {
return false; 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())) { if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) {
@ -199,7 +205,7 @@ bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
} }
if (filterTree != nullptr) { if (filterTree != nullptr) {
return filterTree->acceptsCard(info); return filterTree->acceptsCard(info, searchLanguage);
} }
return true; return true;
@ -235,6 +241,28 @@ void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree)
invalidate(); 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() void CardDatabaseDisplayModel::filterTreeChanged()
{ {
invalidate(); invalidate();

View file

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

View file

@ -110,6 +110,11 @@ QString CardsDisplaySettings::getCardLang() const
return getValue("cardLang", QString(), QString(), "en").toString(); 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) void CardsDisplaySettings::setDisplayCardNames(bool _displayCardNames)
{ {
setValue(_displayCardNames, "displayCardNames"); setValue(_displayCardNames, "displayCardNames");
@ -242,3 +247,12 @@ void CardsDisplaySettings::setCardLang(const QString &_cardLang)
sync(); sync();
emit cardLangChanged(_cardLang); 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 "settings_manager.h"
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/interfaces/interface_cards_display_settings_provider.h> #include <libcockatrice/interfaces/interface_cards_display_settings_provider.h>
class CardsDisplaySettings : public SettingsManager, public ICardsDisplaySettingsProvider class CardsDisplaySettings : public SettingsManager, public ICardsDisplaySettingsProvider
@ -32,6 +33,7 @@ public:
[[nodiscard]] int getArchidektPreviewSize() const override; [[nodiscard]] int getArchidektPreviewSize() const override;
[[nodiscard]] int getSampleHandSize() const override; [[nodiscard]] int getSampleHandSize() const override;
[[nodiscard]] QString getCardLang() const override; [[nodiscard]] QString getCardLang() const override;
[[nodiscard]] int getCardSearchLanguage() const override;
void setDisplayCardNames(bool _displayCardNames); void setDisplayCardNames(bool _displayCardNames);
void setRoundCardCorners(bool _roundCardCorners); void setRoundCardCorners(bool _roundCardCorners);
@ -54,6 +56,7 @@ public:
void setArchidektPreviewCardSize(int _archidektPreviewCardSize); void setArchidektPreviewCardSize(int _archidektPreviewCardSize);
void setSampleHandSize(int _sampleHandSize); void setSampleHandSize(int _sampleHandSize);
void setCardLang(const QString &_cardLang); void setCardLang(const QString &_cardLang);
void setCardSearchLanguage(int _cardSearchLanguage);
signals: signals:
void displayCardNamesChanged(); void displayCardNamesChanged();
@ -71,6 +74,7 @@ signals:
void archidektPreviewSizeChanged(); void archidektPreviewSizeChanged();
void sampleHandSizeChanged(int amount); void sampleHandSizeChanged(int amount);
void cardLangChanged(const QString &lang); void cardLangChanged(const QString &lang);
void cardSearchLanguageChanged(int cardSearchLanguage);
public: public:
explicit CardsDisplaySettings(const QString &settingPath, QObject *parent = nullptr); 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) 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 } // namespace
int main(int argc, char **argv) int main(int argc, char **argv)

View file

@ -575,6 +575,19 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_SetAndGet)
ASSERT_EQ(s.getCardLang(), QString("de")); 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 --- // --- VisualDeckStorageSettings ---
TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)