diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index c00f1b9ce..7690fb32a 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -230,7 +230,6 @@ set(cockatrice_SOURCES src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp src/interface/widgets/general/display/charts/pies/color_pie.cpp src/interface/widgets/general/home_styled_button.cpp - src/interface/widgets/general/home_tab_button_color.h src/interface/widgets/general/home_widget.cpp src/interface/widgets/general/layout_containers/flow_widget.cpp src/interface/widgets/general/layout_containers/overlap_control_widget.cpp diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index bc07ac183..23ed4316d 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -9,7 +9,6 @@ #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/cards/additional_info/color_identity_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp index 1ea1bcb10..a4cb86751 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp @@ -79,6 +79,8 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); + // Layout passes resize this widget repeatedly with identical sizes, so bail out before + // touching the children when neither the width nor the resulting symbol size changed. const int totalWidth = event->size().width(); if (totalWidth == lastWidth && lastIconSize != -1) { return; @@ -88,12 +90,13 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event) const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width setFixedHeight(totalHeight); - const int count = layout->count(); - if (count == 0) { + QList manaSymbols = findChildren(); + if (manaSymbols.isEmpty()) { return; } const int spacing = layout->spacing(); + const int count = manaSymbols.size(); const int availableWidth = totalWidth - (spacing * (count - 1)); const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height @@ -102,10 +105,8 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event) } lastIconSize = iconSize; - for (int i = 0; i < count; ++i) { - if (auto *w = qobject_cast(layout->itemAt(i)->widget())) { - w->setFixedSize(iconSize, iconSize); - } + for (ManaSymbolWidget *manaSymbol : manaSymbols) { + manaSymbol->setFixedSize(iconSize, iconSize); } } 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 9db01e2e5..a55056bda 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,11 +15,8 @@ #include "deck_list_history_manager_widget.h" #include "deck_list_style_proxy.h" -#include -#include #include #include -#include #include #include #include diff --git a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp index 31f384cac..5de5457ea 100644 --- a/cockatrice/src/interface/widgets/general/display/banner_widget.cpp +++ b/cockatrice/src/interface/widgets/general/display/banner_widget.cpp @@ -32,7 +32,6 @@ BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation // Set minimum height for the widget setMinimumHeight(50); - setMaximumHeight(100); connect(this, &BannerWidget::buddyVisibilityChanged, this, &BannerWidget::toggleBuddyVisibility); updateDropdownIconState(); diff --git a/cockatrice/src/interface/widgets/general/home_tab_button_color.h b/cockatrice/src/interface/widgets/general/home_tab_button_color.h deleted file mode 100644 index 1550b57e7..000000000 --- a/cockatrice/src/interface/widgets/general/home_tab_button_color.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef COCKATRICE_HOME_TAB_BUTTON_COLOR_H -#define COCKATRICE_HOME_TAB_BUTTON_COLOR_H - -#include - -namespace HomeTabButtonColor -{ - -/** - * @brief Where to get the colors for the home tab buttons from - */ -enum Source -{ - Automatic, ///< Extract color from background, or use theme color if no background - FromBackground, ///< Always extract color from background -}; - -struct Entry -{ - Source source; - const char *trKey; ///< key for translation -}; - -inline QList all() -{ - static QList entries = {{Automatic, QT_TR_NOOP("Automatic")}, - {FromBackground, QT_TR_NOOP("Extract from background")}}; - - return entries; -} - -/** - * Safely converts an int into the corresponding Source. - * - * @param value The int value - * @return The Source. Returns Source::Automatic if the value is not within range - */ -inline Source intToSource(int value) -{ - if (value > FromBackground) { - return Automatic; // default - } - - return static_cast(value); -} - -} // namespace HomeTabButtonColor - -#endif // COCKATRICE_HOME_TAB_BUTTON_COLOR_H diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 10fcdcb43..91f0d12b5 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -7,7 +7,6 @@ #include "../cards/art_crop_attribution.h" #include "background_sources.h" #include "home_styled_button.h" -#include "home_tab_button_color.h" #include #include @@ -26,7 +25,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) backgroundSourceCard = new CardInfoPictureArtCropWidget(this); - gradientColors = determineButtonColor(); + gradientColors = extractDominantColors(background); layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter); @@ -56,8 +55,6 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) &HomeWidget::initializeBackgroundFromSource); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::updateButtonsToBackgroundColor); - connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this, - &HomeWidget::updateButtonsToBackgroundColor); } void HomeWidget::initializeBackgroundFromSource() @@ -100,34 +97,6 @@ void HomeWidget::loadBackgroundSourceDeck() backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList(); } -static bool isDefaultBackgroundAndTheme() -{ - QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); - return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme; -} - -QPair HomeWidget::determineButtonColor() const -{ - static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)}; - - auto colorSource = - HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex()); - - switch (colorSource) { - case HomeTabButtonColor::Automatic: { - if (isDefaultBackgroundAndTheme()) { - return defaultColor; - } else { - return extractDominantColors(background); - } - } - case HomeTabButtonColor::FromBackground: - return extractDominantColors(background); - } - - return defaultColor; -} - void HomeWidget::setRandomCard(ExactCard &newCard) { static constexpr int ATTEMPTS = 10; @@ -202,7 +171,7 @@ void HomeWidget::updateBackgroundProperties() void HomeWidget::updateButtonsToBackgroundColor() { - gradientColors = determineButtonColor(); + gradientColors = extractDominantColors(background); for (HomeStyledButton *button : findChildren()) { button->updateStylesheet(gradientColors); button->update(); @@ -297,6 +266,11 @@ void HomeWidget::updateConnectButton(const ClientStatus status) QPair HomeWidget::extractDominantColors(const QPixmap &pixmap) { + QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); + if (themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme) { + return QPair(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)); + } + // Step 1: Downscale image for performance QImage image = pixmap.toImage() .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation) diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h index 9df0d7b6a..90d003aa7 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.h +++ b/cockatrice/src/interface/widgets/general/home_widget.h @@ -23,7 +23,7 @@ class HomeWidget : public QWidget public: HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor); void updateRandomCard(); - static QPair extractDominantColors(const QPixmap &pixmap); + QPair extractDominantColors(const QPixmap &pixmap); public slots: void paintEvent(QPaintEvent *event) override; @@ -47,7 +47,6 @@ private: void setRandomCard(ExactCard &newCard); void loadBackgroundSourceDeck(); - QPair determineButtonColor() const; }; #endif // HOME_WIDGET_H 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 0b77ca185..2b9201e6c 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,7 +1,6 @@ #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/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index 881c54167..9272c36d9 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -5,7 +5,6 @@ #include "../../client/settings/card_counter_settings.h" #include "../../palette_editor/palette_editor_dialog.h" #include "../dialogs/override_printing_warning.h" -#include "../general/home_tab_button_color.h" #include "../interface/theme_manager.h" #include "../interface/widgets/general/background_sources.h" #include "../playmat/playmat_collection_dialog.h" @@ -132,14 +131,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), &AppearanceSettings::setHomeTabDisplayCardName); - for (const auto &entry : HomeTabButtonColor::all()) { - homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey)); - } - - homeTabButtonColorSourceBox.setCurrentIndex(settings.appearance().getHomeTabButtonColorSourceIndex()); - connect(&homeTabButtonColorSourceBox, QOverload::of(&QComboBox::currentIndexChanged), &settings.appearance(), - &AppearanceSettings::setHomeTabButtonColorSourceIndex); - updateHomeTabSettingsVisibility(); auto *homeTabGrid = new QGridLayout; @@ -148,8 +139,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 1, 0); homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 1, 1); homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2); - homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0); - homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1); homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); @@ -508,9 +497,6 @@ void AppearanceSettingsPage::retranslateUi() homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:")); homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled")); homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right")); - homeTabButtonColorSourceLabel.setText(tr("Home tab button color:")); - homeTabButtonColorSourceBox.setToolTip( - tr("Automatic: extract from background if present, otherwise use theme default")); stylingGroupBox->setTitle(tr("Styling settings")); styleUserListCheckBox.setText(tr("Style user list")); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index 6b0369694..28abbd537 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -35,15 +35,11 @@ private: QLabel styleComboLabel; QComboBox styleCombo; QPushButton editPaletteButton; - QLabel homeTabBackgroundSourceLabel; QComboBox homeTabBackgroundSourceBox; QLabel homeTabBackgroundShuffleFrequencyLabel; QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; - QLabel homeTabButtonColorSourceLabel; - QComboBox homeTabButtonColorSourceBox; - QCheckBox styleUserListCheckBox; QCheckBox showShortcutsCheckBox; QCheckBox showGameSelectorFilterToolbarCheckBox; 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 a3cda2bfc..e1f255199 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -8,7 +8,6 @@ #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 fd529ff69..f1dcf113f 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,9 +1,9 @@ #include "deck_preview_color_identity_filter_widget.h" #include "../../cards/additional_info/mana_symbol_widget.h" -#include "../visual_deck_storage_widget.h" +#include "deck_preview_widget.h" -#include +#include DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent) : QWidget(parent), layout(new QHBoxLayout(this)) @@ -32,6 +32,10 @@ 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(); @@ -41,33 +45,19 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi() { // Set the toggle button text based on the current mode switch (filterMode) { - case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: + case ExactMatch: toggleButton->setText(tr("Mode: Exact Match")); break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: + case Includes: toggleButton->setText(tr("Mode: Includes")); break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: + case Excludes: toggleButton->setText(tr("Mode: Excludes")); break; } toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)")); } -/** - * @brief The colors that are currently toggled on. - */ -QSet DeckPreviewColorIdentityFilterWidget::getActiveColors() const -{ - QSet activeColorSet; - for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) { - if (it.value()) { - activeColorSet.insert(it.key()); - } - } - return activeColorSet; -} - void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active) { activeColors[color] = active; @@ -78,17 +68,88 @@ void DeckPreviewColorIdentityFilterWidget::updateFilterMode() { // Cycle through the modes switch (filterMode) { - case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes; + case ExactMatch: + filterMode = Includes; break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes; + case Includes: + filterMode = Excludes; break; - case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes: - filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch; + case Excludes: + 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; + } +} 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 def45de66..8e60b16fb 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,18 @@ * @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_sort_filter_proxy_model.h" +#include "../visual_deck_storage_widget.h" #include -#include #include -#include #include +class DeckPreviewWidget; class VisualDeckStorageWidget; class DeckPreviewColorIdentityFilterWidget : public QWidget @@ -21,34 +21,25 @@ 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(); - - /** - * @brief The currently active color identity filter mode. - */ - [[nodiscard]] VisualDeckStorageSortFilterProxyModel::FilterMode getFilterMode() const - { - return filterMode; - } - - /** - * @brief The colors that are currently toggled on. - */ - [[nodiscard]] QSet getActiveColors() const; + void filterWidgets(QList widgets); signals: - /** - * Emitted when the set of active colors changed due to user interaction. - */ + void filterModeChanged(FilterMode mode); void activeColorsChanged(); - /** - * Emitted when the user cycles the color identity filter mode. - * @param mode The new filter mode. - */ - void filterModeChanged(VisualDeckStorageSortFilterProxyModel::FilterMode mode); - private slots: void handleColorToggled(QChar color, bool active); void updateFilterMode(); @@ -57,7 +48,7 @@ private: QHBoxLayout *layout; QPushButton *toggleButton; QMap activeColors; - VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes; + FilterMode filterMode = Includes; // Default to "includes" mode }; #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 41d87ccb1..b45f61be7 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,15 +1,19 @@ #include "deck_preview_deck_tags_display_widget.h" #include "../../../../client/settings/cache_settings.h" -#include "../../../deck_loader/deck_loader.h" +#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h" +#include "../../../../interface/widgets/tabs/tab_deck_editor.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) @@ -50,16 +54,6 @@ 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 */ @@ -98,13 +92,93 @@ static QStringList findAllKnownTags() void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() { - // The deck editor path has no conversion prompt; the VDS path registers one. - if (conversionPromptHandler && !conversionPromptHandler()) { - return; + 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; } - const QStringList knownTags = knownTagsProvider ? knownTagsProvider() : findAllKnownTags(); - execTagDialog(knownTags); + // 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; } void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags) @@ -117,4 +191,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 64bd5aa1a..4bd7915cd 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,53 +2,41 @@ * @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 +#include "../../../deck_loader/deck_loader.h" +#include "deck_preview_widget.h" + #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 04dcdf7f2..ffe954308 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,69 +1,47 @@ #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), model(_model), filePath(_filePath) + : QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath), + colorIdentityWidget(nullptr), deckTagsDisplayWidget(nullptr) { layout = new QVBoxLayout(this); setLayout(layout); - auto *pictureWidget = + 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 = new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled); - pictureWidget->setFontSize(24); - connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent); - connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, + + connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, + &DeckPreviewWidget::imageClickedEvent); + connect(bannerCardDisplayWidget, &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, @@ -78,29 +56,6 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, &DeckPreviewWidget::refreshBannerCardToolTip); layout->addWidget(bannerCardDisplayWidget); - layout->addWidget(colorIdentityWidget); - layout->addWidget(deckTagsDisplayWidget); - layout->addWidget(bannerCardLabel); - layout->addWidget(bannerCardComboBox); - - // Only re-sync when this widget's own row changed. Without the row check, every - // finished deck load would trigger a full resync (card db lookup + combo rebuild) - // in every preview widget. - connect(model, &QAbstractItemModel::dataChanged, this, - [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { - const int r = row(); - if (r >= topLeft.row() && r <= bottomRight.row()) { - syncFromModel(); - } - }); - - retranslateUi(); - syncFromModel(); - - // resizeEvent clamps every child to the picture's width, so collect them once here - // to keep the resize handler from searching the widget tree on every layout pass. - fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel, - bannerCardComboBox}; } void DeckPreviewWidget::retranslateUi() @@ -114,15 +69,9 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event) if (bannerCardDisplayWidget == nullptr) { return; } - - const int width = bannerCardDisplayWidget->width(); - if (width == lastKnownBannerWidth) { - return; - } - lastKnownBannerWidth = width; - - for (QWidget *widget : fixedWidthChildren) { - widget->setMaximumWidth(width); + QList widgets = findChildren(); + for (QWidget *widget : widgets) { + widget->setMaximumWidth(bannerCardDisplayWidget->width()); } } @@ -130,28 +79,83 @@ void DeckPreviewWidget::enterEvent(QEnterEvent *event) { QWidget::enterEvent(event); - // 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 {}; + // don't do reloads until widgets have been created + if (bannerCardComboBox != nullptr) { + reloadIfModified(); } - return model->dataForRow(r).displayName; +} + +/** + * @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()); } /** @@ -159,36 +163,33 @@ QString DeckPreviewWidget::getDisplayName() const */ void DeckPreviewWidget::reloadIfModified() { - const int r = row(); - if (r == -1 || !model->dataForRow(r).loadSucceeded) { + QFileInfo fileInfo(filePath); + QDateTime newLastModifiedTime = fileInfo.lastModified(); + + if (!newLastModifiedTime.isValid() || newLastModifiedTime <= lastModifiedTime) { return; } - model->reloadIfModified(r); + bool success = deckLoader->reload(); + + if (success) { + fileInfo.refresh(); + lastModifiedTime = fileInfo.lastModified(); + resyncWidgets(); + } } -/** - * @brief Syncs the contents of the child widgets with the current row's data. - */ -void DeckPreviewWidget::syncFromModel() +void DeckPreviewWidget::updateVisibility() { - const int r = row(); - if (r == -1) { - return; + setHidden(!checkVisibility()); +} + +bool DeckPreviewWidget::checkVisibility() const +{ + if (filteredBySearch || filteredByColor || filteredByTags) { + return false; } - - 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); + return true; } void DeckPreviewWidget::updateColorIdentityVisibility(bool visible) @@ -228,6 +229,51 @@ 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. @@ -264,15 +310,11 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) // Prepare the new items with deduplication QSet> bannerCardSet; - const int r = row(); - if (r != -1) { - const DeckList &deckList = model->dataForRow(r).deck.deckList; - const QList cardsInDeck = deckList.getCardNodes(); + QList cardsInDeck = deckLoader->getDeck().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())); } } @@ -285,16 +327,16 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) // This is *slightly* more performant than using addItem in a loop. - QStandardItemModel *comboModel = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox); + QStandardItemModel *model = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox); int row = 0; for (const auto &pair : pairList) { QStandardItem *item = new QStandardItem(pair.first); item->setData(QVariant::fromValue(pair), Qt::UserRole); - comboModel->setItem(row++, 0, item); + model->setItem(row++, 0, item); } - bannerCardComboBox->setModel(comboModel); + bannerCardComboBox->setModel(model); // Try to restore the previous selection by finding the currentText int restoredIndex = bannerCardComboBox->findText(currentText); @@ -302,9 +344,7 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText) bannerCardComboBox->setCurrentIndex(restoredIndex); } else { // Add a placeholder "-" and set it as the current selection - const QString currentBannerCardName = - r == -1 ? QString() : model->dataForRow(r).deck.deckList.getBannerCard().name; - int bannerIndex = bannerCardComboBox->findText(currentBannerCardName); + int bannerIndex = bannerCardComboBox->findText(deckLoader->getDeck().deckList.getBannerCard().name); if (bannerIndex != -1) { bannerCardComboBox->setCurrentIndex(bannerIndex); } else { @@ -322,11 +362,8 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */) { auto [name, id] = bannerCardComboBox->currentData().value>(); CardRef cardRef = {name, id}; - const int r = row(); - if (r == -1) { - return; - } - model->setBannerCard(r, cardRef); + deckLoader->getDeck().deckList.setBannerCard(cardRef); + writeDeckToFile(); bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef)); } @@ -348,24 +385,17 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC void DeckPreviewWidget::setTags(const QStringList &tags) { - const int r = row(); - if (r != -1) { - model->setTags(r, tags); - } + deckLoader->getDeck().deckList.setTags(tags); + writeDeckToFile(); } 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, r] { - if (r != -1) { - emit openDeckEditor(model->deckForRow(r)); - } - }); + connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, + [this] { emit openDeckEditor(deckLoader->getDeck()); }); connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::openTagEditDlg); @@ -378,26 +408,14 @@ QMenu *DeckPreviewWidget::createRightClickMenu() auto saveToClipboardMenu = menu->addMenu(tr("Save Deck to Clipboard")); - 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); - } - }); + 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); }); menu->addSeparator(); @@ -432,58 +450,57 @@ void DeckPreviewWidget::addSetBannerCardMenu(QMenu *menu) void DeckPreviewWidget::actRenameDeck() { - const int r = row(); - if (r == -1) { - return; - } - // read input - const QString oldName = model->dataForRow(r).deckName; + const QString oldName = deckLoader->getDeck().deckList.getName(); bool ok; - QString newName = QInputDialog::getText(this, tr("Rename deck"), tr("New name:"), QLineEdit::Normal, oldName, &ok); + QString newName = QInputDialog::getText(this, "Rename deck", tr("New name:"), QLineEdit::Normal, oldName, &ok); if (!ok || oldName == newName) { return; } // write change - model->renameDeck(r, newName); + deckLoader->getDeck().deckList.setName(newName); + writeDeckToFile(); - // The banner card text updates via the model's dataChanged signal. + // update VDS + refreshBannerCardText(); } 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, tr("Rename file"), tr("New name:"), QLineEdit::Normal, oldName, &ok); + QString newName = QInputDialog::getText(this, "Rename file", tr("New name:"), QLineEdit::Normal, oldName, &ok); if (!ok || newName.isEmpty() || oldName == newName) { return; } - // write change - if (!model->renameFile(r, newName)) { - QMessageBox::critical(this, tr("Error"), tr("Rename failed")); + QString newFileName = newName; + if (!info.suffix().isEmpty()) { + newFileName += "." + info.suffix(); } - // The file path and banner card text update via the model's signals. + // write change + const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath(); + if (!QFile::rename(info.filePath(), newFilePath)) { + QMessageBox::critical(this, tr("Error"), tr("Rename failed")); + return; + } + + deckLoader->getDeck().lastLoadInfo.fileName = newFilePath; + setFilePath(newFilePath); + + // update VDS + updateLastModifiedTime(); + refreshBannerCardText(); } 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); @@ -492,74 +509,11 @@ void DeckPreviewWidget::actDeleteFile() } // write change - if (!model->deleteFile(r)) { + if (!QFile::remove(QFileInfo(filePath).filePath())) { QMessageBox::critical(this, tr("Error"), tr("Delete failed")); + return; } - // 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; + // update VDS + this->deleteLater(); } 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 7bb69f9b9..de66c194b 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,12 +2,16 @@ * @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_model.h" +#include "../visual_deck_storage_widget.h" +#include "deck_preview_deck_tags_display_widget.h" #include #include @@ -16,82 +20,72 @@ #include #include -class QEnterEvent; -class QLabel; class QMenu; -class QMouseEvent; -class ColorIdentityWidget; -class DeckPreviewCardPictureWidget; -class DeckPreviewDeckTagsDisplayWidget; -class VisualDeckStorageModel; class VisualDeckStorageWidget; +class DeckPreviewDeckTagsDisplayWidget; 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; - /** - * @brief The banner card picture; the parent widget wires its size to the card size setting. - */ - DeckPreviewCardPictureWidget *bannerCardDisplayWidget; + 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; signals: void deckLoadRequested(const QString &filePath); void openDeckEditor(const LoadedDeck &deck); public slots: - /** - * @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 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(); + void reloadIfModified(); + void updateVisibility(); void updateColorIdentityVisibility(bool visible); void updateBannerCardComboBoxVisibility(bool visible); void updateTagsVisibility(bool visible); - void setBannerCard(int); - void setTags(const QStringList &tags); + void resizeEvent(QResizeEvent *event) override; protected: void enterEvent(QEnterEvent *event) override; - void resizeEvent(QResizeEvent *event) override; private: - [[nodiscard]] int row() const; - [[nodiscard]] QString getDisplayName() const; - void refreshBannerCardText(); - void updateBannerCardComboBox(const QString ¤tText); - bool promptFileConversionIfRequired(); + void updateLastModifiedTime(); + void writeDeckToFile(); QMenu *createRightClickMenu(); void addSetBannerCardMenu(QMenu *menu); - void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); + +private slots: + void setTags(const QStringList &tags); void actRenameDeck(); void actRenameFile(); void actDeleteFile(); - - VisualDeckStorageWidget *visualDeckStorageWidget; - VisualDeckStorageModel *model; - QString filePath; - QVBoxLayout *layout; - ColorIdentityWidget *colorIdentityWidget; - DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; - QLabel *bannerCardLabel; - QComboBox *bannerCardComboBox; - QList fixedWidthChildren; ///< Children clamped to the picture width on resize. - int lastKnownBannerWidth = -1; ///< The picture width last applied to the children. }; 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 fbaabf90f..cc7f07871 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,27 +1,20 @@ #include "visual_deck_storage_folder_display_widget.h" -#include "../cards/card_info_picture_widget.h" -#include "../general/display/banner_widget.h" -#include "../general/layout_containers/flow_widget.h" +#include "../../../client/settings/cache_settings.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 +#include VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( QWidget *parent, VisualDeckStorageWidget *_visualDeckStorageWidget, - const QString &_folderPath, + QString _filePath, bool canBeHidden, bool _showFolders) - : QWidget(parent), showFolders(_showFolders), folderPath(_folderPath), - visualDeckStorageWidget(_visualDeckStorageWidget) + : QWidget(parent), showFolders(_showFolders), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath) { layout = new QVBoxLayout(this); setLayout(layout); @@ -29,9 +22,6 @@ 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); @@ -45,285 +35,192 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget( flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); containerLayout->addWidget(flowWidget); - 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); + createWidgetsForFiles(); + createWidgetsForFolders(); - reconcileTimer = new QTimer(this); - reconcileTimer->setSingleShot(true); - reconcileTimer->setInterval(150); - connect(reconcileTimer, &QTimer::timeout, this, &VisualDeckStorageFolderDisplayWidget::reconcile); - - // Building the whole folder subtree synchronously here would stall the ui thread on large - // collections, so the first reconcile runs as a chunked pass on later event loop turns. - scheduleReconcile(); + refreshUi(); } -void VisualDeckStorageFolderDisplayWidget::scheduleReconcile() +void VisualDeckStorageFolderDisplayWidget::refreshUi() { - if (deckPassActive) { - // The active pass may be scanning stale model state, so restart it from a clean - // slate once the current chunk yields. - deckPassRestartRequested = true; - return; + 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; } - reconcileTimer->start(); + header->setText(bannerText); } /** - * @brief Starts a new chunked scan of the source model, yielding to the event loop between chunks. - */ -void VisualDeckStorageFolderDisplayWidget::reconcile() -{ - beginDeckPass(); -} - -void VisualDeckStorageFolderDisplayWidget::beginDeckPass() -{ - deckPassActive = true; - deckPassRestartRequested = false; - deckPassRow = 0; - visibleDeckCount = 0; - deckPassPresentPaths.clear(); - - continueDeckPass(); -} - -void VisualDeckStorageFolderDisplayWidget::continueDeckPass() -{ - if (!deckPassActive) { - return; - } - - QElapsedTimer passTimer; - passTimer.start(); - - auto *proxy = visualDeckStorageWidget->proxyModel(); - const int proxyRowCount = proxy->rowCount(); - - // Scan rows of this folder, creating missing previews, until the time budget for this - // event loop turn runs out. The rest continues on the next turn. - while (deckPassRow < proxyRowCount) { - const int row = deckPassRow++; - const QModelIndex index = proxy->index(row, 0); - if (showFolders && index.data(VisualDeckStorageRoles::FolderPathRole).toString() != folderPath) { - continue; - } - const QString filePath = index.data(VisualDeckStorageRoles::FilePathRole).toString(); - deckPassPresentPaths.insert(filePath); - - DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(filePath, nullptr); - if (!deckPreviewWidget) { - deckPreviewWidget = createDeckPreviewWidget(filePath); - flowWidget->addWidget(deckPreviewWidget); - } - - const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool(); - if (matches == deckPreviewWidget->isHidden()) { - deckPreviewWidget->setVisible(matches); - } - if (matches) { - ++visibleDeckCount; - } - - if (passTimer.elapsed() >= DECK_PASS_TIME_BUDGET_MS) { - break; - } - } - - if (deckPassRestartRequested) { - beginDeckPass(); - return; - } - - if (deckPassRow < proxyRowCount) { - QMetaObject::invokeMethod(this, &VisualDeckStorageFolderDisplayWidget::continueDeckPass, Qt::QueuedConnection); - return; - } - - finishDeckPass(); -} - -void VisualDeckStorageFolderDisplayWidget::finishDeckPass() -{ - auto *proxy = visualDeckStorageWidget->proxyModel(); - - // Drop previews of decks that no longer exist in the model. - for (auto it = deckWidgets.begin(); it != deckWidgets.end();) { - if (!deckPassPresentPaths.contains(it.key())) { - flowWidget->removeWidget(it.value()); - it.value()->deleteLater(); - it = deckWidgets.erase(it); - } else { - ++it; - } - } - - // Order the flow layout like the proxy sorts its rows. Filtered-out decks stay part of - // the layout, hidden in their sorted place until a filter lets them through again. - QStringList orderedFilePaths; - orderedFilePaths.reserve(proxy->rowCount()); - for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { - const QString filePath = proxy->index(proxyRow, 0).data(VisualDeckStorageRoles::FilePathRole).toString(); - if (deckWidgets.contains(filePath)) { - orderedFilePaths.append(filePath); - } - } - - // 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 (orderedFilePaths != lastOrderedFilePaths) { - for (const QString &filePath : orderedFilePaths) { - flowWidget->removeWidget(deckWidgets.value(filePath)); - } - for (const QString &filePath : orderedFilePaths) { - flowWidget->addWidget(deckWidgets.value(filePath)); - } - lastOrderedFilePaths = orderedFilePaths; - } - - createSubFolderWidgets(); - - // Mark completion before evaluating visibility so this pass's own numbers decide whether - // the folder has content. The flag only guards evaluations made *during* a build. - deckPassActive = false; - initialPassCompleted = true; - - refreshVisibility(); -} - -/** - * @brief Creates a deck preview widget and wires it up to the storage widget. + * Gets all files in the directory that have an accepted decklist file extension * - * @param filePath The absolute path of the deck file to preview. + * @param filePath The directory to search through + * @param recursive Whether to search through subdirectories */ -DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget(const QString &filePath) +static QStringList getAllFiles(const QString &filePath, bool recursive) { - auto *deckPreviewWidget = - new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, 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); - return deckPreviewWidget; + 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; +} + +void VisualDeckStorageFolderDisplayWidget::createWidgetsForFiles() +{ + QList allDecks; + for (const QString &file : getAllFiles(filePath, !showFolders)) { + auto *display = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, file); + + 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); + } + + flowWidget->clearLayout(); // Clear existing widgets in the flow layout + + for (DeckPreviewWidget *deck : allDecks) { + flowWidget->addWidget(deck); + } } /** - * @brief Creates, removes and keeps in sync the subfolder widgets of this folder. + * Updates the visibility of this folder and all its DeckPreviewWidgets * - * Only direct child folders are created here; each child manages its own - * children, mirroring the folder tree on disk. + * @param recursive Also update the visibility of all subfolders and their DeckPreviewWidgets. */ -void VisualDeckStorageFolderDisplayWidget::createSubFolderWidgets() +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() { if (!showFolders) { return; } - 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); + for (const QString &dir : getAllSubFolders(filePath)) { + auto *display = new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, dir, true, showFolders); + containerLayout->addWidget(display); } } void VisualDeckStorageFolderDisplayWidget::updateShowFolders(bool enabled) { showFolders = enabled; - header->setHidden(!showFolders); if (!showFolders) { - for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end(); ++it) { - containerLayout->removeWidget(it.value()); - it.value()->deleteLater(); - } - subFolderWidgets.clear(); + flattenFolderStructure(); + } else { + // if setting was switched from disabled to enabled, we assume that there aren't any existing subfolders + createWidgetsForFiles(); + createWidgetsForFolders(); } - scheduleReconcile(); + header->setHidden(!showFolders); } /** - * @brief Hides the folder when it contains nothing visible, and reports the change upward. + * Steals all DeckPreviewWidgets from this widget's nested subfolders, and deletes those subfolders */ -void VisualDeckStorageFolderDisplayWidget::refreshVisibility() +void VisualDeckStorageFolderDisplayWidget::flattenFolderStructure() { - const bool shouldBeVisible = hasContent(); - if (isHidden() == !shouldBeVisible) { - return; + for (auto *subFolder : findChildren()) { + // steal all DeckPreviewWidgets from the subfolder + for (auto *deck : subFolder->getFlowWidget()->findChildren()) { + flowWidget->addWidget(deck); + } + + // delete the subfolder + subFolder->deleteLater(); } - setHidden(!shouldBeVisible); - emit contentVisibilityChanged(); } -/** - * @brief Whether this folder shows any deck previews or has any visible subfolder. - * - * While the first pass is still building, the folder counts as having content so it - * doesn't flicker or hide prematurely before its previews have been created. - */ -bool VisualDeckStorageFolderDisplayWidget::hasContent() const +QStringList VisualDeckStorageFolderDisplayWidget::gatherAllTagsFromFlowWidget() const { - if (!initialPassCompleted || visibleDeckCount > 0) { - return true; - } + QStringList allTags; - for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : subFolderWidgets) { - if (subFolderWidget->hasContent()) { - 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); } } - return false; -} + // Remove duplicates by calling 'removeDuplicates' + allTags.removeDuplicates(); -/** - * @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; -} + return allTags; +} \ No newline at end of file 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 257ce1778..a5e3be212 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,105 +1,49 @@ /** * @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 keeps one persistent DeckPreviewWidget alive per deck - * in its folder, and shows or hides those widgets according to each row's - * FilterMatchRole in the VisualDeckStorageSortFilterProxyModel. Subfolders are - * shown as nested VisualDeckStorageFolderDisplayWidgets when the "show folders" - * setting is enabled. - * - * Reconciling runs as a time-budgeted chunked pass that yields to the event loop - * between chunks, so scanning a large collection never stalls the ui thread. */ +//! \todo Document this file. #ifndef VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H #define VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H -#include -#include -#include -#include +#include "../general/display/banner_widget.h" +#include "../general/layout_containers/flow_widget.h" -class BannerWidget; -class DeckPreviewWidget; -class FlowWidget; -class QTimer; -class QVBoxLayout; class VisualDeckStorageWidget; - class VisualDeckStorageFolderDisplayWidget : public QWidget { Q_OBJECT public: VisualDeckStorageFolderDisplayWidget(QWidget *parent, - VisualDeckStorageWidget *visualDeckStorageWidget, - const QString &folderPath, + VisualDeckStorageWidget *_visualDeckStorageWidget, + QString _filePath, bool canBeHidden, - bool showFolders); + bool _showFolders); + void refreshUi(); + void createWidgetsForFiles(); + void createWidgetsForFolders(); + void flattenFolderStructure(); + [[nodiscard]] QStringList gatherAllTagsFromFlowWidget() const; + [[nodiscard]] FlowWidget *getFlowWidget() const + { + return flowWidget; + } public slots: - /** - * @brief Starts a new chunked reconcile pass that 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 updateVisibility(bool recursive = true); + bool checkVisibility(); 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 beginDeckPass(); - void continueDeckPass(); - void finishDeckPass(); - [[nodiscard]] DeckPreviewWidget *createDeckPreviewWidget(const QString &filePath); - void createSubFolderWidgets(); - void refreshVisibility(); - [[nodiscard]] bool hasContent() const; - [[nodiscard]] QStringList childFolderPaths() const; - - /** - * @brief The maximum time in milliseconds spent creating deck previews per event loop turn. - * - * Creating all previews of a large folder at once blocks the ui thread for hundreds of - * milliseconds, so the pass is split into chunks that yield to the event loop instead. - */ - static constexpr int DECK_PASS_TIME_BUDGET_MS = 20; - bool showFolders; - QString folderPath; ///< Path relative to the deck folder, empty for the root folder. - int visibleDeckCount = 0; ///< The number of this folder's deck previews not filtered out. 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. - - /// Whether a chunked reconcile pass is currently running. - bool deckPassActive = false; - /// Set when the model changes mid-pass. Discards progress and restarts the scan once - /// the current chunk finishes so the pass always converges on the latest model state. - bool deckPassRestartRequested = false; - /// Whether the first reconcile pass has run to completion at least once. - bool initialPassCompleted = false; - int deckPassRow = 0; ///< Next proxy row to scan in the active pass. - QSet deckPassPresentPaths; ///< File paths seen so far in the active pass. }; #endif // VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp index 5d0006539..bf2c49604 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -33,18 +32,8 @@ struct DeckLoadResult { LoadedDeck deck; ///< The parsed deck. QDateTime lastModified; ///< File modification time at load. - QString colorIdentity; ///< WUBRG color identity, computed off the UI thread. }; -/** - * @brief How long per event loop turn the pending-load drain applies finished loads. - * - * Applying a load updates the row and wakes up the proxies and views, which is - * not free. A time budget instead of a fixed count lets fast machines apply - * many loads in one turn while keeping the ui thread responsive everywhere. - */ -constexpr int LOAD_DRAIN_TIME_BUDGET_MS = 8; - /** * @brief The path of \a path relative to the deck root, or empty if \a path * is not below it. @@ -119,8 +108,6 @@ DeckScanResult scanDeckDirectory(const QString &deckPath) } } // namespace -static QString computeColorIdentity(const LoadedDeck &deck); - VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListModel(parent) { } @@ -195,22 +182,12 @@ const LoadedDeck &VisualDeckStorageModel::deckForRow(int row) const int VisualDeckStorageModel::rowForFilePath(const QString &filePath) const { - return rowByFilePath.value(filePath, -1); -} - -/** - * @brief Rebuilds the file path -> row index from scratch after bulk changes. - */ -void VisualDeckStorageModel::reindexFilePaths() -{ - rowByFilePath.clear(); - for (int row = 0; row < decks.size(); ++row) { - const QString &filePath = decks.at(row).filePath; - // First row wins, mirroring what a linear scan would return for duplicates. - if (!rowByFilePath.contains(filePath)) { - rowByFilePath.insert(filePath, row); + for (int i = 0; i < decks.size(); ++i) { + if (decks.at(i).filePath == filePath) { + return i; } } + return -1; } void VisualDeckStorageModel::startScan() @@ -218,9 +195,7 @@ void VisualDeckStorageModel::startScan() ++scanGeneration; beginResetModel(); decks.clear(); - rowByFilePath.clear(); folderPaths.clear(); - pendingLoads.clear(); endResetModel(); if (deckPath.isEmpty()) { @@ -249,7 +224,6 @@ void VisualDeckStorageModel::startScan() beginInsertRows(QModelIndex(), 0, result.decks.size() - 1); decks = result.decks; - reindexFilePaths(); endInsertRows(); for (int row = 0; row < decks.size(); ++row) { @@ -283,20 +257,26 @@ void VisualDeckStorageModel::beginLoad(int row) return; // The deck list was re-scanned while this load was running; drop the stale result. } - // Queue the result and apply a bounded number per event loop turn so that - // finishing hundreds of loads at once cannot stall the UI thread. - const std::optional result = watcher->result(); - PendingDeckLoad pending; - pending.filePath = filePath; - pending.generation = generation; - if (result) { - pending.ok = true; - pending.deck = std::move(result->deck); - pending.lastModified = result->lastModified; - pending.colorIdentity = std::move(result->colorIdentity); + const int row = rowForFilePath(filePath); + if (row == -1) { + return; } - pendingLoads.append(std::move(pending)); - schedulePendingLoadDrain(); + + DeckPreviewData &data = decks[row]; + data.loadInProgress = false; + + std::optional result = watcher->result(); + if (!result) { + return; // Leave the row unloaded; it stays visible but without deck data. + } + + data.deck = std::move(result->deck); + data.loadSucceeded = true; + data.lastModified = result->lastModified; + recomputeDeckMetadata(data); + + emit dataChanged(index(row), index(row)); + emit deckLoaded(row); }); watcher->setFuture(QtConcurrent::run([filePath, fmt]() -> std::optional { @@ -304,69 +284,10 @@ void VisualDeckStorageModel::beginLoad(int row) if (!deck) { return std::nullopt; } - // Color identity walks every card through the database, so compute it here to - // keep the completion handler on the UI thread cheap. - const QString colorIdentity = computeColorIdentity(*deck); - return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity}; + return DeckLoadResult{*deck, QFileInfo(filePath).lastModified()}; })); } -void VisualDeckStorageModel::schedulePendingLoadDrain() -{ - if (drainScheduled) { - return; - } - drainScheduled = true; - QMetaObject::invokeMethod(this, &VisualDeckStorageModel::drainPendingLoads, Qt::QueuedConnection); -} - -void VisualDeckStorageModel::drainPendingLoads() -{ - drainScheduled = false; - - QElapsedTimer timer; - timer.start(); - - while (!pendingLoads.isEmpty()) { - PendingDeckLoad pending = pendingLoads.takeFirst(); - - if (pending.generation != scanGeneration) { - continue; - } - - const int row = rowForFilePath(pending.filePath); - if (row == -1) { - continue; - } - - DeckPreviewData &data = decks[row]; - data.loadInProgress = false; - - if (!pending.ok) { - continue; // Leave the row unloaded so it stays visible without deck data. - } - - data.deck = std::move(pending.deck); - data.loadSucceeded = true; - data.lastModified = pending.lastModified; - recomputeDeckMetadata(data, false); - data.colorIdentity = std::move(pending.colorIdentity); - - emit dataChanged(index(row), index(row)); - emit deckLoaded(row); - - // Checked after applying at least one load, so a single slow application - // still makes progress instead of starving the queue. - if (timer.elapsed() >= LOAD_DRAIN_TIME_BUDGET_MS) { - break; - } - } - - if (!pendingLoads.isEmpty()) { - schedulePendingLoadDrain(); - } -} - /** * @brief Computes the color identity of a deck in WUBRG order. */ @@ -404,7 +325,7 @@ static QString computeColorIdentity(const LoadedDeck &deck) /** * @brief Recomputes all derived metadata of a row from its loaded deck. */ -void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity) +void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data) { const DeckList &deckList = data.deck.deckList; @@ -413,9 +334,7 @@ void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool r data.tags = deckList.getTags(); data.lastLoaded = QDateTime::fromString(deckList.getLastLoadedTimestamp()); data.bannerCard = deckList.getBannerCard(); - if (recomputeColorIdentity) { - data.colorIdentity = computeColorIdentity(data.deck); - } + data.colorIdentity = computeColorIdentity(data.deck); } void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePath) @@ -425,13 +344,9 @@ void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePa } DeckPreviewData &data = decks[row]; - rowByFilePath.remove(data.filePath); data.filePath = newFilePath; data.relativeFilePath = relativeFilePathFor(newFilePath, deckPath); data.folderPath = folderPathFor(newFilePath, deckPath); - if (!rowByFilePath.contains(newFilePath)) { - rowByFilePath.insert(newFilePath, row); - } } bool VisualDeckStorageModel::renameDeck(int row, const QString &newName) @@ -495,14 +410,7 @@ bool VisualDeckStorageModel::deleteFile(int row) } beginRemoveRows(QModelIndex(), row, row); - rowByFilePath.remove(filePath); decks.removeAt(row); - // Rows after the deleted one shift down by one. - for (auto it = rowByFilePath.begin(); it != rowByFilePath.end(); ++it) { - if (it.value() > row) { - --it.value(); - } - } endRemoveRows(); return true; } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h index 330205356..a44e7412d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h @@ -17,7 +17,6 @@ #include #include -#include #include #include #include @@ -39,14 +38,7 @@ enum LastModifiedRole, /**< QDateTime of the deck file's last modification. */ LastLoadedRole, /**< QDateTime when the deck was last loaded from the file. */ BannerCardNameRole, /**< Name of the deck's banner card. */ - BannerCardProviderIdRole, /**< Provider id of the deck's banner card. */ - /** - * @brief Whether the row passes the proxy's current search / tag / color filters. - * - * Not served by this model, but by VisualDeckStorageSortFilterProxyModel on top - * of it. Declared here so every role read through a proxy index stays unique. - */ - FilterMatchRole + BannerCardProviderIdRole /**< Provider id of the deck's banner card. */ }; } // namespace VisualDeckStorageRoles @@ -73,25 +65,11 @@ struct DeckPreviewData bool loadInProgress = false; ///< Whether the deck file is currently being loaded. }; -/** - * @brief One finished background deck load that has not been applied to the model yet. - */ -struct PendingDeckLoad -{ - QString filePath; ///< Identifies the row the result belongs to. - int generation; ///< Scan generation the load was started in. - bool ok = false; ///< Whether the file parsed successfully. - LoadedDeck deck; ///< The parsed deck, valid when ok. - QDateTime lastModified; ///< File modification time at load, valid when ok. - QString colorIdentity; ///< WUBRG color identity computed off the UI thread, valid when ok. -}; - /** * @brief The list model backing the Visual Deck Storage widget tree. * * Rows are in filesystem scan order; ordering and filtering are handled by - * VisualDeckStorageSortFilterProxyModel on top of this model. The proxy keeps - * every row and exposes each row's filter result through its FilterMatchRole. + * VisualDeckStorageSortFilterProxyModel on top of this model. */ class VisualDeckStorageModel : public QAbstractListModel { @@ -166,23 +144,13 @@ signals: private: void startScan(); void beginLoad(int row); - static void recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity = true); - void reindexFilePaths(); - void schedulePendingLoadDrain(); - -private slots: - void drainPendingLoads(); - -private: + static void recomputeDeckMetadata(DeckPreviewData &data); void setFilePathForRow(int row, const QString &newFilePath); QString deckPath; QList decks; - QHash rowByFilePath; ///< Maps each deck's file path to its row for O(1) lookups. - QStringList folderPaths; ///< All subdirectories of the deck folder, sorted. - int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored. - QVector pendingLoads; ///< Finished background loads waiting to be applied. - bool drainScheduled = false; ///< Whether a queued drain pass is already pending. + QStringList folderPaths; ///< All subdirectories of the deck folder, sorted. + int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored. }; #endif // VISUAL_DECK_STORAGE_MODEL_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 478431703..cce3ff6ce 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,7 +1,6 @@ #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 baa5e5792..0580126c4 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,20 +1,23 @@ #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 -#include +#include +#include /** - * @brief Constructs a search bar for filtering decks in the Visual Deck Storage. + * @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code. * - * 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. + * 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 widget. + * @param parent The parent PrintingSelector widget that will handle the search results. */ -VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : QWidget(parent) +VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent) : parent(parent) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -37,5 +40,47 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : searchDebounceTimer->start(300); // 300ms debounce }); - connect(searchDebounceTimer, &QTimer::timeout, this, [this] { emit searchTextChanged(searchBar->text()); }); + 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); + } } 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 7769ea911..2f3d81aeb 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,32 +2,30 @@ * @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 { Q_OBJECT public: - explicit VisualDeckStorageSearchWidget(QWidget *parent); - -signals: - /** - * Emitted once the debounce timer fires after the user stopped typing. - * @param text The current contents of the search bar. - */ - void searchTextChanged(const QString &text); + explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent); + QString getSearchText(); + void filterWidgets(QList widgets, const QString &searchText); private: QHBoxLayout *layout; + VisualDeckStorageWidget *parent; QLineEdit *searchBar; QTimer *searchDebounceTimer; }; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp index c05da1cb3..8968f6cb3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp @@ -104,21 +104,12 @@ void VisualDeckStorageSortFilterProxyModel::resort() sort(0); } -QVariant VisualDeckStorageSortFilterProxyModel::data(const QModelIndex &index, int role) const +bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - if (role == VisualDeckStorageRoles::FilterMatchRole) { - if (!index.isValid()) { - return true; - } - const QModelIndex sourceIndex = mapToSource(index); - return rowMatches(sourceIndex.row()); + if (sourceParent.isValid()) { + return true; } - return QSortFilterProxyModel::data(index, role); -} - -bool VisualDeckStorageSortFilterProxyModel::rowMatches(int sourceRow) const -{ // If the match lists aren't sized to the current model yet, don't hide anything. if (sourceRow < 0 || sourceRow >= searchMatches.size() || sourceRow >= tagMatches.size() || sourceRow >= colorMatches.size()) { @@ -128,14 +119,6 @@ bool VisualDeckStorageSortFilterProxyModel::rowMatches(int sourceRow) const return searchMatches.at(sourceRow) && tagMatches.at(sourceRow) && colorMatches.at(sourceRow); } -bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int /*sourceRow*/, - const QModelIndex & /*sourceParent*/) const -{ - // Rows are never dropped: the filter result is exposed per row through - // FilterMatchRole, so views can keep their widgets alive and just hide them. - return true; -} - bool VisualDeckStorageSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const auto *source = deckSourceModel(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h index 7e771f6a9..d2842a02f 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h @@ -6,10 +6,6 @@ * Owns all search / tag / color filter state and the sort order. Filtering is * evaluated against the model's data (never against widgets), so it can run * before any view exists and re-evaluate whenever deck data finishes loading. - * - * Rows are never removed by filtering. Instead, every row carries the - * FilterMatchRole, which views read to show or hide their widgets while keeping - * them alive; all rows stay in the proxy so they keep their sorted position. */ #ifndef VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H @@ -51,8 +47,6 @@ public: explicit VisualDeckStorageSortFilterProxyModel(QObject *parent = nullptr); - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - void setSourceModel(QAbstractItemModel *model) override; /// @name Filter input setters (each re-evaluates the affected matches) @@ -83,7 +77,6 @@ protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; private: - [[nodiscard]] bool rowMatches(int sourceRow) const; void resizeMatchLists(); void updateSearchMatches(); void updateTagMatches(); 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 6289a4941..e4eedb078 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,11 +1,20 @@ #include "visual_deck_storage_sort_widget.h" #include "../../../client/settings/cache_settings.h" -#include "visual_deck_storage_widget.h" +#include #include -VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) : QWidget(parent) +/** + * @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) { layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -21,10 +30,12 @@ VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget // Set the current sort order sortComboBox->setCurrentIndex(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSortingOrder()); + sortOrder = static_cast(sortComboBox->currentIndex()); - // Connect sorting change signal to persist the order and refresh the file list + // Connect sorting change signal to refresh the file list connect(sortComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &VisualDeckStorageSortWidget::updateSortOrder); + connect(this, &VisualDeckStorageSortWidget::sortOrderChanged, parent, &VisualDeckStorageWidget::updateSortOrder); } void VisualDeckStorageSortWidget::retranslateUi() @@ -36,13 +47,10 @@ void VisualDeckStorageSortWidget::retranslateUi() // Clear and repopulate the ComboBox with translated items sortComboBox->clear(); - 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); + 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); // Restore the current index sortComboBox->setCurrentIndex(oldIndex); @@ -51,13 +59,60 @@ 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 633924d84..24eddba33 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,17 +2,19 @@ * @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_sort_filter_proxy_model.h" +#include "visual_deck_storage_widget.h" #include #include #include class VisualDeckStorageWidget; +class VisualDeckStorageFolderDisplayWidget; class VisualDeckStorageSortWidget : public QWidget { Q_OBJECT @@ -20,23 +22,25 @@ class VisualDeckStorageSortWidget : public QWidget public: explicit VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent); void retranslateUi(); - - /** - * @brief The currently selected sort order. - */ - [[nodiscard]] VisualDeckStorageSortFilterProxyModel::SortOrder currentSortOrder() const; + void updateSortOrder(); + void sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget); + QString getSearchText(); + QList filterFiles(QList widgets); 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 ba52cf8e9..c4c8d18a8 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 @@ -2,8 +2,6 @@ #include "../general/layout_containers/flow_widget.h" #include "deck_preview/deck_preview_tag_display_widget.h" -#include "visual_deck_storage_model.h" -#include "visual_deck_storage_sort_filter_proxy_model.h" #include "visual_deck_storage_widget.h" #include @@ -20,7 +18,7 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto setFixedHeight(100); - flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); + auto *flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); layout->addWidget(flowWidget); } @@ -31,26 +29,45 @@ void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event) refreshTags(); } -/** - * @brief The tags of all decks currently accepted by the proxy model. - */ -QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const +void VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList &deckPreviews) const { - QSet allTags; - auto *proxy = parent->proxyModel(); + QStringList selectedTags; + QStringList excludedTags; - for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { - const QModelIndex index = proxy->index(proxyRow, 0); - if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) { - continue; - } - const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList(); - for (const QString &tag : deckTags) { - allTags.insert(tag); + // 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; } } - return allTags; + // 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); + } } void VisualDeckStorageTagFilterWidget::refreshTags() @@ -63,6 +80,8 @@ void VisualDeckStorageTagFilterWidget::refreshTags() void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet &tags) { + auto *flowWidget = findChild(); + for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { const QString &tagName = tagWidget->getTagName(); @@ -97,12 +116,20 @@ 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(); @@ -120,26 +147,19 @@ void VisualDeckStorageTagFilterWidget::sortTags() } } -QStringList VisualDeckStorageTagFilterWidget::selectedTags() const +QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const { - QStringList selected; - for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { - if (tagWidget->getState() == TagState::Selected) { - selected.append(tagWidget->getTagName()); - } - } - return selected; -} + QSet allTags; + QList deckWidgets = parent->findChildren(); -QStringList VisualDeckStorageTagFilterWidget::excludedTags() const -{ - QStringList excluded; - for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { - if (tagWidget->getState() == TagState::Excluded) { - excluded.append(tagWidget->getTagName()); + for (DeckPreviewWidget *widget : deckWidgets) { + if (widget->checkVisibility()) { + for (const QString &tag : widget->deckLoader->getDeck().deckList.getTags()) { + allTags.insert(tag); + } } } - return excluded; + return allTags; } 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 337c053c7..3290c9e9a 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,22 +2,21 @@ * @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 -#include +#include "deck_preview/deck_preview_widget.h" + #include -class FlowWidget; class VisualDeckStorageWidget; class VisualDeckStorageTagFilterWidget : public QWidget { Q_OBJECT VisualDeckStorageWidget *parent; - FlowWidget *flowWidget; [[nodiscard]] QSet gatherAllTags() const; void removeTagsNotInList(const QSet &tags); @@ -28,21 +27,9 @@ class VisualDeckStorageTagFilterWidget : public QWidget public: explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent); [[nodiscard]] QStringList getAllKnownTags() const; - - /** - * @brief The tags currently in "selected" state. - */ - [[nodiscard]] QStringList selectedTags() const; - - /** - * @brief The tags currently in "excluded" state. - */ - [[nodiscard]] QStringList excludedTags() const; + void filterDecksBySelectedTags(const QList &deckPreviews) 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 acb0dcab2..4b4dee55b 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,29 +2,24 @@ #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) +VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent), folderWidget(nullptr) { - // The model and proxy own all deck data, sorting and filtering. The view widgets below - // only display the proxy's rows and their FilterMatchRole, so nothing touches the - // filesystem outside the model. - storageModel = new VisualDeckStorageModel(this); - storageProxyModel = new VisualDeckStorageSortFilterProxyModel(this); - storageProxyModel->setSourceModel(storageModel); + deckListModel = new DeckListModel(this); + deckListModel->setObjectName("visualDeckModel"); layout = new QVBoxLayout(this); layout->setSpacing(0); @@ -80,33 +75,6 @@ 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] { - storageProxyModel->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(storageModel, &QAbstractItemModel::dataChanged, this, [this] { refreshTimer->start(); }); - connect(storageModel, &VisualDeckStorageModel::deckLoaded, this, [this] { refreshTimer->start(); }); - // A deck's file path changed: re-apply the sort, since orders like "filename" depend on it. - connect(storageModel, &VisualDeckStorageModel::deckFilePathChanged, this, [this] { storageProxyModel->resort(); }); - connect(sortWidget, &VisualDeckStorageSortWidget::sortOrderChanged, this, - &VisualDeckStorageWidget::updateSortOrder); - // The filter widgets only own their ui state. Pushing it into the proxy model - // happens here, so the children stay decoupled from the model layer. - connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this, - &VisualDeckStorageWidget::updateColorFilter); - connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this, - &VisualDeckStorageWidget::updateColorFilter); - connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this, - &VisualDeckStorageWidget::updateSearchFilter); - connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, &VisualDeckStorageWidget::createRootFolderWidget); @@ -155,8 +123,6 @@ void VisualDeckStorageWidget::retranslateUi() refreshButton->setToolTip(tr("Refresh loaded files")); quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings")); - - sortWidget->retranslateUi(); } /** @@ -168,69 +134,72 @@ const VisualDeckStorageQuickSettingsWidget *VisualDeckStorageWidget::settings() } /** - * Reapplies all sort and filter options by updating the proxy model. + * Reapplies all sort and filter options by calling the appropriate update methods. */ void VisualDeckStorageWidget::reapplySortAndFilters() { - storageProxyModel->setSortOrder(sortWidget->currentSortOrder()); - storageProxyModel->reapplyFilters(); + updateSortOrder(); + updateTagFilter(); + updateColorFilter(); + updateSearchFilter(); } -/** - * @brief Scans the deck folder and rebuilds the folder tree of deck previews. - */ void VisualDeckStorageWidget::createRootFolderWidget() { - storageModel->setDeckPath(SettingsCache::instance().paths().getDeckPath()); - - folderWidget = - new VisualDeckStorageFolderDisplayWidget(this, this, QString(), false, quickSettingsWidget->getShowFolders()); + folderWidget = new VisualDeckStorageFolderDisplayWidget(this, this, SettingsCache::instance().paths().getDeckPath(), + false, quickSettingsWidget->getShowFolders()); scrollArea->setWidget(folderWidget); // this automatically destroys the old folderWidget scrollArea->widget()->setMaximumWidth(scrollArea->viewport()->width()); scrollArea->widget()->adjustSize(); - // Sort and filter runs against the model data, so it is safe to apply immediately. - reapplySortAndFilters(); + /* 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); } void VisualDeckStorageWidget::updateShowFolders(bool enabled) { if (folderWidget) { folderWidget->updateShowFolders(enabled); + QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters); } } void VisualDeckStorageWidget::updateSortOrder() { - storageProxyModel->setSortOrder(sortWidget->currentSortOrder()); + if (folderWidget) { + sortWidget->sortFolder(folderWidget); + for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : + folderWidget->findChildren()) { + sortWidget->sortFolder(subFolderWidget); + } + } } void VisualDeckStorageWidget::updateTagFilter() { - const QStringList selected = tagFilterWidget->selectedTags(); - const QStringList excluded = tagFilterWidget->excludedTags(); - storageProxyModel->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(); + if (folderWidget) { + tagFilterWidget->filterDecksBySelectedTags(folderWidget->findChildren()); + folderWidget->updateVisibility(); + } } -/** - * Pushes the color identity filter widget's state into the proxy model. - */ void VisualDeckStorageWidget::updateColorFilter() { - storageProxyModel->setColorFilter(deckPreviewColorIdentityFilterWidget->getFilterMode(), - deckPreviewColorIdentityFilterWidget->getActiveColors()); + if (folderWidget) { + deckPreviewColorIdentityFilterWidget->filterWidgets(folderWidget->findChildren()); + folderWidget->updateVisibility(); + } } -/** - * Pushes the search bar's text into the proxy model. - */ -void VisualDeckStorageWidget::updateSearchFilter(const QString &text) +void VisualDeckStorageWidget::updateSearchFilter() { - storageProxyModel->setSearchText(text); + if (folderWidget) { + searchWidget->filterWidgets(folderWidget->findChildren(), searchWidget->getSearchText()); + folderWidget->updateVisibility(); + } } void VisualDeckStorageWidget::updateTagsVisibility(const bool visible) @@ -246,4 +215,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 fe6389414..c3c0ae91b 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,30 +2,29 @@ * @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 "visual_deck_storage_model.h" -#include "visual_deck_storage_sort_filter_proxy_model.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 -#include -#include -#include -#include +#include -class QLabel; -class QResizeEvent; -class QShowEvent; -class QTimer; -class DeckPreviewColorIdentityFilterWidget; -class VisualDeckStorageFolderDisplayWidget; -class VisualDeckStorageQuickSettingsWidget; +class QSpinBox; class VisualDeckStorageSearchWidget; class VisualDeckStorageSortWidget; class VisualDeckStorageTagFilterWidget; - +class VisualDeckStorageFolderDisplayWidget; +class DeckPreviewColorIdentityFilterWidget; class VisualDeckStorageWidget final : public QWidget { Q_OBJECT @@ -38,43 +37,29 @@ public: bool deckPreviewSelectionAnimationEnabled; [[nodiscard]] const VisualDeckStorageQuickSettingsWidget *settings() const; - [[nodiscard]] VisualDeckStorageModel *model() const - { - return storageModel; - } - [[nodiscard]] VisualDeckStorageSortFilterProxyModel *proxyModel() const - { - return storageProxyModel; - } public slots: - /** - * @brief Starts scanning the deck folder and rebuilds the folder tree and previews. - */ - void createRootFolderWidget(); + void createRootFolderWidget(); // Refresh the display of cards based on the current sorting option void updateShowFolders(bool enabled); + void updateTagFilter(); + void updateColorFilter(); + void updateSearchFilter(); void updateTagsVisibility(bool visible); void updateSelectionAnimationEnabled(bool enabled); void updateSortOrder(); - void updateTagFilter(); - void updateColorFilter(); - void updateSearchFilter(const QString &text); - -signals: - void deckLoadRequested(const QString &filePath); - void openDeckEditor(const LoadedDeck &deck); - -protected: void resizeEvent(QResizeEvent *event) override; void showEvent(QShowEvent *event) override; -private: - void reapplySortAndFilters(); +signals: + void bannerCardsRefreshed(); + void deckLoadRequested(const QString &filePath); + void openDeckEditor(const LoadedDeck &deck); private: QVBoxLayout *layout; QWidget *searchAndSortContainer; QHBoxLayout *searchAndSortLayout; + DeckListModel *deckListModel; QLabel *databaseLoadIndicator; VisualDeckStorageSortWidget *sortWidget; VisualDeckStorageSearchWidget *searchWidget; @@ -82,10 +67,9 @@ private: QToolButton *refreshButton; VisualDeckStorageQuickSettingsWidget *quickSettingsWidget; QScrollArea *scrollArea; - VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr; - VisualDeckStorageModel *storageModel = nullptr; - VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr; - QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads. + VisualDeckStorageFolderDisplayWidget *folderWidget; + + void reapplySortAndFilters(); }; #endif // VISUAL_DECK_STORAGE_WIDGET_H diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp index 2f19d6224..45a02299e 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp @@ -69,14 +69,3 @@ void AppearanceSettings::setHomeTabDisplayCardName(bool _displayCardName) setValue(_displayCardName, "homeTabDisplayCardName"); emit homeTabDisplayCardNameChanged(); } - -int AppearanceSettings::getHomeTabButtonColorSourceIndex() const -{ - return getValue("homeTabButtonColorSource", "", "", 0).toInt(); -} - -void AppearanceSettings::setHomeTabButtonColorSourceIndex(int index) -{ - setValue(index, "homeTabButtonColorSource"); - emit homeTabButtonColorChanged(); -} diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h index 3a63f0df0..d9b326bee 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h @@ -27,8 +27,6 @@ public: void setHomeTabBackgroundShuffleFrequency(int _frequency); [[nodiscard]] bool getHomeTabDisplayCardName() const; void setHomeTabDisplayCardName(bool _displayCardName); - [[nodiscard]] int getHomeTabButtonColorSourceIndex() const; - void setHomeTabButtonColorSourceIndex(int index); signals: void themeNameChanged(); @@ -36,7 +34,6 @@ signals: void homeTabBackgroundSourceChanged(); void homeTabBackgroundShuffleFrequencyChanged(); void homeTabDisplayCardNameChanged(); - void homeTabButtonColorChanged(); public: explicit AppearanceSettings(const QString &settingPath, QObject *parent = nullptr);