diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index 9b31310e6..e84bce9eb 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -311,6 +311,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 14cf15b2f..ba35ec16a 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 a59a068dd..97a4ab74e 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
@@ -78,6 +78,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();
}
@@ -102,6 +105,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);
@@ -109,6 +113,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
quickFilterSubTypeWidget->addSettingsWidget(subTypeFilterWidget);
quickFilterSetWidget->addSettingsWidget(setFilterWidget);
quickFilterFormatLegalityWidget->addSettingsWidget(formatLegalityWidget);
+ quickFilterTagWidget->addSettingsWidget(tagFilterWidget);
// fill groupbox layouts
sortLayout->addWidget(sortByLabel);
@@ -121,6 +126,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
filterLayout->addWidget(quickFilterSubTypeWidget);
filterLayout->addWidget(quickFilterSetWidget);
filterLayout->addWidget(quickFilterFormatLegalityWidget);
+ filterLayout->addWidget(quickFilterTagWidget);
// put everything into main layout
addWidget(sortGroupBox);
@@ -148,6 +154,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"));
@@ -155,6 +162,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)
@@ -168,8 +176,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 8a3555455..74cea3603 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
@@ -7,6 +7,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"
class VisualDatabaseDisplayWidget;
@@ -46,6 +47,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..eb7168b84
--- /dev/null
+++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_tag_filter_widget.cpp
@@ -0,0 +1,197 @@
+#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
+
+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 5b9c5a4b5..7455ad53c 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"
@@ -23,6 +24,7 @@ void CardDatabaseQuerier::invalidateCaches()
mainCardTypeCountsCache.clear();
subCardTypeCountsCache.clear();
formatsCountCache.clear();
+ tagCountsCache.clear();
}
/**
@@ -359,6 +361,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 f195a8170..74d540157 100644
--- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h
+++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h
@@ -214,6 +214,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;
@@ -227,6 +237,7 @@ private:
mutable QMap mainCardTypeCountsCache;
mutable QMap subCardTypeCountsCache;
mutable QMap formatsCountCache;
+ mutable QMap tagCountsCache;
private slots:
void invalidateCaches();