From 009137d538da3655dd6e28e11e8e0a7c978291a0 Mon Sep 17 00:00:00 2001 From: Christian Daniel Jensen Date: Tue, 3 Nov 2026 18:00:35 -0700 Subject: [PATCH] Add MSE gender quick filter to Continuumatrice card browser --- cockatrice/CMakeLists.txt | 1 + ...database_display_filter_toolbar_widget.cpp | 12 +- ...l_database_display_filter_toolbar_widget.h | 3 + ..._database_display_gender_filter_widget.cpp | 116 ++++++++++++++++++ ...al_database_display_gender_filter_widget.h | 26 ++++ .../libcockatrice/filters/filter_card.cpp | 2 + .../libcockatrice/filters/filter_card.h | 1 + .../libcockatrice/filters/filter_tree.cpp | 9 ++ .../libcockatrice/filters/filter_tree.h | 1 + tests/carddatabase/filter_string_test.cpp | 54 ++++++++ 10 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.cpp create mode 100644 cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index bb0caf1c6..a2f6b765e 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -295,6 +295,7 @@ set(cockatrice_SOURCES src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp + src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index 5063cce14..9e4ca1af3 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -6,6 +6,7 @@ #include "card_database_display_model.h" #include "visual_database_display_filter_save_load_widget.h" #include "visual_database_display_format_legality_filter_widget.h" +#include "visual_database_display_gender_filter_widget.h" #include "visual_database_display_main_type_filter_widget.h" #include "visual_database_display_name_filter_widget.h" #include "visual_database_display_set_filter_widget.h" @@ -103,6 +104,9 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg quickFilterSetWidget = new SettingsButtonWidget(this); quickFilterSetWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scroll"))); + quickFilterGenderWidget = new SettingsButtonWidget(this); + quickFilterGenderWidget->setButtonIcon(themePixmap(QStringLiteral("icons/filter"))); + quickFilterFormatLegalityWidget = new SettingsButtonWidget(this); quickFilterFormatLegalityWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced"))); @@ -126,12 +130,14 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize() formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel); subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel); setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel); + genderFilterWidget = new VisualDatabaseDisplayGenderFilterWidget(this, filterModel); quickFilterSaveLoadWidget->addSettingsWidget(saveLoadWidget); quickFilterNameWidget->addSettingsWidget(nameFilterWidget); quickFilterMainTypeWidget->addSettingsWidget(mainTypeFilterWidget); quickFilterSubTypeWidget->addSettingsWidget(subTypeFilterWidget); quickFilterSetWidget->addSettingsWidget(setFilterWidget); + quickFilterGenderWidget->addSettingsWidget(genderFilterWidget); quickFilterFormatLegalityWidget->addSettingsWidget(formatLegalityWidget); // fill groupbox layouts @@ -144,6 +150,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize() filterLayout->addWidget(quickFilterMainTypeWidget); filterLayout->addWidget(quickFilterSubTypeWidget); filterLayout->addWidget(quickFilterSetWidget); + filterLayout->addWidget(quickFilterGenderWidget); filterLayout->addWidget(quickFilterFormatLegalityWidget); // put everything into main layout @@ -171,6 +178,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi() quickFilterMainTypeWidget->setToolTip(tr("Filter by card main-type")); quickFilterSubTypeWidget->setToolTip(tr("Filter by card sub-type")); quickFilterSetWidget->setToolTip(tr("Filter by set")); + quickFilterGenderWidget->setToolTip(tr("Filter by card gender")); quickFilterFormatLegalityWidget->setToolTip(tr("Filter by format legality")); quickFilterSaveLoadWidget->setButtonText(tr("Save/Load")); @@ -178,6 +186,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi() quickFilterMainTypeWidget->setButtonText(tr("Main Type")); quickFilterSubTypeWidget->setButtonText(tr("Sub Type")); quickFilterSetWidget->setButtonText(tr("Sets")); + quickFilterGenderWidget->setButtonText(tr("Gender")); quickFilterFormatLegalityWidget->setButtonText(tr("Formats")); } @@ -193,7 +202,8 @@ void VisualDatabaseDisplayFilterToolbarWidget::updateCompactMode(int availableWi const QList filterButtons = { quickFilterSaveLoadWidget, quickFilterNameWidget, quickFilterMainTypeWidget, - quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterFormatLegalityWidget, + quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterGenderWidget, + quickFilterFormatLegalityWidget, }; for (auto *btn : filterButtons) { diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h index f97310c1b..d55073c6a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h @@ -13,6 +13,7 @@ class QLabel; class SettingsButtonWidget; class VisualDatabaseDisplayFilterSaveLoadWidget; class VisualDatabaseDisplayFormatLegalityFilterWidget; +class VisualDatabaseDisplayGenderFilterWidget; class VisualDatabaseDisplayMainTypeFilterWidget; class VisualDatabaseDisplayNameFilterWidget; class VisualDatabaseDisplaySetFilterWidget; @@ -52,6 +53,8 @@ private: VisualDatabaseDisplaySubTypeFilterWidget *subTypeFilterWidget; SettingsButtonWidget *quickFilterSetWidget; VisualDatabaseDisplaySetFilterWidget *setFilterWidget; + SettingsButtonWidget *quickFilterGenderWidget; + VisualDatabaseDisplayGenderFilterWidget *genderFilterWidget; SettingsButtonWidget *quickFilterFormatLegalityWidget; VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget; diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.cpp new file mode 100644 index 000000000..5d5110fe5 --- /dev/null +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.cpp @@ -0,0 +1,116 @@ +#include "visual_database_display_gender_filter_widget.h" + +#include "../../../filters/filter_tree_model.h" +#include "../general/layout_containers/flow_widget.h" +#include "libcockatrice/filters/filter_card.h" +#include "libcockatrice/filters/filter_tree.h" +#include "visual_database_display_filter_button.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +VisualDatabaseDisplayGenderFilterWidget::VisualDatabaseDisplayGenderFilterWidget(QWidget *parent, + FilterTreeModel *_filterModel) + : QWidget(parent), filterModel(_filterModel) +{ + setObjectName("genderFilterWidget"); + setMinimumWidth(300); + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(4, 4, 4, 4); + + auto *flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + flowWidget->setMaximumHeight(300); + layout->addWidget(flowWidget); + + // Authoritative source: continuum-modules.mse-include/genders/choice_names + // The MSE "group horde" option is intentionally omitted. + const QList> genders = { + {QStringLiteral("none"), tr("None")}, + {QStringLiteral("sex female"), tr("Female")}, + {QStringLiteral("sex male"), tr("Male")}, + {QStringLiteral("sex neuter"), tr("Neuter")}, + {QStringLiteral("sex hermaphrodite"), tr("Hermaphrodite")}, + {QStringLiteral("sex null"), tr("Sex Null")}, + {QStringLiteral("artificial artifice"), tr("Artifice")}, + {QStringLiteral("artificial masculine"), tr("Masculine (Artificial)")}, + {QStringLiteral("artificial feminine"), tr("Feminine (Artificial)")}, + {QStringLiteral("duos couple"), tr("Couple")}, + {QStringLiteral("duos brothers"), tr("Brothers")}, + {QStringLiteral("duos sisters"), tr("Sisters")}, + {QStringLiteral("duos siblings"), tr("Siblings")}, + {QStringLiteral("special not applicable"), tr("Not Applicable")}, + {QStringLiteral("custom gender one"), tr("Custom One")}, + {QStringLiteral("custom gender two"), tr("Custom Two")}, + {QStringLiteral("custom gender three"), tr("Custom Three")}, + }; + + for (const auto &gender : genders) { + auto *button = new QPushButton(gender.second, flowWidget); + button->setObjectName(QStringLiteral("genderFilter_") + gender.first); + button->setToolTip(gender.first); + button->setCheckable(true); + button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle); + flowWidget->addWidget(button); + genderButtons.insert(gender.first, button); + connect(button, &QPushButton::toggled, this, [this](bool) { updateGenderFilter(); }); + } + + auto *clearButton = new QPushButton(tr("Clear gender filter"), this); + layout->addWidget(clearButton); + connect(clearButton, &QPushButton::clicked, this, [this]() { + for (auto *button : genderButtons) { + const QSignalBlocker blocker(button); + button->setChecked(false); + } + updateGenderFilter(); + }); + + connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() { syncWithFilterModel(); }); + syncWithFilterModel(); +} + +void VisualDatabaseDisplayGenderFilterWidget::updateGenderFilter() +{ + // Selected genders are ORed together; other filter attributes still use AND. + // No selections means there is no gender restriction. + { + const QSignalBlocker modelBlocker(filterModel); + const QSignalBlocker treeBlocker(filterModel->filterTree()); + filterModel->clearFiltersOfType(CardFilter::AttrGender); + + for (auto it = genderButtons.cbegin(); it != genderButtons.cend(); ++it) { + if (it.value()->isChecked()) { + QString gender = it.key(); + filterModel->addFilter(new CardFilter(gender, CardFilter::TypeOr, CardFilter::AttrGender)); + } + } + } + + emit filterModel->filterTree()->changed(); + emit filterModel->layoutChanged(); +} + +void VisualDatabaseDisplayGenderFilterWidget::syncWithFilterModel() +{ + QSet selected; + const auto filters = filterModel->getFiltersOfType(CardFilter::AttrGender); + for (const CardFilter *filter : filters) { + if (filter->type() == CardFilter::TypeOr || filter->type() == CardFilter::TypeAnd) { + selected.insert(filter->term()); + } + delete filter; // FilterTreeModel::getFiltersOfType returns heap copies. + } + + for (auto it = genderButtons.cbegin(); it != genderButtons.cend(); ++it) { + const QSignalBlocker blocker(it.value()); + it.value()->setChecked(selected.contains(it.key())); + } +} diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.h new file mode 100644 index 000000000..b9d5999f5 --- /dev/null +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_gender_filter_widget.h @@ -0,0 +1,26 @@ +#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_GENDER_FILTER_WIDGET_H +#define COCKATRICE_VISUAL_DATABASE_DISPLAY_GENDER_FILTER_WIDGET_H + +#include +#include +#include + +class FilterTreeModel; +class FlowWidget; +class QPushButton; + +// Uses the literal MSE gender option values, not labels or filename suffixes. +class VisualDatabaseDisplayGenderFilterWidget : public QWidget +{ +public: + explicit VisualDatabaseDisplayGenderFilterWidget(QWidget *parent, FilterTreeModel *filterModel); + +private: + FilterTreeModel *filterModel; + QMap genderButtons; + + void updateGenderFilter(); + void syncWithFilterModel(); +}; + +#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_GENDER_FILTER_WIDGET_H diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.cpp b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp index b35b730b9..1d7c89349 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp @@ -91,6 +91,8 @@ const QString CardFilter::attrName(Attr a) return tr("Main Type"); case AttrSubType: return tr("Sub Type"); + case AttrGender: + return tr("Gender"); default: return QString(""); } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h index e6a37ee40..de0ae9ba9 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -45,6 +45,7 @@ public: AttrMainType, AttrSubType, AttrFormat, + AttrGender, AttrEnd, }; diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 8bc0cdf24..de09e3bb9 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -351,6 +351,13 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const return subType.contains(term, Qt::CaseInsensitive); } +bool FilterItem::acceptGender(const CardInfoPtr &info) const +{ + // MSE exports the full option value (e.g. "sex male", "duos siblings"). + // Do not substring-match "male" against "artificial masculine". + return info->getProperty(QStringLiteral("gender")).trimmed().compare(term.trimmed(), Qt::CaseInsensitive) == 0; +} + bool FilterItem::acceptColor(const CardInfoPtr &info) const { if (convertedColorTerm.isEmpty()) { @@ -545,6 +552,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr &info, return acceptMainType(info); case CardFilter::AttrSubType: return acceptSubType(info); + case CardFilter::AttrGender: + return acceptGender(info); default: return true; /* ignore this attribute */ } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h index 85a57304d..fecf56adc 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -243,6 +243,7 @@ public: [[nodiscard]] bool acceptType(const CardInfoPtr &info) const; [[nodiscard]] bool acceptMainType(const CardInfoPtr &info) const; [[nodiscard]] bool acceptSubType(const CardInfoPtr &info) const; + [[nodiscard]] bool acceptGender(const CardInfoPtr &info) const; [[nodiscard]] bool acceptColor(const CardInfoPtr &info) const; [[nodiscard]] bool acceptText(const CardInfoPtr &info, const CardSearchLanguage &searchLanguage) const; [[nodiscard]] bool acceptSet(const CardInfoPtr &info) const; diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp index e561db590..fb370b653 100644 --- a/tests/carddatabase/filter_string_test.cpp +++ b/tests/carddatabase/filter_string_test.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -186,6 +188,58 @@ TEST_F(CardQuery, SearchLanguageIsBoundPerInstance) } // namespace +TEST(GenderFilter, MatchesMseValuesExactlyAndCaseInsensitively) +{ + auto male = CardInfo::newInstance("Guardian-m"); + male->setProperty("gender", "sex male"); + auto female = CardInfo::newInstance("Guardian-f"); + female->setProperty("gender", "sex female"); + auto artificial = CardInfo::newInstance("Automaton"); + artificial->setProperty("gender", "artificial masculine"); + auto missing = CardInfo::newInstance("Unknown"); + + FilterTree tree; + QString term = "SEX MALE"; + CardFilter genderFilter(term, CardFilter::TypeOr, CardFilter::AttrGender); + tree.termNode(&genderFilter); + + EXPECT_TRUE(tree.acceptsCard(male, CardSearchLanguage{})); + EXPECT_FALSE(tree.acceptsCard(female, CardSearchLanguage{})); + EXPECT_FALSE(tree.acceptsCard(artificial, CardSearchLanguage{})); + EXPECT_FALSE(tree.acceptsCard(missing, CardSearchLanguage{})); +} + +TEST(GenderFilter, MultipleSelectionsUseOrAndClearingRestoresAllCards) +{ + auto male = CardInfo::newInstance("Guardian-m"); + male->setProperty("gender", "sex male"); + auto female = CardInfo::newInstance("Guardian-f"); + female->setProperty("gender", "sex female"); + auto neuter = CardInfo::newInstance("Guardian"); + neuter->setProperty("gender", "sex neuter"); + auto duo = CardInfo::newInstance("Sibling Heroes"); + duo->setProperty("gender", "duos siblings"); + + FilterTree tree; + QString maleTerm = "sex male"; + QString femaleTerm = "sex female"; + CardFilter maleFilter(maleTerm, CardFilter::TypeOr, CardFilter::AttrGender); + CardFilter femaleFilter(femaleTerm, CardFilter::TypeOr, CardFilter::AttrGender); + tree.termNode(&maleFilter); + tree.termNode(&femaleFilter); + + EXPECT_TRUE(tree.acceptsCard(male, CardSearchLanguage{})); + EXPECT_TRUE(tree.acceptsCard(female, CardSearchLanguage{})); + EXPECT_FALSE(tree.acceptsCard(neuter, CardSearchLanguage{})); + EXPECT_FALSE(tree.acceptsCard(duo, CardSearchLanguage{})); + + tree.removeFiltersByAttr(CardFilter::AttrGender); + EXPECT_TRUE(tree.acceptsCard(male, CardSearchLanguage{})); + EXPECT_TRUE(tree.acceptsCard(female, CardSearchLanguage{})); + EXPECT_TRUE(tree.acceptsCard(neuter, CardSearchLanguage{})); + EXPECT_TRUE(tree.acceptsCard(duo, CardSearchLanguage{})); +} + class SetQuery : public ::testing::Test { protected: