From 286982350c4632140b52ee516922bf7f4cd390a9 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 19 Oct 2026 03:15:01 +0200 Subject: [PATCH 01/23] Added Exclude and Color Identity filter modes. --- ...l_database_display_color_filter_widget.cpp | 139 ++++++++---------- ...ual_database_display_color_filter_widget.h | 8 +- .../libcockatrice/card/card_info.cpp | 4 + .../libcockatrice/card/card_info.h | 1 + .../libcockatrice/filters/filter_card.cpp | 2 + .../libcockatrice/filters/filter_card.h | 3 +- .../libcockatrice/filters/filter_tree.cpp | 47 +++--- .../libcockatrice/filters/filter_tree.h | 1 + 8 files changed, 107 insertions(+), 98 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index cf815ace0..ac6d78a63 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -62,7 +62,9 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch)); modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes)); + modeComboBox->addItem(tr("Excludes"), QVariant::fromValue(FilterMode::Excludes)); modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude)); + modeComboBox->addItem(tr("Commander"), QVariant::fromValue(FilterMode::Commander)); modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); @@ -84,75 +86,69 @@ void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, boo } } -void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) +static void removeFilters(FilterTreeModel *filterModel, QChar color, CardFilter::Attr attr) { - QString colorString = color; - QString typeStr; - - // Remove previous filters - - QList allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); - QList matchingFilters; - - for (const CardFilter *filter : allColorFilters) { + for (const CardFilter *filter : filterModel->getFiltersOfType(attr)) { if (filter->term() == color) { - matchingFilters.append(filter); + filterModel->removeFilter(filter); } } +} - for (const CardFilter *filter : matchingFilters) { - filterModel->removeFilter(filter); - } - - // Add actual filter - +void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) +{ switch (currentMode) { case FilterMode::ExactMatch: - filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor)); + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor)); break; case FilterMode::Includes: - filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); + break; + + case FilterMode::Excludes: + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); break; case FilterMode::IncludeExclude: - filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); + break; + + case FilterMode::Commander: + removeFilters(filterModel, color, CardFilter::Attr::AttrColorIdentity); break; } } void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) { - QString colorString = color; - - // Remove inclusion filters - QList allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); - QList matchingFilters; - - for (const CardFilter *filter : allColorFilters) { - if (filter->term() == color) { - matchingFilters.append(filter); - } - } - - for (const CardFilter *filter : matchingFilters) { - filterModel->removeFilter(filter); - } - // Add exclusion filters if the mode demands it switch (currentMode) { case FilterMode::ExactMatch: - filterModel->addFilter( - new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); - break; - - case FilterMode::IncludeExclude: - filterModel->addFilter( - new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); break; case FilterMode::Includes: - // No exclusion in Includes mode + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + break; + + case FilterMode::Excludes: + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + break; + + case FilterMode::IncludeExclude: + removeFilters(filterModel, color, CardFilter::Attr::AttrColor); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); + break; + + case FilterMode::Commander: + removeFilters(filterModel, color, CardFilter::Attr::AttrColorIdentity); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity)); break; } } @@ -170,6 +166,7 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode() filterModel->filterTree()->blockSignals(true); filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor); + filterModel->clearFiltersOfType(CardFilter::Attr::AttrColorIdentity); const QList manaSymbolWidgets = findChildren(); @@ -212,52 +209,42 @@ QList VisualDatabaseDisplayColorFilterWidget::getActiveColors() void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() { - QList allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor); + QList allColorFilters = filterModel->getFiltersOfType( + currentMode == FilterMode::Commander + ? CardFilter::Attr::AttrColorIdentity + : CardFilter::Attr::AttrColor); QList manaSymbolWidgets = findChildren(); for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { - bool found = false; + QChar symbol = manaSymbolWidget->getSymbolChar(); + bool found = currentMode == FilterMode::Commander; for (const CardFilter *filter : allColorFilters) { - if (manaSymbolWidget->getSymbolChar() == filter->term()) { + if (symbol == filter->term()) { switch (currentMode) { case FilterMode::ExactMatch: - switch (filter->type()) { - case CardFilter::Type::TypeAnd: - setManaSymbolActive(filter->term().at(0), true); - break; - default: - setManaSymbolActive(filter->term().at(0), false); - break; - } + if (filter->type() == CardFilter::Type::TypeAnd) + found = true; break; case FilterMode::Includes: - switch (filter->type()) { - case CardFilter::Type::TypeOr: - setManaSymbolActive(filter->term().at(0), true); - break; - default: - setManaSymbolActive(filter->term().at(0), false); - break; - } - break; case FilterMode::IncludeExclude: - switch (filter->type()) { - case CardFilter::Type::TypeOr: - setManaSymbolActive(filter->term().at(0), true); - break; - default: - setManaSymbolActive(filter->term().at(0), false); - break; - } + if (filter->type() == CardFilter::Type::TypeOr) + found = true; + break; + case FilterMode::Excludes: + if (filter->type() == CardFilter::Type::TypeAndNot) + found = true; + break; + case FilterMode::Commander: + if (filter->type() == CardFilter::Type::TypeAndNot) + found = false; break; } - found = true; + if (found) + break; } } - if (!found) { - setManaSymbolActive(manaSymbolWidget->getSymbolChar(), false); - } + setManaSymbolActive(manaSymbolWidget->getSymbolChar(), found); } } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index 38812b02a..c1c8e173f 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -39,9 +39,11 @@ private: enum class FilterMode { - ExactMatch, // Only selected colors are included, all others are excluded. - Includes, // Include selected colors (OR condition). - IncludeExclude // Include selected colors (OR) and exclude unselected colors (AND NOT). + ExactMatch, // Only selected colors are included, all others are excluded. + Includes, // Include selected colors (OR condition). + Excludes, // Exclude selected colors (OR). + IncludeExclude, // Include selected colors (OR) and exclude unselected colors (AND NOT). + Commander // Only include colors in the selected color identity. }; Q_DECLARE_METATYPE(FilterMode) diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index bd33f18e7..3aaa4c5bc 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -285,6 +285,10 @@ void CardInfo::setColors(const QString &value) { setProperty(Mtg::Colors, value); } +QString CardInfo::getColorIdentity() const +{ + return getProperty(Mtg::ColorIdentity); +} QString CardInfo::getLoyalty() const { return getProperty(Mtg::Loyalty); diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 356da0546..1670e245d 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -454,6 +454,7 @@ public: [[nodiscard]] QString getCmc() const; [[nodiscard]] QString getColors() const; void setColors(const QString &value); + [[nodiscard]] QString getColorIdentity() const; [[nodiscard]] QString getLoyalty() const; [[nodiscard]] QString getMainCardType() const; [[nodiscard]] QString getManaCost() const; diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.cpp b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp index 26cddd53c..8af39c098 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp @@ -69,6 +69,8 @@ const QString CardFilter::attrName(Attr a) return tr("Type"); case AttrColor: return tr("Color"); + case AttrColorIdentity: + return tr("Color Identity"); case AttrText: return tr("Text"); case AttrSet: diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h index e6a37ee40..7e4efbd96 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -32,6 +32,7 @@ public: { AttrCmc = 0, AttrColor, + AttrColorIdentity, AttrLoyalty, AttrManaCost, AttrName, @@ -54,7 +55,7 @@ private: enum Attr a; public: - CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) + CardFilter(QString const &term, Type type, Attr attr) : trm(term), t(type), a(attr) { } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 742b44e97..1448c7ba1 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -227,27 +227,26 @@ bool FilterItem::acceptSubType(const CardInfoPtr info) const return typeParts.size() > 1 && typeParts[1].contains(term, Qt::CaseInsensitive); } -bool FilterItem::acceptColor(const CardInfoPtr info) const -{ - QString converted_term = term.trimmed(); +static bool matchesColor(QString const &term, QString const &colors) { + QString converted_term = term.trimmed().toLower(); - converted_term.replace("green", "g", Qt::CaseInsensitive); - converted_term.replace("grn", "g", Qt::CaseInsensitive); - converted_term.replace("blue", "u", Qt::CaseInsensitive); - converted_term.replace("blu", "u", Qt::CaseInsensitive); - converted_term.replace("black", "b", Qt::CaseInsensitive); - converted_term.replace("blk", "b", Qt::CaseInsensitive); - converted_term.replace("red", "r", Qt::CaseInsensitive); - converted_term.replace("white", "w", Qt::CaseInsensitive); - converted_term.replace("wht", "w", Qt::CaseInsensitive); - converted_term.replace("colorless", "c", Qt::CaseInsensitive); - converted_term.replace("colourless", "c", Qt::CaseInsensitive); - converted_term.replace("none", "c", Qt::CaseInsensitive); + converted_term.replace("green", "g"); + converted_term.replace("grn", "g"); + converted_term.replace("blue", "u"); + converted_term.replace("blu", "u"); + converted_term.replace("black", "b"); + converted_term.replace("blk", "b"); + converted_term.replace("red", "r"); + converted_term.replace("white", "w"); + converted_term.replace("wht", "w"); + converted_term.replace("colorless", "c"); + converted_term.replace("colourless", "c"); + converted_term.replace("none", "c"); - converted_term.replace(QString(" "), QString(""), Qt::CaseInsensitive); + converted_term.replace(QString(" "), QString("")); // Colorless card filter - if (converted_term.toLower() == "c" && info->getColors().length() < 1) { + if (converted_term == "c" && colors.length() < 1) { return true; } @@ -257,7 +256,7 @@ bool FilterItem::acceptColor(const CardInfoPtr info) const */ int match_count = 0; for (auto &it : converted_term) { - if (info->getColors().contains(it, Qt::CaseInsensitive)) { + if (colors.contains(it, Qt::CaseInsensitive)) { match_count++; } } @@ -265,6 +264,16 @@ bool FilterItem::acceptColor(const CardInfoPtr info) const return match_count == converted_term.length(); } +bool FilterItem::acceptColor(const CardInfoPtr info) const +{ + return matchesColor(term, info->getColors()); +} + +bool FilterItem::acceptColorIdentity(const CardInfoPtr info) const +{ + return matchesColor(term, info->getColorIdentity()); +} + bool FilterItem::acceptText(const CardInfoPtr info, const CardSearchLanguage &searchLanguage) const { const QString &englishText = info->getText(); @@ -485,6 +494,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr info, return acceptType(info); case CardFilter::AttrColor: return acceptColor(info); + case CardFilter::AttrColorIdentity: + return acceptColorIdentity(info); case CardFilter::AttrText: return acceptText(info, searchLanguage); case CardFilter::AttrSet: diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h index 0defd466b..e94e0a948 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -223,6 +223,7 @@ public: [[nodiscard]] bool acceptMainType(CardInfoPtr info) const; [[nodiscard]] bool acceptSubType(CardInfoPtr info) const; [[nodiscard]] bool acceptColor(CardInfoPtr info) const; + [[nodiscard]] bool acceptColorIdentity(CardInfoPtr info) const; [[nodiscard]] bool acceptText(CardInfoPtr info, const CardSearchLanguage &searchLanguage) const; [[nodiscard]] bool acceptSet(CardInfoPtr info) const; [[nodiscard]] bool acceptManaCost(CardInfoPtr info) const; From f02f572dac157360f3f015fed01f736a48031375 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 19 Oct 2026 03:24:04 +0200 Subject: [PATCH 02/23] Style fix --- ...l_database_display_color_filter_widget.cpp | 22 +++++++++++-------- .../libcockatrice/filters/filter_card.h | 2 +- .../libcockatrice/filters/filter_tree.cpp | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index ac6d78a63..90cfd9cd8 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -148,7 +148,8 @@ void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) case FilterMode::Commander: removeFilters(filterModel, color, CardFilter::Attr::AttrColorIdentity); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity)); + filterModel->addFilter( + new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity)); break; } } @@ -210,9 +211,7 @@ QList VisualDatabaseDisplayColorFilterWidget::getActiveColors() void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() { QList allColorFilters = filterModel->getFiltersOfType( - currentMode == FilterMode::Commander - ? CardFilter::Attr::AttrColorIdentity - : CardFilter::Attr::AttrColor); + currentMode == FilterMode::Commander ? CardFilter::Attr::AttrColorIdentity : CardFilter::Attr::AttrColor); QList manaSymbolWidgets = findChildren(); @@ -223,25 +222,30 @@ void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() if (symbol == filter->term()) { switch (currentMode) { case FilterMode::ExactMatch: - if (filter->type() == CardFilter::Type::TypeAnd) + if (filter->type() == CardFilter::Type::TypeAnd) { found = true; + } break; case FilterMode::Includes: case FilterMode::IncludeExclude: - if (filter->type() == CardFilter::Type::TypeOr) + if (filter->type() == CardFilter::Type::TypeOr) { found = true; + } break; case FilterMode::Excludes: - if (filter->type() == CardFilter::Type::TypeAndNot) + if (filter->type() == CardFilter::Type::TypeAndNot) { found = true; + } break; case FilterMode::Commander: - if (filter->type() == CardFilter::Type::TypeAndNot) + if (filter->type() == CardFilter::Type::TypeAndNot) { found = false; + } break; } - if (found) + if (found) { break; + } } } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h index 7e4efbd96..bc613fc9c 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_card.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -55,7 +55,7 @@ private: enum Attr a; public: - CardFilter(QString const &term, Type type, Attr attr) : trm(term), t(type), a(attr) + CardFilter(const QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) { } diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 1448c7ba1..1c5ebe2e3 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -227,7 +227,7 @@ bool FilterItem::acceptSubType(const CardInfoPtr info) const return typeParts.size() > 1 && typeParts[1].contains(term, Qt::CaseInsensitive); } -static bool matchesColor(QString const &term, QString const &colors) { +static bool matchesColor(const QString &term, const QString &colors) { QString converted_term = term.trimmed().toLower(); converted_term.replace("green", "g"); From 9d97ba229fc7304302b407bb7b36279fae430c04 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 19 Oct 2026 15:01:37 +0200 Subject: [PATCH 03/23] Style fix --- libcockatrice_filters/libcockatrice/filters/filter_tree.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 1c5ebe2e3..392571d50 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -227,7 +227,8 @@ bool FilterItem::acceptSubType(const CardInfoPtr info) const return typeParts.size() > 1 && typeParts[1].contains(term, Qt::CaseInsensitive); } -static bool matchesColor(const QString &term, const QString &colors) { +static bool matchesColor(const QString &term, const QString &colors) +{ QString converted_term = term.trimmed().toLower(); converted_term.replace("green", "g"); From b13395eb4d62c1618999b97779083bffbbf798da Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Sun, 1 Nov 2026 23:23:38 +0100 Subject: [PATCH 04/23] Renamed Commander to ColorIdentity --- ...l_database_display_color_filter_widget.cpp | 38 ++++++++++--------- ...ual_database_display_color_filter_widget.h | 4 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index 1b4af8123..ccc65078d 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -63,7 +63,13 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes)); modeComboBox->addItem(tr("Excludes"), QVariant::fromValue(FilterMode::Excludes)); modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude)); - modeComboBox->addItem(tr("Commander"), QVariant::fromValue(FilterMode::Commander)); + modeComboBox->addItem(tr("Color identity"), QVariant::fromValue(FilterMode::ColorIdentity)); + + modeComboBox->setItemData(0, tr("Cards being exactly of the selected colors"), Qt::ToolTipRole); + modeComboBox->setItemData(1, tr("Cards containing any of the selected colors"), Qt::ToolTipRole); + modeComboBox->setItemData(2, tr("Cards containing none of the selected colors"), Qt::ToolTipRole); + modeComboBox->setItemData(3, tr("Cards containing any of the selected colors and none of the unselected colors"), Qt::ToolTipRole); + modeComboBox->setItemData(4, tr("Cards whose color identity fits within the selected colors"), Qt::ToolTipRole); modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); @@ -117,7 +123,7 @@ void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); break; - case FilterMode::Commander: + case FilterMode::ColorIdentity: removeFilters(filterModel, color, CardFilter::Attr::AttrColorIdentity); break; } @@ -145,10 +151,9 @@ void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); break; - case FilterMode::Commander: + case FilterMode::ColorIdentity: removeFilters(filterModel, color, CardFilter::Attr::AttrColorIdentity); - filterModel->addFilter( - new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity)); + filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity)); break; } } @@ -210,41 +215,38 @@ QList VisualDatabaseDisplayColorFilterWidget::getActiveColors() void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() { QList allColorFilters = filterModel->getFiltersOfType( - currentMode == FilterMode::Commander ? CardFilter::Attr::AttrColorIdentity : CardFilter::Attr::AttrColor); + currentMode == FilterMode::ColorIdentity + ? CardFilter::Attr::AttrColorIdentity + : CardFilter::Attr::AttrColor); QList manaSymbolWidgets = findChildren(); for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { QChar symbol = manaSymbolWidget->getSymbolChar(); - bool found = currentMode == FilterMode::Commander; + bool found = currentMode == FilterMode::ColorIdentity; for (const CardFilter *filter : allColorFilters) { if (symbol == filter->term()) { switch (currentMode) { case FilterMode::ExactMatch: - if (filter->type() == CardFilter::Type::TypeAnd) { + if (filter->type() == CardFilter::Type::TypeAnd) found = true; - } break; case FilterMode::Includes: case FilterMode::IncludeExclude: - if (filter->type() == CardFilter::Type::TypeOr) { + if (filter->type() == CardFilter::Type::TypeOr) found = true; - } break; case FilterMode::Excludes: - if (filter->type() == CardFilter::Type::TypeAndNot) { + if (filter->type() == CardFilter::Type::TypeAndNot) found = true; - } break; - case FilterMode::Commander: - if (filter->type() == CardFilter::Type::TypeAndNot) { + case FilterMode::ColorIdentity: + if (filter->type() == CardFilter::Type::TypeAndNot) found = false; - } break; } - if (found) { + if (found) break; - } } } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index 90755a2ab..fa9c32204 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -42,9 +42,9 @@ enum class FilterMode { ExactMatch, // Only selected colors are included, all others are excluded. Includes, // Include selected colors (OR condition). - Excludes, // Exclude selected colors (OR). + Excludes, // Exclude selected colors (AND NOT). IncludeExclude, // Include selected colors (OR) and exclude unselected colors (AND NOT). - Commander // Only include colors in the selected color identity. + ColorIdentity // Keep cards whose color identity is a subset of the selected colors (stored as ANDNOT for each unselected color). }; Q_DECLARE_METATYPE(FilterMode) From de683434e48f57ec7c41e1d38b0ed4b6beff55e9 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Sun, 1 Nov 2026 23:24:08 +0100 Subject: [PATCH 05/23] Rebase fix, moved toLower to constructor Rebase fix --- .../libcockatrice/filters/filter_tree.cpp | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 2bd3ea7ef..b3cbf8926 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -176,23 +176,24 @@ FilterItem::FilterItem(QString trm, FilterItemList *parent) : p(parent), term(st lowercaseTerm = term.toLower(); switch (attr()) { - case CardFilter::AttrColor: { - QString converted = term.trimmed(); + case CardFilter::AttrColor: + case CardFilter::AttrColorIdentity: { + QString converted = term.trimmed().toLower(); - converted.replace("green", "g", Qt::CaseInsensitive); - converted.replace("grn", "g", Qt::CaseInsensitive); - converted.replace("blue", "u", Qt::CaseInsensitive); - converted.replace("blu", "u", Qt::CaseInsensitive); - converted.replace("black", "b", Qt::CaseInsensitive); - converted.replace("blk", "b", Qt::CaseInsensitive); - converted.replace("red", "r", Qt::CaseInsensitive); - converted.replace("white", "w", Qt::CaseInsensitive); - converted.replace("wht", "w", Qt::CaseInsensitive); - converted.replace("colorless", "c", Qt::CaseInsensitive); - converted.replace("colourless", "c", Qt::CaseInsensitive); - converted.replace("none", "c", Qt::CaseInsensitive); + converted.replace("green", "g"); + converted.replace("grn", "g"); + converted.replace("blue", "u"); + converted.replace("blu", "u"); + converted.replace("black", "b"); + converted.replace("blk", "b"); + converted.replace("red", "r"); + converted.replace("white", "w"); + converted.replace("wht", "w"); + converted.replace("colorless", "c"); + converted.replace("colourless", "c"); + converted.replace("none", "c"); - converted.replace(QString(" "), QString(""), Qt::CaseInsensitive); + converted.replace(QString(" "), QString("")); convertedColorTerm = converted; break; @@ -358,10 +359,10 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const return subType.contains(term, Qt::CaseInsensitive); } -static bool matchesColor(const QString &convertedColorTerm, const QString &color) +static bool matchesColor(const QString &convertedColorTerm, const QString &colors) { // Colorless card filter - if (convertedColorTerm.toLower() == "c" && colors.length() < 1) { + if (convertedColorTerm == "c" && colors.length() < 1) { return true; } @@ -386,6 +387,8 @@ bool FilterItem::acceptColor(const CardInfoPtr &info) const bool FilterItem::acceptColorIdentity(const CardInfoPtr &info) const { + // if (info->getColorIdentity() == "") + // system(std::format("notify-send \"{}, {}\"", convertedColorTerm.toStdString(), info->getColorIdentity().toStdString()).c_str()); return matchesColor(convertedColorTerm, info->getColorIdentity()); } From 4386d6549f955bb0ed6e7ce6ae138b6aabd0bbc1 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 00:51:09 +0100 Subject: [PATCH 06/23] Added doc comment --- .../libcockatrice/filters/filter_tree.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index b3cbf8926..817c7298d 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -359,10 +359,17 @@ bool FilterItem::acceptSubType(const CardInfoPtr &info) const return subType.contains(term, Qt::CaseInsensitive); } -static bool matchesColor(const QString &convertedColorTerm, const QString &colors) +/** + * @brief Return true when every letter of the given term appears in the given colors string. + * If the term is equal to "c", then only return true if the color string is empty. + * + * @param term The letters which should be included, or "c" + * @param colors The string to check for letters in + */ +static bool matchesColor(const QString &term, const QString &colors) { // Colorless card filter - if (convertedColorTerm == "c" && colors.length() < 1) { + if (term == "c" && colors.length() < 1) { return true; } @@ -371,13 +378,13 @@ static bool matchesColor(const QString &convertedColorTerm, const QString &color * then we should match all of them to the card's colors */ int match_count = 0; - for (const QChar &color : convertedColorTerm) { + for (const QChar &color : term) { if (colors.contains(color, Qt::CaseInsensitive)) { match_count++; } } - return match_count == convertedColorTerm.length(); + return match_count == term.length(); } bool FilterItem::acceptColor(const CardInfoPtr &info) const @@ -387,8 +394,6 @@ bool FilterItem::acceptColor(const CardInfoPtr &info) const bool FilterItem::acceptColorIdentity(const CardInfoPtr &info) const { - // if (info->getColorIdentity() == "") - // system(std::format("notify-send \"{}, {}\"", convertedColorTerm.toStdString(), info->getColorIdentity().toStdString()).c_str()); return matchesColor(convertedColorTerm, info->getColorIdentity()); } From e6b381e02d1a00bbc3ac969b6f42d27df472b801 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 00:54:12 +0100 Subject: [PATCH 07/23] Use getColorIdentity() --- libcockatrice_filters/libcockatrice/filters/filter_string.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp index 081ada824..1bcb1646b 100644 --- a/libcockatrice_filters/libcockatrice/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -409,7 +409,7 @@ static void setupParserRules() const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i'; if (sv.tokens[1][0] == ':') { return [=](const CardData &x) { - const QString match = identity ? x->getColors() : x->getProperty("coloridentity"); + const QString match = identity ? x->getColors() : x->getColorIdentity(); if (parts.contains("m") && match.length() < 2) { return false; } @@ -427,7 +427,7 @@ static void setupParserRules() } return [=](const CardData &x) { - const QString match = identity ? x->getColors() : x->getProperty("colorIdentity"); + const QString match = identity ? x->getColors() : x->getColorIdentity(); if (parts.contains("m") && match.length() < 2) { return false; } From e85e81ce85b3a9acf588a3815c289feac1ead603 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 01:02:09 +0100 Subject: [PATCH 08/23] Added test file --- tests/carddatabase/CMakeLists.txt | 12 +++++- tests/carddatabase/filter_tree_test.cpp | 56 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/carddatabase/filter_tree_test.cpp diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 22171a9e8..adc54bc8b 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -21,7 +21,7 @@ cockatrice_add_test( ) # ------------------------ -# Filter String Test +# Filter String and Tree Test # (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt) # ------------------------ if(WITH_ORACLE OR WITH_CLIENT) @@ -35,6 +35,16 @@ if(WITH_ORACLE OR WITH_CLIENT) LIBS libcockatrice_filters ) + cockatrice_add_test( + NAME + filter_tree_test + SOURCES + ${VERSION_STRING_CPP} + filter_tree_test.cpp + mocks.cpp + LIBS + libcockatrice_filters + ) endif() # ------------------------ diff --git a/tests/carddatabase/filter_tree_test.cpp b/tests/carddatabase/filter_tree_test.cpp new file mode 100644 index 000000000..0a192126c --- /dev/null +++ b/tests/carddatabase/filter_tree_test.cpp @@ -0,0 +1,56 @@ +#include "libcockatrice/card/card_localization.h" +#include "libcockatrice/card/database/card_database.h" +#include "libcockatrice/card/database/card_database_querier.h" +#include "libcockatrice/card/set/card_set.h" +#include "test_card_database_path_provider.h" + +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +class FilterTreeTest : public ::testing::Test +{ +protected: + void SetUp() override + { + CardDatabase *db = new CardDatabase(nullptr, new NoopCardPreferenceProvider(), + new TestCardDatabasePathProvider(), new NoopCardSetPriorityController()); + db->loadCardDatabases(); + + cat = db->query()->getCardBySimpleName("Cat"); + notDeadAfterAll = db->query()->getCardBySimpleName("Not Dead"); + truth = db->query()->getCardBySimpleName("Truth"); + doctor = db->query()->getCardBySimpleName("Doctor"); + } + // void TearDown() override {} + + CardData cat; + CardData notDeadAfterAll; + CardData truth; + CardData doctor; +}; + +TEST_F(FilterTreeTest, TestOne) +{ + ASSERT_TRUE(true); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +} From f2e4aa3c3432a468c76d4948884a8361fac69a43 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 14:20:04 +0100 Subject: [PATCH 09/23] Rederive the mode from saved filters, added Custom mode --- ...l_database_display_color_filter_widget.cpp | 204 +++++++++++------- ...ual_database_display_color_filter_widget.h | 9 +- 2 files changed, 130 insertions(+), 83 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index ccc65078d..2fb328bf0 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -15,6 +15,39 @@ #include #include +/** + * @brief Information about the filter mode. + */ +struct FilterModeInfo +{ + CardFilter::Type includeType; + CardFilter::Type excludeType; + CardFilter::Attr attr; +}; + +/** + * @brief Return information about the filter mode. + */ +static FilterModeInfo getFilterModeInfo(FilterMode filterMode) +{ + switch (filterMode) { + case FilterMode::Includes: + return {CardFilter::TypeOr, CardFilter::TypeEnd, CardFilter::AttrColor}; + case FilterMode::Excludes: + return {CardFilter::TypeAndNot, CardFilter::TypeEnd, CardFilter::AttrColor}; + case FilterMode::IncludeExclude: + return {CardFilter::TypeOr, CardFilter::TypeAndNot, CardFilter::AttrColor}; + case FilterMode::ExactMatch: + return {CardFilter::TypeAnd, CardFilter::TypeAndNot, CardFilter::AttrColor}; + case FilterMode::ColorIdentity: + return {CardFilter::TypeEnd, CardFilter::TypeAndNot, CardFilter::AttrColorIdentity}; + case FilterMode::Custom: + case FilterMode::End: + break; + } + return {CardFilter::TypeEnd, CardFilter::TypeEnd, CardFilter::AttrEnd}; +} + /** * This widget provides a graphical control element for the CardFilter::Attr::AttrColor filters applied to the filter * model. @@ -59,16 +92,17 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->blockSignals(true); modeComboBox->clear(); - modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch)); modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes)); modeComboBox->addItem(tr("Excludes"), QVariant::fromValue(FilterMode::Excludes)); modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude)); + modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch)); modeComboBox->addItem(tr("Color identity"), QVariant::fromValue(FilterMode::ColorIdentity)); - modeComboBox->setItemData(0, tr("Cards being exactly of the selected colors"), Qt::ToolTipRole); - modeComboBox->setItemData(1, tr("Cards containing any of the selected colors"), Qt::ToolTipRole); - modeComboBox->setItemData(2, tr("Cards containing none of the selected colors"), Qt::ToolTipRole); - modeComboBox->setItemData(3, tr("Cards containing any of the selected colors and none of the unselected colors"), Qt::ToolTipRole); + modeComboBox->setItemData(0, tr("Cards containing any of the selected colors"), Qt::ToolTipRole); + modeComboBox->setItemData(1, tr("Cards containing none of the selected colors"), Qt::ToolTipRole); + modeComboBox->setItemData(2, tr("Cards containing any of the selected colors and none of the unselected colors"), + Qt::ToolTipRole); + modeComboBox->setItemData(3, tr("Cards being exactly of the selected colors"), Qt::ToolTipRole); modeComboBox->setItemData(4, tr("Cards whose color identity fits within the selected colors"), Qt::ToolTipRole); modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); @@ -91,6 +125,7 @@ void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, boo } } +/** @brief TODO */ static void removeFilters(FilterTreeModel *filterModel, QChar color, CardFilter::Attr attr) { for (const CardFilter *filter : filterModel->getFiltersOfType(attr)) { @@ -102,59 +137,24 @@ static void removeFilters(FilterTreeModel *filterModel, QChar color, CardFilter: void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) { - switch (currentMode) { - case FilterMode::ExactMatch: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor)); - break; + FilterModeInfo info = getFilterModeInfo(currentMode); - case FilterMode::Includes: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); - break; + removeFilters(filterModel, color, info.attr); - case FilterMode::Excludes: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); - break; - - case FilterMode::IncludeExclude: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor)); - break; - - case FilterMode::ColorIdentity: - removeFilters(filterModel, color, CardFilter::Attr::AttrColorIdentity); - break; + if (info.includeType != CardFilter::TypeEnd) { + filterModel->addFilter(new CardFilter(color, info.includeType, info.attr)); } } void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) { - // Add exclusion filters if the mode demands it - switch (currentMode) { - case FilterMode::ExactMatch: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); - break; + FilterModeInfo info = getFilterModeInfo(currentMode); - case FilterMode::Includes: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - break; + removeFilters(filterModel, color, info.attr); - case FilterMode::Excludes: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - break; - - case FilterMode::IncludeExclude: - removeFilters(filterModel, color, CardFilter::Attr::AttrColor); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); - break; - - case FilterMode::ColorIdentity: - removeFilters(filterModel, color, CardFilter::Attr::AttrColorIdentity); - filterModel->addFilter(new CardFilter(color, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity)); - break; + // Add exclusion filters if the mode demands it. + if (info.excludeType != CardFilter::TypeEnd) { + filterModel->addFilter(new CardFilter(color, info.excludeType, info.attr)); } } @@ -214,42 +214,86 @@ QList VisualDatabaseDisplayColorFilterWidget::getActiveColors() void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() { - QList allColorFilters = filterModel->getFiltersOfType( - currentMode == FilterMode::ColorIdentity - ? CardFilter::Attr::AttrColorIdentity - : CardFilter::Attr::AttrColor); + // Determine the correct currentMode from the filter model by finding which modes + // are not possible due to the model containing filters that that mode cannot add. + constexpr int modeCount = static_cast(FilterMode::End); + bool modeImpossible[modeCount] = {}; - QList manaSymbolWidgets = findChildren(); + QList filters; + filters.append(filterModel->getFiltersOfType(CardFilter::AttrColor)); + filters.append(filterModel->getFiltersOfType(CardFilter::AttrColorIdentity)); - for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { - QChar symbol = manaSymbolWidget->getSymbolChar(); - bool found = currentMode == FilterMode::ColorIdentity; - for (const CardFilter *filter : allColorFilters) { - if (symbol == filter->term()) { - switch (currentMode) { - case FilterMode::ExactMatch: - if (filter->type() == CardFilter::Type::TypeAnd) - found = true; - break; - case FilterMode::Includes: - case FilterMode::IncludeExclude: - if (filter->type() == CardFilter::Type::TypeOr) - found = true; - break; - case FilterMode::Excludes: - if (filter->type() == CardFilter::Type::TypeAndNot) - found = true; - break; - case FilterMode::ColorIdentity: - if (filter->type() == CardFilter::Type::TypeAndNot) - found = false; - break; + if (filters.empty()) { + // Remove the modes that should always contain filters. + for (int i = 0; i < modeCount; ++i) { + FilterModeInfo modeInfo = getFilterModeInfo(static_cast(i)); + if (modeInfo.includeType != CardFilter::TypeEnd && modeInfo.excludeType != CardFilter::TypeEnd) { + modeImpossible[i] = true; + } + } + } else { + // Remove the modes that cannot create one of the filters. + for (const CardFilter *filter : filters) { + for (int i = 0; i < modeCount; ++i) { + FilterModeInfo modeInfo = getFilterModeInfo(static_cast(i)); + if (filter->attr() != modeInfo.attr || + (filter->type() != modeInfo.includeType && filter->type() != modeInfo.excludeType)) { + modeImpossible[i] = true; } - if (found) - break; + } + } + } + + // If the current mode is not possible with the current filters, set it to the first + // one that is. Switching to and from the custom mode is always possible. + if (modeImpossible[static_cast(currentMode)] || currentMode == FilterMode::Custom) { + for (int i = 0; i < modeCount; ++i) { + if (!modeImpossible[i] || i == static_cast(FilterMode::Custom)) { + currentMode = static_cast(i); + break; + } + } + } + + // Update the combobox. Add a Custom item only if we have a custom filter mode. + int customIndex = modeComboBox->findData(QVariant::fromValue(FilterMode::Custom)); + modeComboBox->blockSignals(true); + if (currentMode == FilterMode::Custom) { + if (customIndex < 0) { + customIndex = modeComboBox->count(); + modeComboBox->addItem(tr("Custom"), QVariant::fromValue(FilterMode::Custom)); + modeComboBox->setItemData(customIndex, tr("Cards matching an unknown combination of color filters"), + Qt::ToolTipRole); + } + modeComboBox->setCurrentIndex(customIndex); + } else { + if (customIndex >= 0) { + modeComboBox->removeItem(customIndex); + } + int index = modeComboBox->findData(QVariant::fromValue(currentMode)); + if (index >= 0) { + modeComboBox->setCurrentIndex(index); + } + } + modeComboBox->blockSignals(false); + + // Update the mana symbol widgets by looking for the current mode's include or exclude filters. + const FilterModeInfo modeInfo = getFilterModeInfo(currentMode); + + const bool useExclude = modeInfo.includeType == CardFilter::TypeEnd && modeInfo.excludeType != CardFilter::TypeEnd; + const CardFilter::Type filterType = useExclude ? modeInfo.excludeType : modeInfo.includeType; + + filters = filterModel->getFiltersOfType(modeInfo.attr); + + for (ManaSymbolWidget *manaSymbolWidget : findChildren()) { + bool found = false; + for (const CardFilter *filter : filters) { + if (filter->type() == filterType && manaSymbolWidget->getSymbolChar() == filter->term()) { + found = true; + break; } } - setManaSymbolActive(manaSymbolWidget->getSymbolChar(), found); + manaSymbolWidget->setColorActive(useExclude ? !found : found); } } diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index fa9c32204..68012d6e8 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -40,11 +40,14 @@ private: enum class FilterMode { - ExactMatch, // Only selected colors are included, all others are excluded. - Includes, // Include selected colors (OR condition). + Includes = 0, // Include selected colors (OR condition). Excludes, // Exclude selected colors (AND NOT). IncludeExclude, // Include selected colors (OR) and exclude unselected colors (AND NOT). - ColorIdentity // Keep cards whose color identity is a subset of the selected colors (stored as ANDNOT for each unselected color). + ExactMatch, // Only selected colors are included, all others are excluded. + ColorIdentity, // Keep cards whose color identity is a subset of the selected colors (stored as ANDNOT for each + // unselected color). + Custom, // The filters do not match any of the modes. Not included in the combo box. + End }; Q_DECLARE_METATYPE(FilterMode) From b086230831d5b58fa3ffccd54e74c40bdad88597 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 15:18:18 +0100 Subject: [PATCH 10/23] Added filter mode reconstructing, FilterMode::Custom, setFilterMode, and refactor --- ...l_database_display_color_filter_widget.cpp | 73 ++++++++++++------- ...ual_database_display_color_filter_widget.h | 1 + 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index 2fb328bf0..581b31ea0 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -15,42 +15,58 @@ #include #include +namespace +{ /** - * @brief Information about the filter mode. + * @brief Information about the behaviour of a filter mode. */ struct FilterModeInfo { + // When a color symbol is enabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd. CardFilter::Type includeType; + + // When a color symbol is disabled, add filters of this type. Skipped if equal to CardFilter::Type::TypeEnd. CardFilter::Type excludeType; + + // The attribute to filter for when adding the filters. CardFilter::Attr attr; }; -/** - * @brief Return information about the filter mode. - */ +/** @brief Return information about the given filter mode. */ static FilterModeInfo getFilterModeInfo(FilterMode filterMode) { switch (filterMode) { case FilterMode::Includes: - return {CardFilter::TypeOr, CardFilter::TypeEnd, CardFilter::AttrColor}; + return {CardFilter::Type::TypeOr, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor}; case FilterMode::Excludes: - return {CardFilter::TypeAndNot, CardFilter::TypeEnd, CardFilter::AttrColor}; + return {CardFilter::Type::TypeAndNot, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrColor}; case FilterMode::IncludeExclude: - return {CardFilter::TypeOr, CardFilter::TypeAndNot, CardFilter::AttrColor}; + return {CardFilter::Type::TypeOr, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor}; case FilterMode::ExactMatch: - return {CardFilter::TypeAnd, CardFilter::TypeAndNot, CardFilter::AttrColor}; + return {CardFilter::Type::TypeAnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor}; case FilterMode::ColorIdentity: - return {CardFilter::TypeEnd, CardFilter::TypeAndNot, CardFilter::AttrColorIdentity}; + return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColorIdentity}; case FilterMode::Custom: case FilterMode::End: break; } - return {CardFilter::TypeEnd, CardFilter::TypeEnd, CardFilter::AttrEnd}; + return {CardFilter::Type::TypeEnd, CardFilter::Type::TypeEnd, CardFilter::Attr::AttrEnd}; } +/** @brief Remove the filters that apply to the given color and attribute from the given model. */ +static void removeFilters(FilterTreeModel *filterModel, QChar color, CardFilter::Attr attr) +{ + for (const CardFilter *filter : filterModel->getFiltersOfType(attr)) { + if (filter->term() == color) { + filterModel->removeFilter(filter); + } + } +} +} // namespace + /** - * This widget provides a graphical control element for the CardFilter::Attr::AttrColor filters applied to the filter - * model. + * This widget provides a graphical control element for the CardFilter::Attr::AttrColor and + * CardFilter::Attr::AttrColorIdentity filters applied to the filter model. * @param parent The Qt Widget that this widget will parent to * @param _filterModel The filter model that this widget will manipulate */ @@ -106,6 +122,7 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->setItemData(4, tr("Cards whose color identity fits within the selected colors"), Qt::ToolTipRole); modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); + modeComboBox->setAccessibleName(tr("Color Filter Mode")); // Restore current mode const int index = modeComboBox->findData(QVariant::fromValue(currentMode)); @@ -116,6 +133,14 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->blockSignals(false); } +void VisualDatabaseDisplayColorFilterWidget::setFilterMode(FilterMode filterMode) +{ + const int index = modeComboBox->findData(QVariant::fromValue(filterMode)); + if (index >= 0) { + modeComboBox->setCurrentIndex(index); + } +} + void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) { if (active) { @@ -125,23 +150,13 @@ void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, boo } } -/** @brief TODO */ -static void removeFilters(FilterTreeModel *filterModel, QChar color, CardFilter::Attr attr) -{ - for (const CardFilter *filter : filterModel->getFiltersOfType(attr)) { - if (filter->term() == color) { - filterModel->removeFilter(filter); - } - } -} - void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color) { FilterModeInfo info = getFilterModeInfo(currentMode); removeFilters(filterModel, color, info.attr); - if (info.includeType != CardFilter::TypeEnd) { + if (info.includeType != CardFilter::Type::TypeEnd) { filterModel->addFilter(new CardFilter(color, info.includeType, info.attr)); } } @@ -153,7 +168,7 @@ void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color) removeFilters(filterModel, color, info.attr); // Add exclusion filters if the mode demands it. - if (info.excludeType != CardFilter::TypeEnd) { + if (info.excludeType != CardFilter::Type::TypeEnd) { filterModel->addFilter(new CardFilter(color, info.excludeType, info.attr)); } } @@ -220,14 +235,15 @@ void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() bool modeImpossible[modeCount] = {}; QList filters; - filters.append(filterModel->getFiltersOfType(CardFilter::AttrColor)); - filters.append(filterModel->getFiltersOfType(CardFilter::AttrColorIdentity)); + filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColor)); + filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColorIdentity)); if (filters.empty()) { // Remove the modes that should always contain filters. for (int i = 0; i < modeCount; ++i) { FilterModeInfo modeInfo = getFilterModeInfo(static_cast(i)); - if (modeInfo.includeType != CardFilter::TypeEnd && modeInfo.excludeType != CardFilter::TypeEnd) { + if (modeInfo.includeType != CardFilter::Type::TypeEnd && + modeInfo.excludeType != CardFilter::Type::TypeEnd) { modeImpossible[i] = true; } } @@ -280,7 +296,8 @@ void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() // Update the mana symbol widgets by looking for the current mode's include or exclude filters. const FilterModeInfo modeInfo = getFilterModeInfo(currentMode); - const bool useExclude = modeInfo.includeType == CardFilter::TypeEnd && modeInfo.excludeType != CardFilter::TypeEnd; + const bool useExclude = + modeInfo.includeType == CardFilter::Type::TypeEnd && modeInfo.excludeType != CardFilter::Type::TypeEnd; const CardFilter::Type filterType = useExclude ? modeInfo.excludeType : modeInfo.includeType; filters = filterModel->getFiltersOfType(modeInfo.attr); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index 68012d6e8..d65e5373d 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -59,6 +59,7 @@ class VisualDatabaseDisplayColorFilterWidget : public QWidget public: explicit VisualDatabaseDisplayColorFilterWidget(QWidget *parent, FilterTreeModel *filterModel); void retranslateUi(); + void setFilterMode(FilterMode filterMode); signals: void filterModeChanged(FilterMode filterMode); From fce2161d3732ebdb126cd56c85a86a0f9d26795c Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 15:18:49 +0100 Subject: [PATCH 11/23] Also clear the FilterMode of the color filter widget --- .../visual_database_display/visual_database_display_widget.cpp | 1 + 1 file changed, 1 insertion(+) 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 5a1097b0d..ee5c7c208 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 @@ -169,6 +169,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, connect(clearFilterWidget, &QToolButton::clicked, this, [this] { filterModel->blockSignals(true); filterModel->filterTree()->blockSignals(true); + colorFilterWidget->setFilterMode(FilterMode::Includes); filterModel->clear(); filterModel->blockSignals(false); filterModel->filterTree()->blockSignals(false); From e456e4d4614505b253a843569a4be9abbb0edf5c Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 16:21:58 +0100 Subject: [PATCH 12/23] Added extra cards to the test database --- tests/carddatabase/carddatabase_test.cpp | 6 +- tests/carddatabase/data/cards.xml | 76 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 22cbe9074..1381d1b48 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -52,9 +52,9 @@ TEST(CardDatabaseTest, LoadXml) // load dummy cards and test result db->loadCardDatabases(); - ASSERT_EQ(9, db->getCardList().size()) << "Wrong card count after load"; - ASSERT_EQ(5, db->getSetList().size()) << "Wrong sets count after load"; - ASSERT_EQ(3, db->query()->getAllMainCardTypes().size()) << "Wrong types count after load"; + ASSERT_EQ(13, db->getCardList().size()) << "Wrong card count after load"; + ASSERT_EQ(14, db->getSetList().size()) << "Wrong sets count after load"; + ASSERT_EQ(5, db->query()->getAllMainCardTypes().size()) << "Wrong types count after load"; ASSERT_EQ(Ok, db->getLoadStatus()) << "Wrong status after load"; // ensure the card database is empty after clear() diff --git a/tests/carddatabase/data/cards.xml b/tests/carddatabase/data/cards.xml index 2c1c09ed8..01668f57f 100644 --- a/tests/carddatabase/data/cards.xml +++ b/tests/carddatabase/data/cards.xml @@ -1,6 +1,20 @@ + + Island + 0 + ({T}: Add {U}.) + + 000 + Basic Land — Island + 0 + normal + front + U + Land + + Cat CAT @@ -10,6 +24,7 @@ 111 G 2G + G 2 Creature — Cat Creature @@ -24,6 +39,7 @@ 222 R + R 2RR 4 Creature — Dog @@ -54,6 +70,7 @@ 333 B + B B 1 Instant @@ -67,10 +84,69 @@ 444 U + U 2U 2 Instant + + Artifact + Not a Card + {T}: Add {C}. + + 555 + Artifact + 1 + normal + front + 1 + Artifact + + + + Wall of Denial + Defender, flying +Shroud (This creature can't be the target of spells or abilities.) + + Creature — Wall + WU + 3 + normal + front + WU + 1WU + 0/8 + Creature + + ARB + CMD + CMM + DDI + MM3 + PLST + 2 + + + Maelstrom Archangel + Flying +Whenever this creature deals combat damage to a player, you may cast a spell from your hand without paying its mana cost. + + Creature — Angel + WUBRG + 5 + normal + front + WUBRG + WUBRG + 5/5 + Creature + + CON + DMC + JMP + PLST + 2 + From 31944076fbb2116609919611f0a8fa7de35033ca Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 16:30:40 +0100 Subject: [PATCH 13/23] Added tests --- tests/carddatabase/filter_tree_test.cpp | 186 ++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 12 deletions(-) diff --git a/tests/carddatabase/filter_tree_test.cpp b/tests/carddatabase/filter_tree_test.cpp index 0a192126c..c6fcdfe0a 100644 --- a/tests/carddatabase/filter_tree_test.cpp +++ b/tests/carddatabase/filter_tree_test.cpp @@ -2,6 +2,7 @@ #include "libcockatrice/card/database/card_database.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/card/set/card_set.h" +#include "libcockatrice/filters/filter_tree.h" #include "test_card_database_path_provider.h" #include "gtest/gtest.h" @@ -20,7 +21,6 @@ namespace { - class FilterTreeTest : public ::testing::Test { protected: @@ -30,22 +30,185 @@ protected: new TestCardDatabasePathProvider(), new NoopCardSetPriorityController()); db->loadCardDatabases(); - cat = db->query()->getCardBySimpleName("Cat"); - notDeadAfterAll = db->query()->getCardBySimpleName("Not Dead"); + language = CardSearchLanguage{}; + + island = db->query()->getCardBySimpleName("Island"); + artifact = db->query()->getCardBySimpleName("Artifact"); truth = db->query()->getCardBySimpleName("Truth"); - doctor = db->query()->getCardBySimpleName("Doctor"); + notDead = db->query()->getCardBySimpleName("Not Dead"); + wallOfDenial = db->query()->getCardBySimpleName("Wall of Denial"); + maelstromAngel = db->query()->getCardBySimpleName("Maelstrom Archangel"); } - // void TearDown() override {} - CardData cat; - CardData notDeadAfterAll; - CardData truth; - CardData doctor; + CardSearchLanguage language; + + CardData island; // c/u + CardData artifact; // c + CardData truth; // u + CardData notDead; // b + CardData wallOfDenial; // wu + CardData maelstromAngel; // wubrg }; +} // namespace -TEST_F(FilterTreeTest, TestOne) +// ===================================================================================================================== +// acceptColor +// ===================================================================================================================== + +TEST_F(FilterTreeTest, Color) { - ASSERT_TRUE(true); + FilterTree filterTree; + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "wu"); + ASSERT_FALSE(filterTree.acceptsCard(island, language)); + ASSERT_FALSE(filterTree.acceptsCard(artifact, language)); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); + ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); +} + +TEST_F(FilterTreeTest, ColorColorless) +{ + FilterTree filterTree; + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "c"); + ASSERT_TRUE(filterTree.acceptsCard(island, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + ASSERT_FALSE(filterTree.acceptsCard(wallOfDenial, language)); + ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); +} + +TEST_F(FilterTreeTest, ColorNot) +{ + FilterTree filterTree; + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAndNot, "b"); + ASSERT_TRUE(filterTree.acceptsCard(island, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + ASSERT_TRUE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); + ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); +} + +TEST_F(FilterTreeTest, ColorAliases) +{ + FilterTree filterTree; + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "green"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "grn"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blue"); + ASSERT_TRUE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blu"); + ASSERT_TRUE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "black"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(notDead, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "blk"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(notDead, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "red"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "white"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "wht"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "colorless"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "colourless"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + filterTree.clear(); + + filterTree.termNode(CardFilter::Attr::AttrColor, CardFilter::Type::TypeAnd, "none"); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + filterTree.clear(); +} + +// ===================================================================================================================== +// acceptColorIdentity +// ===================================================================================================================== + +TEST_F(FilterTreeTest, ColorIdentity) +{ + FilterTree filterTree; + filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAnd, "wu"); + ASSERT_FALSE(filterTree.acceptsCard(island, language)); + ASSERT_FALSE(filterTree.acceptsCard(artifact, language)); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); + ASSERT_TRUE(filterTree.acceptsCard(maelstromAngel, language)); +} + +TEST_F(FilterTreeTest, ColorIdentityColorless) +{ + FilterTree filterTree; + filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAnd, "c"); + ASSERT_FALSE(filterTree.acceptsCard(island, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + ASSERT_FALSE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + ASSERT_FALSE(filterTree.acceptsCard(wallOfDenial, language)); + ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); +} + +TEST_F(FilterTreeTest, ColorIdentityNot) +{ + FilterTree filterTree; + filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "b"); + ASSERT_TRUE(filterTree.acceptsCard(island, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + ASSERT_TRUE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); + ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); +} + +TEST_F(FilterTreeTest, ColorIdentityMultiple) +{ + // Simulate a WU commander. + FilterTree filterTree; + filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "b"); + filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "r"); + filterTree.termNode(CardFilter::Attr::AttrColorIdentity, CardFilter::Type::TypeAndNot, "g"); + ASSERT_TRUE(filterTree.acceptsCard(island, language)); + ASSERT_TRUE(filterTree.acceptsCard(artifact, language)); + ASSERT_TRUE(filterTree.acceptsCard(truth, language)); + ASSERT_FALSE(filterTree.acceptsCard(notDead, language)); + ASSERT_TRUE(filterTree.acceptsCard(wallOfDenial, language)); + ASSERT_FALSE(filterTree.acceptsCard(maelstromAngel, language)); } int main(int argc, char **argv) @@ -53,4 +216,3 @@ int main(int argc, char **argv) ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } -} From b3e6ecbef76d521729518151eb4bd89fb1bdae34 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 17:06:49 +0100 Subject: [PATCH 14/23] Custom item retranslate fix --- ...l_database_display_color_filter_widget.cpp | 24 +++++++++++++++---- ...ual_database_display_color_filter_widget.h | 1 + 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index 581b31ea0..827a45bff 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -121,6 +121,10 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() modeComboBox->setItemData(3, tr("Cards being exactly of the selected colors"), Qt::ToolTipRole); modeComboBox->setItemData(4, tr("Cards whose color identity fits within the selected colors"), Qt::ToolTipRole); + if (currentMode == FilterMode::Custom) { + addCustomFilterModeItem(); + } + modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); modeComboBox->setAccessibleName(tr("Color Filter Mode")); @@ -135,12 +139,26 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() void VisualDatabaseDisplayColorFilterWidget::setFilterMode(FilterMode filterMode) { - const int index = modeComboBox->findData(QVariant::fromValue(filterMode)); + int index = modeComboBox->findData(QVariant::fromValue(filterMode)); + + if (index < 0 && filterMode == FilterMode::Custom) { + index = modeComboBox->count(); + addCustomFilterModeItem(); + } + if (index >= 0) { modeComboBox->setCurrentIndex(index); } } +void VisualDatabaseDisplayColorFilterWidget::addCustomFilterModeItem() +{ + int customIndex = modeComboBox->count(); + modeComboBox->addItem(tr("Custom"), QVariant::fromValue(FilterMode::Custom)); + modeComboBox->setItemData(customIndex, tr("Cards matching an unknown combination of color filters"), + Qt::ToolTipRole); +} + void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) { if (active) { @@ -277,9 +295,7 @@ void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() if (currentMode == FilterMode::Custom) { if (customIndex < 0) { customIndex = modeComboBox->count(); - modeComboBox->addItem(tr("Custom"), QVariant::fromValue(FilterMode::Custom)); - modeComboBox->setItemData(customIndex, tr("Cards matching an unknown combination of color filters"), - Qt::ToolTipRole); + addCustomFilterModeItem(); } modeComboBox->setCurrentIndex(customIndex); } else { diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index d65e5373d..d12a30081 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -65,6 +65,7 @@ signals: void filterModeChanged(FilterMode filterMode); private slots: + void addCustomFilterModeItem(); void handleColorToggled(QChar color, bool active); void addFilter(QChar color); void removeFilter(QChar color); From 1a46aeaaff6f5e9a9a2c2e4fad8005f752449df7 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 17:13:50 +0100 Subject: [PATCH 15/23] Revert "Added extra cards to the test database" This reverts commit e456e4d4614505b253a843569a4be9abbb0edf5c. --- tests/carddatabase/carddatabase_test.cpp | 6 +- tests/carddatabase/data/cards.xml | 76 ------------------------ 2 files changed, 3 insertions(+), 79 deletions(-) diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 1381d1b48..22cbe9074 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -52,9 +52,9 @@ TEST(CardDatabaseTest, LoadXml) // load dummy cards and test result db->loadCardDatabases(); - ASSERT_EQ(13, db->getCardList().size()) << "Wrong card count after load"; - ASSERT_EQ(14, db->getSetList().size()) << "Wrong sets count after load"; - ASSERT_EQ(5, db->query()->getAllMainCardTypes().size()) << "Wrong types count after load"; + ASSERT_EQ(9, db->getCardList().size()) << "Wrong card count after load"; + ASSERT_EQ(5, db->getSetList().size()) << "Wrong sets count after load"; + ASSERT_EQ(3, db->query()->getAllMainCardTypes().size()) << "Wrong types count after load"; ASSERT_EQ(Ok, db->getLoadStatus()) << "Wrong status after load"; // ensure the card database is empty after clear() diff --git a/tests/carddatabase/data/cards.xml b/tests/carddatabase/data/cards.xml index 01668f57f..2c1c09ed8 100644 --- a/tests/carddatabase/data/cards.xml +++ b/tests/carddatabase/data/cards.xml @@ -1,20 +1,6 @@ - - Island - 0 - ({T}: Add {U}.) - - 000 - Basic Land — Island - 0 - normal - front - U - Land - - Cat CAT @@ -24,7 +10,6 @@ 111 G 2G - G 2 Creature — Cat Creature @@ -39,7 +24,6 @@ 222 R - R 2RR 4 Creature — Dog @@ -70,7 +54,6 @@ 333 B - B B 1 Instant @@ -84,69 +67,10 @@ 444 U - U 2U 2 Instant - - Artifact - Not a Card - {T}: Add {C}. - - 555 - Artifact - 1 - normal - front - 1 - Artifact - - - - Wall of Denial - Defender, flying -Shroud (This creature can't be the target of spells or abilities.) - - Creature — Wall - WU - 3 - normal - front - WU - 1WU - 0/8 - Creature - - ARB - CMD - CMM - DDI - MM3 - PLST - 2 - - - Maelstrom Archangel - Flying -Whenever this creature deals combat damage to a player, you may cast a spell from your hand without paying its mana cost. - - Creature — Angel - WUBRG - 5 - normal - front - WUBRG - WUBRG - 5/5 - Creature - - CON - DMC - JMP - PLST - 2 - From 722d7340f19a5aaac5c46dca3d541d1e48db33dd Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 17:14:19 +0100 Subject: [PATCH 16/23] Removed database dependency --- tests/carddatabase/filter_tree_test.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/carddatabase/filter_tree_test.cpp b/tests/carddatabase/filter_tree_test.cpp index c6fcdfe0a..a30cca9de 100644 --- a/tests/carddatabase/filter_tree_test.cpp +++ b/tests/carddatabase/filter_tree_test.cpp @@ -26,18 +26,18 @@ class FilterTreeTest : public ::testing::Test protected: void SetUp() override { - CardDatabase *db = new CardDatabase(nullptr, new NoopCardPreferenceProvider(), - new TestCardDatabasePathProvider(), new NoopCardSetPriorityController()); - db->loadCardDatabases(); - language = CardSearchLanguage{}; - island = db->query()->getCardBySimpleName("Island"); - artifact = db->query()->getCardBySimpleName("Artifact"); - truth = db->query()->getCardBySimpleName("Truth"); - notDead = db->query()->getCardBySimpleName("Not Dead"); - wallOfDenial = db->query()->getCardBySimpleName("Wall of Denial"); - maelstromAngel = db->query()->getCardBySimpleName("Maelstrom Archangel"); + island = CardInfo::newInstance("Island", "", false, {{"colors", ""}, {"coloridentity", "U"}}, {}, {}, {}, {}); + artifact = + CardInfo::newInstance("Artifact", "", false, {{"colors", ""}, {"coloridentity", ""}}, {}, {}, {}, {}); + truth = CardInfo::newInstance("Truth", "", false, {{"colors", "U"}, {"coloridentity", "U"}}, {}, {}, {}, {}); + notDead = + CardInfo::newInstance("Not Dead", "", false, {{"colors", "B"}, {"coloridentity", "B"}}, {}, {}, {}, {}); + wallOfDenial = CardInfo::newInstance("Wall of Denial", "", false, {{"colors", "WU"}, {"coloridentity", "WU"}}, + {}, {}, {}, {}); + maelstromAngel = CardInfo::newInstance("Maelstrom Archangel", "", false, + {{"colors", "WUBRG"}, {"coloridentity", "WUBRG"}}, {}, {}, {}, {}); } CardSearchLanguage language; From 87edec5e04a76999edd210a5ee0abdfe51d82d6b Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 17:38:29 +0100 Subject: [PATCH 17/23] Include cleanup --- tests/carddatabase/filter_tree_test.cpp | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/tests/carddatabase/filter_tree_test.cpp b/tests/carddatabase/filter_tree_test.cpp index a30cca9de..8d3706071 100644 --- a/tests/carddatabase/filter_tree_test.cpp +++ b/tests/carddatabase/filter_tree_test.cpp @@ -1,23 +1,9 @@ -#include "libcockatrice/card/card_localization.h" -#include "libcockatrice/card/database/card_database.h" -#include "libcockatrice/card/database/card_database_querier.h" -#include "libcockatrice/card/set/card_set.h" -#include "libcockatrice/filters/filter_tree.h" -#include "test_card_database_path_provider.h" -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "libcockatrice/filters/filter_tree.h" + +#include +#include #include -#include -#include namespace { From 89985929fc5832a4b9e459158af60fc55b739956 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Mon, 2 Nov 2026 17:59:07 +0100 Subject: [PATCH 18/23] Comment update --- .../visual_database_display_color_filter_widget.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h index d12a30081..eb3c1ec3b 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -44,9 +44,9 @@ enum class FilterMode Excludes, // Exclude selected colors (AND NOT). IncludeExclude, // Include selected colors (OR) and exclude unselected colors (AND NOT). ExactMatch, // Only selected colors are included, all others are excluded. - ColorIdentity, // Keep cards whose color identity is a subset of the selected colors (stored as ANDNOT for each + ColorIdentity, // Keep cards whose color identity is a subset of the selected colors (stored as AND NOT for each // unselected color). - Custom, // The filters do not match any of the modes. Not included in the combo box. + Custom, // The color filters do not match any of the above modes. Not included in the combo box. End }; From 1288e9acb6df6b5b8938e01c9812fdf6a4560741 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Wed, 4 Nov 2026 10:22:57 +0100 Subject: [PATCH 19/23] Changed accessible name to sentence case --- .../visual_database_display_color_filter_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index 827a45bff..ab3adcc39 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -126,7 +126,7 @@ void VisualDatabaseDisplayColorFilterWidget::retranslateUi() } modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter")); - modeComboBox->setAccessibleName(tr("Color Filter Mode")); + modeComboBox->setAccessibleName(tr("Color filter mode")); // Restore current mode const int index = modeComboBox->findData(QVariant::fromValue(currentMode)); From bf7c6f93134dabdba57c07b43296393b5282e32a Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Wed, 4 Nov 2026 10:24:26 +0100 Subject: [PATCH 20/23] Removed trailing whitespace --- tests/carddatabase/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 5815ab152..c8cfbe543 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -37,7 +37,7 @@ if(WITH_ORACLE OR WITH_CLIENT) LIBS libcockatrice_filters ) - + # ------------------------ # Filter Tree Test # ------------------------ From 7c065ebae9166257149f9c9829c6b3fb85718727 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Wed, 4 Nov 2026 10:27:57 +0100 Subject: [PATCH 21/23] Don't allow toggling mana symbols in custom mode --- .../visual_database_display_color_filter_widget.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index ab3adcc39..82c3f5801 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -161,10 +161,16 @@ void VisualDatabaseDisplayColorFilterWidget::addCustomFilterModeItem() void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) { - if (active) { - addFilter(color); + if (currentMode == FilterMode::Custom) { + for (ManaSymbolWidget *manaSymbolWidget : findChildren()) { + manaSymbolWidget->setColorActive(false); + } } else { - removeFilter(color); + if (active) { + addFilter(color); + } else { + removeFilter(color); + } } } From 0bb93fa909d0642bf53be0a058fb23478fb65bba Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Wed, 4 Nov 2026 20:37:09 +0100 Subject: [PATCH 22/23] Improved checking that ExactMatch is valid by making sure all colors have a valid filter --- ...l_database_display_color_filter_widget.cpp | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index 82c3f5801..dba081de9 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -254,7 +254,8 @@ QList VisualDatabaseDisplayColorFilterWidget::getActiveColors() void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() { // Determine the correct currentMode from the filter model by finding which modes - // are not possible due to the model containing filters that that mode cannot add. + // are not possible due to the model containing (a combination of) filters that + // that mode cannot add. constexpr int modeCount = static_cast(FilterMode::End); bool modeImpossible[modeCount] = {}; @@ -262,23 +263,29 @@ void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColor)); filters.append(filterModel->getFiltersOfType(CardFilter::Attr::AttrColorIdentity)); - if (filters.empty()) { - // Remove the modes that should always contain filters. - for (int i = 0; i < modeCount; ++i) { - FilterModeInfo modeInfo = getFilterModeInfo(static_cast(i)); - if (modeInfo.includeType != CardFilter::Type::TypeEnd && - modeInfo.excludeType != CardFilter::Type::TypeEnd) { - modeImpossible[i] = true; - } - } - } else { + for (int i = 0; i < modeCount; ++i) { + FilterModeInfo modeInfo = getFilterModeInfo(static_cast(i)); + // Remove the modes that cannot create one of the filters. for (const CardFilter *filter : filters) { - for (int i = 0; i < modeCount; ++i) { - FilterModeInfo modeInfo = getFilterModeInfo(static_cast(i)); - if (filter->attr() != modeInfo.attr || - (filter->type() != modeInfo.includeType && filter->type() != modeInfo.excludeType)) { + if (filter->attr() != modeInfo.attr || + (filter->type() != modeInfo.includeType && filter->type() != modeInfo.excludeType)) { + modeImpossible[i] = true; + break; + } + } + + // Modes that have both an include and exclude filter should always have a filter for each color. + if (modeInfo.includeType != CardFilter::Type::TypeEnd && modeInfo.excludeType != CardFilter::Type::TypeEnd && + !modeImpossible[i]) { + for (const QChar &color : QString("WUBRG")) { + int count = 0; + for (const CardFilter *filter : filters) { + count += filter->term() == color; + } + if (count != 1) { modeImpossible[i] = true; + break; } } } From e540126056acaf72bd4fe09aca9fabc6332131b4 Mon Sep 17 00:00:00 2001 From: Jurriaan van den Berg Date: Wed, 4 Nov 2026 20:43:33 +0100 Subject: [PATCH 23/23] Disable mana symbols when in Custom mode --- .../visual_database_display_color_filter_widget.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp index dba081de9..de502a4f9 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -161,16 +161,10 @@ void VisualDatabaseDisplayColorFilterWidget::addCustomFilterModeItem() void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) { - if (currentMode == FilterMode::Custom) { - for (ManaSymbolWidget *manaSymbolWidget : findChildren()) { - manaSymbolWidget->setColorActive(false); - } + if (active) { + addFilter(color); } else { - if (active) { - addFilter(color); - } else { - removeFilter(color); - } + removeFilter(color); } } @@ -341,5 +335,6 @@ void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() } manaSymbolWidget->setColorActive(useExclude ? !found : found); + manaSymbolWidget->setDisabled(currentMode == FilterMode::Custom); } }