Merge pull request #7 from Continuum-Universes-Studio/feature/continuumatrice-gender-filter

Add Gender filter using Continuum MSE gender choices
This commit is contained in:
Christian Daniel Jensen 2026-11-03 18:34:56 -07:00 • committed by GitHub
commit f29766d6bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 225 additions and 1 deletions

View file

@ -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

View file

@ -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<SettingsButtonWidget *> filterButtons = {
quickFilterSaveLoadWidget, quickFilterNameWidget, quickFilterMainTypeWidget,
quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterFormatLegalityWidget,
quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterGenderWidget,
quickFilterFormatLegalityWidget,
};
for (auto *btn : filterButtons) {

View file

@ -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;

View file

@ -0,0 +1,117 @@
#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 <QList>
#include <QPair>
#include <QPushButton>
#include <QSet>
#include <QSignalBlocker>
#include <QString>
#include <QVBoxLayout>
#include <QWidget>
#include <qnamespace.h>
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<QPair<QString, QString>> 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();
CardFilter filter(gender, CardFilter::TypeOr, CardFilter::AttrGender);
filterModel->addFilter(&filter);
}
}
}
emit filterModel->filterTree()->changed();
emit filterModel->layoutChanged();
}
void VisualDatabaseDisplayGenderFilterWidget::syncWithFilterModel()
{
QSet<QString> 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()));
}
}

View file

@ -0,0 +1,26 @@
#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_GENDER_FILTER_WIDGET_H
#define COCKATRICE_VISUAL_DATABASE_DISPLAY_GENDER_FILTER_WIDGET_H
#include <QMap>
#include <QString>
#include <QWidget>
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<QString, QPushButton *> genderButtons;
void updateGenderFilter();
void syncWithFilterModel();
};
#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_GENDER_FILTER_WIDGET_H

View file

@ -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("");
}

View file

@ -45,6 +45,7 @@ public:
AttrMainType,
AttrSubType,
AttrFormat,
AttrGender,
AttrEnd,
};

View file

@ -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 */
}

View file

@ -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;

View file

@ -15,6 +15,8 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/printing/printing_info.h>
#include <libcockatrice/filters/filter_string.h>
#include <libcockatrice/filters/filter_card.h>
#include <libcockatrice/filters/filter_tree.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <string>
@ -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: