diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index 23ed4316d..bc07ac183 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -9,6 +9,7 @@ #include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h" #include "../../interface/widgets/dialogs/dlg_load_remote_deck.h" #include "../../interface/widgets/tabs/tab_game.h" +#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h" #include "deck_view.h" #include diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index a55056bda..388cacd2c 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -15,6 +15,8 @@ #include "deck_list_history_manager_widget.h" #include "deck_list_style_proxy.h" +#include +#include #include #include #include diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index 2b9201e6c..0b77ca185 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -1,6 +1,7 @@ #include "printing_selector_card_overlay_widget.h" #include "../../../client/settings/cache_settings.h" +#include "../cards/card_info_picture_widget.h" #include "printing_selector_card_display_widget.h" #include diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index e1f255199..a3cda2bfc 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -8,6 +8,7 @@ #ifndef TAB_GENERIC_DECK_EDITOR_H #define TAB_GENERIC_DECK_EDITOR_H +#include "../../deck_loader/deck_loader.h" #include "../interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h" #include "../interface/widgets/deck_editor/deck_editor_card_info_dock_widget.h" #include "../interface/widgets/deck_editor/deck_editor_database_display_widget.h" diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp index f1dcf113f..fc0e5c2b3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp @@ -1,12 +1,12 @@ #include "deck_preview_color_identity_filter_widget.h" #include "../../cards/additional_info/mana_symbol_widget.h" -#include "deck_preview_widget.h" +#include "../visual_deck_storage_widget.h" -#include +#include DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent) - : QWidget(parent), layout(new QHBoxLayout(this)) + : QWidget(parent), layout(new QHBoxLayout(this)), visualDeckStorageWidget(parent) { setLayout(layout); layout->setSpacing(5); @@ -32,10 +32,6 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua // Connect the button's clicked signal connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode); - connect(this, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, parent, - &VisualDeckStorageWidget::updateColorFilter); - connect(this, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, parent, - &VisualDeckStorageWidget::updateColorFilter); // Call retranslateUi to set the initial text retranslateUi(); @@ -45,111 +41,56 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi() { // Set the toggle button text based on the current mode switch (filterMode) { - case ExactMatch: + case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: toggleButton->setText(tr("Mode: Exact Match")); break; - case Includes: + case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: toggleButton->setText(tr("Mode: Includes")); break; - case Excludes: + case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: toggleButton->setText(tr("Mode: Excludes")); break; } toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)")); } +/** + * @brief Pushes the current filter state to the proxy model. + */ +void DeckPreviewColorIdentityFilterWidget::applyFilter() +{ + QSet activeColorSet; + for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { + if (it.value()) { + activeColorSet.insert(it.key()); + } + } + + // The VDS owns the proxy model, which exists for the widget's whole lifetime. + visualDeckStorageWidget->proxyModel()->setColorFilter(filterMode, activeColorSet); +} + void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active) { activeColors[color] = active; - emit activeColorsChanged(); + applyFilter(); } void DeckPreviewColorIdentityFilterWidget::updateFilterMode() { // Cycle through the modes switch (filterMode) { - case ExactMatch: - filterMode = Includes; + case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: + filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes; break; - case Includes: - filterMode = Excludes; + case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: + filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes; break; - case Excludes: - filterMode = ExactMatch; + case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: + filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch; break; } retranslateUi(); // Update the button text - emit filterModeChanged(filterMode); -} - -void DeckPreviewColorIdentityFilterWidget::filterWidgets(QList widgets) -{ - // Check if no colors are active - bool noColorsActive = true; - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value()) { - noColorsActive = false; - break; - } - } - - // If no colors are active, return the unfiltered list of widgets - if (noColorsActive) { - for (DeckPreviewWidget *previewWidget : widgets) { - previewWidget->filteredByColor = false; - } - return; - } - - for (const auto &widget : widgets) { - QString colorIdentity = widget->getColorIdentity(); - - bool matchesFilter = true; - switch (filterMode) { - case ExactMatch: { - // Exact match mode: active colors must exactly match colorIdentity - - // Create a set of active colors - QSet activeColorSet; - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value()) { - activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity - } - } - - // Create a set of colors from the color identity string - QSet colorIdentitySet; - for (const QChar &color : colorIdentity) { - colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity - } - - // Compare the sets: the sets must match exactly - if (activeColorSet != colorIdentitySet) { - matchesFilter = false; - } - break; - } - case Includes: - // Includes mode: colorIdentity must contain all active colors - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value() && !colorIdentity.contains(it.key())) { - matchesFilter = false; - break; - } - } - break; - case Excludes: - // Excludes mode: colorIdentity must contain none of the active colors - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value() && colorIdentity.contains(it.key())) { - matchesFilter = false; - break; - } - } - break; - } - - widget->filteredByColor = !matchesFilter; - } + applyFilter(); } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h index 8e60b16fb..5b6a05646 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h @@ -2,18 +2,17 @@ * @file deck_preview_color_identity_filter_widget.h * @ingroup VisualDeckPreviewWidgets */ -//! \todo Document this file. #ifndef DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H #define DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H -#include "../visual_deck_storage_widget.h" +#include "../visual_deck_storage_sort_filter_proxy_model.h" #include +#include #include #include -class DeckPreviewWidget; class VisualDeckStorageWidget; class DeckPreviewColorIdentityFilterWidget : public QWidget @@ -21,34 +20,21 @@ class DeckPreviewColorIdentityFilterWidget : public QWidget Q_OBJECT public: - /** - * How the active colors are matched against a deck's color identity. - */ - enum FilterMode - { - ExactMatch, ///< The color identity consists of exactly the active colors. - Includes, ///< The color identity contains all of the active colors. - Excludes ///< The color identity contains none of the active colors. - }; - Q_ENUM(FilterMode) - explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent); void retranslateUi(); - void filterWidgets(QList widgets); - -signals: - void filterModeChanged(FilterMode mode); - void activeColorsChanged(); private slots: void handleColorToggled(QChar color, bool active); void updateFilterMode(); private: + void applyFilter(); + QHBoxLayout *layout; QPushButton *toggleButton; QMap activeColors; - FilterMode filterMode = Includes; // Default to "includes" mode + VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes; + VisualDeckStorageWidget *visualDeckStorageWidget; ///< The VDS owning the proxy model. }; #endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp index b45f61be7..8c6557ad0 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp @@ -1,19 +1,15 @@ #include "deck_preview_deck_tags_display_widget.h" #include "../../../../client/settings/cache_settings.h" -#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h" -#include "../../../../interface/widgets/tabs/tab_deck_editor.h" +#include "../../../deck_loader/deck_loader.h" #include "../../general/layout_containers/flow_widget.h" #include "deck_preview_tag_addition_widget.h" #include "deck_preview_tag_dialog.h" #include "deck_preview_tag_display_widget.h" -#include "deck_preview_widget.h" #include #include -#include #include -#include DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags) : QWidget(_parent), currentTags(_tags) @@ -54,6 +50,16 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags() flowWidget->addWidget(tagAdditionWidget); } +void DeckPreviewDeckTagsDisplayWidget::setKnownTagsProvider(const std::function &provider) +{ + knownTagsProvider_ = provider; +} + +void DeckPreviewDeckTagsDisplayWidget::setConversionPromptHandler(const std::function &handler) +{ + conversionPromptHandler_ = handler; +} + /** * Gets the filepath of all files (no directories) in target directory and all subdirectories */ @@ -92,93 +98,13 @@ static QStringList findAllKnownTags() void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() { - if (qobject_cast(parentWidget())) { - // If we're the child of a DeckPreviewWidget, then we need to handle conversion - auto *deckPreviewWidget = qobject_cast(parentWidget()); - - bool canAddTags = promptFileConversionIfRequired(deckPreviewWidget); - - if (canAddTags) { - QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); - execTagDialog(knownTags); - } - } else { - // If we're the child of an AbstractTabDeckEditor, then we don't bother with conversion - QStringList knownTags = findAllKnownTags(); - execTagDialog(knownTags); - } -} - -static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) -{ - QFileInfo fileInfo(filePath); - QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); - - if (QFile::exists(newFileName)) { - QMessageBox::StandardButton reply = - QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), - QObject::tr("A .cod version of this deck already exists. Overwrite it?"), - QMessageBox::Yes | QMessageBox::No); - return reply == QMessageBox::Yes; - } - return true; // Safe to proceed -} - -static void convertFileToCockatriceFormat(DeckPreviewWidget *deckPreviewWidget) -{ - DeckLoader::convertToCockatriceFormat(deckPreviewWidget->deckLoader->getDeck()); - deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getDeck().lastLoadInfo.fileName; - deckPreviewWidget->refreshBannerCardText(); -} - -/** - * Checks if the deck's file format supports tags. - * If not, then prompt the user for file conversion. - * @return whether the resulting file can support adding tags - */ -bool DeckPreviewDeckTagsDisplayWidget::promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget) -{ - if (DeckFileFormat::getFormatFromName(deckPreviewWidget->filePath) == DeckFileFormat::Cockatrice) { - return true; + // The deck editor path has no conversion prompt; the VDS path registers one. + if (conversionPromptHandler_ && !conversionPromptHandler_()) { + return; } - // Retrieve saved preference if the prompt is disabled - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { - return false; - } - - if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) { - return false; - } - - convertFileToCockatriceFormat(deckPreviewWidget); - return true; - } - - // Show the dialog to the user - DialogConvertDeckToCodFormat conversionDialog(parentWidget()); - if (conversionDialog.exec() != QDialog::Accepted) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( - !conversionDialog.dontAskAgain()); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); - - return false; - } - - // Try to convert file - if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) { - return false; - } - - convertFileToCockatriceFormat(deckPreviewWidget); - - if (conversionDialog.dontAskAgain()) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); - } - - return true; + const QStringList knownTags = knownTagsProvider_ ? knownTagsProvider_() : findAllKnownTags(); + execTagDialog(knownTags); } void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags) @@ -191,4 +117,4 @@ void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTag emit tagsChanged(updatedTags); } } -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h index 4bd7915cd..a9238aef1 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h @@ -2,41 +2,53 @@ * @file deck_preview_deck_tags_display_widget.h * @ingroup VisualDeckPreviewWidgets */ -//! \todo Document this file. #ifndef DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H #define DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H -#include "../../../deck_loader/deck_loader.h" -#include "deck_preview_widget.h" - +#include #include +#include + +class FlowWidget; -class DeckPreviewWidget; class DeckPreviewDeckTagsDisplayWidget : public QWidget { Q_OBJECT QStringList currentTags; FlowWidget *flowWidget; + std::function knownTagsProvider_; + std::function conversionPromptHandler_; public: explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags = {}); void setTags(const QStringList &_tags); void refreshTags(); + /** + * @brief Sets a provider for the tags shown in the edit dialog. + * Defaults to scanning all deck files in the deck folder. + */ + void setKnownTagsProvider(const std::function &provider); + + /** + * @brief Sets a handler run before opening the tag dialog. Returning false + * cancels the dialog. Defaults to no handler (the deck editor path). + */ + void setConversionPromptHandler(const std::function &handler); + public slots: void openTagEditDlg(); -private: - bool promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget); - void execTagDialog(const QStringList &knownTags); - signals: /** * Emitted when the tags have changed due to user interaction. * @param tags The new list of tags. */ void tagsChanged(const QStringList &tags); + +private: + void execTagDialog(const QStringList &knownTags); }; #endif // DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index ffe954308..d16c904a5 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -1,47 +1,69 @@ #include "deck_preview_widget.h" #include "../../../../client/settings/cache_settings.h" +#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h" +#include "../../../deck_loader/deck_loader.h" #include "../../cards/additional_info/color_identity_widget.h" #include "../../cards/deck_preview_card_picture_widget.h" +#include "../visual_deck_storage_quick_settings_widget.h" +#include "../visual_deck_storage_tag_filter_widget.h" +#include "../visual_deck_storage_widget.h" #include "deck_preview_deck_tags_display_widget.h" -#include #include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, VisualDeckStorageWidget *_visualDeckStorageWidget, + VisualDeckStorageModel *model, const QString &_filePath) - : QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath), - colorIdentityWidget(nullptr), deckTagsDisplayWidget(nullptr) + : QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model_(model), filePath(_filePath) { layout = new QVBoxLayout(this); setLayout(layout); - deckLoader = new DeckLoader(this); - connect(deckLoader, &DeckLoader::loadFinished, this, &DeckPreviewWidget::initializeUi); - //! \todo Batch tag refresh: count finished deck loads and refresh tags once all decks are loaded. - // Currently expensive: refreshes on each individual deck load instead of once at the end. - connect(deckLoader, &DeckLoader::loadFinished, visualDeckStorageWidget->tagFilterWidget, - &VisualDeckStorageTagFilterWidget::refreshTags); - deckLoader->loadFromFileAsync(filePath, DeckFileFormat::getFormatFromName(filePath), false); - - bannerCardDisplayWidget = + auto *pictureWidget = new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled); - - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, - &DeckPreviewWidget::imageClickedEvent); - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, + pictureWidget->setFontSize(24); + connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent); + connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, &DeckPreviewWidget::imageDoubleClickedEvent); + bannerCardDisplayWidget = pictureWidget; + + colorIdentityWidget = new ColorIdentityWidget(this); + deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this); + connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags); + deckTagsDisplayWidget->setKnownTagsProvider( + [this] { return visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); }); + deckTagsDisplayWidget->setConversionPromptHandler([this] { return promptFileConversionIfRequired(); }); + + bannerCardLabel = new QLabel(this); + bannerCardLabel->setObjectName("bannerCardLabel"); + bannerCardComboBox = new QComboBox(this); + bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + bannerCardComboBox->setObjectName("bannerCardComboBox"); + bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox)); + connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &DeckPreviewWidget::setBannerCard); + + // Apply the initial visibility settings and keep them in sync while they change. + updateColorIdentityVisibility( + SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity()); + updateBannerCardComboBoxVisibility( + SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowBannerCardComboBox()); + updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews()); connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageShowColorIdentityChanged, this, @@ -56,6 +78,15 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, &DeckPreviewWidget::refreshBannerCardToolTip); layout->addWidget(bannerCardDisplayWidget); + layout->addWidget(colorIdentityWidget); + layout->addWidget(deckTagsDisplayWidget); + layout->addWidget(bannerCardLabel); + layout->addWidget(bannerCardComboBox); + + connect(model_, &QAbstractItemModel::dataChanged, this, &DeckPreviewWidget::syncFromModel); + + retranslateUi(); + syncFromModel(); } void DeckPreviewWidget::retranslateUi() @@ -79,83 +110,28 @@ void DeckPreviewWidget::enterEvent(QEnterEvent *event) { QWidget::enterEvent(event); - // don't do reloads until widgets have been created - if (bannerCardComboBox != nullptr) { - reloadIfModified(); + // Don't do reloads until the deck has actually been loaded once. + reloadIfModified(); +} + +/** + * @brief The row of this deck in the source model, or -1 if it no longer exists. + */ +int DeckPreviewWidget::row() const +{ + return model_->rowForFilePath(filePath); +} + +/** + * @brief The display name is given by the deck name, or the filename if the deck name is not set. + */ +QString DeckPreviewWidget::getDisplayName() const +{ + const int r = row(); + if (r == -1) { + return {}; } -} - -/** - * @brief Sets the lastModifiedTime to the value given by the file. - */ -void DeckPreviewWidget::updateLastModifiedTime() -{ - QFileInfo fileInfo(filePath); - lastModifiedTime = fileInfo.lastModified(); -} - -/** - * @brief Writes the current contents of the deck to file. Updates the lastModifiedTime afterward. - */ -void DeckPreviewWidget::writeDeckToFile() -{ - DeckLoader::saveToFile(deckLoader->getDeck()); - updateLastModifiedTime(); -} - -void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess) -{ - if (!deckLoadSuccess) { - return; - } - - QFileInfo fileInfo(filePath); - lastModifiedTime = fileInfo.lastModified(); - - bannerCardDisplayWidget->setFontSize(24); - setFilePath(deckLoader->getDeck().lastLoadInfo.fileName); - - colorIdentityWidget = new ColorIdentityWidget(this); - deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this); - connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags); - - bannerCardLabel = new QLabel(this); - bannerCardLabel->setObjectName("bannerCardLabel"); - bannerCardComboBox = new QComboBox(this); - bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - bannerCardComboBox->setObjectName("bannerCardComboBox"); - bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox)); - connect(bannerCardComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &DeckPreviewWidget::setBannerCard); - - updateColorIdentityVisibility( - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity()); - updateBannerCardComboBoxVisibility( - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowBannerCardComboBox()); - updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews()); - - layout->addWidget(colorIdentityWidget); - layout->addWidget(deckTagsDisplayWidget); - layout->addWidget(bannerCardLabel); - layout->addWidget(bannerCardComboBox); - - retranslateUi(); - resyncWidgets(); -} - -/** - * @brief Syncs the contents of the child widgets with the current deck. - */ -void DeckPreviewWidget::resyncWidgets() -{ - auto bannerCardRef = deckLoader->getDeck().deckList.getBannerCard(); - auto bannerCard = bannerCardRef.name.isEmpty() ? ExactCard() : CardDatabaseManager::query()->getCard(bannerCardRef); - - bannerCardDisplayWidget->setCard(bannerCard); - refreshBannerCardText(); - updateBannerCardComboBox(bannerCardRef.name); - colorIdentityWidget->setColorIdentity(getColorIdentity()); - deckTagsDisplayWidget->setTags(deckLoader->getDeck().deckList.getTags()); + return model_->dataForRow(r).displayName; } /** @@ -163,33 +139,36 @@ void DeckPreviewWidget::resyncWidgets() */ void DeckPreviewWidget::reloadIfModified() { - QFileInfo fileInfo(filePath); - QDateTime newLastModifiedTime = fileInfo.lastModified(); - - if (!newLastModifiedTime.isValid() || newLastModifiedTime <= lastModifiedTime) { + const int r = row(); + if (r == -1 || !model_->dataForRow(r).loadSucceeded) { return; } - bool success = deckLoader->reload(); - - if (success) { - fileInfo.refresh(); - lastModifiedTime = fileInfo.lastModified(); - resyncWidgets(); - } + model_->reloadIfModified(r); } -void DeckPreviewWidget::updateVisibility() +/** + * @brief Syncs the contents of the child widgets with the current row's data. + */ +void DeckPreviewWidget::syncFromModel() { - setHidden(!checkVisibility()); -} - -bool DeckPreviewWidget::checkVisibility() const -{ - if (filteredBySearch || filteredByColor || filteredByTags) { - return false; + const int r = row(); + if (r == -1) { + return; } - return true; + + const DeckPreviewData &data = model_->dataForRow(r); + filePath = data.filePath; + + const CardRef bannerCardRef = data.deck.deckList.getBannerCard(); + const ExactCard bannerCard = + bannerCardRef.name.isEmpty() ? ExactCard() : CardDatabaseManager::query()->getCard(bannerCardRef); + + bannerCardDisplayWidget->setCard(bannerCard); + refreshBannerCardText(); + updateBannerCardComboBox(bannerCardRef.name); + colorIdentityWidget->setColorIdentity(data.colorIdentity); + deckTagsDisplayWidget->setTags(data.tags); } void DeckPreviewWidget::updateColorIdentityVisibility(bool visible) @@ -229,51 +208,6 @@ void DeckPreviewWidget::updateTagsVisibility(bool visible) } } -QString DeckPreviewWidget::getColorIdentity() -{ - QStringList cardList = deckLoader->getDeck().deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE}); - if (cardList.isEmpty()) { - return {}; - } - - QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G) - - for (const QString &cardName : cardList) { - CardInfoPtr currentCard = CardDatabaseManager::query()->getCardInfo(cardName); - if (currentCard) { - QString colors = currentCard->getColors(); // Assuming this returns something like "WUB" - for (const QChar &color : colors) { - colorSet.insert(color); - } - } - } - - // Ensure the color identity is in WUBRG order - QString colorIdentity; - const QString wubrgOrder = "WUBRG"; - for (const QChar &color : wubrgOrder) { - if (colorSet.contains(color)) { - colorIdentity.append(color); - } - } - - return colorIdentity; -} - -/** - * The display name is given by the deck name, or the filename if the deck name is not set. - */ -QString DeckPreviewWidget::getDisplayName() const -{ - QString deckName = deckLoader->getDeck().deckList.getName(); - return !deckName.isEmpty() ? deckName : QFileInfo(deckLoader->getDeck().lastLoadInfo.fileName).fileName(); -} - -void DeckPreviewWidget::setFilePath(const QString &_filePath) -{ - filePath = _filePath; -} - /** * Refreshes the banner card text. * This also calls `refreshBannerCardToolTip`, since those two often need to be updated together. @@ -310,11 +244,15 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) // Prepare the new items with deduplication QSet> bannerCardSet; - QList cardsInDeck = deckLoader->getDeck().deckList.getCardNodes(); + const int r = row(); + if (r != -1) { + const DeckList &deckList = model_->dataForRow(r).deck.deckList; + const QList cardsInDeck = deckList.getCardNodes(); - for (auto currentCard : cardsInDeck) { - for (int k = 0; k < currentCard->getNumber(); ++k) { - bannerCardSet.insert(QPair(currentCard->getName(), currentCard->getCardProviderId())); + for (auto currentCard : cardsInDeck) { + for (int k = 0; k < currentCard->getNumber(); ++k) { + bannerCardSet.insert(QPair(currentCard->getName(), currentCard->getCardProviderId())); + } } } @@ -344,7 +282,9 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) bannerCardComboBox->setCurrentIndex(restoredIndex); } else { // Add a placeholder "-" and set it as the current selection - int bannerIndex = bannerCardComboBox->findText(deckLoader->getDeck().deckList.getBannerCard().name); + const QString currentBannerCardName = + r == -1 ? QString() : model_->dataForRow(r).deck.deckList.getBannerCard().name; + int bannerIndex = bannerCardComboBox->findText(currentBannerCardName); if (bannerIndex != -1) { bannerCardComboBox->setCurrentIndex(bannerIndex); } else { @@ -362,8 +302,11 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */) { auto [name, id] = bannerCardComboBox->currentData().value>(); CardRef cardRef = {name, id}; - deckLoader->getDeck().deckList.setBannerCard(cardRef); - writeDeckToFile(); + const int r = row(); + if (r == -1) { + return; + } + model_->setBannerCard(r, cardRef); bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef)); } @@ -385,17 +328,24 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC void DeckPreviewWidget::setTags(const QStringList &tags) { - deckLoader->getDeck().deckList.setTags(tags); - writeDeckToFile(); + const int r = row(); + if (r != -1) { + model_->setTags(r, tags); + } } QMenu *DeckPreviewWidget::createRightClickMenu() { + const int r = row(); + auto *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); - connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, - [this] { emit openDeckEditor(deckLoader->getDeck()); }); + connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, [this, r] { + if (r != -1) { + emit openDeckEditor(model_->deckForRow(r)); + } + }); connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::openTagEditDlg); @@ -408,14 +358,26 @@ QMenu *DeckPreviewWidget::createRightClickMenu() auto saveToClipboardMenu = menu->addMenu(tr("Save Deck to Clipboard")); - connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, true); }); - connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, false); }); - connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, true); }); - connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, - [this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, false); }); + connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, [this, r] { + if (r != -1) { + DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, true, true); + } + }); + connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, [this, r] { + if (r != -1) { + DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, true, false); + } + }); + connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, [this, r] { + if (r != -1) { + DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, false, true); + } + }); + connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, [this, r] { + if (r != -1) { + DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, false, false); + } + }); menu->addSeparator(); @@ -450,57 +412,58 @@ void DeckPreviewWidget::addSetBannerCardMenu(QMenu *menu) void DeckPreviewWidget::actRenameDeck() { + const int r = row(); + if (r == -1) { + return; + } + // read input - const QString oldName = deckLoader->getDeck().deckList.getName(); + const QString oldName = model_->dataForRow(r).deckName; bool ok; - QString newName = QInputDialog::getText(this, "Rename deck", tr("New name:"), QLineEdit::Normal, oldName, &ok); + QString newName = QInputDialog::getText(this, tr("Rename deck"), tr("New name:"), QLineEdit::Normal, oldName, &ok); if (!ok || oldName == newName) { return; } // write change - deckLoader->getDeck().deckList.setName(newName); - writeDeckToFile(); + model_->renameDeck(r, newName); - // update VDS - refreshBannerCardText(); + // The banner card text updates via the model's dataChanged signal. } void DeckPreviewWidget::actRenameFile() { + const int r = row(); + if (r == -1) { + return; + } + // read input const auto info = QFileInfo(filePath); const QString oldName = info.baseName(); bool ok; - QString newName = QInputDialog::getText(this, "Rename file", tr("New name:"), QLineEdit::Normal, oldName, &ok); + QString newName = QInputDialog::getText(this, tr("Rename file"), tr("New name:"), QLineEdit::Normal, oldName, &ok); if (!ok || newName.isEmpty() || oldName == newName) { return; } - QString newFileName = newName; - if (!info.suffix().isEmpty()) { - newFileName += "." + info.suffix(); - } - // write change - const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath(); - if (!QFile::rename(info.filePath(), newFilePath)) { + if (!model_->renameFile(r, newName)) { QMessageBox::critical(this, tr("Error"), tr("Rename failed")); - return; } - deckLoader->getDeck().lastLoadInfo.fileName = newFilePath; - setFilePath(newFilePath); - - // update VDS - updateLastModifiedTime(); - refreshBannerCardText(); + // The file path and banner card text update via the model's signals. } void DeckPreviewWidget::actDeleteFile() { + const int r = row(); + if (r == -1) { + return; + } + // read input auto res = QMessageBox::warning(this, tr("Delete file"), tr("Are you sure you want to delete the selected file?"), QMessageBox::Yes | QMessageBox::No); @@ -509,11 +472,74 @@ void DeckPreviewWidget::actDeleteFile() } // write change - if (!QFile::remove(QFileInfo(filePath).filePath())) { + if (!model_->deleteFile(r)) { QMessageBox::critical(this, tr("Error"), tr("Delete failed")); - return; } - // update VDS - this->deleteLater(); + // The folder widget removes this preview once the row is gone. +} + +static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) +{ + QFileInfo fileInfo(filePath); + QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); + + if (QFile::exists(newFileName)) { + QMessageBox::StandardButton reply = + QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), + QObject::tr("A .cod version of this deck already exists. Overwrite it?"), + QMessageBox::Yes | QMessageBox::No); + return reply == QMessageBox::Yes; + } + return true; // Safe to proceed +} + +/** + * Checks if the deck's file format supports tags. + * If not, then prompt the user for file conversion. + * @return whether the resulting file can support adding tags + */ +bool DeckPreviewWidget::promptFileConversionIfRequired() +{ + if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) { + return true; + } + + // Retrieve saved preference if the prompt is disabled + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { + return false; + } + + if (!confirmOverwriteIfExists(this, filePath)) { + return false; + } + + model_->convertToCockatriceFormat(row()); + return true; + } + + // Show the dialog to the user + DialogConvertDeckToCodFormat conversionDialog(this); + if (conversionDialog.exec() != QDialog::Accepted) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( + !conversionDialog.dontAskAgain()); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); + + return false; + } + + // Try to convert file + if (!confirmOverwriteIfExists(this, filePath)) { + return false; + } + + model_->convertToCockatriceFormat(row()); + + if (conversionDialog.dontAskAgain()) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); + } + + return true; } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index de66c194b..e2f744ae0 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -2,16 +2,12 @@ * @file deck_preview_widget.h * @ingroup VisualDeckPreviewWidgets */ -//! \todo Document this file. #ifndef DECK_PREVIEW_WIDGET_H #define DECK_PREVIEW_WIDGET_H -#include "../../../deck_loader/deck_loader.h" -#include "../../cards/additional_info/color_identity_widget.h" #include "../../cards/deck_preview_card_picture_widget.h" -#include "../visual_deck_storage_widget.h" -#include "deck_preview_deck_tags_display_widget.h" +#include "../visual_deck_storage_model.h" #include #include @@ -20,72 +16,80 @@ #include #include +class QEnterEvent; +class QLabel; class QMenu; -class VisualDeckStorageWidget; +class QMouseEvent; +class ColorIdentityWidget; +class DeckPreviewCardPictureWidget; class DeckPreviewDeckTagsDisplayWidget; +class VisualDeckStorageModel; +class VisualDeckStorageWidget; class DeckPreviewWidget final : public QWidget { Q_OBJECT public: - explicit DeckPreviewWidget(QWidget *_parent, + explicit DeckPreviewWidget(QWidget *parent, VisualDeckStorageWidget *_visualDeckStorageWidget, + VisualDeckStorageModel *model, const QString &_filePath); void retranslateUi(); - QString getColorIdentity(); - [[nodiscard]] QString getDisplayName() const; - VisualDeckStorageWidget *visualDeckStorageWidget; - QVBoxLayout *layout; - QString filePath; - QDateTime lastModifiedTime; - DeckLoader *deckLoader; - DeckPreviewCardPictureWidget *bannerCardDisplayWidget = nullptr; - ColorIdentityWidget *colorIdentityWidget = nullptr; - DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget = nullptr; - QLabel *bannerCardLabel = nullptr; - QComboBox *bannerCardComboBox = nullptr; - bool filteredBySearch = false; - bool filteredByColor = false; - bool filteredByTags = false; - [[nodiscard]] bool checkVisibility() const; + /** + * @brief The banner card picture; the parent widget wires its size to the card size setting. + */ + DeckPreviewCardPictureWidget *bannerCardDisplayWidget; signals: void deckLoadRequested(const QString &filePath); void openDeckEditor(const LoadedDeck &deck); public slots: - void setFilePath(const QString &filePath); - void refreshBannerCardText(); - void refreshBannerCardToolTip(); - void updateBannerCardComboBox(const QString ¤tText); - void setBannerCard(int); - void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void initializeUi(bool deckLoadSuccess); - void resyncWidgets(); + /** + * @brief Re-reads the row's data from the model and syncs every child widget. + * Connected to the model's dataChanged signal. + */ + void syncFromModel(); + + /** + * @brief Reloads the deck file if its modification time is newer than the stored one. + */ void reloadIfModified(); - void updateVisibility(); + void refreshBannerCardToolTip(); void updateColorIdentityVisibility(bool visible); void updateBannerCardComboBoxVisibility(bool visible); void updateTagsVisibility(bool visible); - void resizeEvent(QResizeEvent *event) override; + void setBannerCard(int); + void setTags(const QStringList &tags); protected: void enterEvent(QEnterEvent *event) override; + void resizeEvent(QResizeEvent *event) override; private: - void updateLastModifiedTime(); - void writeDeckToFile(); + [[nodiscard]] int row() const; + [[nodiscard]] QString getDisplayName() const; + void refreshBannerCardText(); + void updateBannerCardComboBox(const QString ¤tText); + bool promptFileConversionIfRequired(); QMenu *createRightClickMenu(); void addSetBannerCardMenu(QMenu *menu); - -private slots: - void setTags(const QStringList &tags); + void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); void actRenameDeck(); void actRenameFile(); void actDeleteFile(); + + VisualDeckStorageWidget *visualDeckStorageWidget; + VisualDeckStorageModel *model_; + QString filePath; + QVBoxLayout *layout; + ColorIdentityWidget *colorIdentityWidget; + DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; + QLabel *bannerCardLabel; + QComboBox *bannerCardComboBox; }; class NoScrollFilter : public QObject diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp index cc7f07871..6e1217ccf 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp @@ -1,20 +1,26 @@ #include "visual_deck_storage_folder_display_widget.h" -#include "../../../client/settings/cache_settings.h" +#include "../cards/card_info_picture_widget.h" +#include "../general/display/banner_widget.h" +#include "../general/layout_containers/flow_widget.h" #include "deck_preview/deck_preview_widget.h" +#include "visual_deck_storage_model.h" +#include "visual_deck_storage_quick_settings_widget.h" +#include "visual_deck_storage_sort_filter_proxy_model.h" #include "visual_deck_storage_widget.h" -#include -#include -#include +#include +#include +#include VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( QWidget *parent, VisualDeckStorageWidget *_visualDeckStorageWidget, - QString _filePath, + const QString &_folderPath, bool canBeHidden, bool _showFolders) - : QWidget(parent), showFolders(_showFolders), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath) + : QWidget(parent), showFolders(_showFolders), folderPath(_folderPath), + visualDeckStorageWidget(_visualDeckStorageWidget) { layout = new QVBoxLayout(this); setLayout(layout); @@ -22,6 +28,9 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( header = new BannerWidget(this, ""); header->setClickable(canBeHidden); header->setHidden(!showFolders); + + const QString bannerText = folderPath.isEmpty() ? tr("Deck Storage") : folderPath; + header->setText(bannerText); layout->addWidget(header); container = new QWidget(this); @@ -35,192 +44,214 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); containerLayout->addWidget(flowWidget); - createWidgetsForFiles(); - createWidgetsForFolders(); + auto *proxy = visualDeckStorageWidget->proxyModel(); + // A burst of proxy changes (one dataChanged per finished deck load, plus the filter + // invalidations) coalesces into a single reconcile, so a scan of many decks doesn't + // rebuild the flow layout once per deck. + connect(proxy, &QAbstractItemModel::modelReset, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); + connect(proxy, &QAbstractItemModel::rowsInserted, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); + connect(proxy, &QAbstractItemModel::rowsRemoved, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); + connect(proxy, &QAbstractItemModel::dataChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); + connect(proxy, &QAbstractItemModel::layoutChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile); - refreshUi(); + reconcileTimer = new QTimer(this); + reconcileTimer->setSingleShot(true); + reconcileTimer->setInterval(150); + connect(reconcileTimer, &QTimer::timeout, this, &VisualDeckStorageFolderDisplayWidget::reconcile); + + reconcile(); } -void VisualDeckStorageFolderDisplayWidget::refreshUi() +void VisualDeckStorageFolderDisplayWidget::scheduleReconcile() { - QString bannerText = tr("Deck Storage"); - QString deckPath = SettingsCache::instance().paths().getDeckPath(); - if (filePath != deckPath) { - QString relativePath = filePath; - - if (filePath.startsWith(deckPath)) { - relativePath = filePath.mid(deckPath.length()); // Remove the deckPath prefix - if (relativePath.startsWith('/')) { - relativePath.remove(0, 1); // Remove leading '/' if it exists - } - } - - bannerText = relativePath; - } - header->setText(bannerText); + reconcileTimer->start(); } /** - * Gets all files in the directory that have an accepted decklist file extension - * - * @param filePath The directory to search through - * @param recursive Whether to search through subdirectories + * @brief Re-reads the proxy and rebuilds the deck previews and subfolder widgets to match. */ -static QStringList getAllFiles(const QString &filePath, bool recursive) +void VisualDeckStorageFolderDisplayWidget::reconcile() { - QStringList allFiles; - - // QDirIterator with QDir::Files ensures only files are listed (no directories) - auto flags = - recursive ? QDirIterator::Subdirectories | QDirIterator::FollowSymlinks : QDirIterator::NoIteratorFlags; - QDirIterator it(filePath, DeckLoader::ACCEPTED_FILE_EXTENSIONS, QDir::Files, flags); - - while (it.hasNext()) { - allFiles << it.next(); // Add each file path to the list - } - - return allFiles; + reconcileDecks(); + createSubFolderWidgets(); + refreshVisibility(); } -void VisualDeckStorageFolderDisplayWidget::createWidgetsForFiles() +/** + * @brief Reconciles the deck preview widgets in the flow layout with the accepted proxy rows. + * + * Decks are keyed by file path so that already-created preview widgets are reused + * (their banner combo box, tag editor state, etc. is preserved) whenever possible. + */ +void VisualDeckStorageFolderDisplayWidget::reconcileDecks() { - QList allDecks; - for (const QString &file : getAllFiles(filePath, !showFolders)) { - auto *display = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, file); + auto *proxy = visualDeckStorageWidget->proxyModel(); + auto *model = visualDeckStorageWidget->model(); - connect(display, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget, - &VisualDeckStorageWidget::deckLoadRequested); - connect(display, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget, - &VisualDeckStorageWidget::openDeckEditor); - connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, - display->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor); - display->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize()); - allDecks.append(display); + // Collect the file paths this folder should display, in proxy (sorted) order. + QStringList desiredOrder; + for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { + const QModelIndex sourceIndex = proxy->mapToSource(proxy->index(proxyRow, 0)); + if (!sourceIndex.isValid()) { + continue; + } + + const DeckPreviewData &data = model->dataForRow(sourceIndex.row()); + if (showFolders && data.folderPath != folderPath) { + continue; + } + desiredOrder.append(data.filePath); } - flowWidget->clearLayout(); // Clear existing widgets in the flow layout + const QSet desiredSet(desiredOrder.cbegin(), desiredOrder.cend()); - for (DeckPreviewWidget *deck : allDecks) { - flowWidget->addWidget(deck); + // Drop previews of decks that no longer match. + for (auto it = deckWidgets.begin(); it != deckWidgets.end();) { + if (!desiredSet.contains(it.key())) { + flowWidget->removeWidget(it.value()); + it.value()->deleteLater(); + it = deckWidgets.erase(it); + } else { + ++it; + } + } + + // Create or reuse the preview widgets in the desired order. + QList orderedWidgets; + orderedWidgets.reserve(desiredOrder.size()); + for (const QString &filePath : desiredOrder) { + DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(filePath, nullptr); + if (!deckPreviewWidget) { + deckPreviewWidget = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, model, filePath); + connect(deckPreviewWidget, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget, + &VisualDeckStorageWidget::deckLoadRequested); + connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget, + &VisualDeckStorageWidget::openDeckEditor); + connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, + deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor); + deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor( + visualDeckStorageWidget->settings()->getCardSize()); + deckWidgets.insert(filePath, deckPreviewWidget); + } + orderedWidgets.append(deckPreviewWidget); + } + + // Re-add all widgets so the flow layout order matches the proxy order. Skipped when the + // order is unchanged so that data-only updates don't invalidate the flow layout. + if (desiredOrder != lastOrderedFilePaths) { + for (DeckPreviewWidget *deckPreviewWidget : orderedWidgets) { + flowWidget->removeWidget(deckPreviewWidget); + } + for (DeckPreviewWidget *deckPreviewWidget : orderedWidgets) { + flowWidget->addWidget(deckPreviewWidget); + } + lastOrderedFilePaths = desiredOrder; } } /** - * Updates the visibility of this folder and all its DeckPreviewWidgets + * @brief Creates, removes and keeps in sync the subfolder widgets of this folder. * - * @param recursive Also update the visibility of all subfolders and their DeckPreviewWidgets. + * Only direct child folders are created here; each child manages its own + * children, mirroring the folder tree on disk. */ -void VisualDeckStorageFolderDisplayWidget::updateVisibility(bool recursive) -{ - bool atLeastOneWidgetVisible = checkVisibility(); - if (atLeastOneWidgetVisible) { - setVisible(true); - for (DeckPreviewWidget *display : flowWidget->findChildren()) { - display->updateVisibility(); - } - if (recursive) { - for (auto *subFolder : findChildren()) { - subFolder->updateVisibility(false); - } - } - } else { - setVisible(false); - } -} - -bool VisualDeckStorageFolderDisplayWidget::checkVisibility() -{ - bool atLeastOneWidgetVisible = false; - if (flowWidget) { - // Iterate through all DeckPreviewWidgets - for (DeckPreviewWidget *display : flowWidget->findChildren()) { - if (display->checkVisibility()) { - atLeastOneWidgetVisible = true; - } - } - } - for (VisualDeckStorageFolderDisplayWidget *subFolder : findChildren()) { - if (subFolder->checkVisibility()) { - atLeastOneWidgetVisible = true; - } - } - return atLeastOneWidgetVisible; -} - -static QStringList getAllSubFolders(const QString &filePath) -{ - QStringList allFolders; - - // QDirIterator with QDir::Files ensures only files are listed (no directories) - QDirIterator it(filePath, QDir::Dirs | QDir::NoDotAndDotDot); - - while (it.hasNext()) { - allFolders << it.next(); // Add each file path to the list - } - - return allFolders; -} - -void VisualDeckStorageFolderDisplayWidget::createWidgetsForFolders() +void VisualDeckStorageFolderDisplayWidget::createSubFolderWidgets() { if (!showFolders) { return; } - for (const QString &dir : getAllSubFolders(filePath)) { - auto *display = new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, dir, true, showFolders); - containerLayout->addWidget(display); + const QStringList children = childFolderPaths(); + + for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end();) { + if (!children.contains(it.key())) { + containerLayout->removeWidget(it.value()); + it.value()->deleteLater(); + it = subFolderWidgets.erase(it); + } else { + ++it; + } + } + + for (const QString &child : children) { + if (subFolderWidgets.contains(child)) { + continue; + } + + auto *subFolderWidget = + new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, child, true, showFolders); + connect(subFolderWidget, &VisualDeckStorageFolderDisplayWidget::contentVisibilityChanged, this, + &VisualDeckStorageFolderDisplayWidget::refreshVisibility); + containerLayout->addWidget(subFolderWidget); + subFolderWidgets.insert(child, subFolderWidget); } } void VisualDeckStorageFolderDisplayWidget::updateShowFolders(bool enabled) { showFolders = enabled; + header->setHidden(!showFolders); if (!showFolders) { - flattenFolderStructure(); - } else { - // if setting was switched from disabled to enabled, we assume that there aren't any existing subfolders - createWidgetsForFiles(); - createWidgetsForFolders(); + for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end(); ++it) { + containerLayout->removeWidget(it.value()); + it.value()->deleteLater(); + } + subFolderWidgets.clear(); } - header->setHidden(!showFolders); + reconcile(); } /** - * Steals all DeckPreviewWidgets from this widget's nested subfolders, and deletes those subfolders + * @brief Hides the folder when it contains nothing visible, and reports the change upward. */ -void VisualDeckStorageFolderDisplayWidget::flattenFolderStructure() +void VisualDeckStorageFolderDisplayWidget::refreshVisibility() { - for (auto *subFolder : findChildren()) { - // steal all DeckPreviewWidgets from the subfolder - for (auto *deck : subFolder->getFlowWidget()->findChildren()) { - flowWidget->addWidget(deck); - } - - // delete the subfolder - subFolder->deleteLater(); + const bool shouldBeVisible = hasContent(); + if (isHidden() == !shouldBeVisible) { + return; } + setHidden(!shouldBeVisible); + emit contentVisibilityChanged(); } -QStringList VisualDeckStorageFolderDisplayWidget::gatherAllTagsFromFlowWidget() const +/** + * @brief Whether this folder shows any deck previews or has any visible subfolder. + */ +bool VisualDeckStorageFolderDisplayWidget::hasContent() const { - QStringList allTags; + if (!deckWidgets.isEmpty()) { + return true; + } - if (flowWidget) { - // Iterate through all DeckPreviewWidgets - for (DeckPreviewWidget *display : flowWidget->findChildren()) { - // Get tags from each DeckPreviewWidget - QStringList tags = display->deckLoader->getDeck().deckList.getTags(); - - // Add tags to the list while avoiding duplicates - allTags.append(tags); + for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : subFolderWidgets) { + if (subFolderWidget->hasContent()) { + return true; } } - // Remove duplicates by calling 'removeDuplicates' - allTags.removeDuplicates(); + return false; +} - return allTags; -} \ No newline at end of file +/** + * @brief The direct child folder paths of this folder, sorted by name. + */ +QStringList VisualDeckStorageFolderDisplayWidget::childFolderPaths() const +{ + QStringList children; + const QString prefix = folderPath.isEmpty() ? QString() : folderPath + "/"; + + for (const QString &candidate : visualDeckStorageWidget->model()->getFolderPaths()) { + if (!candidate.startsWith(prefix)) { + continue; + } + const QString rest = candidate.mid(prefix.length()); + if (rest.isEmpty() || rest.contains('/')) { + continue; + } + children.append(candidate); + } + + return children; +} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h index a5e3be212..84e12a0ae 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h @@ -1,49 +1,79 @@ /** * @file visual_deck_storage_folder_display_widget.h * @ingroup VisualDeckStorageWidgets + * @brief Renders the decks of one folder of the Visual Deck Storage. + * + * This is a pure view: it reads the accepted rows of the + * VisualDeckStorageSortFilterProxyModel whose folder matches this widget's + * folder path, and keeps a set of child DeckPreviewWidgets in sync with those + * rows. Subfolders are shown as nested VisualDeckStorageFolderDisplayWidgets + * when the "show folders" setting is enabled. */ -//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H #define VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H -#include "../general/display/banner_widget.h" -#include "../general/layout_containers/flow_widget.h" +#include +#include +#include +class BannerWidget; +class DeckPreviewWidget; +class FlowWidget; +class QTimer; +class QVBoxLayout; class VisualDeckStorageWidget; + class VisualDeckStorageFolderDisplayWidget : public QWidget { Q_OBJECT public: VisualDeckStorageFolderDisplayWidget(QWidget *parent, - VisualDeckStorageWidget *_visualDeckStorageWidget, - QString _filePath, + VisualDeckStorageWidget *visualDeckStorageWidget, + const QString &folderPath, bool canBeHidden, - bool _showFolders); - void refreshUi(); - void createWidgetsForFiles(); - void createWidgetsForFolders(); - void flattenFolderStructure(); - [[nodiscard]] QStringList gatherAllTagsFromFlowWidget() const; - [[nodiscard]] FlowWidget *getFlowWidget() const - { - return flowWidget; - } + bool showFolders); public slots: - void updateVisibility(bool recursive = true); - bool checkVisibility(); + /** + * @brief Re-reads the proxy and rebuilds the deck previews and subfolder + * widgets to match. + */ + void reconcile(); + + /** + * @brief Coalesces proxy change signals (a burst of deck loads or filter + * invalidations) into a single reconcile on the next event loop turn. + */ + void scheduleReconcile(); void updateShowFolders(bool enabled); +signals: + /** + * @brief Emitted whenever this folder's visible content changes, so parent + * folders can re-evaluate their own visibility. + */ + void contentVisibilityChanged(); + private: + void reconcileDecks(); + void createSubFolderWidgets(); + void refreshVisibility(); + [[nodiscard]] bool hasContent() const; + [[nodiscard]] QStringList childFolderPaths() const; + bool showFolders; + QString folderPath; ///< Path relative to the deck folder; empty for the root folder. QVBoxLayout *layout; - VisualDeckStorageWidget *visualDeckStorageWidget; - QString filePath; - BannerWidget *header; QWidget *container; QVBoxLayout *containerLayout; FlowWidget *flowWidget; + BannerWidget *header; + VisualDeckStorageWidget *visualDeckStorageWidget; + QHash deckWidgets; ///< Deck file path -> preview widget. + QHash subFolderWidgets; ///< Folder path -> subfolder widget. + QTimer *reconcileTimer = nullptr; ///< Coalesces proxy change bursts. + QStringList lastOrderedFilePaths; ///< The deck order last applied to the flow layout. }; #endif // VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp index cce3ff6ce..478431703 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -1,6 +1,7 @@ #include "visual_deck_storage_quick_settings_widget.h" #include "../../../client/settings/cache_settings.h" +#include "../cards/card_size_widget.h" #include "visual_deck_storage_widget.h" #include diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp index 0580126c4..428cbd5f4 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp @@ -1,21 +1,19 @@ #include "visual_deck_storage_search_widget.h" -#include "../../../client/settings/cache_settings.h" -#include "../../../filters/deck_filter_string.h" #include "../../../filters/syntax_help.h" #include "../../pixel_map_generator.h" +#include "visual_deck_storage_widget.h" #include -#include -#include +#include /** - * @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code. + * @brief Constructs a search bar for filtering decks in the Visual Deck Storage. * - * This widget provides a search bar that allows users to search for cards by either their set name - * or set code. It uses a debounced timer to trigger the search action after the user stops typing. + * Provides a search bar that allows users to search decks by filename or search + * expression, with a debounced timer to trigger the search after the user stops typing. * - * @param parent The parent PrintingSelector widget that will handle the search results. + * @param parent The VisualDeckStorageWidget owning the search. */ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent) : parent(parent) { @@ -40,47 +38,6 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi searchDebounceTimer->start(300); // 300ms debounce }); - connect(searchDebounceTimer, &QTimer::timeout, parent, &VisualDeckStorageWidget::updateSearchFilter); -} - -/** - * @brief Retrieves the current text in the search bar. - * - * @return The text entered by the user in the search bar. - */ -QString VisualDeckStorageSearchWidget::getSearchText() -{ - return searchBar->text(); -} - -/** - * Converts the filepath into a relative filepath starting from the deck folder. - * If the file isn't in the deck folder, then this will just return the filename. - * - * @param filePath The filepath to convert into a relative filepath - */ -static QString toRelativeFilepath(const QString &filePath) -{ - QString deckPath = SettingsCache::instance().paths().getDeckPath(); - if (filePath.startsWith(deckPath)) { - return filePath.mid(deckPath.length()); - } - - QFileInfo fileInfo(filePath); - QString fileName = fileInfo.fileName(); - return fileName; -} - -void VisualDeckStorageSearchWidget::filterWidgets(QList widgets, const QString &searchText) -{ - const auto filterString = DeckFilterString(searchText); - - for (auto widget : widgets) { - const DeckSearchData searchData{.deck = &widget->deckLoader->getDeck(), - .filePath = widget->filePath, - .displayName = widget->getDisplayName(), - .relativeFilePath = toRelativeFilepath(widget->filePath)}; - - widget->filteredBySearch = !filterString.check(searchData); - } + connect(searchDebounceTimer, &QTimer::timeout, this, + [this] { this->parent->proxyModel()->setSearchText(searchBar->text()); }); } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h index 2f3d81aeb..b674f28e2 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_search_widget.h @@ -2,17 +2,15 @@ * @file visual_deck_storage_search_widget.h * @ingroup VisualDeckStorageWidgets */ -//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_SEARCH_WIDGET_H #define VISUAL_DECK_STORAGE_SEARCH_WIDGET_H -#include "deck_preview/deck_preview_widget.h" - #include #include #include +class QTimer; class VisualDeckStorageWidget; class VisualDeckStorageSearchWidget : public QWidget { @@ -20,8 +18,6 @@ class VisualDeckStorageSearchWidget : public QWidget public: explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent); - QString getSearchText(); - void filterWidgets(QList widgets, const QString &searchText); private: QHBoxLayout *layout; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp index e4eedb078..6289a4941 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp @@ -1,20 +1,11 @@ #include "visual_deck_storage_sort_widget.h" #include "../../../client/settings/cache_settings.h" +#include "visual_deck_storage_widget.h" -#include #include -/** - * @brief Constructs a PrintingSelectorCardSortWidget for searching cards by set name or set code. - * - * This widget provides a search bar that allows users to search for cards by either their set name - * or set code. It uses a debounced timer to trigger the search action after the user stops typing. - * - * @param parent The parent PrintingSelector widget that will handle the search results. - */ -VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) - : parent(parent), sortOrder(Alphabetical) +VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) : QWidget(parent) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -30,12 +21,10 @@ VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget // Set the current sort order sortComboBox->setCurrentIndex(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSortingOrder()); - sortOrder = static_cast(sortComboBox->currentIndex()); - // Connect sorting change signal to refresh the file list + // Connect sorting change signal to persist the order and refresh the file list connect(sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &VisualDeckStorageSortWidget::updateSortOrder); - connect(this, &VisualDeckStorageSortWidget::sortOrderChanged, parent, &VisualDeckStorageWidget::updateSortOrder); } void VisualDeckStorageSortWidget::retranslateUi() @@ -47,10 +36,13 @@ void VisualDeckStorageSortWidget::retranslateUi() // Clear and repopulate the ComboBox with translated items sortComboBox->clear(); - sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"), ByName); - sortComboBox->addItem(tr("Sort Alphabetically (Filename)"), Alphabetical); - sortComboBox->addItem(tr("Sort by Last Modified"), ByLastModified); - sortComboBox->addItem(tr("Sort by Last Loaded"), ByLastLoaded); + sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"), + VisualDeckStorageSortFilterProxyModel::SortOrder::ByName); + sortComboBox->addItem(tr("Sort Alphabetically (Filename)"), + VisualDeckStorageSortFilterProxyModel::SortOrder::Alphabetical); + sortComboBox->addItem(tr("Sort by Last Modified"), + VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastModified); + sortComboBox->addItem(tr("Sort by Last Loaded"), VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastLoaded); // Restore the current index sortComboBox->setCurrentIndex(oldIndex); @@ -59,60 +51,13 @@ void VisualDeckStorageSortWidget::retranslateUi() sortComboBox->blockSignals(false); } +VisualDeckStorageSortFilterProxyModel::SortOrder VisualDeckStorageSortWidget::currentSortOrder() const +{ + return static_cast(sortComboBox->currentIndex()); +} + void VisualDeckStorageSortWidget::updateSortOrder() { - sortOrder = static_cast(sortComboBox->currentIndex()); SettingsCache::instance().visualDeckStorage().setVisualDeckStorageSortingOrder(sortComboBox->currentIndex()); emit sortOrderChanged(); } - -void VisualDeckStorageSortWidget::sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget) -{ - auto children = - folderWidget->getFlowWidget()->findChildren(QString(), Qt::FindChildOption::FindDirectChildrenOnly); - for (auto widget : children) { - auto deckPreviewWidgets = - widget->findChildren(QString(), Qt::FindChildOption::FindDirectChildrenOnly); - auto newOrder = filterFiles(deckPreviewWidgets); - for (DeckPreviewWidget *previewWidget : newOrder) { - folderWidget->getFlowWidget()->removeWidget(previewWidget); - } - for (DeckPreviewWidget *previewWidget : newOrder) { - folderWidget->getFlowWidget()->addWidget(previewWidget); - } - } -} - -QList VisualDeckStorageSortWidget::filterFiles(QList widgets) -{ - // Sort the widgets list based on the current sort order - std::sort(widgets.begin(), widgets.end(), [this](DeckPreviewWidget *widget1, DeckPreviewWidget *widget2) { - if (!widget1 || !widget2) { - return false; // Handle null pointers gracefully - } - - QFileInfo info1(widget1->filePath); - QFileInfo info2(widget2->filePath); - - switch (sortOrder) { - case ByName: - return widget1->deckLoader->getDeck().deckList.getName() < - widget2->deckLoader->getDeck().deckList.getName(); - case Alphabetical: - return QString::localeAwareCompare(info1.fileName(), info2.fileName()) <= 0; - case ByLastModified: - return info1.lastModified() > info2.lastModified(); - case ByLastLoaded: { - QDateTime time1 = - QDateTime::fromString(widget1->deckLoader->getDeck().deckList.getLastLoadedTimestamp()); - QDateTime time2 = - QDateTime::fromString(widget2->deckLoader->getDeck().deckList.getLastLoadedTimestamp()); - return time1 > time2; - } - } - - return false; // Default case, no sorting applied - }); - - return widgets; -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h index 24eddba33..633924d84 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_widget.h @@ -2,19 +2,17 @@ * @file visual_deck_storage_sort_widget.h * @ingroup VisualDeckStorageWidgets */ -//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_SORT_WIDGET_H #define VISUAL_DECK_STORAGE_SORT_WIDGET_H -#include "visual_deck_storage_widget.h" +#include "visual_deck_storage_sort_filter_proxy_model.h" #include #include #include class VisualDeckStorageWidget; -class VisualDeckStorageFolderDisplayWidget; class VisualDeckStorageSortWidget : public QWidget { Q_OBJECT @@ -22,25 +20,23 @@ class VisualDeckStorageSortWidget : public QWidget public: explicit VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent); void retranslateUi(); - void updateSortOrder(); - void sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget); - QString getSearchText(); - QList filterFiles(QList widgets); + + /** + * @brief The currently selected sort order. + */ + [[nodiscard]] VisualDeckStorageSortFilterProxyModel::SortOrder currentSortOrder() const; signals: + /** + * @brief Emitted when the user picks a different sort order. + */ void sortOrderChanged(); +private slots: + void updateSortOrder(); + private: - enum SortOrder - { - ByName, - Alphabetical, - ByLastModified, - ByLastLoaded, - }; QHBoxLayout *layout; - VisualDeckStorageWidget *parent; - SortOrder sortOrder; // Current sorting option QComboBox *sortComboBox; }; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp index c4c8d18a8..33740e01a 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp @@ -18,7 +18,7 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto setFixedHeight(100); - auto *flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); } @@ -29,45 +29,26 @@ void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event) refreshTags(); } -void VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList &deckPreviews) const +/** + * @brief The tags of all decks currently accepted by the proxy model. + */ +QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const { - QStringList selectedTags; - QStringList excludedTags; + QSet allTags; + auto *proxy = parent->proxyModel(); + auto *model = parent->model(); - // Collect selected and excluded tags - for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { - switch (tagWidget->getState()) { - case TagState::Selected: - selectedTags.append(tagWidget->getTagName()); - break; - case TagState::Excluded: - excludedTags.append(tagWidget->getTagName()); - break; - default: - break; + for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { + const QModelIndex sourceIndex = proxy->mapToSource(proxy->index(proxyRow, 0)); + if (!sourceIndex.isValid()) { + continue; + } + for (const QString &tag : model->dataForRow(sourceIndex.row()).tags) { + allTags.insert(tag); } } - // If no tags are selected or excluded, show all - if (selectedTags.isEmpty() && excludedTags.isEmpty()) { - for (DeckPreviewWidget *deckPreview : deckPreviews) { - deckPreview->filteredByTags = false; - } - return; - } - - for (DeckPreviewWidget *deckPreview : deckPreviews) { - QStringList deckTags = deckPreview->deckLoader->getDeck().deckList.getTags(); - - bool hasAllSelected = std::all_of(selectedTags.begin(), selectedTags.end(), - [&deckTags](const QString &tag) { return deckTags.contains(tag); }); - - bool hasAnyExcluded = std::any_of(excludedTags.begin(), excludedTags.end(), - [&deckTags](const QString &tag) { return deckTags.contains(tag); }); - - // Filter out if any excluded tag is present or if any selected tag is missing - deckPreview->filteredByTags = !(hasAllSelected && !hasAnyExcluded); - } + return allTags; } void VisualDeckStorageTagFilterWidget::refreshTags() @@ -80,8 +61,6 @@ void VisualDeckStorageTagFilterWidget::refreshTags() void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet &tags) { - auto *flowWidget = findChild(); - for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { const QString &tagName = tagWidget->getTagName(); @@ -116,20 +95,12 @@ void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag) auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag); connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent, &VisualDeckStorageWidget::updateTagFilter); - connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this, - &VisualDeckStorageTagFilterWidget::refreshTags); - auto *flowWidget = findChild(); flowWidget->addWidget(newTagWidget); } } void VisualDeckStorageTagFilterWidget::sortTags() { - auto *flowWidget = findChild(); - if (!flowWidget) { - return; - } - // Get all tag widgets QList tagWidgets = findChildren(); @@ -147,19 +118,26 @@ void VisualDeckStorageTagFilterWidget::sortTags() } } -QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const +QStringList VisualDeckStorageTagFilterWidget::selectedTags() const { - QSet allTags; - QList deckWidgets = parent->findChildren(); - - for (DeckPreviewWidget *widget : deckWidgets) { - if (widget->checkVisibility()) { - for (const QString &tag : widget->deckLoader->getDeck().deckList.getTags()) { - allTags.insert(tag); - } + QStringList selected; + for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { + if (tagWidget->getState() == TagState::Selected) { + selected.append(tagWidget->getTagName()); } } - return allTags; + return selected; +} + +QStringList VisualDeckStorageTagFilterWidget::excludedTags() const +{ + QStringList excluded; + for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { + if (tagWidget->getState() == TagState::Excluded) { + excluded.append(tagWidget->getTagName()); + } + } + return excluded; } QStringList VisualDeckStorageTagFilterWidget::getAllKnownTags() const diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h index 3290c9e9a..337c053c7 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h @@ -2,21 +2,22 @@ * @file visual_deck_storage_tag_filter_widget.h * @ingroup VisualDeckStorageWidgets */ -//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H #define VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H -#include "deck_preview/deck_preview_widget.h" - +#include +#include #include +class FlowWidget; class VisualDeckStorageWidget; class VisualDeckStorageTagFilterWidget : public QWidget { Q_OBJECT VisualDeckStorageWidget *parent; + FlowWidget *flowWidget; [[nodiscard]] QSet gatherAllTags() const; void removeTagsNotInList(const QSet &tags); @@ -27,9 +28,21 @@ class VisualDeckStorageTagFilterWidget : public QWidget public: explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent); [[nodiscard]] QStringList getAllKnownTags() const; - void filterDecksBySelectedTags(const QList &deckPreviews) const; + + /** + * @brief The tags currently in "selected" state. + */ + [[nodiscard]] QStringList selectedTags() const; + + /** + * @brief The tags currently in "excluded" state. + */ + [[nodiscard]] QStringList excludedTags() const; public slots: + /** + * @brief Rebuilds the tag chips from the tags of the currently visible decks. + */ void refreshTags(); void showEvent(QShowEvent *event) override; }; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index 4b4dee55b..1bd1148d7 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -2,24 +2,28 @@ #include "../../../client/settings/cache_settings.h" #include "../quick_settings/settings_button_widget.h" +#include "deck_preview/deck_preview_color_identity_filter_widget.h" #include "deck_preview/deck_preview_widget.h" #include "visual_deck_storage_folder_display_widget.h" +#include "visual_deck_storage_quick_settings_widget.h" #include "visual_deck_storage_search_widget.h" #include "visual_deck_storage_sort_widget.h" #include "visual_deck_storage_tag_filter_widget.h" -#include -#include -#include +#include +#include #include #include #include #include -VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent), folderWidget(nullptr) +VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent) { - deckListModel = new DeckListModel(this); - deckListModel->setObjectName("visualDeckModel"); + // The model and proxy own all deck data, sorting and filtering. The view widgets below only + // display the proxy's accepted rows, so nothing touches the filesystem outside the model. + model_ = new VisualDeckStorageModel(this); + proxyModel_ = new VisualDeckStorageSortFilterProxyModel(this); + proxyModel_->setSourceModel(model_); layout = new QVBoxLayout(this); layout->setSpacing(0); @@ -75,6 +79,25 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare layout->addWidget(tagFilterWidget); layout->addWidget(scrollArea); + // The deck data changed (a load finished, or a mutation happened): re-evaluate the filters, + // since search/tag/color matches are computed against the data. Debounced so that a burst of + // load completions triggers a single re-application instead of one O(n) pass per deck. + refreshTimer = new QTimer(this); + refreshTimer->setSingleShot(true); + refreshTimer->setInterval(150); + connect(refreshTimer, &QTimer::timeout, this, [this] { + proxyModel_->reapplyFilters(); + // A batch of decks finished loading: re-gather the tag chips from the visible decks once + // the burst settles instead of on every individual load. + tagFilterWidget->refreshTags(); + }); + connect(model_, &QAbstractItemModel::dataChanged, this, [this] { refreshTimer->start(); }); + connect(model_, &VisualDeckStorageModel::deckLoaded, this, [this] { refreshTimer->start(); }); + // A deck's file path changed: re-apply the sort, since orders like "filename" depend on it. + connect(model_, &VisualDeckStorageModel::deckFilePathChanged, this, [this] { proxyModel_->resort(); }); + connect(sortWidget, &VisualDeckStorageSortWidget::sortOrderChanged, this, + &VisualDeckStorageWidget::updateSortOrder); + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, &VisualDeckStorageWidget::createRootFolderWidget); @@ -123,6 +146,8 @@ void VisualDeckStorageWidget::retranslateUi() refreshButton->setToolTip(tr("Refresh loaded files")); quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings")); + + sortWidget->retranslateUi(); } /** @@ -134,72 +159,52 @@ const VisualDeckStorageQuickSettingsWidget *VisualDeckStorageWidget::settings() } /** - * Reapplies all sort and filter options by calling the appropriate update methods. + * Reapplies all sort and filter options by updating the proxy model. */ void VisualDeckStorageWidget::reapplySortAndFilters() { - updateSortOrder(); - updateTagFilter(); - updateColorFilter(); - updateSearchFilter(); + proxyModel_->setSortOrder(sortWidget->currentSortOrder()); + proxyModel_->reapplyFilters(); } +/** + * @brief Scans the deck folder and rebuilds the folder tree of deck previews. + */ void VisualDeckStorageWidget::createRootFolderWidget() { - folderWidget = new VisualDeckStorageFolderDisplayWidget(this, this, SettingsCache::instance().paths().getDeckPath(), - false, quickSettingsWidget->getShowFolders()); + model_->setDeckPath(SettingsCache::instance().paths().getDeckPath()); + + folderWidget = + new VisualDeckStorageFolderDisplayWidget(this, this, QString(), false, quickSettingsWidget->getShowFolders()); scrollArea->setWidget(folderWidget); // this automatically destroys the old folderWidget scrollArea->widget()->setMaximumWidth(scrollArea->viewport()->width()); scrollArea->widget()->adjustSize(); - /* We have to schedule a QTimer here so that the sorting logic doesn't try to access widgets that haven't been - * processed by the event loop yet. Otherwise, deck sorting will intermittently segfault on some systems. - */ - QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters); + // Sort and filter runs against the model data, so it is safe to apply immediately. + reapplySortAndFilters(); } void VisualDeckStorageWidget::updateShowFolders(bool enabled) { if (folderWidget) { folderWidget->updateShowFolders(enabled); - QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters); } } void VisualDeckStorageWidget::updateSortOrder() { - if (folderWidget) { - sortWidget->sortFolder(folderWidget); - for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : - folderWidget->findChildren()) { - sortWidget->sortFolder(subFolderWidget); - } - } + proxyModel_->setSortOrder(sortWidget->currentSortOrder()); } void VisualDeckStorageWidget::updateTagFilter() { - if (folderWidget) { - tagFilterWidget->filterDecksBySelectedTags(folderWidget->findChildren()); - folderWidget->updateVisibility(); - } -} - -void VisualDeckStorageWidget::updateColorFilter() -{ - if (folderWidget) { - deckPreviewColorIdentityFilterWidget->filterWidgets(folderWidget->findChildren()); - folderWidget->updateVisibility(); - } -} - -void VisualDeckStorageWidget::updateSearchFilter() -{ - if (folderWidget) { - searchWidget->filterWidgets(folderWidget->findChildren(), searchWidget->getSearchText()); - folderWidget->updateVisibility(); - } + const QStringList selected = tagFilterWidget->selectedTags(); + const QStringList excluded = tagFilterWidget->excludedTags(); + proxyModel_->setTagFilter(QSet(selected.cbegin(), selected.cend()), + QSet(excluded.cbegin(), excluded.cend())); + // The visible deck set changed, so the chips are re-gathered from it. + tagFilterWidget->refreshTags(); } void VisualDeckStorageWidget::updateTagsVisibility(const bool visible) @@ -215,4 +220,4 @@ void VisualDeckStorageWidget::updateTagsVisibility(const bool visible) void VisualDeckStorageWidget::updateSelectionAnimationEnabled(const bool enabled) { deckPreviewSelectionAnimationEnabled = enabled; -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h index c3c0ae91b..8a8c19066 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h @@ -2,29 +2,30 @@ * @file visual_deck_storage_widget.h * @ingroup VisualDeckStorageWidgets */ -//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_WIDGET_H #define VISUAL_DECK_STORAGE_WIDGET_H -#include "../../deck_loader/deck_loader.h" -#include "../cards/card_size_widget.h" -#include "../quick_settings/settings_button_widget.h" -#include "deck_preview/deck_preview_color_identity_filter_widget.h" -#include "visual_deck_storage_folder_display_widget.h" -#include "visual_deck_storage_quick_settings_widget.h" -#include "visual_deck_storage_search_widget.h" -#include "visual_deck_storage_sort_widget.h" -#include "visual_deck_storage_tag_filter_widget.h" +#include "visual_deck_storage_model.h" +#include "visual_deck_storage_sort_filter_proxy_model.h" -#include +#include +#include +#include +#include +#include -class QSpinBox; +class QLabel; +class QResizeEvent; +class QShowEvent; +class QTimer; +class DeckPreviewColorIdentityFilterWidget; +class VisualDeckStorageFolderDisplayWidget; +class VisualDeckStorageQuickSettingsWidget; class VisualDeckStorageSearchWidget; class VisualDeckStorageSortWidget; class VisualDeckStorageTagFilterWidget; -class VisualDeckStorageFolderDisplayWidget; -class DeckPreviewColorIdentityFilterWidget; + class VisualDeckStorageWidget final : public QWidget { Q_OBJECT @@ -37,29 +38,41 @@ public: bool deckPreviewSelectionAnimationEnabled; [[nodiscard]] const VisualDeckStorageQuickSettingsWidget *settings() const; + [[nodiscard]] VisualDeckStorageModel *model() const + { + return model_; + } + [[nodiscard]] VisualDeckStorageSortFilterProxyModel *proxyModel() const + { + return proxyModel_; + } public slots: - void createRootFolderWidget(); // Refresh the display of cards based on the current sorting option + /** + * @brief Starts scanning the deck folder and rebuilds the folder tree and previews. + */ + void createRootFolderWidget(); void updateShowFolders(bool enabled); - void updateTagFilter(); - void updateColorFilter(); - void updateSearchFilter(); void updateTagsVisibility(bool visible); void updateSelectionAnimationEnabled(bool enabled); void updateSortOrder(); + void updateTagFilter(); + +signals: + void deckLoadRequested(const QString &filePath); + void openDeckEditor(const LoadedDeck &deck); + +protected: void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; -signals: - void bannerCardsRefreshed(); - void deckLoadRequested(const QString &filePath); - void openDeckEditor(const LoadedDeck &deck); +private: + void reapplySortAndFilters(); private: QVBoxLayout *layout; QWidget *searchAndSortContainer; QHBoxLayout *searchAndSortLayout; - DeckListModel *deckListModel; QLabel *databaseLoadIndicator; VisualDeckStorageSortWidget *sortWidget; VisualDeckStorageSearchWidget *searchWidget; @@ -67,9 +80,10 @@ private: QToolButton *refreshButton; VisualDeckStorageQuickSettingsWidget *quickSettingsWidget; QScrollArea *scrollArea; - VisualDeckStorageFolderDisplayWidget *folderWidget; - - void reapplySortAndFilters(); + VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr; + VisualDeckStorageModel *model_ = nullptr; + VisualDeckStorageSortFilterProxyModel *proxyModel_ = nullptr; + QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads. }; #endif // VISUAL_DECK_STORAGE_WIDGET_H