Compare commits

..

No commits in common. "dba7cc73a440b6bf77444f5d7db2c8c29de97737" and "e589429bd987d5b76789363380a7fa3a35b5826e" have entirely different histories.

35 changed files with 939 additions and 1238 deletions

View file

@ -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

View file

@ -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 <QMessageBox>

View file

@ -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<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
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<ManaSymbolWidget *>(layout->itemAt(i)->widget())) {
w->setFixedSize(iconSize, iconSize);
}
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
manaSymbol->setFixedSize(iconSize, iconSize);
}
}

View file

@ -15,11 +15,8 @@
#include "deck_list_history_manager_widget.h"
#include "deck_list_style_proxy.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDockWidget>
#include <QLabel>
#include <QPushButton>
#include <QTextEdit>
#include <QTreeView>
#include <libcockatrice/card/card_info.h>

View file

@ -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();

View file

@ -1,49 +0,0 @@
#ifndef COCKATRICE_HOME_TAB_BUTTON_COLOR_H
#define COCKATRICE_HOME_TAB_BUTTON_COLOR_H
#include <QList>
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<Entry> all()
{
static QList<Entry> 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<Source>(value);
}
} // namespace HomeTabButtonColor
#endif // COCKATRICE_HOME_TAB_BUTTON_COLOR_H

View file

@ -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 <QGroupBox>
#include <QPainter>
@ -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<QColor, QColor> 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<HomeStyledButton *>()) {
button->updateStylesheet(gradientColors);
button->update();
@ -297,6 +266,11 @@ void HomeWidget::updateConnectButton(const ClientStatus status)
QPair<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap)
{
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
if (themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme) {
return QPair<QColor, QColor>(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)

View file

@ -23,7 +23,7 @@ class HomeWidget : public QWidget
public:
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
void updateRandomCard();
static QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
public slots:
void paintEvent(QPaintEvent *event) override;
@ -47,7 +47,6 @@ private:
void setRandomCard(ExactCard &newCard);
void loadBackgroundSourceDeck();
QPair<QColor, QColor> determineButtonColor() const;
};
#endif // HOME_WIDGET_H

View file

@ -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 <QImageReader>

View file

@ -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<int>::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"));

View file

@ -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;

View file

@ -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"

View file

@ -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 <QSet>
#include <QMouseEvent>
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<QChar> DeckPreviewColorIdentityFilterWidget::getActiveColors() const
{
QSet<QChar> 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<DeckPreviewWidget *> 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<QChar> 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<QChar> 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;
}
}

View file

@ -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 <QHBoxLayout>
#include <QMap>
#include <QPushButton>
#include <QSet>
#include <QWidget>
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<QChar> getActiveColors() const;
void filterWidgets(QList<DeckPreviewWidget *> 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<QChar, bool> activeColors;
VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes;
FilterMode filterMode = Includes; // Default to "includes" mode
};
#endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H

View file

@ -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 <QDirIterator>
#include <QHBoxLayout>
#include <QMessageBox>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
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<QStringList()> &provider)
{
knownTagsProvider = provider;
}
void DeckPreviewDeckTagsDisplayWidget::setConversionPromptHandler(const std::function<bool()> &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<DeckPreviewWidget *>(parentWidget())) {
// If we're the child of a DeckPreviewWidget, then we need to handle conversion
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(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);
}
}
}
}

View file

@ -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 <QStringList>
#include "../../../deck_loader/deck_loader.h"
#include "deck_preview_widget.h"
#include <QWidget>
#include <functional>
class FlowWidget;
class DeckPreviewWidget;
class DeckPreviewDeckTagsDisplayWidget : public QWidget
{
Q_OBJECT
QStringList currentTags;
FlowWidget *flowWidget;
std::function<QStringList()> knownTagsProvider;
std::function<bool()> 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<QStringList()> &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<bool()> &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

View file

@ -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 <QClipboard>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QInputDialog>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QSet>
#include <QStandardItemModel>
#include <QVBoxLayout>
#include <algorithm>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
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<int>::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<QWidget *> widgets = findChildren<QWidget *>();
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<int>::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<QChar> 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 &currentText)
// Prepare the new items with deduplication
QSet<QPair<QString, QString>> bannerCardSet;
const int r = row();
if (r != -1) {
const DeckList &deckList = model->dataForRow(r).deck.deckList;
const QList<const DecklistCardNode *> cardsInDeck = deckList.getCardNodes();
QList<const DecklistCardNode *> cardsInDeck = deckLoader->getDeck().deckList.getCardNodes();
for (auto currentCard : cardsInDeck) {
for (int k = 0; k < currentCard->getNumber(); ++k) {
bannerCardSet.insert(QPair<QString, QString>(currentCard->getName(), currentCard->getCardProviderId()));
}
for (auto currentCard : cardsInDeck) {
for (int k = 0; k < currentCard->getNumber(); ++k) {
bannerCardSet.insert(QPair<QString, QString>(currentCard->getName(), currentCard->getCardProviderId()));
}
}
@ -285,16 +327,16 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString &currentText)
// 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 &currentText)
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<QPair<QString, QString>>();
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();
}

View file

@ -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 <QAbstractItemView>
#include <QApplication>
@ -16,82 +20,72 @@
#include <QVBoxLayout>
#include <QWidget>
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 &currentText);
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 &currentText);
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<QWidget *> fixedWidthChildren; ///< Children clamped to the picture width on resize.
int lastKnownBannerWidth = -1; ///< The picture width last applied to the children.
};
class NoScrollFilter : public QObject

View file

@ -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 <QElapsedTimer>
#include <QSet>
#include <QTimer>
#include <QVBoxLayout>
#include <QDirIterator>
#include <QMouseEvent>
#include <libcockatrice/settings/paths_settings.h>
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<DeckPreviewWidget *> 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<DeckPreviewWidget *>()) {
display->updateVisibility();
}
if (recursive) {
for (auto *subFolder : findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
subFolder->updateVisibility(false);
}
}
} else {
setVisible(false);
}
}
bool VisualDeckStorageFolderDisplayWidget::checkVisibility()
{
bool atLeastOneWidgetVisible = false;
if (flowWidget) {
// Iterate through all DeckPreviewWidgets
for (DeckPreviewWidget *display : flowWidget->findChildren<DeckPreviewWidget *>()) {
if (display->checkVisibility()) {
atLeastOneWidgetVisible = true;
}
}
}
for (VisualDeckStorageFolderDisplayWidget *subFolder : findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
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<VisualDeckStorageFolderDisplayWidget *>()) {
// steal all DeckPreviewWidgets from the subfolder
for (auto *deck : subFolder->getFlowWidget()->findChildren<DeckPreviewWidget *>()) {
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<DeckPreviewWidget *>()) {
// 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;
}

View file

@ -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 <QHash>
#include <QSet>
#include <QString>
#include <QWidget>
#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<QString, DeckPreviewWidget *> deckWidgets; ///< Deck file path -> preview widget.
QHash<QString, VisualDeckStorageFolderDisplayWidget *> 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<QString> deckPassPresentPaths; ///< File paths seen so far in the active pass.
};
#endif // VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H

View file

@ -4,7 +4,6 @@
#include <QDir>
#include <QDirIterator>
#include <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QFutureWatcher>
@ -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<DeckLoadResult> 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<DeckLoadResult> 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<DeckLoadResult> {
@ -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;
}

View file

@ -17,7 +17,6 @@
#include <QAbstractListModel>
#include <QDateTime>
#include <QHash>
#include <QList>
#include <QStringList>
#include <libcockatrice/deck_list/deck_list.h>
@ -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<DeckPreviewData> decks;
QHash<QString, int> 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<PendingDeckLoad> 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

View file

@ -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 <QCheckBox>

View file

@ -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 <QAction>
#include <QTimer>
#include <QFileInfo>
#include <libcockatrice/settings/paths_settings.h>
/**
* @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<DeckPreviewWidget *> 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);
}
}

View file

@ -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 <QHBoxLayout>
#include <QLineEdit>
#include <QWidget>
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<DeckPreviewWidget *> widgets, const QString &searchText);
private:
QHBoxLayout *layout;
VisualDeckStorageWidget *parent;
QLineEdit *searchBar;
QTimer *searchDebounceTimer;
};

View file

@ -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();

View file

@ -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();

View file

@ -1,11 +1,20 @@
#include "visual_deck_storage_sort_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "visual_deck_storage_widget.h"
#include <QFileInfo>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
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<SortOrder>(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<int>::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<VisualDeckStorageSortFilterProxyModel::SortOrder>(sortComboBox->currentIndex());
}
void VisualDeckStorageSortWidget::updateSortOrder()
{
sortOrder = static_cast<SortOrder>(sortComboBox->currentIndex());
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageSortingOrder(sortComboBox->currentIndex());
emit sortOrderChanged();
}
void VisualDeckStorageSortWidget::sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget)
{
auto children =
folderWidget->getFlowWidget()->findChildren<QWidget *>(QString(), Qt::FindChildOption::FindDirectChildrenOnly);
for (auto widget : children) {
auto deckPreviewWidgets =
widget->findChildren<DeckPreviewWidget *>(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<DeckPreviewWidget *> VisualDeckStorageSortWidget::filterFiles(QList<DeckPreviewWidget *> 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;
}

View file

@ -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 <QComboBox>
#include <QHBoxLayout>
#include <QWidget>
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<DeckPreviewWidget *> filterFiles(QList<DeckPreviewWidget *> 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;
};

View file

@ -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 <QHBoxLayout>
@ -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<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
void VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList<DeckPreviewWidget *> &deckPreviews) const
{
QSet<QString> 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<DeckPreviewTagDisplayWidget *>()) {
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<QString> &tags)
{
auto *flowWidget = findChild<FlowWidget *>();
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
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 *>();
flowWidget->addWidget(newTagWidget);
}
}
void VisualDeckStorageTagFilterWidget::sortTags()
{
auto *flowWidget = findChild<FlowWidget *>();
if (!flowWidget) {
return;
}
// Get all tag widgets
QList<DeckPreviewTagDisplayWidget *> tagWidgets = findChildren<DeckPreviewTagDisplayWidget *>();
@ -120,26 +147,19 @@ void VisualDeckStorageTagFilterWidget::sortTags()
}
}
QStringList VisualDeckStorageTagFilterWidget::selectedTags() const
QSet<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
{
QStringList selected;
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
if (tagWidget->getState() == TagState::Selected) {
selected.append(tagWidget->getTagName());
}
}
return selected;
}
QSet<QString> allTags;
QList<DeckPreviewWidget *> deckWidgets = parent->findChildren<DeckPreviewWidget *>();
QStringList VisualDeckStorageTagFilterWidget::excludedTags() const
{
QStringList excluded;
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
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

View file

@ -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 <QSet>
#include <QStringList>
#include "deck_preview/deck_preview_widget.h"
#include <QWidget>
class FlowWidget;
class VisualDeckStorageWidget;
class VisualDeckStorageTagFilterWidget : public QWidget
{
Q_OBJECT
VisualDeckStorageWidget *parent;
FlowWidget *flowWidget;
[[nodiscard]] QSet<QString> gatherAllTags() const;
void removeTagsNotInList(const QSet<QString> &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<DeckPreviewWidget *> &deckPreviews) const;
public slots:
/**
* @brief Rebuilds the tag chips from the tags of the currently visible decks.
*/
void refreshTags();
void showEvent(QShowEvent *event) override;
};

View file

@ -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 <QLabel>
#include <QTimer>
#include <QComboBox>
#include <QDirIterator>
#include <QMouseEvent>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
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<VisualDeckStorageFolderDisplayWidget *>()) {
sortWidget->sortFolder(subFolderWidget);
}
}
}
void VisualDeckStorageWidget::updateTagFilter()
{
const QStringList selected = tagFilterWidget->selectedTags();
const QStringList excluded = tagFilterWidget->excludedTags();
storageProxyModel->setTagFilter(QSet<QString>(selected.cbegin(), selected.cend()),
QSet<QString>(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<DeckPreviewWidget *>());
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<DeckPreviewWidget *>());
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<DeckPreviewWidget *>(), 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;
}
}

View file

@ -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 <QHBoxLayout>
#include <QScrollArea>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
#include <libcockatrice/models/deck_list/deck_list_model.h>
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

View file

@ -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();
}

View file

@ -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);