diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 642d998a0..e228efd0c 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -300,6 +300,7 @@ set(cockatrice_SOURCES
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
src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp
+ src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_widget.cpp
src/interface/widgets/visual_database_display/visual_database_filter_display_widget.cpp
src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp
diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc
index 4ec3d2385..da1e2bb46 100644
--- a/cockatrice/cockatrice.qrc
+++ b/cockatrice/cockatrice.qrc
@@ -52,6 +52,7 @@
resources/icons/swap.svg
resources/icons/sync.svg
resources/icons/tab_changed.svg
+ resources/icons/tags.svg
resources/icons/update.png
resources/icons/view.svg
diff --git a/cockatrice/resources/icons/tags.svg b/cockatrice/resources/icons/tags.svg
new file mode 100644
index 000000000..d29dc1e01
--- /dev/null
+++ b/cockatrice/resources/icons/tags.svg
@@ -0,0 +1 @@
+
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
index 5063cce14..4249559fe 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp
@@ -10,6 +10,7 @@
#include "visual_database_display_name_filter_widget.h"
#include "visual_database_display_set_filter_widget.h"
#include "visual_database_display_sub_type_filter_widget.h"
+#include "visual_database_display_tag_filter_widget.h"
#include "visual_database_display_widget.h"
#include
@@ -106,6 +107,9 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg
quickFilterFormatLegalityWidget = new SettingsButtonWidget(this);
quickFilterFormatLegalityWidget->setButtonIcon(themePixmap(QStringLiteral("icons/scale_balanced")));
+ quickFilterTagWidget = new SettingsButtonWidget(this);
+ quickFilterTagWidget->setButtonIcon(themePixmap(QStringLiteral("icons/tags")));
+
retranslateUi();
}
@@ -126,6 +130,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel);
subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel);
setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel);
+ tagFilterWidget = new VisualDatabaseDisplayTagFilterWidget(this, filterModel);
quickFilterSaveLoadWidget->addSettingsWidget(saveLoadWidget);
quickFilterNameWidget->addSettingsWidget(nameFilterWidget);
@@ -133,6 +138,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
quickFilterSubTypeWidget->addSettingsWidget(subTypeFilterWidget);
quickFilterSetWidget->addSettingsWidget(setFilterWidget);
quickFilterFormatLegalityWidget->addSettingsWidget(formatLegalityWidget);
+ quickFilterTagWidget->addSettingsWidget(tagFilterWidget);
// fill groupbox layouts
sortLayout->addWidget(sortByLabel);
@@ -145,6 +151,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
filterLayout->addWidget(quickFilterSubTypeWidget);
filterLayout->addWidget(quickFilterSetWidget);
filterLayout->addWidget(quickFilterFormatLegalityWidget);
+ filterLayout->addWidget(quickFilterTagWidget);
// put everything into main layout
addWidget(sortGroupBox);
@@ -172,6 +179,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi()
quickFilterSubTypeWidget->setToolTip(tr("Filter by card sub-type"));
quickFilterSetWidget->setToolTip(tr("Filter by set"));
quickFilterFormatLegalityWidget->setToolTip(tr("Filter by format legality"));
+ quickFilterTagWidget->setToolTip(tr("Filter by Scryfall Tagger tag"));
quickFilterSaveLoadWidget->setButtonText(tr("Save/Load"));
quickFilterNameWidget->setButtonText(tr("Name"));
@@ -179,6 +187,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi()
quickFilterSubTypeWidget->setButtonText(tr("Sub Type"));
quickFilterSetWidget->setButtonText(tr("Sets"));
quickFilterFormatLegalityWidget->setButtonText(tr("Formats"));
+ quickFilterTagWidget->setButtonText(tr("Tags"));
}
void VisualDatabaseDisplayFilterToolbarWidget::resizeEvent(QResizeEvent *event)
@@ -192,8 +201,8 @@ void VisualDatabaseDisplayFilterToolbarWidget::updateCompactMode(int availableWi
const bool compact = availableWidth < fullWidthHint;
const QList filterButtons = {
- quickFilterSaveLoadWidget, quickFilterNameWidget, quickFilterMainTypeWidget,
- quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterFormatLegalityWidget,
+ quickFilterSaveLoadWidget, quickFilterNameWidget, quickFilterMainTypeWidget, quickFilterSubTypeWidget,
+ quickFilterSetWidget, quickFilterFormatLegalityWidget, quickFilterTagWidget,
};
for (auto *btn : filterButtons) {
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h
index f97310c1b..9f36f3cd1 100644
--- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h
@@ -17,6 +17,7 @@ class VisualDatabaseDisplayMainTypeFilterWidget;
class VisualDatabaseDisplayNameFilterWidget;
class VisualDatabaseDisplaySetFilterWidget;
class VisualDatabaseDisplaySubTypeFilterWidget;
+class VisualDatabaseDisplayTagFilterWidget;
class VisualDatabaseDisplayFilterToolbarWidget : public FlowWidget
{
@@ -54,6 +55,8 @@ private:
VisualDatabaseDisplaySetFilterWidget *setFilterWidget;
SettingsButtonWidget *quickFilterFormatLegalityWidget;
VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget;
+ SettingsButtonWidget *quickFilterTagWidget;
+ VisualDatabaseDisplayTagFilterWidget *tagFilterWidget;
int fullWidthHint = 0;
void updateCompactMode(int availableWidth);
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.cpp
new file mode 100644
index 000000000..ab7bb27a4
--- /dev/null
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.cpp
@@ -0,0 +1,198 @@
+#include "visual_database_display_tag_filter_widget.h"
+
+#include "../../../filters/filter_tree_model.h"
+#include "visual_database_display_filter_button.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+VisualDatabaseDisplayTagFilterWidget::VisualDatabaseDisplayTagFilterWidget(QWidget *parent,
+ FilterTreeModel *_filterModel)
+ : QWidget(parent), filterModel(_filterModel)
+{
+ allTagsWithCount = CardDatabaseManager::query()->getAllTagsWithCount();
+
+ setMinimumWidth(300);
+ setMaximumHeight(300);
+
+ layout = new QVBoxLayout(this);
+ setLayout(layout);
+ layout->setContentsMargins(0, 1, 0, 1);
+ layout->setSpacing(1);
+ layout->setAlignment(Qt::AlignTop);
+
+ // Create search box
+ searchBox = new QLineEdit(this);
+ layout->addWidget(searchBox);
+ connect(searchBox, &QLineEdit::textChanged, this,
+ &VisualDatabaseDisplayTagFilterWidget::updateTagButtonsVisibility);
+
+ flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
+ layout->addWidget(flowWidget);
+
+ // Create a container for the threshold control
+ auto *thresholdLayout = new QHBoxLayout();
+ thresholdLayout->setContentsMargins(0, 0, 0, 0);
+
+ thresholdLabel = new QLabel(this);
+ thresholdLayout->addWidget(thresholdLabel);
+
+ // Create the spinbox
+ spinBox = new QSpinBox(this);
+ spinBox->setMinimum(1);
+ spinBox->setMaximum(getMaxTagCount());
+ spinBox->setValue(150);
+ thresholdLayout->addWidget(spinBox);
+ thresholdLayout->addStretch();
+
+ layout->addLayout(thresholdLayout);
+
+ connect(spinBox, qOverload(&QSpinBox::valueChanged), this,
+ &VisualDatabaseDisplayTagFilterWidget::updateTagButtonsVisibility);
+
+ // Create the toggle button for Exact Match/Includes mode
+ toggleButton = new QPushButton(this);
+ layout->addWidget(toggleButton);
+ connect(toggleButton, &QPushButton::clicked, this, &VisualDatabaseDisplayTagFilterWidget::updateFilterMode);
+ connect(filterModel, &FilterTreeModel::layoutChanged, this,
+ [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayTagFilterWidget::syncWithFilterModel); });
+
+ createTagButtons(); // Populate buttons initially
+ updateFilterMode(); // Initialize toggle button text
+
+ retranslateUi();
+}
+
+void VisualDatabaseDisplayTagFilterWidget::retranslateUi()
+{
+ searchBox->setPlaceholderText(tr("Search tags..."));
+ thresholdLabel->setText(tr("Show tags with at least:"));
+ spinBox->setSuffix(tr(" cards"));
+ spinBox->setToolTip(tr("Do not display tags with less than this amount of cards in the database"));
+ toggleButton->setToolTip(tr("Filter mode (require all selected tags, or any of them)"));
+}
+
+void VisualDatabaseDisplayTagFilterWidget::createTagButtons()
+{
+ // Iterate through tags and create buttons
+ for (auto it = allTagsWithCount.begin(); it != allTagsWithCount.end(); ++it) {
+ auto *button = new QPushButton(it.key(), flowWidget);
+ button->setCheckable(true);
+ button->setStyleSheet(visualDatabaseDisplayFilterButtonStyle);
+
+ flowWidget->addWidget(button);
+ tagButtons[it.key()] = button;
+
+ // Connect toggle signal for each button
+ connect(button, &QPushButton::toggled, this,
+ [this, tag = it.key()](bool checked) { handleTagToggled(tag, checked); });
+ }
+ updateTagButtonsVisibility(); // Ensure visibility is updated initially
+}
+
+void VisualDatabaseDisplayTagFilterWidget::updateTagButtonsVisibility()
+{
+ const int threshold = spinBox->value();
+ const QString filterText = searchBox->text().trimmed().toLower();
+
+ // Iterate through buttons and hide/disable those below the threshold. An
+ // active search overrides the threshold so rare tags stay reachable.
+ for (auto it = tagButtons.begin(); it != tagButtons.end(); ++it) {
+ const bool isActive = activeTags.value(it.key(), false);
+ const bool visible = isActive || (filterText.isEmpty() ? allTagsWithCount[it.key()] >= threshold
+ : it.key().toLower().contains(filterText));
+
+ it.value()->setVisible(visible);
+ it.value()->setEnabled(visible);
+ }
+}
+
+int VisualDatabaseDisplayTagFilterWidget::getMaxTagCount() const
+{
+ int maxCount = 1;
+ for (auto it = allTagsWithCount.begin(); it != allTagsWithCount.end(); ++it) {
+ maxCount = qMax(maxCount, it.value());
+ }
+ return maxCount;
+}
+
+void VisualDatabaseDisplayTagFilterWidget::handleTagToggled(const QString &tag, bool active)
+{
+ activeTags[tag] = active;
+
+ if (tagButtons.contains(tag)) {
+ tagButtons[tag]->setChecked(active);
+ }
+
+ updateTagFilter();
+}
+
+void VisualDatabaseDisplayTagFilterWidget::updateTagFilter()
+{
+ filterModel->blockSignals(true);
+ filterModel->filterTree()->blockSignals(true);
+ filterModel->clearFiltersOfType(CardFilter::Attr::AttrTag);
+
+ for (const auto &activeTag : activeTags.keys()) {
+ if (activeTags[activeTag]) {
+ QString tag = activeTag;
+ filterModel->addFilter(new CardFilter(
+ tag, exactMatchMode ? CardFilter::Type::TypeAnd : CardFilter::Type::TypeOr, CardFilter::Attr::AttrTag));
+ }
+ }
+
+ filterModel->blockSignals(false);
+ filterModel->filterTree()->blockSignals(false);
+
+ emit filterModel->filterTree()->changed();
+ emit filterModel->layoutChanged();
+}
+
+void VisualDatabaseDisplayTagFilterWidget::updateFilterMode()
+{
+ exactMatchMode = !exactMatchMode;
+ toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes"));
+ updateTagFilter();
+}
+
+void VisualDatabaseDisplayTagFilterWidget::syncWithFilterModel()
+{
+ // Temporarily block signals for each button to prevent toggling while updating button states
+ for (auto it = tagButtons.begin(); it != tagButtons.end(); ++it) {
+ it.value()->blockSignals(true);
+ }
+
+ // Uncheck all buttons
+ for (auto it = tagButtons.begin(); it != tagButtons.end(); ++it) {
+ it.value()->setChecked(false);
+ }
+
+ // Get active filters for tags
+ QSet activeTypes;
+ for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrTag)) {
+ if (filter->type() == CardFilter::Type::TypeAnd || filter->type() == CardFilter::Type::TypeOr) {
+ activeTypes.insert(filter->term());
+ }
+ }
+
+ // Check the buttons for active tags
+ for (const auto &tag : activeTypes) {
+ activeTags[tag] = true;
+ if (tagButtons.contains(tag)) {
+ tagButtons[tag]->setChecked(true);
+ }
+ }
+
+ // Re-enable signal emissions for each button
+ for (auto it = tagButtons.begin(); it != tagButtons.end(); ++it) {
+ it.value()->blockSignals(false);
+ }
+
+ // Update the visibility of buttons
+ updateTagButtonsVisibility();
+}
diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.h
new file mode 100644
index 000000000..092a10587
--- /dev/null
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.h
@@ -0,0 +1,47 @@
+#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_TAG_FILTER_WIDGET_H
+#define COCKATRICE_VISUAL_DATABASE_DISPLAY_TAG_FILTER_WIDGET_H
+
+#include "../../../filters/filter_tree_model.h"
+#include "../general/layout_containers/flow_widget.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class VisualDatabaseDisplayTagFilterWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit VisualDatabaseDisplayTagFilterWidget(QWidget *parent, FilterTreeModel *filterModel);
+ void retranslateUi();
+ void createTagButtons();
+ void updateTagButtonsVisibility();
+ [[nodiscard]] int getMaxTagCount() const;
+
+ void handleTagToggled(const QString &tag, bool active);
+ void updateTagFilter();
+ void updateFilterMode();
+ void syncWithFilterModel();
+
+private:
+ FilterTreeModel *filterModel;
+ QMap allTagsWithCount;
+
+ QVBoxLayout *layout;
+ QLineEdit *searchBox;
+ FlowWidget *flowWidget;
+ QLabel *thresholdLabel;
+ QSpinBox *spinBox;
+ QPushButton *toggleButton; // Mode switch button
+
+ QMap activeTags; // Track active filters
+ QMap tagButtons; // Store toggle buttons
+
+ bool exactMatchMode = true; // Toggle between "Exact Match" (all) and "Includes" (any)
+};
+
+#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_TAG_FILTER_WIDGET_H
diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp
index 8df0d2e20..fb8542808 100644
--- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp
+++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp
@@ -1,6 +1,7 @@
#include "card_database_querier.h"
#include "../card_info.h"
+#include "../game_specific_terms.h"
#include "../printing/exact_card.h"
#include "../set/card_set_comparator.h"
#include "card_database.h"
@@ -31,6 +32,7 @@ void CardDatabaseQuerier::invalidateCaches()
mainCardTypeCountsCache.clear();
subCardTypeCountsCache.clear();
formatsCountCache.clear();
+ tagCountsCache.clear();
}
/**
@@ -381,6 +383,21 @@ FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const
return db->formats.value(formatName.toLower());
}
+QMap CardDatabaseQuerier::getAllTagsWithCount() const
+{
+ if (tagCountsCache.isEmpty()) {
+ for (const auto &card : db->cards.values()) {
+ const QStringList tags = card->getProperty(Mtg::Tags).split(" ", Qt::SkipEmptyParts);
+
+ for (const QString &tag : tags) {
+ tagCountsCache[tag]++;
+ }
+ }
+ }
+
+ return tagCountsCache;
+}
+
QMap CardDatabaseQuerier::getAllFormatsWithCount() const
{
if (formatsCountCache.isEmpty()) {
diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h
index a1409b3cc..8508e4245 100644
--- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h
+++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h
@@ -220,6 +220,16 @@ public:
* @return Map of subtype string to count.
*/
[[nodiscard]] QMap getAllSubCardTypesWithCount() const;
+
+ /**
+ * @brief Returns a mapping of Scryfall Tagger tag slugs to their occurrence counts.
+ *
+ * Tags are stored space-separated in the `tags` card property, so a card
+ * tagged "ramp" and "removal" contributes one to each slug's count.
+ *
+ * @return Map of tag slug to count.
+ */
+ [[nodiscard]] QMap getAllTagsWithCount() const;
FormatRulesPtr getFormat(const QString &formatName) const;
QMap getAllFormatsWithCount() const;
@@ -233,6 +243,7 @@ private:
mutable QMap mainCardTypeCountsCache;
mutable QMap subCardTypeCountsCache;
mutable QMap formatsCountCache;
+ mutable QMap tagCountsCache;
private slots:
void invalidateCaches();