From ef68a7bdccb69297797c63d2c95bb36436a3d292 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:50:48 +0200 Subject: [PATCH] [Card] Add a setting for the language used in card search (#7314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- cockatrice/src/filters/deck_filter_string.cpp | 12 +- cockatrice/src/filters/deck_filter_string.h | 3 +- .../dialogs/dlg_create_token.cpp | 13 ++ .../src/game_graphics/zones/view_zone.cpp | 8 +- .../game_graphics/zones/view_zone_widget.cpp | 7 ++ .../card_picture_to_load.cpp | 3 +- .../deck_editor_database_display_widget.cpp | 13 ++ .../settings_page/general_settings_page.cpp | 26 +++- .../settings_page/general_settings_page.h | 4 + .../widgets/utility/completer_utils.cpp | 20 +++ .../visual_database_display_widget.cpp | 11 ++ ...l_deck_storage_sort_filter_proxy_model.cpp | 8 +- .../visual_deck_storage_widget.cpp | 8 ++ .../libcockatrice/card/card_localization.h | 36 ++++++ .../libcockatrice/filters/filter_string.cpp | 46 ++++++- .../libcockatrice/filters/filter_string.h | 3 +- .../libcockatrice/filters/filter_tree.cpp | 114 ++++++++++++++---- .../libcockatrice/filters/filter_tree.h | 26 ++-- ...nterface_cards_display_settings_provider.h | 1 + .../database/card/card_search_model.cpp | 50 ++++++-- .../models/database/card/card_search_model.h | 15 +++ .../database/card_database_display_model.cpp | 36 +++++- .../database/card_database_display_model.h | 11 +- .../settings/cards_display_settings.cpp | 14 +++ .../settings/cards_display_settings.h | 4 + tests/carddatabase/filter_string_test.cpp | 51 ++++++++ tests/settings/settings_defaults_test.cpp | 13 ++ 27 files changed, 488 insertions(+), 68 deletions(-) diff --git a/cockatrice/src/filters/deck_filter_string.cpp b/cockatrice/src/filters/deck_filter_string.cpp index 4abb8210c..a2b7519a8 100644 --- a/cockatrice/src/filters/deck_filter_string.cpp +++ b/cockatrice/src/filters/deck_filter_string.cpp @@ -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(sv[0])); + auto cardFilter = FilterString(std::any_cast(sv[0]), deckSearchLanguageContext); auto numberMatcher = sv.size() > 1 ? std::any_cast(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)); }); diff --git a/cockatrice/src/filters/deck_filter_string.h b/cockatrice/src/filters/deck_filter_string.h index 90a6a17eb..5b1419004 100644 --- a/cockatrice/src/filters/deck_filter_string.h +++ b/cockatrice/src/filters/deck_filter_string.h @@ -12,6 +12,7 @@ #include #include #include +#include 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); diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index b311d2ebd..6d8ad0534 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -16,11 +16,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -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(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")); diff --git a/cockatrice/src/game_graphics/zones/view_zone.cpp b/cockatrice/src/game_graphics/zones/view_zone.cpp index baf7b8b30..5bd5d262f 100644 --- a/cockatrice/src/game_graphics/zones/view_zone.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone.cpp @@ -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 #include #include +#include #include #include #include #include #include +#include /** * @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(cardsDisplay.getCardSearchLanguage())}); reorganizeCards(); } diff --git a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp index c13b79fea..fa6733413 100644 --- a/cockatrice/src/game_graphics/zones/view_zone_widget.cpp +++ b/cockatrice/src/game_graphics/zones/view_zone_widget.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include 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); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp index 7cb502e92..4f933207e 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp @@ -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()) { diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index c8bdacf0d..a83e25f5f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -11,8 +11,10 @@ #include #include #include +#include #include #include +#include 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(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); diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index 36436f8a3..b0fd0e018 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -63,13 +63,26 @@ GeneralSettingsPage::GeneralSettingsPage() connect(&cardLanguageBox, qOverload(&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(SearchLanguageMode::English) + : cardSearchLanguageIndex); + + connect(&cardSearchLanguageBox, qOverload(&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(SearchLanguageMode::English), tr("English")); + cardSearchLanguageBox.setItemText(static_cast(SearchLanguageMode::Selected), + tr("Selected card language (untranslated cards still match in English)")); + cardSearchLanguageBox.setItemText(static_cast(SearchLanguageMode::Both), + tr("English and selected card language")); advertiseTranslationPageLabel.setText( QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h index 7afba158f..07e0ba3a9 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h @@ -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; diff --git a/cockatrice/src/interface/widgets/utility/completer_utils.cpp b/cockatrice/src/interface/widgets/utility/completer_utils.cpp index 16d5cfd13..23ddb30b0 100644 --- a/cockatrice/src/interface/widgets/utility/completer_utils.cpp +++ b/cockatrice/src/interface/widgets/utility/completer_utils.cpp @@ -1,5 +1,6 @@ #include "completer_utils.h" +#include "../../../client/settings/cache_settings.h" #include "card_completer_styler.h" #include @@ -7,13 +8,26 @@ #include #include #include +#include #include #include #include +#include + +namespace +{ +void applyCardSearchLanguage(CardSearchModel *searchModel) +{ + const CardsDisplaySettings &cardsDisplay = SettingsCache::instance().cardsDisplay(); + searchModel->setSearchLanguage(CardSearchLanguage{ + cardsDisplay.getCardLang(), static_cast(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}; } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index f4530a12b..a20d56f63 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -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(cardsDisplay.getCardSearchLanguage())}); + }; + applyCardSearchLanguage(); + connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, + applyCardSearchLanguage); + connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardSearchLanguageChanged, this, + applyCardSearchLanguage); + cards = new QList; connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, &VisualDatabaseDisplayWidget::modelDirty); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp index 4b3a1ac29..0ad6ebcb2 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp @@ -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 #include +#include +#include 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(cardsDisplay.getCardSearchLanguage())}); for (int row = 0; row < count; ++row) { const DeckPreviewData &data = source->dataForRow(row); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index bd167ec5b..c7218f195 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -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); diff --git a/libcockatrice_card/libcockatrice/card/card_localization.h b/libcockatrice_card/libcockatrice/card/card_localization.h index a9c8d28ea..1bde506cb 100644 --- a/libcockatrice_card/libcockatrice/card/card_localization.h +++ b/libcockatrice_card/libcockatrice/card/card_localization.h @@ -4,6 +4,42 @@ #include #include +/** + * @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 diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index aaf391c03..1b63ca122 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -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(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(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)); }); diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.h b/libcockatrice_filters/libcockatrice/filters/filter_string.h index a058f7d07..015df0cf4 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.h @@ -14,6 +14,7 @@ #include #include #include +#include #include 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()) { diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 8502db50b..a5d91d9d3 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -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); + } + + 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 @@ -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; } } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h index aac1777e0..dd47a1ebc 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -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(); diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h index 304ab3cb7..900f51f2b 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -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 diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp index 621f28983..3a734ed37 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp @@ -65,25 +65,31 @@ void CardSearchModel::updateSearchResults(const QString &query) continue; } - const QString lowerName = card->getName().toLower(); - if (!lowerName.contains(lowerQuery)) { - continue; - } + // The completer suggestions match against the same languages the card + // search uses, so typing a localized name finds the card. In Both mode + // either language can match. + for (const QString &matchName : searchableNames(card)) { + const QString lowerName = matchName.toLower(); + if (!lowerName.contains(lowerQuery)) { + continue; + } - const int distance = levenshteinDistance(lowerQuery, lowerName); + const int distance = levenshteinDistance(lowerQuery, lowerName); - if (lowerName.startsWith(lowerQuery)) { - prefixMatches.append({card, distance}); - } else { - containsMatches.append({card, distance}); + if (lowerName.startsWith(lowerQuery)) { + prefixMatches.append({card, distance}); + } else { + containsMatches.append({card, distance}); + } + break; } } - auto sortByDistanceThenLength = [](const SearchResult &a, const SearchResult &b) { + auto sortByDistanceThenLength = [this](const SearchResult &a, const SearchResult &b) { if (a.distance != b.distance) { return a.distance < b.distance; } - return a.card->getName().size() < b.card->getName().size(); + return sortableName(a.card).size() < sortableName(b.card).size(); }; std::sort(prefixMatches.begin(), prefixMatches.end(), sortByDistanceThenLength); @@ -101,3 +107,25 @@ void CardSearchModel::updateSearchResults(const QString &query) endResetModel(); } + +QStringList CardSearchModel::searchableNames(const CardInfoPtr &card) const +{ + if (searchLanguage.isEnglishOnly()) { + return {card->getName()}; + } + + const QString localizedName = card->getLocalizedName(searchLanguage.language); + if (searchLanguage.mode == SearchLanguageMode::Selected) { + return {localizedName}; + } + + return {card->getName(), localizedName}; +} + +QString CardSearchModel::sortableName(const CardInfoPtr &card) const +{ + if (searchLanguage.isEnglishOnly()) { + return card->getName(); + } + return card->getLocalizedName(searchLanguage.language); +} diff --git a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h index 646bf7e61..b877a9385 100644 --- a/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h @@ -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 searchResults; + CardSearchLanguage searchLanguage; }; #endif // CARD_SEARCH_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp index 724ee61f2..7f89677c8 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp @@ -179,7 +179,7 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex } if (filterString != nullptr) { - if (filterTree != nullptr && !filterTree->acceptsCard(info)) { + if (filterTree != nullptr && !filterTree->acceptsCard(info, searchLanguage)) { return false; } return filterString->check(info); @@ -190,8 +190,14 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const { - if (!cardName.isEmpty() && !info->getName().contains(cardName, Qt::CaseInsensitive)) { - return false; + if (!cardName.isEmpty()) { + const bool matchesEnglish = info->getName().contains(cardName, Qt::CaseInsensitive); + const bool matchesLocalized = + !searchLanguage.isEnglishOnly() && + info->getLocalizedName(searchLanguage.language).contains(cardName, Qt::CaseInsensitive); + if (!matchesEnglish && !matchesLocalized) { + return false; + } } if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) { @@ -199,7 +205,7 @@ bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const } if (filterTree != nullptr) { - return filterTree->acceptsCard(info); + return filterTree->acceptsCard(info, searchLanguage); } return true; @@ -235,6 +241,28 @@ void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree) invalidate(); } +void CardDatabaseDisplayModel::setStringFilter(const QString &_src) +{ + searchText = _src; + delete filterString; + filterString = new FilterString(_src, searchLanguage); + dirty(); +} + +void CardDatabaseDisplayModel::setSearchLanguage(const CardSearchLanguage &searchLang) +{ + if (searchLanguage == searchLang) { + return; + } + + searchLanguage = searchLang; + + if (filterString != nullptr) { + setStringFilter(searchText); + } + dirty(); +} + void CardDatabaseDisplayModel::filterTreeChanged() { invalidate(); diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h index c3145c356..e6aadd8c6 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h @@ -10,6 +10,7 @@ #include #include +#include #include 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 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 &_cardNameSet) { cardNameSet = _cardNameSet; dirty(); } + void setSearchLanguage(const CardSearchLanguage &searchLang); void dirty() { diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp index b8eafca6c..94e888504 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp @@ -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(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); +} diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h index 0b47ce490..85eb5adbd 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h @@ -3,6 +3,7 @@ #include "settings_manager.h" +#include #include 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); diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp index c6d68be1f..2b92c65df 100644 --- a/tests/carddatabase/filter_string_test.cpp +++ b/tests/carddatabase/filter_string_test.cpp @@ -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) diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 0341b2b99..6e674cb86 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -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(SearchLanguageMode::English)); +} + +TEST_F(SettingsDefaultsTest, CardsDisplay_CardSearchLanguage_SetAndGet) +{ + CardsDisplaySettings s(settingsPath, nullptr); + s.setCardSearchLanguage(static_cast(SearchLanguageMode::Selected)); + ASSERT_EQ(s.getCardSearchLanguage(), static_cast(SearchLanguageMode::Selected)); +} + // --- VisualDeckStorageSettings --- TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)