diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 18df9795f..4dd7c8a8a 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -43,6 +43,7 @@ set(cockatrice_SOURCES src/client/tabs/tab_room.cpp src/client/tabs/tab_server.cpp src/client/tabs/tab_supervisor.cpp + src/client/tabs/tab_visual_database_display.cpp src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/client/tapped_out_interface.cpp src/client/translate_counter_name.cpp @@ -92,6 +93,14 @@ set(cockatrice_SOURCES src/client/ui/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp src/client/ui/widgets/quick_settings/settings_button_widget.cpp src/client/ui/widgets/quick_settings/settings_popup_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp + src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.cpp src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp @@ -204,6 +213,7 @@ set(cockatrice_SOURCES src/settings/settings_manager.cpp src/settings/shortcut_treeview.cpp src/settings/shortcuts_settings.cpp + src/utility/card_info_comparator.cpp src/utility/logger.cpp src/utility/sequence_edit.cpp ) diff --git a/cockatrice/src/client/tabs/tab_supervisor.cpp b/cockatrice/src/client/tabs/tab_supervisor.cpp index e7be2a1ff..81504d76f 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.cpp +++ b/cockatrice/src/client/tabs/tab_supervisor.cpp @@ -25,6 +25,7 @@ #include "tab_replays.h" #include "tab_room.h" #include "tab_server.h" +#include "tab_visual_database_display.h" #include "visual_deck_storage/tab_deck_storage_visual.h" #include @@ -134,6 +135,9 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget * aTabVisualDeckStorage->setCheckable(true); connect(aTabVisualDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabVisualDeckStorage); + aTabVisualDatabaseDisplay = new QAction(this); + connect(aTabVisualDatabaseDisplay, &QAction::triggered, this, [this] { addVisualDatabaseDisplayTab(); }); + aTabServer = new QAction(this); aTabServer->setCheckable(true); connect(aTabServer, &QAction::triggered, this, &TabSupervisor::actTabServer); @@ -177,6 +181,7 @@ void TabSupervisor::retranslateUi() // tab menu actions aTabDeckEditor->setText(tr("Deck Editor")); aTabVisualDeckStorage->setText(tr("&Visual Deck Storage")); + aTabVisualDatabaseDisplay->setText(tr("Visual Database Display")); aTabServer->setText(tr("Server")); aTabAccount->setText(tr("Account")); aTabDeckStorage->setText(tr("Deck Storage")); @@ -372,6 +377,7 @@ void TabSupervisor::resetTabsMenu() tabsMenu->addAction(aTabDeckEditor); tabsMenu->addSeparator(); tabsMenu->addAction(aTabVisualDeckStorage); + tabsMenu->addAction(aTabVisualDatabaseDisplay); tabsMenu->addAction(aTabDeckStorage); tabsMenu->addAction(aTabReplays); } @@ -804,6 +810,14 @@ TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen) return tab; } +TabVisualDatabaseDisplay *TabSupervisor::addVisualDatabaseDisplayTab() +{ + auto *tab = new TabVisualDatabaseDisplay(this); + myAddTab(tab); + setCurrentWidget(tab); + return tab; +} + TabEdhRec *TabSupervisor::addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander) { auto *tab = new TabEdhRec(this); diff --git a/cockatrice/src/client/tabs/tab_supervisor.h b/cockatrice/src/client/tabs/tab_supervisor.h index c8ae109d8..3819a8be1 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.h +++ b/cockatrice/src/client/tabs/tab_supervisor.h @@ -5,6 +5,7 @@ #include "../../server/user/user_list_proxy.h" #include "abstract_tab_deck_editor.h" #include "api/edhrec/tab_edhrec.h" +#include "tab_visual_database_display.h" #include "visual_deck_storage/tab_deck_storage_visual.h" #include @@ -91,8 +92,8 @@ private: QList deckEditorTabs; bool isLocalGame; - QAction *aTabDeckEditor, *aTabVisualDeckStorage, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, - *aTabAdmin, *aTabLog; + QAction *aTabDeckEditor, *aTabVisualDeckStorage, *aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, + *aTabDeckStorage, *aTabReplays, *aTabAdmin, *aTabLog; int myAddTab(Tab *tab, QAction *manager = nullptr); void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager); @@ -149,6 +150,7 @@ signals: public slots: TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen); + TabVisualDatabaseDisplay *addVisualDatabaseDisplayTab(); TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); void openReplay(GameReplay *replay); void maximizeMainWindow(); diff --git a/cockatrice/src/client/tabs/tab_visual_database_display.cpp b/cockatrice/src/client/tabs/tab_visual_database_display.cpp new file mode 100644 index 000000000..d45cdded0 --- /dev/null +++ b/cockatrice/src/client/tabs/tab_visual_database_display.cpp @@ -0,0 +1,20 @@ +#include "tab_visual_database_display.h" + +#include "tab_deck_editor.h" + +TabVisualDatabaseDisplay::TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) +{ + deckEditor = new TabDeckEditor(_tabSupervisor); + deckEditor->setHidden(true); + visualDatabaseDisplayWidget = + new VisualDatabaseDisplayWidget(this, deckEditor, deckEditor->databaseDisplayDockWidget->databaseModel, + deckEditor->databaseDisplayDockWidget->databaseDisplayModel); + + setCentralWidget(visualDatabaseDisplayWidget); + + TabVisualDatabaseDisplay::retranslateUi(); +} + +void TabVisualDatabaseDisplay::retranslateUi() +{ +} diff --git a/cockatrice/src/client/tabs/tab_visual_database_display.h b/cockatrice/src/client/tabs/tab_visual_database_display.h new file mode 100644 index 000000000..55f895e5b --- /dev/null +++ b/cockatrice/src/client/tabs/tab_visual_database_display.h @@ -0,0 +1,26 @@ +#ifndef TAB_VISUAL_DATABASE_DISPLAY_H +#define TAB_VISUAL_DATABASE_DISPLAY_H + +#include "../ui/widgets/visual_database_display/visual_database_display_widget.h" +#include "tab.h" + +#include + +class TabVisualDatabaseDisplay : public Tab +{ + Q_OBJECT + +private: + TabDeckEditor *deckEditor; + VisualDatabaseDisplayWidget *visualDatabaseDisplayWidget; + +public: + TabVisualDatabaseDisplay(TabSupervisor *_tabSupervisor); + void retranslateUi() override; + QString getTabText() const override + { + return tr("Visual Database Display"); + } +}; + +#endif // TAB_VISUAL_DATABASE_DISPLAY_H diff --git a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.cpp index 0374503bc..528a97107 100644 --- a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -235,6 +235,42 @@ void DeckEditorDatabaseDisplayWidget::saveDbHeaderState() SettingsCache::instance().layouts().setDeckEditorDbHeaderState(databaseView->header()->saveState()); } +void DeckEditorDatabaseDisplayWidget::showSearchSyntaxHelp() +{ + + QFile file("theme:help/search.md"); + + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return; + } + + QTextStream in(&file); + QString text = in.readAll(); + file.close(); + + // Poor Markdown Converter + auto opts = QRegularExpression::MultilineOption; + text = text.replace(QRegularExpression("^(###)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^(##)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^(#)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^------*", opts), "
") + .replace(QRegularExpression(R"(\[([^[]+)\]\(([^\)]+)\))", opts), R"(\1)"); + + auto browser = new QTextBrowser; + browser->setParent(this, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | + Qt::WindowCloseButtonHint | Qt::WindowFullscreenButtonHint); + browser->setWindowTitle("Search Help"); + browser->setReadOnly(true); + browser->setMinimumSize({500, 600}); + + QString sheet = QString("a { text-decoration: underline; color: rgb(71,158,252) };"); + browser->document()->setDefaultStyleSheet(sheet); + + browser->setHtml(text); + connect(browser, &QTextBrowser::anchorClicked, [this](const QUrl &link) { searchEdit->setText(link.fragment()); }); + browser->show(); +} + void DeckEditorDatabaseDisplayWidget::setFilterTree(FilterTree *filterTree) { databaseDisplayModel->setFilterTree(filterTree); diff --git a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.h index 4010a2954..7d50e8359 100644 --- a/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/client/ui/widgets/deck_editor/deck_editor_database_display_widget.h @@ -22,6 +22,7 @@ public: CardDatabaseDisplayModel *databaseDisplayModel; public slots: + void showSearchSyntaxHelp(); CardInfoPtr currentCardInfo() const; void setFilterTree(FilterTree *filterTree); void clearAllDatabaseFilters(); diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp new file mode 100644 index 000000000..e7dfd6f43 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp @@ -0,0 +1,228 @@ +#include "visual_database_display_color_filter_widget.h" + +#include "../../../../game/filters/filter_tree.h" +#include "../cards/additional_info/mana_symbol_widget.h" + +#include +#include + +/** + * This widget provides a graphical control element for the CardFilter::Attr::AttrColor 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 + */ +VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(QWidget *parent, + FilterTreeModel *_filterModel) + : QWidget(parent), filterModel(_filterModel), layout(new QHBoxLayout(this)) +{ + setLayout(layout); + layout->setSpacing(5); + layout->setContentsMargins(0, 0, 0, 0); + + QString fullColorIdentity = "WUBRG"; + for (const QChar &color : fullColorIdentity) { + auto *manaSymbol = new ManaSymbolWidget(this, color, false, true); + manaSymbol->setFixedWidth(25); + + layout->addWidget(manaSymbol); + + // Initialize the activeColors map + activeColors[color] = false; + + // Connect the color toggled signal + connect(manaSymbol, &ManaSymbolWidget::colorToggled, this, + &VisualDatabaseDisplayColorFilterWidget::handleColorToggled); + } + + toggleButton = new QPushButton(this); + toggleButton->setCheckable(true); + layout->addWidget(toggleButton); + + // Connect the button's toggled signal + connect(toggleButton, &QPushButton::toggled, this, &VisualDatabaseDisplayColorFilterWidget::updateFilterMode); + connect(this, &VisualDatabaseDisplayColorFilterWidget::activeColorsChanged, this, + &VisualDatabaseDisplayColorFilterWidget::updateColorFilter); + connect(this, &VisualDatabaseDisplayColorFilterWidget::filterModeChanged, this, + &VisualDatabaseDisplayColorFilterWidget::updateColorFilter); + connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() { + if (blockSync) { + return; // Skip sync if we're blocking it + } + QTimer::singleShot(100, this, &VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel); + }); + + // Call retranslateUi to set the initial text + retranslateUi(); +} + +void VisualDatabaseDisplayColorFilterWidget::retranslateUi() +{ + switch (currentMode) { + case FilterMode::ExactMatch: + toggleButton->setText(tr("Mode: Exact Match")); + break; + case FilterMode::Includes: + toggleButton->setText(tr("Mode: Includes")); + break; + case FilterMode::IncludeExclude: + toggleButton->setText(tr("Mode: Include/Exclude")); + break; + } +} + +void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active) +{ + activeColors[color] = active; + emit activeColorsChanged(); // Notify listeners that the active colors have changed +} + +void VisualDatabaseDisplayColorFilterWidget::updateColorFilter() +{ + blockSync = true; + + // Clear previous filters + filterModel->blockSignals(true); + filterModel->filterTree()->blockSignals(true); + filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor); + + QSet selectedColors; + QSet excludedColors; + + // Collect active colors in the selected and excluded sets + for (const auto &color : activeColors.keys()) { + if (activeColors[color]) { + selectedColors.insert(color); // Include this color + } else { + excludedColors.insert(color); // Exclude this color + } + } + + switch (currentMode) { + case FilterMode::ExactMatch: + // Exact Match Mode: Only selected colors are allowed + if (!selectedColors.isEmpty()) { + // Require all selected colors (TypeAnd) + for (const auto &color : selectedColors) { + QString colorString = color; + filterModel->addFilter( + new CardFilter(colorString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor)); + } + + // Exclude all other colors + QStringList allPossibleColors = {"W", "U", "B", "R", "G"}; + for (const auto &color : allPossibleColors) { + if (!selectedColors.contains(color)) { + QString colorString = color; + filterModel->addFilter( + new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor)); + } + } + } + break; + + case FilterMode::Includes: + // Includes Mode: Just include selected colors without restrictions + for (const auto &color : selectedColors) { + QString colorString = color; + filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, + CardFilter::Attr::AttrColor)); // OR for selected colors + } + break; + + case FilterMode::IncludeExclude: + // Include/Exclude Mode: Include selected colors and exclude unselected colors + for (const auto &color : selectedColors) { + QString colorString = color; + filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, + CardFilter::Attr::AttrColor)); // OR for selected colors + } + for (const auto &color : excludedColors) { + QString colorString = color; + filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeAndNot, + CardFilter::Attr::AttrColor)); // AND NOT for excluded colors + } + break; + } + + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + emit filterModel->filterTree()->changed(); + emit filterModel->layoutChanged(); + + blockSync = false; +} + +void VisualDatabaseDisplayColorFilterWidget::updateFilterMode() +{ + blockSync = true; + + switch (currentMode) { + case FilterMode::ExactMatch: + currentMode = FilterMode::Includes; // Switch to Includes + break; + case FilterMode::Includes: + currentMode = FilterMode::IncludeExclude; // Switch to Include/Exclude + break; + case FilterMode::IncludeExclude: + currentMode = FilterMode::ExactMatch; // Switch to Exact Match + break; + } + + retranslateUi(); // Update button text based on the mode + emit filterModeChanged(currentMode); // Signal mode change + updateColorFilter(); // Reapply the filter based on the new mode + + blockSync = false; +} + +void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel() +{ + blockSync = true; + QSet currentFilters; + + // Get current filters of type color + for (const auto &filter : filterModel->getFiltersOfType(CardFilter::Attr::AttrColor)) { + if (filter->type() == CardFilter::Type::TypeAnd || filter->type() == CardFilter::Type::TypeOr) { + currentFilters.insert(filter->term()); + } + } + + QSet activeFilterList; + + // Iterate over the activeColors map and collect the active colors as strings + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { // Only add active colors + activeFilterList.insert(QString(it.key())); + } + } + + // Check if the filters in the model match the active filters + if (currentFilters == activeFilterList) { + return; + } + + // Remove filters that are in the UI but not in the model + for (const auto &color : activeFilterList) { + if (!currentFilters.contains(color)) { + activeColors[color[0]] = false; // Disable the color + } + } + + // Add filters that are in the model but not in the UI + for (const auto &color : currentFilters) { + if (!activeFilterList.contains(color)) { + activeColors[color[0]] = true; // Enable the color + } + } + + QList manaSymbolWidgets = findChildren(); + + for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) { + manaSymbolWidget->setColorActive(activeColors[manaSymbolWidget->getSymbolChar()]); + } + + updateColorFilter(); + blockSync = false; +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_color_filter_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_color_filter_widget.h new file mode 100644 index 000000000..191d00eee --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_color_filter_widget.h @@ -0,0 +1,63 @@ +#ifndef VISUAL_DATABASE_DISPLAY_COLOR_FILTER_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_COLOR_FILTER_WIDGET_H + +#include "../../../../game/filters/filter_tree_model.h" + +#include +#include +#include + +class VisualDatabaseDisplayColorFilterCircleWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDatabaseDisplayColorFilterCircleWidget(QChar color, QWidget *parent = nullptr); + void setColorActive(bool active); + bool isColorActive() const; + QChar getColorChar() const; + +signals: + void colorToggled(QChar color, bool active); + +private: + QChar colorChar; + bool isActive; + int circleDiameter; +}; + +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). +}; + +class VisualDatabaseDisplayColorFilterWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDatabaseDisplayColorFilterWidget(QWidget *parent, FilterTreeModel *filterModel); + void retranslateUi(); + +signals: + void filterModeChanged(FilterMode filterMode); + void activeColorsChanged(); + +private slots: + void handleColorToggled(QChar color, bool active); + void updateColorFilter(); + void updateFilterMode(); + void syncWithFilterModel(); + +private: + FilterTreeModel *filterModel; + QHBoxLayout *layout; + QPushButton *toggleButton; + QMap activeColors; + FilterMode currentMode = FilterMode::Includes; // Default mode + bool blockSync = false; +}; + +#endif // VISUAL_DATABASE_DISPLAY_COLOR_FILTER_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp new file mode 100644 index 000000000..1a099501e --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp @@ -0,0 +1,153 @@ +#include "visual_database_display_filter_save_load_widget.h" + +#include "../../../../game/filters/filter_tree.h" +#include "../../../../settings/cache_settings.h" +#include "visual_database_filter_display_widget.h" + +#include +#include +#include +#include + +VisualDatabaseDisplayFilterSaveLoadWidget::VisualDatabaseDisplayFilterSaveLoadWidget(QWidget *parent, + FilterTreeModel *_filterModel) + : QWidget(parent), filterModel(_filterModel) +{ + setMinimumWidth(300); + setMaximumHeight(300); + + layout = new QVBoxLayout(this); + setLayout(layout); + + // Input for filter filename + filenameInput = new QLineEdit(this); + layout->addWidget(filenameInput); + + // Save button + saveButton = new QPushButton(this); + layout->addWidget(saveButton); + connect(saveButton, &QPushButton::clicked, this, &VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter); + + // File list container + fileListWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + layout->addWidget(fileListWidget); + + refreshFilterList(); // Populate the file list on startup + retranslateUi(); +} + +void VisualDatabaseDisplayFilterSaveLoadWidget::retranslateUi() +{ + saveButton->setText(tr("Save Filter")); + filenameInput->setPlaceholderText(tr("Enter filename...")); +} + +void VisualDatabaseDisplayFilterSaveLoadWidget::saveFilter() +{ + QString filename = filenameInput->text().trimmed(); + if (filename.isEmpty()) + return; + + QString filePath = SettingsCache::instance().getFiltersPath() + QDir::separator() + filename + ".json"; + + // Serialize the filter model to JSON + QJsonArray filtersArray; + QList cardFilters = filterModel->allFilters(); + + for (const CardFilter *filter : cardFilters) { + filtersArray.append(filter->toJson()); + } + + QJsonObject root; + root["filters"] = filtersArray; + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly)) { + file.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + file.close(); + } + + filenameInput->clear(); + refreshFilterList(); // Update the file list +} + +void VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter(const QString &filename) +{ + QString filePath = SettingsCache::instance().getFiltersPath() + QDir::separator() + filename; + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) + return; + + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonDocument doc = QJsonDocument::fromJson(jsonData); + if (!doc.isObject()) + return; + + QJsonObject root = doc.object(); + if (!root.contains("filters") || !root["filters"].isArray()) + return; + + QJsonArray filtersArray = root["filters"].toArray(); + + filterModel->clear(); + + filterModel->blockSignals(true); + filterModel->filterTree()->blockSignals(true); + for (const QJsonValue &value : filtersArray) { + if (value.isObject()) { + QJsonObject filterObj = value.toObject(); + CardFilter *filter = CardFilter::fromJson(filterObj); + if (filter) { + filterModel->addFilter(filter); + } + } + } + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + emit filterModel->filterTree()->changed(); + emit filterModel->layoutChanged(); +} + +void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList() +{ + fileListWidget->clearLayout(); + // Clear existing widgets + for (auto buttonPair : fileButtons) { + buttonPair.first->deleteLater(); + buttonPair.second->deleteLater(); + } + fileButtons.clear(); // Clear the list of buttons + + // Refresh the filter file list + QDir dir(SettingsCache::instance().getFiltersPath()); + QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Time); + + // Loop through the filter files and create widgets for them + for (const QString &filename : filterFiles) { + bool alreadyAdded = false; + + // Check if the widget for this filter file already exists to avoid duplicates + for (const auto &pair : fileButtons) { + if (pair.first->text() == filename) { + alreadyAdded = true; + break; + } + } + + if (!alreadyAdded) { + // Create a new custom widget for the filter + FilterDisplayWidget *filterWidget = new FilterDisplayWidget(this, filename, filterModel); + fileListWidget->addWidget(filterWidget); + + // Connect signals to handle loading and deletion + connect(filterWidget, &FilterDisplayWidget::filterLoadRequested, this, + &VisualDatabaseDisplayFilterSaveLoadWidget::loadFilter); + connect(filterWidget, &FilterDisplayWidget::filterDeleted, this, + &VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList); + } + } +} diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.h new file mode 100644 index 000000000..52b2af2bf --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_filter_save_load_widget.h @@ -0,0 +1,43 @@ +#ifndef VISUAL_DATABASE_DISPLAY_FILTER_SAVE_LOAD_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_FILTER_SAVE_LOAD_WIDGET_H + +#include "../../../../game/filters/filter_tree_model.h" +#include "../general/layout_containers/flow_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class VisualDatabaseDisplayFilterSaveLoadWidget : public QWidget +{ + Q_OBJECT +public: + explicit VisualDatabaseDisplayFilterSaveLoadWidget(QWidget *parent, FilterTreeModel *filterModel); + + void saveFilter(); + void loadFilter(const QString &filename); + void refreshFilterList(); + void deleteFilter(const QString &filename, QPushButton *deleteButton); + +public slots: + void retranslateUi(); + +private: + FilterTreeModel *filterModel; + + QVBoxLayout *layout; + QLineEdit *filenameInput; + QPushButton *saveButton; + FlowWidget *fileListWidget; + + QMap> fileButtons; +}; + +#endif // VISUAL_DATABASE_DISPLAY_FILTER_SAVE_LOAD_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp new file mode 100644 index 000000000..688f8f4d2 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp @@ -0,0 +1,196 @@ +#include "visual_database_display_main_type_filter_widget.h" + +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../game/filters/filter_tree.h" +#include "../../../../game/filters/filter_tree_model.h" + +#include +#include +#include + +VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWidget(QWidget *parent, + FilterTreeModel *_filterModel) + : QWidget(parent), filterModel(_filterModel) +{ + allMainCardTypesWithCount = CardDatabaseManager::getInstance()->getAllMainCardTypesWithCount(); + // Get all main card types with their count + + setMaximumHeight(75); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + + layout = new QHBoxLayout(this); + setLayout(layout); + layout->setContentsMargins(0, 1, 0, 1); + layout->setSpacing(1); + layout->setAlignment(Qt::AlignTop); + + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + layout->addWidget(flowWidget); + + // Create the spinbox + spinBox = new QSpinBox(this); + spinBox->setMinimum(1); + spinBox->setMaximum(getMaxMainTypeCount()); // Set the max value dynamically + spinBox->setValue(150); + layout->addWidget(spinBox); + connect(spinBox, QOverload::of(&QSpinBox::valueChanged), this, + &VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeButtonsVisibility); + + // Create the toggle button for Exact Match/Includes mode + toggleButton = new QPushButton(this); + toggleButton->setCheckable(true); + layout->addWidget(toggleButton); + connect(toggleButton, &QPushButton::toggled, this, &VisualDatabaseDisplayMainTypeFilterWidget::updateFilterMode); + connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() { + QTimer::singleShot(100, this, &VisualDatabaseDisplayMainTypeFilterWidget::syncWithFilterModel); + }); + + createMainTypeButtons(); // Populate buttons initially + updateFilterMode(false); // Initialize toggle button text +} + +void VisualDatabaseDisplayMainTypeFilterWidget::createMainTypeButtons() +{ + // Iterate through main types and create buttons + for (auto it = allMainCardTypesWithCount.begin(); it != allMainCardTypesWithCount.end(); ++it) { + auto *button = new QPushButton(it.key(), flowWidget); + button->setCheckable(true); + button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" + "QPushButton:checked { background-color: green; color: white; }"); + + flowWidget->addWidget(button); + typeButtons[it.key()] = button; + + // Connect toggle signal + connect(button, &QPushButton::toggled, this, + [this, mainType = it.key()](bool checked) { handleMainTypeToggled(mainType, checked); }); + } + updateMainTypeButtonsVisibility(); // Ensure visibility is updated initially +} + +void VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeButtonsVisibility() +{ + int threshold = spinBox->value(); // Get the current spinbox value + + // Iterate through buttons and hide/disable those below the threshold + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + bool visible = allMainCardTypesWithCount[it.key()] >= threshold; + it.value()->setVisible(visible); + it.value()->setEnabled(visible); + } +} + +int VisualDatabaseDisplayMainTypeFilterWidget::getMaxMainTypeCount() const +{ + int maxCount = 1; + for (auto it = allMainCardTypesWithCount.begin(); it != allMainCardTypesWithCount.end(); ++it) { + maxCount = qMax(maxCount, it.value()); + } + return maxCount; +} + +void VisualDatabaseDisplayMainTypeFilterWidget::handleMainTypeToggled(const QString &mainType, bool active) +{ + activeMainTypes[mainType] = active; + + if (typeButtons.contains(mainType)) { + typeButtons[mainType]->setChecked(active); + } + + updateMainTypeFilter(); +} + +void VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeFilter() +{ + // Clear existing filters related to main type + filterModel->blockSignals(true); + filterModel->filterTree()->blockSignals(true); + filterModel->clearFiltersOfType(CardFilter::Attr::AttrMainType); + + if (exactMatchMode) { + // Exact Match: Only selected main types are allowed + QSet selectedTypes; + for (const auto &type : activeMainTypes.keys()) { + if (activeMainTypes[type]) { + selectedTypes.insert(type); + } + } + + if (!selectedTypes.isEmpty()) { + // Require all selected types (TypeAnd) + for (const auto &type : selectedTypes) { + QString typeString = type; + filterModel->addFilter( + new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrMainType)); + } + + // Exclude any other types (TypeAndNot) + for (const auto &type : typeButtons.keys()) { + if (!selectedTypes.contains(type)) { + QString typeString = type; + filterModel->addFilter( + new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrMainType)); + } + } + } + } else { + // Default Includes Mode (TypeOr) - match any selected main types + for (const auto &type : activeMainTypes.keys()) { + if (activeMainTypes[type]) { + QString typeString = type; + filterModel->addFilter( + new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrMainType)); + } + } + } + + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + emit filterModel->filterTree()->changed(); + emit filterModel->layoutChanged(); +} + +void VisualDatabaseDisplayMainTypeFilterWidget::updateFilterMode(bool checked) +{ + exactMatchMode = checked; + toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); + updateMainTypeFilter(); +} + +void VisualDatabaseDisplayMainTypeFilterWidget::syncWithFilterModel() +{ + // Temporarily block signals for each button to prevent toggling while updating button states + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + it.value()->blockSignals(true); + } + + // Uncheck all buttons + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + it.value()->setChecked(false); + } + + // Get active filters for main types + QSet activeTypes; + for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrMainType)) { + if (filter->type() == CardFilter::Type::TypeAnd) { + activeTypes.insert(filter->term()); + } + } + + // Check the buttons for active types + for (const auto &type : activeTypes) { + activeMainTypes[type] = true; + if (typeButtons.contains(type)) { + typeButtons[type]->setChecked(true); + } + } + + // Re-enable signal emissions for each button + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + it.value()->blockSignals(false); + } + + // Update the visibility of buttons + updateMainTypeButtonsVisibility(); +} diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_main_type_filter_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_main_type_filter_widget.h new file mode 100644 index 000000000..b13071825 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_main_type_filter_widget.h @@ -0,0 +1,42 @@ +#ifndef VISUAL_DATABASE_DISPLAY_MAIN_TYPE_FILTER_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_MAIN_TYPE_FILTER_WIDGET_H + +#include "../../../../game/filters/filter_tree_model.h" +#include "../general/layout_containers/flow_widget.h" + +#include +#include +#include +#include +#include +#include + +class VisualDatabaseDisplayMainTypeFilterWidget : public QWidget +{ + Q_OBJECT +public: + explicit VisualDatabaseDisplayMainTypeFilterWidget(QWidget *parent, FilterTreeModel *filterModel); + void createMainTypeButtons(); + void updateMainTypeButtonsVisibility(); + int getMaxMainTypeCount() const; + + void handleMainTypeToggled(const QString &mainType, bool active); + void updateMainTypeFilter(); + void updateFilterMode(bool checked); + void syncWithFilterModel(); + +private: + FilterTreeModel *filterModel; + QMap allMainCardTypesWithCount; + QSpinBox *spinBox; + QHBoxLayout *layout; + FlowWidget *flowWidget; + QPushButton *toggleButton; // Mode switch button + + QMap activeMainTypes; // Track active filters + QMap typeButtons; // Store toggle buttons + + bool exactMatchMode = false; // Toggle between "Exact Match" and "Includes" +}; + +#endif // VISUAL_DATABASE_DISPLAY_MAIN_TYPE_FILTER_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp new file mode 100644 index 000000000..69e782b66 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp @@ -0,0 +1,160 @@ +#include "visual_database_display_name_filter_widget.h" + +#include "../../../tabs/abstract_tab_deck_editor.h" + +#include + +VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWidget *parent, + AbstractTabDeckEditor *_deckEditor, + FilterTreeModel *_filterModel) + : QWidget(parent), deckEditor(_deckEditor), filterModel(_filterModel) +{ + setMinimumWidth(300); + setMaximumHeight(300); + + layout = new QVBoxLayout(this); + setLayout(layout); + + searchBox = new QLineEdit(this); + layout->addWidget(searchBox); + + connect(searchBox, &QLineEdit::returnPressed, this, [this]() { + QString text = searchBox->text().trimmed(); + if (!text.isEmpty() && !activeFilters.contains(text)) { + createNameFilter(text); + searchBox->clear(); + updateFilterModel(); + } + }); + + // Create container for active filters + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + layout->addWidget(flowWidget); + + loadFromDeckButton = new QPushButton(this); + layout->addWidget(loadFromDeckButton); + + connect(loadFromDeckButton, &QPushButton::clicked, this, &VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck); + connect(filterModel, &FilterTreeModel::layoutChanged, this, + [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); }); + + retranslateUi(); +} + +void VisualDatabaseDisplayNameFilterWidget::retranslateUi() +{ + searchBox->setPlaceholderText(tr("Filter by name...")); + loadFromDeckButton->setText(tr("Load from Deck")); +} + +void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck() +{ + DeckListModel *deckListModel = deckEditor->deckDockWidget->deckModel; + + if (!deckListModel) + return; + DeckList *decklist = deckListModel->getDeckList(); + if (!decklist) + return; + InnerDecklistNode *listRoot = decklist->getRoot(); + if (!listRoot) + return; + + for (int i = 0; i < listRoot->size(); i++) { + InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); + if (!currentZone) + continue; + for (int j = 0; j < currentZone->size(); j++) { + DecklistCardNode *currentCard = dynamic_cast(currentZone->at(j)); + if (!currentCard) + continue; + createNameFilter(currentCard->getName()); + } + } + updateFilterModel(); +} + +void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name) +{ + if (activeFilters.contains(name)) + return; + + // Create a button for the filter + auto *button = new QPushButton(name, flowWidget); + button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" + "QPushButton:hover { background-color: red; color: white; }"); + + connect(button, &QPushButton::clicked, this, [this, name]() { removeNameFilter(name); }); + + flowWidget->addWidget(button); + activeFilters[name] = button; +} + +void VisualDatabaseDisplayNameFilterWidget::removeNameFilter(const QString &name) +{ + if (activeFilters.contains(name)) { + activeFilters[name]->deleteLater(); // Safe deletion + activeFilters.remove(name); + + QTimer::singleShot(0, this, + &VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification + } +} + +void VisualDatabaseDisplayNameFilterWidget::updateFilterModel() +{ + // Clear existing name filters + emit filterModel->layoutAboutToBeChanged(); + filterModel->clearFiltersOfType(CardFilter::Attr::AttrName); + + filterModel->blockSignals(true); + filterModel->filterTree()->blockSignals(true); + + for (const auto &name : activeFilters.keys()) { + QString nameString = name; + filterModel->addFilter(new CardFilter(nameString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrName)); + } + + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + emit filterModel->filterTree()->changed(); + emit filterModel->layoutChanged(); +} + +void VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel() +{ + QStringList currentFilters; + for (const auto &filter : filterModel->getFiltersOfType(CardFilter::Attr::AttrName)) { + if (filter->type() == CardFilter::Type::TypeOr) { + currentFilters.append(filter->term()); + } + } + + QStringList activeFilterList = activeFilters.keys(); + + // Sort lists for efficient comparison + std::sort(currentFilters.begin(), currentFilters.end()); + std::sort(activeFilterList.begin(), activeFilterList.end()); + + if (currentFilters == activeFilterList) { + return; + } + + // Remove filters that are in the UI but not in the model + for (const auto &name : activeFilterList) { + if (!currentFilters.contains(name)) { + removeNameFilter(name); + } + } + + // Add filters that are in the model but not in the UI + for (const auto &name : currentFilters) { + if (!activeFilters.contains(name)) { + createNameFilter(name); + } + } +} diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_name_filter_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_name_filter_widget.h new file mode 100644 index 000000000..91ad56160 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_name_filter_widget.h @@ -0,0 +1,45 @@ +#ifndef VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H + +#include "../../../../game/filters/filter_tree_model.h" +#include "../../../tabs/abstract_tab_deck_editor.h" +#include "../general/layout_containers/flow_widget.h" + +#include +#include +#include +#include +#include + +class VisualDatabaseDisplayNameFilterWidget : public QWidget +{ + Q_OBJECT +public: + explicit VisualDatabaseDisplayNameFilterWidget(QWidget *parent, + AbstractTabDeckEditor *deckEditor, + FilterTreeModel *filterModel); + + void createNameFilter(const QString &name); + void removeNameFilter(const QString &name); + void updateFilterList(); + void updateFilterModel(); + void syncWithFilterModel(); + +public slots: + void retranslateUi(); + +private: + AbstractTabDeckEditor *deckEditor; + FilterTreeModel *filterModel; + QVBoxLayout *layout; + QLineEdit *searchBox; + FlowWidget *flowWidget; + QPushButton *loadFromDeckButton; + + QMap activeFilters; // Store active name filter buttons + +private slots: + void actLoadFromDeck(); +}; + +#endif // VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp new file mode 100644 index 000000000..795c83d68 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp @@ -0,0 +1,195 @@ +#include "visual_database_display_set_filter_widget.h" + +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../game/filters/filter_tree.h" +#include "../../../../game/filters/filter_tree_model.h" + +#include +#include +#include +#include + +VisualDatabaseDisplaySetFilterWidget::VisualDatabaseDisplaySetFilterWidget(QWidget *parent, + FilterTreeModel *_filterModel) + : QWidget(parent), filterModel(_filterModel) +{ + setMinimumWidth(300); + setMaximumHeight(300); + + layout = new QVBoxLayout(this); + setLayout(layout); + + searchBox = new QLineEdit(this); + searchBox->setPlaceholderText(tr("Search sets...")); + layout->addWidget(searchBox); + connect(searchBox, &QLineEdit::textChanged, this, + &VisualDatabaseDisplaySetFilterWidget::updateSetButtonsVisibility); + + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + layout->addWidget(flowWidget); + + // Create the toggle button for Exact Match/Includes mode + toggleButton = new QPushButton(this); + toggleButton->setCheckable(true); + layout->addWidget(toggleButton); + connect(toggleButton, &QPushButton::toggled, this, &VisualDatabaseDisplaySetFilterWidget::updateFilterMode); + connect(filterModel, &FilterTreeModel::layoutChanged, this, + [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplaySetFilterWidget::syncWithFilterModel); }); + + createSetButtons(); // Populate buttons initially + updateFilterMode(false); // Initialize toggle button text +} + +void VisualDatabaseDisplaySetFilterWidget::createSetButtons() +{ + SetList shared_pointerses = CardDatabaseManager::getInstance()->getSetList(); + + // Sort by release date + std::sort(shared_pointerses.begin(), shared_pointerses.end(), + [](const auto &a, const auto &b) { return a->getReleaseDate() > b->getReleaseDate(); }); + + int setsToPreactivate = 10; + int setsActivated = 0; + + for (const auto &shared_pointer : shared_pointerses) { + QString shortName = shared_pointer->getShortName(); + QString longName = shared_pointer->getLongName(); + + auto *button = new QPushButton(longName + " (" + shortName + ")", flowWidget); + button->setCheckable(true); + button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" + "QPushButton:checked { background-color: green; color: white; }"); + + flowWidget->addWidget(button); + setButtons[shortName] = button; + + // Connect toggle signal + connect(button, &QPushButton::toggled, this, + [this, shortName](bool checked) { handleSetToggled(shortName, checked); }); + if (setsActivated < setsToPreactivate) { + setsActivated++; + activeSets[shortName] = true; + button->setChecked(true); + } + } + updateSetFilter(); + updateSetButtonsVisibility(); // Ensure visibility is updated initially +} + +void VisualDatabaseDisplaySetFilterWidget::updateSetButtonsVisibility() +{ + QString filterText = searchBox->text().trimmed().toLower(); + + for (const QString &setName : setButtons.keys()) { + QPushButton *buttonForSet = setButtons[setName]; + QString shortName = setName.toLower(); + QString longName = buttonForSet->text().toLower(); + bool alwaysVisible = activeSets.contains(setName) && activeSets[setName]; + bool visible = + alwaysVisible || filterText.isEmpty() || shortName.contains(filterText) || longName.contains(filterText); + buttonForSet->setVisible(visible); + } +} + +void VisualDatabaseDisplaySetFilterWidget::handleSetToggled(const QString &setShortName, bool active) +{ + activeSets[setShortName] = active; + + if (setButtons.contains(setShortName)) { + setButtons[setShortName]->setChecked(active); + } + + updateSetFilter(); +} + +void VisualDatabaseDisplaySetFilterWidget::updateSetFilter() +{ + // Clear existing filters related to sets + filterModel->blockSignals(true); + filterModel->filterTree()->blockSignals(true); + filterModel->clearFiltersOfType(CardFilter::Attr::AttrSet); + + if (exactMatchMode) { + // Exact Match: Only selected sets are allowed + QSet selectedSets; + for (const auto &set : activeSets.keys()) { + if (activeSets[set]) { + selectedSets.insert(set); + } + } + + if (!selectedSets.isEmpty()) { + // Require all selected sets (TypeAnd) + for (const auto &set : selectedSets) { + QString setString = set; + filterModel->addFilter(new CardFilter(setString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrSet)); + } + + // Exclude any other sets (TypeAndNot) + for (const auto &set : setButtons.keys()) { + if (!selectedSets.contains(set)) { + QString setString = set; + filterModel->addFilter( + new CardFilter(setString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrSet)); + } + } + } + } else { + // Default Includes Mode (TypeOr) - match any selected sets + for (const auto &set : activeSets.keys()) { + if (activeSets[set]) { + QString setString = set; + filterModel->addFilter(new CardFilter(setString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrSet)); + } + } + } + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + emit filterModel->filterTree()->changed(); + emit filterModel->layoutChanged(); +} + +void VisualDatabaseDisplaySetFilterWidget::syncWithFilterModel() +{ + // Clear activeSets + activeSets.clear(); + + // Read the current filters in filterModel + auto currentFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrSet); + + // Determine if we're in Exact Match mode or Includes mode + QSet selectedSets; + QSet excludedSets; + for (const auto &filter : currentFilters) { + if (filter->type() == CardFilter::Type::TypeAnd) { + selectedSets.insert(filter->term()); + } else if (filter->type() == CardFilter::Type::TypeAndNot) { + excludedSets.insert(filter->term()); + } else if (filter->type() == CardFilter::Type::TypeOr) { + selectedSets.insert(filter->term()); + } + } + + // Determine exact match mode based on filter structure + bool newExactMatchMode = !excludedSets.isEmpty(); + + if (newExactMatchMode != exactMatchMode) { + exactMatchMode = newExactMatchMode; + toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); + } + + // Sync button states with selected sets + for (const auto &key : setButtons.keys()) { + bool active = selectedSets.contains(key); + activeSets[key] = active; + setButtons[key]->setChecked(active); + } +} + +void VisualDatabaseDisplaySetFilterWidget::updateFilterMode(bool checked) +{ + exactMatchMode = checked; + toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); + updateSetFilter(); +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.h new file mode 100644 index 000000000..41da57ef8 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_set_filter_widget.h @@ -0,0 +1,43 @@ +#ifndef VISUAL_DATABASE_DISPLAY_SET_FILTER_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_SET_FILTER_WIDGET_H + +#include "../../../../game/filters/filter_tree_model.h" +#include "../general/layout_containers/flow_widget.h" + +#include +#include +#include +#include +#include +#include + +class VisualDatabaseDisplaySetFilterWidget : public QWidget +{ + Q_OBJECT +public: + explicit VisualDatabaseDisplaySetFilterWidget(QWidget *parent, FilterTreeModel *filterModel); + void createSetButtons(); + void updateSetButtonsVisibility(); + void handleSetToggled(const QString &setShortName, bool active); + + void updateSetFilter(); + void syncWithFilterModel(); + void updateFilterMode(bool checked); + +private: + FilterTreeModel *filterModel; + QMap allMainCardTypesWithCount; + QVBoxLayout *layout; + QLineEdit *searchBox; + FlowWidget *flowWidget; + QPushButton *toggleButton; // Mode switch button + + QMap activeMainTypes; // Track active filters + QMap typeButtons; // Store toggle buttons + QMap setButtons; // Store set filter buttons + QMap activeSets; // Track active set filters + + bool exactMatchMode = false; // Toggle between "Exact Match" and "Includes" +}; + +#endif // VISUAL_DATABASE_DISPLAY_SET_FILTER_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp new file mode 100644 index 000000000..4b8fc21d8 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp @@ -0,0 +1,203 @@ +#include "visual_database_display_sub_type_filter_widget.h" + +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../game/filters/filter_tree.h" +#include "../../../../game/filters/filter_tree_model.h" + +#include +#include +#include +#include + +VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidget(QWidget *parent, + FilterTreeModel *_filterModel) + : QWidget(parent), filterModel(_filterModel) +{ + allSubCardTypesWithCount = CardDatabaseManager::getInstance()->getAllSubCardTypesWithCount(); + + setMinimumWidth(300); + + layout = new QVBoxLayout(this); + setLayout(layout); + + // Create and setup the spinbox + spinBox = new QSpinBox(this); + spinBox->setMinimum(1); + spinBox->setMaximum(getMaxSubTypeCount()); + spinBox->setValue(150); + layout->addWidget(spinBox); + connect(spinBox, QOverload::of(&QSpinBox::valueChanged), this, + &VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility); + + // Create search box + searchBox = new QLineEdit(this); + searchBox->setPlaceholderText(tr("Search subtypes...")); + layout->addWidget(searchBox); + connect(searchBox, &QLineEdit::textChanged, this, + &VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility); + + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + flowWidget->setMaximumHeight(300); + layout->addWidget(flowWidget); + + // Toggle button setup (Exact Match / Includes mode) + toggleButton = new QPushButton(this); + toggleButton->setCheckable(true); + layout->addWidget(toggleButton); + connect(toggleButton, &QPushButton::toggled, this, &VisualDatabaseDisplaySubTypeFilterWidget::updateFilterMode); + connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() { + QTimer::singleShot(100, this, &VisualDatabaseDisplaySubTypeFilterWidget::syncWithFilterModel); + }); + + createSubTypeButtons(); // Populate buttons initially + updateFilterMode(false); // Initialize the toggle button text +} + +void VisualDatabaseDisplaySubTypeFilterWidget::createSubTypeButtons() +{ + // Iterate through sub types and create buttons + for (auto it = allSubCardTypesWithCount.begin(); it != allSubCardTypesWithCount.end(); ++it) { + auto *button = new QPushButton(it.key(), flowWidget); + button->setCheckable(true); + button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" + "QPushButton:checked { background-color: green; color: white; }"); + + flowWidget->addWidget(button); + typeButtons[it.key()] = button; + + // Connect toggle signal for each button + connect(button, &QPushButton::toggled, this, + [this, subType = it.key()](bool checked) { handleSubTypeToggled(subType, checked); }); + } + updateSubTypeButtonsVisibility(); // Ensure visibility is updated initially +} + +void VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility() +{ + int threshold = spinBox->value(); + QString filterText = searchBox->text().trimmed().toLower(); + + // Iterate through buttons and hide/disable those below the threshold + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + QString subType = it.key().toLower(); + bool isActive = activeSubTypes.value(it.key(), false); + bool visible = isActive || (allSubCardTypesWithCount[it.key()] >= threshold && + (filterText.isEmpty() || subType.contains(filterText))); + + it.value()->setVisible(visible); + it.value()->setEnabled(visible); + } +} + +int VisualDatabaseDisplaySubTypeFilterWidget::getMaxSubTypeCount() const +{ + int maxCount = 1; + for (auto it = allSubCardTypesWithCount.begin(); it != allSubCardTypesWithCount.end(); ++it) { + maxCount = qMax(maxCount, it.value()); + } + return maxCount; +} + +void VisualDatabaseDisplaySubTypeFilterWidget::handleSubTypeToggled(const QString &subType, bool active) +{ + activeSubTypes[subType] = active; + + if (typeButtons.contains(subType)) { + typeButtons[subType]->setChecked(active); + } + + updateSubTypeFilter(); +} + +void VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeFilter() +{ + filterModel->blockSignals(true); + // Clear existing filters related to sub types + filterModel->clearFiltersOfType(CardFilter::Attr::AttrSubType); + + if (exactMatchMode) { + // Exact Match: Only selected sub types are allowed + QSet selectedTypes; + for (const auto &type : activeSubTypes.keys()) { + if (activeSubTypes[type]) { + selectedTypes.insert(type); + } + } + + if (!selectedTypes.isEmpty()) { + // Require all selected subtypes (TypeAnd) + for (const auto &type : selectedTypes) { + QString typeString = type; + filterModel->addFilter( + new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrSubType)); + } + + // Exclude any other types (TypeAndNot) + for (const auto &type : typeButtons.keys()) { + if (!selectedTypes.contains(type)) { + QString typeString = type; + filterModel->addFilter( + new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrSubType)); + } + } + } + } else { + // Default Includes Mode (TypeOr) - match any selected subtypes + for (const auto &type : activeSubTypes.keys()) { + if (activeSubTypes[type]) { + QString typeString = type; + filterModel->addFilter( + new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrSubType)); + } + } + } + filterModel->blockSignals(false); + filterModel->filterTree()->blockSignals(false); + + emit filterModel->filterTree()->changed(); + emit filterModel->layoutChanged(); +} + +void VisualDatabaseDisplaySubTypeFilterWidget::updateFilterMode(bool checked) +{ + exactMatchMode = checked; + toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes")); + updateSubTypeFilter(); +} + +void VisualDatabaseDisplaySubTypeFilterWidget::syncWithFilterModel() +{ + // Temporarily block signals for each button to prevent toggling while updating button states + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + it.value()->blockSignals(true); + } + + // Uncheck all buttons + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + it.value()->setChecked(false); + } + + // Get active filters for sub types + QSet activeTypes; + for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrSubType)) { + if (filter->type() == CardFilter::Type::TypeAnd) { + activeTypes.insert(filter->term()); + } + } + + // Check the buttons for active types + for (const auto &type : activeTypes) { + activeSubTypes[type] = true; + if (typeButtons.contains(type)) { + typeButtons[type]->setChecked(true); + } + } + + // Re-enable signal emissions for each button + for (auto it = typeButtons.begin(); it != typeButtons.end(); ++it) { + it.value()->blockSignals(false); + } + + // Update the visibility of buttons + updateSubTypeButtonsVisibility(); +} diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.h new file mode 100644 index 000000000..72a0a25bb --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.h @@ -0,0 +1,43 @@ +#ifndef VISUAL_DATABASE_DISPLAY_SUB_TYPE_FILTER_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_SUB_TYPE_FILTER_WIDGET_H + +#include "../../../../game/filters/filter_tree_model.h" +#include "../general/layout_containers/flow_widget.h" + +#include +#include +#include +#include +#include +#include + +class VisualDatabaseDisplaySubTypeFilterWidget : public QWidget +{ + Q_OBJECT +public: + explicit VisualDatabaseDisplaySubTypeFilterWidget(QWidget *parent, FilterTreeModel *filterModel); + void createSubTypeButtons(); + void updateSubTypeButtonsVisibility(); + int getMaxSubTypeCount() const; + + void handleSubTypeToggled(const QString &mainType, bool active); + void updateSubTypeFilter(); + void updateFilterMode(bool checked); + void syncWithFilterModel(); + +private: + FilterTreeModel *filterModel; + QMap allSubCardTypesWithCount; + QSpinBox *spinBox; + QVBoxLayout *layout; + QLineEdit *searchBox; + FlowWidget *flowWidget; + QPushButton *toggleButton; // Mode switch button + + QMap activeSubTypes; // Track active filters + QMap typeButtons; // Store toggle buttons + + bool exactMatchMode = false; // Toggle between "Exact Match" and "Includes" +}; + +#endif // VISUAL_DATABASE_DISPLAY_SUB_TYPE_FILTER_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp new file mode 100644 index 000000000..f808d7034 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.cpp @@ -0,0 +1,291 @@ +#include "visual_database_display_widget.h" + +#include "../../../../deck/custom_line_edit.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../game/filters/filter_tree_model.h" +#include "../../../../settings/cache_settings.h" +#include "../../../../utility/card_info_comparator.h" +#include "../../pixel_map_generator.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../quick_settings/settings_button_widget.h" +#include "visual_database_display_color_filter_widget.h" +#include "visual_database_display_filter_save_load_widget.h" +#include "visual_database_display_main_type_filter_widget.h" +#include "visual_database_display_name_filter_widget.h" +#include "visual_database_display_set_filter_widget.h" +#include "visual_database_display_sub_type_filter_widget.h" + +#include +#include +#include +#include + +VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, + AbstractTabDeckEditor *_deckEditor, + CardDatabaseModel *database_model, + CardDatabaseDisplayModel *database_display_model) + : QWidget(parent), deckEditor(_deckEditor), databaseModel(database_model), + databaseDisplayModel(database_display_model) +{ + cards = new QList; + connect(databaseDisplayModel, &CardDatabaseDisplayModel::modelDirty, this, + &VisualDatabaseDisplayWidget::modelDirty); + + // Set up main layout and widgets + setMinimumSize(0, 0); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + mainLayout = new QVBoxLayout(this); + setLayout(mainLayout); + mainLayout->setSpacing(1); + mainLayout->setContentsMargins(0, 0, 0, 0); + + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarPolicy::ScrollBarAsNeeded); + cardSizeWidget = new CardSizeWidget(this, flowWidget); + + searchContainer = new QWidget(this); + searchLayout = new QHBoxLayout(searchContainer); + searchContainer->setLayout(searchLayout); + + searchEdit = new SearchLineEdit(); + searchEdit->setObjectName("searchEdit"); + searchEdit->setPlaceholderText(tr("Search by card name (or search expressions)")); + searchEdit->setClearButtonEnabled(true); + searchEdit->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); + searchEdit->installEventFilter(&searchKeySignals); + + setFocusProxy(searchEdit); + setFocusPolicy(Qt::ClickFocus); + + filterModel = new FilterTreeModel(); + filterModel->setObjectName("filterModel"); + databaseDisplayModel->setFilterTree(filterModel->filterTree()); + + connect(filterModel, &FilterTreeModel::layoutChanged, this, &VisualDatabaseDisplayWidget::searchModelChanged); + + searchKeySignals.setObjectName("searchKeySignals"); + connect(searchEdit, &QLineEdit::textChanged, this, &VisualDatabaseDisplayWidget::updateSearch); + /*connect(&searchKeySignals, SIGNAL(onEnter()), this, SLOT(actAddCard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltEqual()), this, SLOT(actAddCard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltRBracket()), this, SLOT(actAddCardToSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltMinus()), this, SLOT(actDecrementCard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltLBracket()), this, SLOT(actDecrementCardFromSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlAltEnter()), this, SLOT(actAddCardToSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlEnter()), this, SLOT(actAddCardToSideboard())); + connect(&searchKeySignals, SIGNAL(onCtrlC()), this, SLOT(copyDatabaseCellContents()));*/ + connect(help, &QAction::triggered, deckEditor->databaseDisplayDockWidget, + &DeckEditorDatabaseDisplayWidget::showSearchSyntaxHelp); + + databaseView = new QTreeView(this); + databaseView->setObjectName("databaseView"); + databaseView->setFocusProxy(searchEdit); + databaseView->setRootIsDecorated(false); + databaseView->setItemDelegate(nullptr); + databaseView->setSortingEnabled(true); + databaseView->sortByColumn(0, Qt::AscendingOrder); + databaseView->setModel(databaseDisplayModel); + databaseView->setVisible(false); + + searchEdit->setTreeView(databaseView); + + colorFilterWidget = new VisualDatabaseDisplayColorFilterWidget(this, filterModel); + + quickFilterWidget = new SettingsButtonWidget(this); + + saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel); + nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, deckEditor, filterModel); + mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel); + subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel); + setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel); + + quickFilterWidget->addSettingsWidget(saveLoadWidget); + quickFilterWidget->addSettingsWidget(nameFilterWidget); + // quickFilterWidget->addSettingsWidget(mainTypeFilterWidget); + quickFilterWidget->addSettingsWidget(subTypeFilterWidget); + quickFilterWidget->addSettingsWidget(setFilterWidget); + + searchLayout->addWidget(colorFilterWidget); + searchLayout->addWidget(quickFilterWidget); + searchLayout->addWidget(searchEdit); + + mainLayout->addWidget(searchContainer); + + mainLayout->addWidget(mainTypeFilterWidget); + + mainLayout->addWidget(flowWidget); + + mainLayout->addWidget(cardSizeWidget); + + debounceTimer = new QTimer(this); + debounceTimer->setSingleShot(true); // Ensure it only fires once after the timeout + + connect(debounceTimer, &QTimer::timeout, this, &VisualDatabaseDisplayWidget::searchModelChanged); + + loadCardsTimer = new QTimer(this); + loadCardsTimer->setSingleShot(true); // Ensure it only fires once after the timeout + + connect(loadCardsTimer, &QTimer::timeout, this, [this]() { loadCurrentPage(); }); + loadCardsTimer->start(5000); +} + +void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + loadCurrentPage(); +} + +void VisualDatabaseDisplayWidget::onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance) +{ + emit cardClickedDatabaseDisplay(event, instance); +} + +void VisualDatabaseDisplayWidget::onHover(const CardInfoPtr &hoveredCard) +{ + emit cardHoveredDatabaseDisplay(hoveredCard); +} + +void VisualDatabaseDisplayWidget::addCard(const CardInfoPtr &cardToAdd) +{ + cards->append(cardToAdd); + auto *display = new CardInfoPictureWithTextOverlayWidget(flowWidget, false); + display->setScaleFactor(cardSizeWidget->getSlider()->value()); + display->setCard(cardToAdd); + flowWidget->addWidget(display); + connect(display, &CardInfoPictureWithTextOverlayWidget::imageClicked, this, &VisualDatabaseDisplayWidget::onClick); + connect(display, &CardInfoPictureWithTextOverlayWidget::hoveredOnCard, this, &VisualDatabaseDisplayWidget::onHover); + connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor); +} + +void VisualDatabaseDisplayWidget::populateCards() +{ + int rowCount = databaseDisplayModel->rowCount(); + cards->clear(); + + // Calculate the start and end indices for the current page + int start = currentPage * cardsPerPage; + int end = qMin(start + cardsPerPage, rowCount); + + qCDebug(VisualDatabaseDisplayLog) << "Fetching from " << start << " to " << end << " cards"; + // Load more cards if we are at the end of the current list and can fetch more + if (end >= rowCount && databaseDisplayModel->canFetchMore(QModelIndex())) { + qCDebug(VisualDatabaseDisplayLog) << "We gotta load more"; + databaseDisplayModel->fetchMore(QModelIndex()); + } + + for (int row = start; row < end; ++row) { + qCDebug(VisualDatabaseDisplayLog) << "Adding " << row; + QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn); + QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole); + qCDebug(VisualDatabaseDisplayLog) << name.toString(); + if (CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(name.toString())) { + addCard(info); + } else { + qCDebug(VisualDatabaseDisplayLog) << "Card not found in database!"; + } + } + currentPage++; +} + +void VisualDatabaseDisplayWidget::updateSearch(const QString &search) const +{ + databaseDisplayModel->setStringFilter(search); + QModelIndexList sel = databaseView->selectionModel()->selectedRows(); + if (sel.isEmpty() && databaseDisplayModel->rowCount()) + databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); +} + +void VisualDatabaseDisplayWidget::searchModelChanged() +{ + // Clear the current page and prepare for new data + flowWidget->clearLayout(); // Clear existing cards + cards->clear(); // Clear the card list + // Reset scrollbar position to the top after loading new cards + if (QScrollBar *scrollBar = flowWidget->scrollArea->verticalScrollBar()) { + scrollBar->setValue(0); // Reset scrollbar to top + } + + currentPage = 0; + loadCurrentPage(); + qCDebug(VisualDatabaseDisplayLog) << "Search model changed"; +} + +void VisualDatabaseDisplayWidget::loadNextPage() +{ + // Calculate the start and end indices for the next page + int rowCount = databaseDisplayModel->rowCount(); + int start = currentPage * cardsPerPage; + int end = qMin(start + cardsPerPage, rowCount); + + // Load more cards if we are at the end of the current list and can fetch more + if (end >= rowCount && databaseDisplayModel->canFetchMore(QModelIndex())) { + databaseDisplayModel->fetchMore(QModelIndex()); + } + + // Load the next page of cards and add them to the flow widget + for (int row = start; row < end; ++row) { + QModelIndex index = databaseDisplayModel->index(row, CardDatabaseModel::NameColumn); + QVariant name = databaseDisplayModel->data(index, Qt::DisplayRole); + if (CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(name.toString())) { + addCard(info); + } else { + qCDebug(VisualDatabaseDisplayLog) << "Card " << name.toString() << " not found in database!"; + } + } + + // Update the current page + currentPage++; +} + +void VisualDatabaseDisplayWidget::loadCurrentPage() +{ + // Ensure only the initial page is loaded + if (currentPage == 0) { + // Only load the first page initially + qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; + populateCards(); + } else { + // If not the first page, just load the next page and append to the flow widget + loadNextPage(); + } +} + +void VisualDatabaseDisplayWidget::modelDirty() const +{ + debounceTimer->start(debounceTime); +} + +void VisualDatabaseDisplayWidget::sortCardList(const QStringList &properties, + Qt::SortOrder order = Qt::AscendingOrder) const +{ + CardInfoComparator comparator(properties, order); + std::sort(cards->begin(), cards->end(), comparator); +} + +void VisualDatabaseDisplayWidget::databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + (void)topLeft; + (void)bottomRight; + qCDebug(VisualDatabaseDisplayLog) << "Database Data changed"; +} + +void VisualDatabaseDisplayWidget::wheelEvent(QWheelEvent *event) +{ + int totalRows = databaseDisplayModel->rowCount(); // Total number of cards + int nextPageStartIndex = (currentPage + 1) * cardsPerPage; + + // Handle scrolling down + if (event->angleDelta().y() < 0) { + // Check if the next page has any cards to load + if (nextPageStartIndex < totalRows) { + loadCurrentPage(); // Load the next page + event->accept(); // Accept the event as valid + return; + } + qCDebug(VisualDatabaseDisplayLog) << nextPageStartIndex << ":" << totalRows; + } + + // Prevent overscrolling when there's no more data to load + event->ignore(); +} diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.h new file mode 100644 index 000000000..31d1013e1 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_display_widget.h @@ -0,0 +1,103 @@ +#ifndef VISUAL_DATABASE_DISPLAY_WIDGET_H +#define VISUAL_DATABASE_DISPLAY_WIDGET_H + +#include "../../../../deck/custom_line_edit.h" +#include "../../../../deck/deck_list_model.h" +#include "../../../../game/cards/card_database.h" +#include "../../../../game/cards/card_database_model.h" +#include "../../../../game/filters/filter_tree_model.h" +#include "../../../game_logic/key_signals.h" +#include "../../../tabs/abstract_tab_deck_editor.h" +#include "../../layouts/flow_layout.h" +#include "../cards/card_info_picture_with_text_overlay_widget.h" +#include "../cards/card_size_widget.h" +#include "../general/layout_containers/flow_widget.h" +#include "../general/layout_containers/overlap_control_widget.h" +#include "visual_database_display_color_filter_widget.h" +#include "visual_database_display_filter_save_load_widget.h" +#include "visual_database_display_main_type_filter_widget.h" +#include "visual_database_display_name_filter_widget.h" +#include "visual_database_display_set_filter_widget.h" +#include "visual_database_display_sub_type_filter_widget.h" + +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display"); + +class VisualDatabaseDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VisualDatabaseDisplayWidget(QWidget *parent, + AbstractTabDeckEditor *deckEditor, + CardDatabaseModel *database_model, + CardDatabaseDisplayModel *database_display_model); + + void adjustCardsPerPage(); + void populateCards(); + void loadNextPage(); + void loadCurrentPage(); + void sortCardList(const QStringList &properties, Qt::SortOrder order) const; + void setDeckList(const DeckList &new_deck_list_model); + + QWidget *searchContainer; + QHBoxLayout *searchLayout; + SearchLineEdit *searchEdit; + FilterTreeModel *filterModel; + VisualDatabaseDisplayColorFilterWidget *colorFilterWidget; + +public slots: + void searchModelChanged(); + +signals: + void cardClickedDatabaseDisplay(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void cardHoveredDatabaseDisplay(CardInfoPtr hoveredCard); + +protected slots: + void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance); + void onHover(const CardInfoPtr &hoveredCard); + void addCard(const CardInfoPtr &cardToAdd); + void databaseDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void wheelEvent(QWheelEvent *event) override; + void modelDirty() const; + void updateSearch(const QString &search) const; + +private: + SettingsButtonWidget *quickFilterWidget; + VisualDatabaseDisplayFilterSaveLoadWidget *saveLoadWidget; + VisualDatabaseDisplayNameFilterWidget *nameFilterWidget; + VisualDatabaseDisplayMainTypeFilterWidget *mainTypeFilterWidget; + VisualDatabaseDisplaySubTypeFilterWidget *subTypeFilterWidget; + VisualDatabaseDisplaySetFilterWidget *setFilterWidget; + KeySignals searchKeySignals; + AbstractTabDeckEditor *deckEditor; + CardDatabaseModel *databaseModel; + CardDatabaseDisplayModel *databaseDisplayModel; + QTreeView *databaseView; + QList *cards; + QVBoxLayout *mainLayout; + QScrollArea *scrollArea; + FlowWidget *flowWidget; + QWidget *overlapCategories; + QVBoxLayout *overlapCategoriesLayout; + OverlapControlWidget *overlapControlWidget; + CardSizeWidget *cardSizeWidget; + QWidget *container; + QTimer *debounceTimer; + QTimer *loadCardsTimer; + + int debounceTime = 300; // in Ms + int currentPage = 0; // Current page index + int cardsPerPage = 100; // Number of cards per page + +protected: + void resizeEvent(QResizeEvent *event) override; +}; + +#endif // VISUAL_DATABASE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.cpp b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.cpp new file mode 100644 index 000000000..a9d15fd1a --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.cpp @@ -0,0 +1,80 @@ +#include "visual_database_filter_display_widget.h" + +#include "../../../../settings/cache_settings.h" + +#include +#include +#include +#include +#include +#include + +FilterDisplayWidget::FilterDisplayWidget(QWidget *parent, const QString &filename, FilterTreeModel *_filterModel) + : QWidget(parent), filterFilename(filename), filterModel(_filterModel) +{ + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + // Create layout + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + + // Filter button (displays filename) + filterButton = new QPushButton(filterFilename, this); + layout->addWidget(filterButton); + + // Create close button (inside the filter button) + closeButton = new QPushButton("x", this); + closeButton->setFixedSize(20, 20); // Small square close button + closeButton->setFocusPolicy(Qt::NoFocus); + layout->addWidget(closeButton); + + // Connect the filter button for loading the filter + connect(filterButton, &QPushButton::clicked, this, &FilterDisplayWidget::loadFilter); + + // Connect the close button for deleting the filter + connect(closeButton, &QPushButton::clicked, this, &FilterDisplayWidget::deleteFilter); +} + +QSize FilterDisplayWidget::sizeHint() const +{ + // Calculate the size based on the filter name and the close button + QFontMetrics fm(font()); + int textWidth = fm.horizontalAdvance(filterFilename); + int width = textWidth + 30 + 16; // Space for the filename and close button + int height = fm.height() + 10; // Height based on font size + padding + + return QSize(width, height); +} + +void FilterDisplayWidget::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + emit filterClicked(); + } + QWidget::mousePressEvent(event); +} + +void FilterDisplayWidget::loadFilter() +{ + // Trigger the loading of the filter + emit filterLoadRequested(filterFilename); +} + +void FilterDisplayWidget::deleteFilter() +{ + // Show confirmation dialog before deleting the filter + QMessageBox::StandardButton reply = QMessageBox::question( + this, tr("Confirm Delete"), tr("Are you sure you want to delete the filter '%1'?").arg(filterFilename), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { + // If confirmed, delete the filter + QString filePath = SettingsCache::instance().getFiltersPath() + QDir::separator() + filterFilename; + QFile file(filePath); + if (file.remove()) { + emit filterDeleted(filterFilename); // Emit signal for deletion + delete this; // Delete this widget + } else { + QMessageBox::warning(this, tr("Delete Failed"), tr("Failed to delete filter '%1'.").arg(filterFilename)); + } + } +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.h b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.h new file mode 100644 index 000000000..f8aabd144 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/visual_database_display/visual_database_filter_display_widget.h @@ -0,0 +1,40 @@ +#ifndef VISUAL_DATABASE_FILTER_DISPLAY_WIDGET_H +#define VISUAL_DATABASE_FILTER_DISPLAY_WIDGET_H + +#include +#include +#include +#include + +class FilterTreeModel; + +class FilterDisplayWidget : public QWidget +{ + Q_OBJECT + +public: + explicit FilterDisplayWidget(QWidget *parent, const QString &filename, FilterTreeModel *_filterModel); + ~FilterDisplayWidget() = default; + + QSize sizeHint() const override; + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private slots: + void loadFilter(); + void deleteFilter(); + +signals: + void filterClicked(); + void filterLoadRequested(const QString &filename); + void filterDeleted(const QString &filename); + +private: + QString filterFilename; + FilterTreeModel *filterModel; + QPushButton *filterButton; // Button for the filter text + QPushButton *closeButton; // Close button to delete the filter +}; + +#endif // VISUAL_DATABASE_FILTER_DISPLAY_WIDGET_H diff --git a/cockatrice/src/dialogs/dlg_settings.cpp b/cockatrice/src/dialogs/dlg_settings.cpp index 8f7077ee3..14982cbd4 100644 --- a/cockatrice/src/dialogs/dlg_settings.cpp +++ b/cockatrice/src/dialogs/dlg_settings.cpp @@ -101,6 +101,11 @@ GeneralSettingsPage::GeneralSettingsPage() QPushButton *deckPathButton = new QPushButton("..."); connect(deckPathButton, SIGNAL(clicked()), this, SLOT(deckPathButtonClicked())); + filtersPathEdit = new QLineEdit(settings.getFiltersPath()); + filtersPathEdit->setReadOnly(true); + QPushButton *filtersPathButton = new QPushButton("..."); + connect(filtersPathButton, SIGNAL(clicked()), this, SLOT(filtersPathButtonClicked())); + replaysPathEdit = new QLineEdit(settings.getReplaysPath()); replaysPathEdit->setReadOnly(true); QPushButton *replaysPathButton = new QPushButton("..."); @@ -132,6 +137,7 @@ GeneralSettingsPage::GeneralSettingsPage() bool isPortable = settings.getIsPortableBuild(); if (isPortable) { deckPathEdit->setEnabled(false); + filtersPathEdit->setEnabled(false); replaysPathEdit->setEnabled(false); picsPathEdit->setEnabled(false); cardDatabasePathEdit->setEnabled(false); @@ -154,24 +160,27 @@ GeneralSettingsPage::GeneralSettingsPage() pathsGrid->addWidget(&deckPathLabel, 0, 0); pathsGrid->addWidget(deckPathEdit, 0, 1); pathsGrid->addWidget(deckPathButton, 0, 2); - pathsGrid->addWidget(&replaysPathLabel, 1, 0); - pathsGrid->addWidget(replaysPathEdit, 1, 1); - pathsGrid->addWidget(replaysPathButton, 1, 2); - pathsGrid->addWidget(&picsPathLabel, 2, 0); - pathsGrid->addWidget(picsPathEdit, 2, 1); - pathsGrid->addWidget(picsPathButton, 2, 2); - pathsGrid->addWidget(&cardDatabasePathLabel, 3, 0); - pathsGrid->addWidget(cardDatabasePathEdit, 3, 1); - pathsGrid->addWidget(cardDatabasePathButton, 3, 2); - pathsGrid->addWidget(&customCardDatabasePathLabel, 4, 0); - pathsGrid->addWidget(customCardDatabasePathEdit, 4, 1); - pathsGrid->addWidget(customCardDatabasePathButton, 4, 2); - pathsGrid->addWidget(&tokenDatabasePathLabel, 5, 0); - pathsGrid->addWidget(tokenDatabasePathEdit, 5, 1); - pathsGrid->addWidget(tokenDatabasePathButton, 5, 2); + pathsGrid->addWidget(&filtersPathLabel, 1, 0); + pathsGrid->addWidget(filtersPathEdit, 1, 1); + pathsGrid->addWidget(filtersPathButton, 1, 2); + pathsGrid->addWidget(&replaysPathLabel, 2, 0); + pathsGrid->addWidget(replaysPathEdit, 2, 1); + pathsGrid->addWidget(replaysPathButton, 2, 2); + pathsGrid->addWidget(&picsPathLabel, 3, 0); + pathsGrid->addWidget(picsPathEdit, 3, 1); + pathsGrid->addWidget(picsPathButton, 3, 2); + pathsGrid->addWidget(&cardDatabasePathLabel, 4, 0); + pathsGrid->addWidget(cardDatabasePathEdit, 4, 1); + pathsGrid->addWidget(cardDatabasePathButton, 4, 2); + pathsGrid->addWidget(&customCardDatabasePathLabel, 5, 0); + pathsGrid->addWidget(customCardDatabasePathEdit, 5, 1); + pathsGrid->addWidget(customCardDatabasePathButton, 5, 2); + pathsGrid->addWidget(&tokenDatabasePathLabel, 6, 0); + pathsGrid->addWidget(tokenDatabasePathEdit, 6, 1); + pathsGrid->addWidget(tokenDatabasePathButton, 6, 2); if (!isPortable) { - pathsGrid->addWidget(resetAllPathsButton, 6, 0); - pathsGrid->addWidget(allPathsResetLabel, 6, 1); + pathsGrid->addWidget(resetAllPathsButton, 7, 0); + pathsGrid->addWidget(allPathsResetLabel, 7, 1); } pathsGroupBox = new QGroupBox; pathsGroupBox->setLayout(pathsGrid); @@ -226,6 +235,16 @@ void GeneralSettingsPage::deckPathButtonClicked() SettingsCache::instance().setDeckPath(path); } +void GeneralSettingsPage::filtersPathButtonClicked() +{ + QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), filtersPathEdit->text()); + if (path.isEmpty()) + return; + + filtersPathEdit->setText(path); + SettingsCache::instance().setFiltersPath(path); +} + void GeneralSettingsPage::replaysPathButtonClicked() { QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), replaysPathEdit->text()); @@ -307,6 +326,7 @@ void GeneralSettingsPage::retranslateUi() advertiseTranslationPageLabel.setText( QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); deckPathLabel.setText(tr("Decks directory:")); + filtersPathLabel.setText(tr("Filters directory:")); replaysPathLabel.setText(tr("Replays directory:")); picsPathLabel.setText(tr("Pictures directory:")); cardDatabasePathLabel.setText(tr("Card database:")); diff --git a/cockatrice/src/dialogs/dlg_settings.h b/cockatrice/src/dialogs/dlg_settings.h index 633bbdc2b..b515dd917 100644 --- a/cockatrice/src/dialogs/dlg_settings.h +++ b/cockatrice/src/dialogs/dlg_settings.h @@ -46,6 +46,7 @@ public: private slots: void deckPathButtonClicked(); + void filtersPathButtonClicked(); void replaysPathButtonClicked(); void picsPathButtonClicked(); void cardDatabasePathButtonClicked(); @@ -58,6 +59,7 @@ private: QStringList findQmFiles(); QString languageName(const QString &lang); QLineEdit *deckPathEdit; + QLineEdit *filtersPathEdit; QLineEdit *replaysPathEdit; QLineEdit *picsPathEdit; QLineEdit *cardDatabasePathEdit; @@ -74,6 +76,7 @@ private: QComboBox updateReleaseChannelBox; QLabel languageLabel; QLabel deckPathLabel; + QLabel filtersPathLabel; QLabel replaysPathLabel; QLabel picsPathLabel; QLabel cardDatabasePathLabel; diff --git a/cockatrice/src/settings/cache_settings.cpp b/cockatrice/src/settings/cache_settings.cpp index 25c7a3d27..e37dbf9e5 100644 --- a/cockatrice/src/settings/cache_settings.cpp +++ b/cockatrice/src/settings/cache_settings.cpp @@ -451,6 +451,12 @@ void SettingsCache::setDeckPath(const QString &_deckPath) settings->setValue("paths/decks", deckPath); } +void SettingsCache::setFiltersPath(const QString &_filtersPath) +{ + filtersPath = _filtersPath; + settings->setValue("paths/filters", filtersPath); +} + void SettingsCache::setReplaysPath(const QString &_replaysPath) { replaysPath = _replaysPath; @@ -1312,6 +1318,7 @@ void SettingsCache::loadPaths() { QString dataPath = getDataPath(); deckPath = getSafeConfigPath("paths/decks", dataPath + "/decks/"); + filtersPath = getSafeConfigPath("paths/filters", dataPath + "/filters/"); replaysPath = getSafeConfigPath("paths/replays", dataPath + "/replays/"); themesPath = getSafeConfigPath("paths/themes", dataPath + "/themes/"); picsPath = getSafeConfigPath("paths/pics", dataPath + "/pics/"); diff --git a/cockatrice/src/settings/cache_settings.h b/cockatrice/src/settings/cache_settings.h index ab5b43401..a311c3ab3 100644 --- a/cockatrice/src/settings/cache_settings.h +++ b/cockatrice/src/settings/cache_settings.h @@ -103,7 +103,7 @@ private: QByteArray tokenDialogGeometry; QByteArray setsDialogGeometry; QString lang; - QString deckPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath, + QString deckPath, filtersPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath, customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName; bool tabVisualDeckStorageOpen, tabServerOpen, tabAccountOpen, tabDeckStorageOpen, tabReplaysOpen, tabAdminOpen, tabLogOpen; @@ -233,6 +233,10 @@ public: { return deckPath; } + QString getFiltersPath() const + { + return filtersPath; + } QString getReplaysPath() const { return replaysPath; @@ -754,6 +758,7 @@ public slots: void setShowTipsOnStartup(bool _showTipsOnStartup); void setSeenTips(const QList &_seenTips); void setDeckPath(const QString &_deckPath); + void setFiltersPath(const QString &_filtersPath); void setReplaysPath(const QString &_replaysPath); void setThemesPath(const QString &_themesPath); void setCustomCardDatabasePath(const QString &_customCardDatabasePath); diff --git a/cockatrice/src/utility/card_info_comparator.cpp b/cockatrice/src/utility/card_info_comparator.cpp new file mode 100644 index 000000000..26d00420f --- /dev/null +++ b/cockatrice/src/utility/card_info_comparator.cpp @@ -0,0 +1,75 @@ +#include "card_info_comparator.h" + +CardInfoComparator::CardInfoComparator(const QStringList &properties, Qt::SortOrder order) + : m_properties(properties), m_order(order) +{ +} + +bool CardInfoComparator::operator()(const CardInfoPtr &a, const CardInfoPtr &b) const +{ + // Iterate over each property in the list + for (const QString &property : m_properties) { + QVariant valueA = getProperty(a, property); + QVariant valueB = getProperty(b, property); + + // Compare the current property + if (valueA != valueB) { + // If values differ, perform comparison + return compareVariants(valueA, valueB) ? (m_order == Qt::AscendingOrder) : (m_order == Qt::DescendingOrder); + } + } + + // If all properties are equal, return false (indicating they are considered equal for sorting purposes) + return false; +} + +bool CardInfoComparator::compareVariants(const QVariant &a, const QVariant &b) const +{ + // Determine the type of QVariant based on Qt version +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (a.typeId() != b.typeId()) { +#else + if (a.type() != b.type()) { +#endif + // If they are not the same type, compare as strings + return a.toString() < b.toString(); + } + + // Perform type-specific comparison +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + switch (a.typeId()) { +#else + switch (a.type()) { +#endif + case QMetaType::Int: + return a.toInt() < b.toInt(); + case QMetaType::Double: + return a.toDouble() < b.toDouble(); + case QMetaType::QString: + return a.toString() < b.toString(); + case QMetaType::Bool: + return a.toBool() < b.toBool(); + default: + // Default to comparing as strings + return a.toString() < b.toString(); + } +} + +QVariant CardInfoComparator::getProperty(const CardInfoPtr &card, const QString &property) const +{ + // Check if the property exists in the main fields of the class + if (property == "name") { + return card->getName(); + } else if (property == "text") { + return card->getText(); + } else if (property == "isToken") { + return card->getIsToken(); + } + + // Otherwise, check if it's a custom property in the QVariantHash + if (card->hasProperty(property)) { + return card->getProperty(property); + } + + return QVariant(); // Return an invalid variant if the property does not exist +} diff --git a/cockatrice/src/utility/card_info_comparator.h b/cockatrice/src/utility/card_info_comparator.h new file mode 100644 index 000000000..012a50c2c --- /dev/null +++ b/cockatrice/src/utility/card_info_comparator.h @@ -0,0 +1,24 @@ +#ifndef CARD_INFO_COMPARATOR_H +#define CARD_INFO_COMPARATOR_H + +#include "../game/cards/card_database.h" + +#include +#include +#include + +class CardInfoComparator +{ +public: + explicit CardInfoComparator(const QStringList &properties, Qt::SortOrder order = Qt::AscendingOrder); + bool operator()(const CardInfoPtr &a, const CardInfoPtr &b) const; + +private: + QStringList m_properties; // List of properties to sort by + Qt::SortOrder m_order; + + QVariant getProperty(const CardInfoPtr &card, const QString &property) const; + bool compareVariants(const QVariant &a, const QVariant &b) const; +}; + +#endif // CARD_INFO_COMPARATOR_H diff --git a/dbconverter/src/mocks.cpp b/dbconverter/src/mocks.cpp index e1d095a5a..67f6a425a 100644 --- a/dbconverter/src/mocks.cpp +++ b/dbconverter/src/mocks.cpp @@ -106,6 +106,9 @@ void SettingsCache::setSeenTips(const QList & /* _seenTips */) void SettingsCache::setDeckPath(const QString & /* _deckPath */) { } +void SettingsCache::setFiltersPath(const QString & /*_filtersPath */) +{ +} void SettingsCache::setReplaysPath(const QString & /* _replaysPath */) { } diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp index f01245dab..23eef3d61 100644 --- a/tests/carddatabase/mocks.cpp +++ b/tests/carddatabase/mocks.cpp @@ -110,6 +110,9 @@ void SettingsCache::setSeenTips(const QList & /* _seenTips */) void SettingsCache::setDeckPath(const QString & /* _deckPath */) { } +void SettingsCache::setFiltersPath(const QString & /*_filtersPath */) +{ +} void SettingsCache::setReplaysPath(const QString & /* _replaysPath */) { }