[VDS] Drive folder and preview widgets from the model (MVC views)

Took 16 minutes

Took 8 minutes
This commit is contained in:
Lukas Brübach 2026-08-13 14:22:57 +02:00
parent c4f4c57722
commit 09d2270d45
21 changed files with 752 additions and 886 deletions

View file

@ -9,6 +9,7 @@
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
#include "../../interface/widgets/dialogs/dlg_load_remote_deck.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
#include "deck_view.h"
#include <QMessageBox>

View file

@ -15,6 +15,8 @@
#include "deck_list_history_manager_widget.h"
#include "deck_list_style_proxy.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDockWidget>
#include <QLabel>
#include <QTextEdit>

View file

@ -1,6 +1,7 @@
#include "printing_selector_card_overlay_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../cards/card_info_picture_widget.h"
#include "printing_selector_card_display_widget.h"
#include <QImageReader>

View file

@ -8,6 +8,7 @@
#ifndef TAB_GENERIC_DECK_EDITOR_H
#define TAB_GENERIC_DECK_EDITOR_H
#include "../../deck_loader/deck_loader.h"
#include "../interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h"
#include "../interface/widgets/deck_editor/deck_editor_card_info_dock_widget.h"
#include "../interface/widgets/deck_editor/deck_editor_database_display_widget.h"

View file

@ -1,12 +1,12 @@
#include "deck_preview_color_identity_filter_widget.h"
#include "../../cards/additional_info/mana_symbol_widget.h"
#include "deck_preview_widget.h"
#include "../visual_deck_storage_widget.h"
#include <QMouseEvent>
#include <QSet>
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent)
: QWidget(parent), layout(new QHBoxLayout(this))
: QWidget(parent), layout(new QHBoxLayout(this)), visualDeckStorageWidget(parent)
{
setLayout(layout);
layout->setSpacing(5);
@ -32,10 +32,6 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua
// Connect the button's clicked signal
connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode);
connect(this, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, parent,
&VisualDeckStorageWidget::updateColorFilter);
connect(this, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, parent,
&VisualDeckStorageWidget::updateColorFilter);
// Call retranslateUi to set the initial text
retranslateUi();
@ -45,111 +41,56 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi()
{
// Set the toggle button text based on the current mode
switch (filterMode) {
case ExactMatch:
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
toggleButton->setText(tr("Mode: Exact Match"));
break;
case Includes:
case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes:
toggleButton->setText(tr("Mode: Includes"));
break;
case Excludes:
case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes:
toggleButton->setText(tr("Mode: Excludes"));
break;
}
toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)"));
}
/**
* @brief Pushes the current filter state to the proxy model.
*/
void DeckPreviewColorIdentityFilterWidget::applyFilter()
{
QSet<QChar> activeColorSet;
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
if (it.value()) {
activeColorSet.insert(it.key());
}
}
// The VDS owns the proxy model, which exists for the widget's whole lifetime.
visualDeckStorageWidget->proxyModel()->setColorFilter(filterMode, activeColorSet);
}
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active)
{
activeColors[color] = active;
emit activeColorsChanged();
applyFilter();
}
void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
{
// Cycle through the modes
switch (filterMode) {
case ExactMatch:
filterMode = Includes;
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes;
break;
case Includes:
filterMode = Excludes;
case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes:
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes;
break;
case Excludes:
filterMode = ExactMatch;
case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes:
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch;
break;
}
retranslateUi(); // Update the button text
emit filterModeChanged(filterMode);
}
void DeckPreviewColorIdentityFilterWidget::filterWidgets(QList<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;
}
applyFilter();
}

View file

@ -2,18 +2,17 @@
* @file deck_preview_color_identity_filter_widget.h
* @ingroup VisualDeckPreviewWidgets
*/
//! \todo Document this file.
#ifndef DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H
#define DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H
#include "../visual_deck_storage_widget.h"
#include "../visual_deck_storage_sort_filter_proxy_model.h"
#include <QHBoxLayout>
#include <QMap>
#include <QPushButton>
#include <QWidget>
class DeckPreviewWidget;
class VisualDeckStorageWidget;
class DeckPreviewColorIdentityFilterWidget : public QWidget
@ -21,34 +20,21 @@ class DeckPreviewColorIdentityFilterWidget : public QWidget
Q_OBJECT
public:
/**
* How the active colors are matched against a deck's color identity.
*/
enum FilterMode
{
ExactMatch, ///< The color identity consists of exactly the active colors.
Includes, ///< The color identity contains all of the active colors.
Excludes ///< The color identity contains none of the active colors.
};
Q_ENUM(FilterMode)
explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent);
void retranslateUi();
void filterWidgets(QList<DeckPreviewWidget *> widgets);
signals:
void filterModeChanged(FilterMode mode);
void activeColorsChanged();
private slots:
void handleColorToggled(QChar color, bool active);
void updateFilterMode();
private:
void applyFilter();
QHBoxLayout *layout;
QPushButton *toggleButton;
QMap<QChar, bool> activeColors;
FilterMode filterMode = Includes; // Default to "includes" mode
VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes;
VisualDeckStorageWidget *visualDeckStorageWidget; ///< The VDS owning the proxy model.
};
#endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H

View file

@ -1,19 +1,15 @@
#include "deck_preview_deck_tags_display_widget.h"
#include "../../../../client/settings/cache_settings.h"
#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h"
#include "../../../../interface/widgets/tabs/tab_deck_editor.h"
#include "../../../deck_loader/deck_loader.h"
#include "../../general/layout_containers/flow_widget.h"
#include "deck_preview_tag_addition_widget.h"
#include "deck_preview_tag_dialog.h"
#include "deck_preview_tag_display_widget.h"
#include "deck_preview_widget.h"
#include <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)
@ -54,6 +50,16 @@ 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
*/
@ -92,93 +98,13 @@ static QStringList findAllKnownTags()
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
{
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;
// The deck editor path has no conversion prompt; the VDS path registers one.
if (conversionPromptHandler_ && !conversionPromptHandler_()) {
return;
}
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
return false;
}
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
return false;
}
convertFileToCockatriceFormat(deckPreviewWidget);
return true;
}
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
if (conversionDialog.exec() != QDialog::Accepted) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
!conversionDialog.dontAskAgain());
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
return false;
}
// Try to convert file
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
return false;
}
convertFileToCockatriceFormat(deckPreviewWidget);
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
}
return true;
const QStringList knownTags = knownTagsProvider_ ? knownTagsProvider_() : findAllKnownTags();
execTagDialog(knownTags);
}
void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags)
@ -191,4 +117,4 @@ void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTag
emit tagsChanged(updatedTags);
}
}
}
}

View file

@ -2,41 +2,53 @@
* @file deck_preview_deck_tags_display_widget.h
* @ingroup VisualDeckPreviewWidgets
*/
//! \todo Document this file.
#ifndef DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H
#define DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H
#include "../../../deck_loader/deck_loader.h"
#include "deck_preview_widget.h"
#include <QStringList>
#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,47 +1,69 @@
#include "deck_preview_widget.h"
#include "../../../../client/settings/cache_settings.h"
#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h"
#include "../../../deck_loader/deck_loader.h"
#include "../../cards/additional_info/color_identity_widget.h"
#include "../../cards/deck_preview_card_picture_widget.h"
#include "../visual_deck_storage_quick_settings_widget.h"
#include "../visual_deck_storage_tag_filter_widget.h"
#include "../visual_deck_storage_widget.h"
#include "deck_preview_deck_tags_display_widget.h"
#include <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), filePath(_filePath),
colorIdentityWidget(nullptr), deckTagsDisplayWidget(nullptr)
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model_(model), filePath(_filePath)
{
layout = new QVBoxLayout(this);
setLayout(layout);
deckLoader = new DeckLoader(this);
connect(deckLoader, &DeckLoader::loadFinished, this, &DeckPreviewWidget::initializeUi);
//! \todo Batch tag refresh: count finished deck loads and refresh tags once all decks are loaded.
// Currently expensive: refreshes on each individual deck load instead of once at the end.
connect(deckLoader, &DeckLoader::loadFinished, visualDeckStorageWidget->tagFilterWidget,
&VisualDeckStorageTagFilterWidget::refreshTags);
deckLoader->loadFromFileAsync(filePath, DeckFileFormat::getFormatFromName(filePath), false);
bannerCardDisplayWidget =
auto *pictureWidget =
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this,
&DeckPreviewWidget::imageClickedEvent);
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
pictureWidget->setFontSize(24);
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent);
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
&DeckPreviewWidget::imageDoubleClickedEvent);
bannerCardDisplayWidget = pictureWidget;
colorIdentityWidget = new ColorIdentityWidget(this);
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this);
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags);
deckTagsDisplayWidget->setKnownTagsProvider(
[this] { return visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); });
deckTagsDisplayWidget->setConversionPromptHandler([this] { return promptFileConversionIfRequired(); });
bannerCardLabel = new QLabel(this);
bannerCardLabel->setObjectName("bannerCardLabel");
bannerCardComboBox = new QComboBox(this);
bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
bannerCardComboBox->setObjectName("bannerCardComboBox");
bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox));
connect(bannerCardComboBox, QOverload<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,
@ -56,6 +78,15 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
&DeckPreviewWidget::refreshBannerCardToolTip);
layout->addWidget(bannerCardDisplayWidget);
layout->addWidget(colorIdentityWidget);
layout->addWidget(deckTagsDisplayWidget);
layout->addWidget(bannerCardLabel);
layout->addWidget(bannerCardComboBox);
connect(model_, &QAbstractItemModel::dataChanged, this, &DeckPreviewWidget::syncFromModel);
retranslateUi();
syncFromModel();
}
void DeckPreviewWidget::retranslateUi()
@ -79,83 +110,28 @@ void DeckPreviewWidget::enterEvent(QEnterEvent *event)
{
QWidget::enterEvent(event);
// don't do reloads until widgets have been created
if (bannerCardComboBox != nullptr) {
reloadIfModified();
// Don't do reloads until the deck has actually been loaded once.
reloadIfModified();
}
/**
* @brief The row of this deck in the source model, or -1 if it no longer exists.
*/
int DeckPreviewWidget::row() const
{
return model_->rowForFilePath(filePath);
}
/**
* @brief The display name is given by the deck name, or the filename if the deck name is not set.
*/
QString DeckPreviewWidget::getDisplayName() const
{
const int r = row();
if (r == -1) {
return {};
}
}
/**
* @brief Sets the lastModifiedTime to the value given by the file.
*/
void DeckPreviewWidget::updateLastModifiedTime()
{
QFileInfo fileInfo(filePath);
lastModifiedTime = fileInfo.lastModified();
}
/**
* @brief Writes the current contents of the deck to file. Updates the lastModifiedTime afterward.
*/
void DeckPreviewWidget::writeDeckToFile()
{
DeckLoader::saveToFile(deckLoader->getDeck());
updateLastModifiedTime();
}
void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
{
if (!deckLoadSuccess) {
return;
}
QFileInfo fileInfo(filePath);
lastModifiedTime = fileInfo.lastModified();
bannerCardDisplayWidget->setFontSize(24);
setFilePath(deckLoader->getDeck().lastLoadInfo.fileName);
colorIdentityWidget = new ColorIdentityWidget(this);
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this);
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags);
bannerCardLabel = new QLabel(this);
bannerCardLabel->setObjectName("bannerCardLabel");
bannerCardComboBox = new QComboBox(this);
bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
bannerCardComboBox->setObjectName("bannerCardComboBox");
bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox));
connect(bannerCardComboBox, QOverload<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());
return model_->dataForRow(r).displayName;
}
/**
@ -163,33 +139,36 @@ void DeckPreviewWidget::resyncWidgets()
*/
void DeckPreviewWidget::reloadIfModified()
{
QFileInfo fileInfo(filePath);
QDateTime newLastModifiedTime = fileInfo.lastModified();
if (!newLastModifiedTime.isValid() || newLastModifiedTime <= lastModifiedTime) {
const int r = row();
if (r == -1 || !model_->dataForRow(r).loadSucceeded) {
return;
}
bool success = deckLoader->reload();
if (success) {
fileInfo.refresh();
lastModifiedTime = fileInfo.lastModified();
resyncWidgets();
}
model_->reloadIfModified(r);
}
void DeckPreviewWidget::updateVisibility()
/**
* @brief Syncs the contents of the child widgets with the current row's data.
*/
void DeckPreviewWidget::syncFromModel()
{
setHidden(!checkVisibility());
}
bool DeckPreviewWidget::checkVisibility() const
{
if (filteredBySearch || filteredByColor || filteredByTags) {
return false;
const int r = row();
if (r == -1) {
return;
}
return true;
const DeckPreviewData &data = model_->dataForRow(r);
filePath = data.filePath;
const CardRef bannerCardRef = data.deck.deckList.getBannerCard();
const ExactCard bannerCard =
bannerCardRef.name.isEmpty() ? ExactCard() : CardDatabaseManager::query()->getCard(bannerCardRef);
bannerCardDisplayWidget->setCard(bannerCard);
refreshBannerCardText();
updateBannerCardComboBox(bannerCardRef.name);
colorIdentityWidget->setColorIdentity(data.colorIdentity);
deckTagsDisplayWidget->setTags(data.tags);
}
void DeckPreviewWidget::updateColorIdentityVisibility(bool visible)
@ -229,51 +208,6 @@ void DeckPreviewWidget::updateTagsVisibility(bool visible)
}
}
QString DeckPreviewWidget::getColorIdentity()
{
QStringList cardList = deckLoader->getDeck().deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
if (cardList.isEmpty()) {
return {};
}
QSet<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.
@ -310,11 +244,15 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString &currentText)
// Prepare the new items with deduplication
QSet<QPair<QString, QString>> bannerCardSet;
QList<const DecklistCardNode *> cardsInDeck = deckLoader->getDeck().deckList.getCardNodes();
const int r = row();
if (r != -1) {
const DeckList &deckList = model_->dataForRow(r).deck.deckList;
const QList<const DecklistCardNode *> cardsInDeck = 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()));
}
}
}
@ -344,7 +282,9 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString &currentText)
bannerCardComboBox->setCurrentIndex(restoredIndex);
} else {
// Add a placeholder "-" and set it as the current selection
int bannerIndex = bannerCardComboBox->findText(deckLoader->getDeck().deckList.getBannerCard().name);
const QString currentBannerCardName =
r == -1 ? QString() : model_->dataForRow(r).deck.deckList.getBannerCard().name;
int bannerIndex = bannerCardComboBox->findText(currentBannerCardName);
if (bannerIndex != -1) {
bannerCardComboBox->setCurrentIndex(bannerIndex);
} else {
@ -362,8 +302,11 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
{
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
CardRef cardRef = {name, id};
deckLoader->getDeck().deckList.setBannerCard(cardRef);
writeDeckToFile();
const int r = row();
if (r == -1) {
return;
}
model_->setBannerCard(r, cardRef);
bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef));
}
@ -385,17 +328,24 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC
void DeckPreviewWidget::setTags(const QStringList &tags)
{
deckLoader->getDeck().deckList.setTags(tags);
writeDeckToFile();
const int r = row();
if (r != -1) {
model_->setTags(r, tags);
}
}
QMenu *DeckPreviewWidget::createRightClickMenu()
{
const int r = row();
auto *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this,
[this] { emit openDeckEditor(deckLoader->getDeck()); });
connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, [this, r] {
if (r != -1) {
emit openDeckEditor(model_->deckForRow(r));
}
});
connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget,
&DeckPreviewDeckTagsDisplayWidget::openTagEditDlg);
@ -408,14 +358,26 @@ QMenu *DeckPreviewWidget::createRightClickMenu()
auto saveToClipboardMenu = menu->addMenu(tr("Save Deck to Clipboard"));
connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this,
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, true); });
connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this,
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, false); });
connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this,
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, true); });
connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this,
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, false); });
connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, [this, r] {
if (r != -1) {
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, true, true);
}
});
connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, [this, r] {
if (r != -1) {
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, true, false);
}
});
connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, [this, r] {
if (r != -1) {
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, false, true);
}
});
connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, [this, r] {
if (r != -1) {
DeckLoader::saveToClipboard(model_->dataForRow(r).deck.deckList, false, false);
}
});
menu->addSeparator();
@ -450,57 +412,58 @@ void DeckPreviewWidget::addSetBannerCardMenu(QMenu *menu)
void DeckPreviewWidget::actRenameDeck()
{
const int r = row();
if (r == -1) {
return;
}
// read input
const QString oldName = deckLoader->getDeck().deckList.getName();
const QString oldName = model_->dataForRow(r).deckName;
bool ok;
QString newName = QInputDialog::getText(this, "Rename deck", tr("New name:"), QLineEdit::Normal, oldName, &ok);
QString newName = QInputDialog::getText(this, tr("Rename deck"), tr("New name:"), QLineEdit::Normal, oldName, &ok);
if (!ok || oldName == newName) {
return;
}
// write change
deckLoader->getDeck().deckList.setName(newName);
writeDeckToFile();
model_->renameDeck(r, newName);
// update VDS
refreshBannerCardText();
// The banner card text updates via the model's dataChanged signal.
}
void DeckPreviewWidget::actRenameFile()
{
const int r = row();
if (r == -1) {
return;
}
// read input
const auto info = QFileInfo(filePath);
const QString oldName = info.baseName();
bool ok;
QString newName = QInputDialog::getText(this, "Rename file", tr("New name:"), QLineEdit::Normal, oldName, &ok);
QString newName = QInputDialog::getText(this, tr("Rename file"), tr("New name:"), QLineEdit::Normal, oldName, &ok);
if (!ok || newName.isEmpty() || oldName == newName) {
return;
}
QString newFileName = newName;
if (!info.suffix().isEmpty()) {
newFileName += "." + info.suffix();
}
// write change
const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath();
if (!QFile::rename(info.filePath(), newFilePath)) {
if (!model_->renameFile(r, newName)) {
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
return;
}
deckLoader->getDeck().lastLoadInfo.fileName = newFilePath;
setFilePath(newFilePath);
// update VDS
updateLastModifiedTime();
refreshBannerCardText();
// The file path and banner card text update via the model's signals.
}
void DeckPreviewWidget::actDeleteFile()
{
const int r = row();
if (r == -1) {
return;
}
// read input
auto res = QMessageBox::warning(this, tr("Delete file"), tr("Are you sure you want to delete the selected file?"),
QMessageBox::Yes | QMessageBox::No);
@ -509,11 +472,74 @@ void DeckPreviewWidget::actDeleteFile()
}
// write change
if (!QFile::remove(QFileInfo(filePath).filePath())) {
if (!model_->deleteFile(r)) {
QMessageBox::critical(this, tr("Error"), tr("Delete failed"));
return;
}
// update VDS
this->deleteLater();
// The folder widget removes this preview once the row is gone.
}
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
{
QFileInfo fileInfo(filePath);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
if (QFile::exists(newFileName)) {
QMessageBox::StandardButton reply =
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
QMessageBox::Yes | QMessageBox::No);
return reply == QMessageBox::Yes;
}
return true; // Safe to proceed
}
/**
* Checks if the deck's file format supports tags.
* If not, then prompt the user for file conversion.
* @return whether the resulting file can support adding tags
*/
bool DeckPreviewWidget::promptFileConversionIfRequired()
{
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
return true;
}
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
return false;
}
if (!confirmOverwriteIfExists(this, filePath)) {
return false;
}
model_->convertToCockatriceFormat(row());
return true;
}
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(this);
if (conversionDialog.exec() != QDialog::Accepted) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
!conversionDialog.dontAskAgain());
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
return false;
}
// Try to convert file
if (!confirmOverwriteIfExists(this, filePath)) {
return false;
}
model_->convertToCockatriceFormat(row());
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
}
return true;
}

View file

@ -2,16 +2,12 @@
* @file deck_preview_widget.h
* @ingroup VisualDeckPreviewWidgets
*/
//! \todo Document this file.
#ifndef DECK_PREVIEW_WIDGET_H
#define DECK_PREVIEW_WIDGET_H
#include "../../../deck_loader/deck_loader.h"
#include "../../cards/additional_info/color_identity_widget.h"
#include "../../cards/deck_preview_card_picture_widget.h"
#include "../visual_deck_storage_widget.h"
#include "deck_preview_deck_tags_display_widget.h"
#include "../visual_deck_storage_model.h"
#include <QAbstractItemView>
#include <QApplication>
@ -20,72 +16,80 @@
#include <QVBoxLayout>
#include <QWidget>
class QEnterEvent;
class QLabel;
class QMenu;
class VisualDeckStorageWidget;
class QMouseEvent;
class ColorIdentityWidget;
class DeckPreviewCardPictureWidget;
class DeckPreviewDeckTagsDisplayWidget;
class VisualDeckStorageModel;
class VisualDeckStorageWidget;
class DeckPreviewWidget final : public QWidget
{
Q_OBJECT
public:
explicit DeckPreviewWidget(QWidget *_parent,
explicit DeckPreviewWidget(QWidget *parent,
VisualDeckStorageWidget *_visualDeckStorageWidget,
VisualDeckStorageModel *model,
const QString &_filePath);
void retranslateUi();
QString getColorIdentity();
[[nodiscard]] QString getDisplayName() const;
VisualDeckStorageWidget *visualDeckStorageWidget;
QVBoxLayout *layout;
QString filePath;
QDateTime lastModifiedTime;
DeckLoader *deckLoader;
DeckPreviewCardPictureWidget *bannerCardDisplayWidget = nullptr;
ColorIdentityWidget *colorIdentityWidget = nullptr;
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget = nullptr;
QLabel *bannerCardLabel = nullptr;
QComboBox *bannerCardComboBox = nullptr;
bool filteredBySearch = false;
bool filteredByColor = false;
bool filteredByTags = false;
[[nodiscard]] bool checkVisibility() const;
/**
* @brief The banner card picture; the parent widget wires its size to the card size setting.
*/
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
signals:
void deckLoadRequested(const QString &filePath);
void openDeckEditor(const LoadedDeck &deck);
public slots:
void setFilePath(const QString &filePath);
void refreshBannerCardText();
void refreshBannerCardToolTip();
void updateBannerCardComboBox(const QString &currentText);
void setBannerCard(int);
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void initializeUi(bool deckLoadSuccess);
void resyncWidgets();
/**
* @brief Re-reads the row's data from the model and syncs every child widget.
* Connected to the model's dataChanged signal.
*/
void syncFromModel();
/**
* @brief Reloads the deck file if its modification time is newer than the stored one.
*/
void reloadIfModified();
void updateVisibility();
void refreshBannerCardToolTip();
void updateColorIdentityVisibility(bool visible);
void updateBannerCardComboBoxVisibility(bool visible);
void updateTagsVisibility(bool visible);
void resizeEvent(QResizeEvent *event) override;
void setBannerCard(int);
void setTags(const QStringList &tags);
protected:
void enterEvent(QEnterEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
void updateLastModifiedTime();
void writeDeckToFile();
[[nodiscard]] int row() const;
[[nodiscard]] QString getDisplayName() const;
void refreshBannerCardText();
void updateBannerCardComboBox(const QString &currentText);
bool promptFileConversionIfRequired();
QMenu *createRightClickMenu();
void addSetBannerCardMenu(QMenu *menu);
private slots:
void setTags(const QStringList &tags);
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void actRenameDeck();
void actRenameFile();
void actDeleteFile();
VisualDeckStorageWidget *visualDeckStorageWidget;
VisualDeckStorageModel *model_;
QString filePath;
QVBoxLayout *layout;
ColorIdentityWidget *colorIdentityWidget;
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
QLabel *bannerCardLabel;
QComboBox *bannerCardComboBox;
};
class NoScrollFilter : public QObject

View file

@ -1,20 +1,26 @@
#include "visual_deck_storage_folder_display_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../cards/card_info_picture_widget.h"
#include "../general/display/banner_widget.h"
#include "../general/layout_containers/flow_widget.h"
#include "deck_preview/deck_preview_widget.h"
#include "visual_deck_storage_model.h"
#include "visual_deck_storage_quick_settings_widget.h"
#include "visual_deck_storage_sort_filter_proxy_model.h"
#include "visual_deck_storage_widget.h"
#include <QDirIterator>
#include <QMouseEvent>
#include <libcockatrice/settings/paths_settings.h>
#include <QSet>
#include <QTimer>
#include <QVBoxLayout>
VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
QWidget *parent,
VisualDeckStorageWidget *_visualDeckStorageWidget,
QString _filePath,
const QString &_folderPath,
bool canBeHidden,
bool _showFolders)
: QWidget(parent), showFolders(_showFolders), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath)
: QWidget(parent), showFolders(_showFolders), folderPath(_folderPath),
visualDeckStorageWidget(_visualDeckStorageWidget)
{
layout = new QVBoxLayout(this);
setLayout(layout);
@ -22,6 +28,9 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
header = new BannerWidget(this, "");
header->setClickable(canBeHidden);
header->setHidden(!showFolders);
const QString bannerText = folderPath.isEmpty() ? tr("Deck Storage") : folderPath;
header->setText(bannerText);
layout->addWidget(header);
container = new QWidget(this);
@ -35,192 +44,214 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
containerLayout->addWidget(flowWidget);
createWidgetsForFiles();
createWidgetsForFolders();
auto *proxy = visualDeckStorageWidget->proxyModel();
// A burst of proxy changes (one dataChanged per finished deck load, plus the filter
// invalidations) coalesces into a single reconcile, so a scan of many decks doesn't
// rebuild the flow layout once per deck.
connect(proxy, &QAbstractItemModel::modelReset, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
connect(proxy, &QAbstractItemModel::rowsInserted, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
connect(proxy, &QAbstractItemModel::rowsRemoved, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
connect(proxy, &QAbstractItemModel::dataChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
connect(proxy, &QAbstractItemModel::layoutChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
refreshUi();
reconcileTimer = new QTimer(this);
reconcileTimer->setSingleShot(true);
reconcileTimer->setInterval(150);
connect(reconcileTimer, &QTimer::timeout, this, &VisualDeckStorageFolderDisplayWidget::reconcile);
reconcile();
}
void VisualDeckStorageFolderDisplayWidget::refreshUi()
void VisualDeckStorageFolderDisplayWidget::scheduleReconcile()
{
QString bannerText = tr("Deck Storage");
QString deckPath = SettingsCache::instance().paths().getDeckPath();
if (filePath != deckPath) {
QString relativePath = filePath;
if (filePath.startsWith(deckPath)) {
relativePath = filePath.mid(deckPath.length()); // Remove the deckPath prefix
if (relativePath.startsWith('/')) {
relativePath.remove(0, 1); // Remove leading '/' if it exists
}
}
bannerText = relativePath;
}
header->setText(bannerText);
reconcileTimer->start();
}
/**
* Gets all files in the directory that have an accepted decklist file extension
*
* @param filePath The directory to search through
* @param recursive Whether to search through subdirectories
* @brief Re-reads the proxy and rebuilds the deck previews and subfolder widgets to match.
*/
static QStringList getAllFiles(const QString &filePath, bool recursive)
void VisualDeckStorageFolderDisplayWidget::reconcile()
{
QStringList allFiles;
// QDirIterator with QDir::Files ensures only files are listed (no directories)
auto flags =
recursive ? QDirIterator::Subdirectories | QDirIterator::FollowSymlinks : QDirIterator::NoIteratorFlags;
QDirIterator it(filePath, DeckLoader::ACCEPTED_FILE_EXTENSIONS, QDir::Files, flags);
while (it.hasNext()) {
allFiles << it.next(); // Add each file path to the list
}
return allFiles;
reconcileDecks();
createSubFolderWidgets();
refreshVisibility();
}
void VisualDeckStorageFolderDisplayWidget::createWidgetsForFiles()
/**
* @brief Reconciles the deck preview widgets in the flow layout with the accepted proxy rows.
*
* Decks are keyed by file path so that already-created preview widgets are reused
* (their banner combo box, tag editor state, etc. is preserved) whenever possible.
*/
void VisualDeckStorageFolderDisplayWidget::reconcileDecks()
{
QList<DeckPreviewWidget *> allDecks;
for (const QString &file : getAllFiles(filePath, !showFolders)) {
auto *display = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, file);
auto *proxy = visualDeckStorageWidget->proxyModel();
auto *model = visualDeckStorageWidget->model();
connect(display, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget,
&VisualDeckStorageWidget::deckLoadRequested);
connect(display, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
&VisualDeckStorageWidget::openDeckEditor);
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
display->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
display->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
allDecks.append(display);
// Collect the file paths this folder should display, in proxy (sorted) order.
QStringList desiredOrder;
for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) {
const QModelIndex sourceIndex = proxy->mapToSource(proxy->index(proxyRow, 0));
if (!sourceIndex.isValid()) {
continue;
}
const DeckPreviewData &data = model->dataForRow(sourceIndex.row());
if (showFolders && data.folderPath != folderPath) {
continue;
}
desiredOrder.append(data.filePath);
}
flowWidget->clearLayout(); // Clear existing widgets in the flow layout
const QSet<QString> desiredSet(desiredOrder.cbegin(), desiredOrder.cend());
for (DeckPreviewWidget *deck : allDecks) {
flowWidget->addWidget(deck);
// Drop previews of decks that no longer match.
for (auto it = deckWidgets.begin(); it != deckWidgets.end();) {
if (!desiredSet.contains(it.key())) {
flowWidget->removeWidget(it.value());
it.value()->deleteLater();
it = deckWidgets.erase(it);
} else {
++it;
}
}
// Create or reuse the preview widgets in the desired order.
QList<DeckPreviewWidget *> orderedWidgets;
orderedWidgets.reserve(desiredOrder.size());
for (const QString &filePath : desiredOrder) {
DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(filePath, nullptr);
if (!deckPreviewWidget) {
deckPreviewWidget = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, model, filePath);
connect(deckPreviewWidget, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget,
&VisualDeckStorageWidget::deckLoadRequested);
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
&VisualDeckStorageWidget::openDeckEditor);
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(
visualDeckStorageWidget->settings()->getCardSize());
deckWidgets.insert(filePath, deckPreviewWidget);
}
orderedWidgets.append(deckPreviewWidget);
}
// Re-add all widgets so the flow layout order matches the proxy order. Skipped when the
// order is unchanged so that data-only updates don't invalidate the flow layout.
if (desiredOrder != lastOrderedFilePaths) {
for (DeckPreviewWidget *deckPreviewWidget : orderedWidgets) {
flowWidget->removeWidget(deckPreviewWidget);
}
for (DeckPreviewWidget *deckPreviewWidget : orderedWidgets) {
flowWidget->addWidget(deckPreviewWidget);
}
lastOrderedFilePaths = desiredOrder;
}
}
/**
* Updates the visibility of this folder and all its DeckPreviewWidgets
* @brief Creates, removes and keeps in sync the subfolder widgets of this folder.
*
* @param recursive Also update the visibility of all subfolders and their DeckPreviewWidgets.
* Only direct child folders are created here; each child manages its own
* children, mirroring the folder tree on disk.
*/
void VisualDeckStorageFolderDisplayWidget::updateVisibility(bool recursive)
{
bool atLeastOneWidgetVisible = checkVisibility();
if (atLeastOneWidgetVisible) {
setVisible(true);
for (DeckPreviewWidget *display : flowWidget->findChildren<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()
void VisualDeckStorageFolderDisplayWidget::createSubFolderWidgets()
{
if (!showFolders) {
return;
}
for (const QString &dir : getAllSubFolders(filePath)) {
auto *display = new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, dir, true, showFolders);
containerLayout->addWidget(display);
const QStringList children = childFolderPaths();
for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end();) {
if (!children.contains(it.key())) {
containerLayout->removeWidget(it.value());
it.value()->deleteLater();
it = subFolderWidgets.erase(it);
} else {
++it;
}
}
for (const QString &child : children) {
if (subFolderWidgets.contains(child)) {
continue;
}
auto *subFolderWidget =
new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, child, true, showFolders);
connect(subFolderWidget, &VisualDeckStorageFolderDisplayWidget::contentVisibilityChanged, this,
&VisualDeckStorageFolderDisplayWidget::refreshVisibility);
containerLayout->addWidget(subFolderWidget);
subFolderWidgets.insert(child, subFolderWidget);
}
}
void VisualDeckStorageFolderDisplayWidget::updateShowFolders(bool enabled)
{
showFolders = enabled;
header->setHidden(!showFolders);
if (!showFolders) {
flattenFolderStructure();
} else {
// if setting was switched from disabled to enabled, we assume that there aren't any existing subfolders
createWidgetsForFiles();
createWidgetsForFolders();
for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end(); ++it) {
containerLayout->removeWidget(it.value());
it.value()->deleteLater();
}
subFolderWidgets.clear();
}
header->setHidden(!showFolders);
reconcile();
}
/**
* Steals all DeckPreviewWidgets from this widget's nested subfolders, and deletes those subfolders
* @brief Hides the folder when it contains nothing visible, and reports the change upward.
*/
void VisualDeckStorageFolderDisplayWidget::flattenFolderStructure()
void VisualDeckStorageFolderDisplayWidget::refreshVisibility()
{
for (auto *subFolder : findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
// steal all DeckPreviewWidgets from the subfolder
for (auto *deck : subFolder->getFlowWidget()->findChildren<DeckPreviewWidget *>()) {
flowWidget->addWidget(deck);
}
// delete the subfolder
subFolder->deleteLater();
const bool shouldBeVisible = hasContent();
if (isHidden() == !shouldBeVisible) {
return;
}
setHidden(!shouldBeVisible);
emit contentVisibilityChanged();
}
QStringList VisualDeckStorageFolderDisplayWidget::gatherAllTagsFromFlowWidget() const
/**
* @brief Whether this folder shows any deck previews or has any visible subfolder.
*/
bool VisualDeckStorageFolderDisplayWidget::hasContent() const
{
QStringList allTags;
if (!deckWidgets.isEmpty()) {
return true;
}
if (flowWidget) {
// Iterate through all DeckPreviewWidgets
for (DeckPreviewWidget *display : flowWidget->findChildren<DeckPreviewWidget *>()) {
// Get tags from each DeckPreviewWidget
QStringList tags = display->deckLoader->getDeck().deckList.getTags();
// Add tags to the list while avoiding duplicates
allTags.append(tags);
for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : subFolderWidgets) {
if (subFolderWidget->hasContent()) {
return true;
}
}
// Remove duplicates by calling 'removeDuplicates'
allTags.removeDuplicates();
return false;
}
return allTags;
}
/**
* @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()->folderPaths()) {
if (!candidate.startsWith(prefix)) {
continue;
}
const QString rest = candidate.mid(prefix.length());
if (rest.isEmpty() || rest.contains('/')) {
continue;
}
children.append(candidate);
}
return children;
}

View file

@ -1,49 +1,79 @@
/**
* @file visual_deck_storage_folder_display_widget.h
* @ingroup VisualDeckStorageWidgets
* @brief Renders the decks of one folder of the Visual Deck Storage.
*
* This is a pure view: it reads the accepted rows of the
* VisualDeckStorageSortFilterProxyModel whose folder matches this widget's
* folder path, and keeps a set of child DeckPreviewWidgets in sync with those
* rows. Subfolders are shown as nested VisualDeckStorageFolderDisplayWidgets
* when the "show folders" setting is enabled.
*/
//! \todo Document this file.
#ifndef VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
#define VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
#include "../general/display/banner_widget.h"
#include "../general/layout_containers/flow_widget.h"
#include <QHash>
#include <QString>
#include <QWidget>
class BannerWidget;
class DeckPreviewWidget;
class FlowWidget;
class QTimer;
class QVBoxLayout;
class VisualDeckStorageWidget;
class VisualDeckStorageFolderDisplayWidget : public QWidget
{
Q_OBJECT
public:
VisualDeckStorageFolderDisplayWidget(QWidget *parent,
VisualDeckStorageWidget *_visualDeckStorageWidget,
QString _filePath,
VisualDeckStorageWidget *visualDeckStorageWidget,
const QString &folderPath,
bool canBeHidden,
bool _showFolders);
void refreshUi();
void createWidgetsForFiles();
void createWidgetsForFolders();
void flattenFolderStructure();
[[nodiscard]] QStringList gatherAllTagsFromFlowWidget() const;
[[nodiscard]] FlowWidget *getFlowWidget() const
{
return flowWidget;
}
bool showFolders);
public slots:
void updateVisibility(bool recursive = true);
bool checkVisibility();
/**
* @brief Re-reads the proxy and rebuilds the deck previews and subfolder
* widgets to match.
*/
void reconcile();
/**
* @brief Coalesces proxy change signals (a burst of deck loads or filter
* invalidations) into a single reconcile on the next event loop turn.
*/
void scheduleReconcile();
void updateShowFolders(bool enabled);
signals:
/**
* @brief Emitted whenever this folder's visible content changes, so parent
* folders can re-evaluate their own visibility.
*/
void contentVisibilityChanged();
private:
void reconcileDecks();
void createSubFolderWidgets();
void refreshVisibility();
[[nodiscard]] bool hasContent() const;
[[nodiscard]] QStringList childFolderPaths() const;
bool showFolders;
QString folderPath; ///< Path relative to the deck folder; empty for the root folder.
QVBoxLayout *layout;
VisualDeckStorageWidget *visualDeckStorageWidget;
QString filePath;
BannerWidget *header;
QWidget *container;
QVBoxLayout *containerLayout;
FlowWidget *flowWidget;
BannerWidget *header;
VisualDeckStorageWidget *visualDeckStorageWidget;
QHash<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.
};
#endif // VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H

View file

@ -1,6 +1,7 @@
#include "visual_deck_storage_quick_settings_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../cards/card_size_widget.h"
#include "visual_deck_storage_widget.h"
#include <QCheckBox>

View file

@ -1,21 +1,19 @@
#include "visual_deck_storage_search_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../../../filters/deck_filter_string.h"
#include "../../../filters/syntax_help.h"
#include "../../pixel_map_generator.h"
#include "visual_deck_storage_widget.h"
#include <QAction>
#include <QFileInfo>
#include <libcockatrice/settings/paths_settings.h>
#include <QTimer>
/**
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
* @brief Constructs a search bar for filtering decks in the Visual Deck Storage.
*
* This widget provides a search bar that allows users to search for cards by either their set name
* or set code. It uses a debounced timer to trigger the search action after the user stops typing.
* Provides a search bar that allows users to search decks by filename or search
* expression, with a debounced timer to trigger the search after the user stops typing.
*
* @param parent The parent PrintingSelector widget that will handle the search results.
* @param parent The VisualDeckStorageWidget owning the search.
*/
VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent) : parent(parent)
{
@ -40,47 +38,6 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi
searchDebounceTimer->start(300); // 300ms debounce
});
connect(searchDebounceTimer, &QTimer::timeout, parent, &VisualDeckStorageWidget::updateSearchFilter);
}
/**
* @brief Retrieves the current text in the search bar.
*
* @return The text entered by the user in the search bar.
*/
QString VisualDeckStorageSearchWidget::getSearchText()
{
return searchBar->text();
}
/**
* Converts the filepath into a relative filepath starting from the deck folder.
* If the file isn't in the deck folder, then this will just return the filename.
*
* @param filePath The filepath to convert into a relative filepath
*/
static QString toRelativeFilepath(const QString &filePath)
{
QString deckPath = SettingsCache::instance().paths().getDeckPath();
if (filePath.startsWith(deckPath)) {
return filePath.mid(deckPath.length());
}
QFileInfo fileInfo(filePath);
QString fileName = fileInfo.fileName();
return fileName;
}
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText)
{
auto filterString = DeckFilterString(searchText);
for (auto widget : widgets) {
DeckSearchData searchData;
searchData.deck = &widget->deckLoader->getDeck();
searchData.filePath = widget->filePath;
searchData.displayName = widget->getDisplayName();
searchData.relativeFilePath = toRelativeFilepath(widget->filePath);
widget->filteredBySearch = !filterString.check(searchData);
}
connect(searchDebounceTimer, &QTimer::timeout, this,
[this] { this->parent->proxyModel()->setSearchText(searchBar->text()); });
}

View file

@ -2,17 +2,15 @@
* @file visual_deck_storage_search_widget.h
* @ingroup VisualDeckStorageWidgets
*/
//! \todo Document this file.
#ifndef VISUAL_DECK_STORAGE_SEARCH_WIDGET_H
#define VISUAL_DECK_STORAGE_SEARCH_WIDGET_H
#include "deck_preview/deck_preview_widget.h"
#include <QHBoxLayout>
#include <QLineEdit>
#include <QWidget>
class QTimer;
class VisualDeckStorageWidget;
class VisualDeckStorageSearchWidget : public QWidget
{
@ -20,8 +18,6 @@ class VisualDeckStorageSearchWidget : public QWidget
public:
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
QString getSearchText();
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText);
private:
QHBoxLayout *layout;

View file

@ -1,20 +1,11 @@
#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>
/**
* @brief Constructs a PrintingSelectorCardSortWidget for searching cards by set name or set code.
*
* This widget provides a search bar that allows users to search for cards by either their set name
* or set code. It uses a debounced timer to trigger the search action after the user stops typing.
*
* @param parent The parent PrintingSelector widget that will handle the search results.
*/
VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent)
: parent(parent), sortOrder(Alphabetical)
VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
@ -30,12 +21,10 @@ VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget
// Set the current sort order
sortComboBox->setCurrentIndex(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSortingOrder());
sortOrder = static_cast<SortOrder>(sortComboBox->currentIndex());
// Connect sorting change signal to refresh the file list
// Connect sorting change signal to persist the order and refresh the file list
connect(sortComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&VisualDeckStorageSortWidget::updateSortOrder);
connect(this, &VisualDeckStorageSortWidget::sortOrderChanged, parent, &VisualDeckStorageWidget::updateSortOrder);
}
void VisualDeckStorageSortWidget::retranslateUi()
@ -47,10 +36,13 @@ void VisualDeckStorageSortWidget::retranslateUi()
// Clear and repopulate the ComboBox with translated items
sortComboBox->clear();
sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"), ByName);
sortComboBox->addItem(tr("Sort Alphabetically (Filename)"), Alphabetical);
sortComboBox->addItem(tr("Sort by Last Modified"), ByLastModified);
sortComboBox->addItem(tr("Sort by Last Loaded"), ByLastLoaded);
sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"),
VisualDeckStorageSortFilterProxyModel::SortOrder::ByName);
sortComboBox->addItem(tr("Sort Alphabetically (Filename)"),
VisualDeckStorageSortFilterProxyModel::SortOrder::Alphabetical);
sortComboBox->addItem(tr("Sort by Last Modified"),
VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastModified);
sortComboBox->addItem(tr("Sort by Last Loaded"), VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastLoaded);
// Restore the current index
sortComboBox->setCurrentIndex(oldIndex);
@ -59,60 +51,13 @@ void VisualDeckStorageSortWidget::retranslateUi()
sortComboBox->blockSignals(false);
}
VisualDeckStorageSortFilterProxyModel::SortOrder VisualDeckStorageSortWidget::currentSortOrder() const
{
return static_cast<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,19 +2,17 @@
* @file visual_deck_storage_sort_widget.h
* @ingroup VisualDeckStorageWidgets
*/
//! \todo Document this file.
#ifndef VISUAL_DECK_STORAGE_SORT_WIDGET_H
#define VISUAL_DECK_STORAGE_SORT_WIDGET_H
#include "visual_deck_storage_widget.h"
#include "visual_deck_storage_sort_filter_proxy_model.h"
#include <QComboBox>
#include <QHBoxLayout>
#include <QWidget>
class VisualDeckStorageWidget;
class VisualDeckStorageFolderDisplayWidget;
class VisualDeckStorageSortWidget : public QWidget
{
Q_OBJECT
@ -22,25 +20,23 @@ class VisualDeckStorageSortWidget : public QWidget
public:
explicit VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent);
void retranslateUi();
void updateSortOrder();
void sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget);
QString getSearchText();
QList<DeckPreviewWidget *> filterFiles(QList<DeckPreviewWidget *> widgets);
/**
* @brief The currently selected sort order.
*/
[[nodiscard]] VisualDeckStorageSortFilterProxyModel::SortOrder currentSortOrder() const;
signals:
/**
* @brief Emitted when the user picks a different sort order.
*/
void sortOrderChanged();
private slots:
void updateSortOrder();
private:
enum SortOrder
{
ByName,
Alphabetical,
ByLastModified,
ByLastLoaded,
};
QHBoxLayout *layout;
VisualDeckStorageWidget *parent;
SortOrder sortOrder; // Current sorting option
QComboBox *sortComboBox;
};

View file

@ -18,7 +18,7 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto
setFixedHeight(100);
auto *flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
layout->addWidget(flowWidget);
}
@ -29,45 +29,26 @@ void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event)
refreshTags();
}
void VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList<DeckPreviewWidget *> &deckPreviews) const
/**
* @brief The tags of all decks currently accepted by the proxy model.
*/
QSet<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
{
QStringList selectedTags;
QStringList excludedTags;
QSet<QString> allTags;
auto *proxy = parent->proxyModel();
auto *model = parent->model();
// 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;
for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) {
const QModelIndex sourceIndex = proxy->mapToSource(proxy->index(proxyRow, 0));
if (!sourceIndex.isValid()) {
continue;
}
for (const QString &tag : model->dataForRow(sourceIndex.row()).tags) {
allTags.insert(tag);
}
}
// If no tags are selected or excluded, show all
if (selectedTags.isEmpty() && excludedTags.isEmpty()) {
for (DeckPreviewWidget *deckPreview : deckPreviews) {
deckPreview->filteredByTags = false;
}
return;
}
for (DeckPreviewWidget *deckPreview : deckPreviews) {
QStringList deckTags = deckPreview->deckLoader->getDeck().deckList.getTags();
bool hasAllSelected = std::all_of(selectedTags.begin(), selectedTags.end(),
[&deckTags](const QString &tag) { return deckTags.contains(tag); });
bool hasAnyExcluded = std::any_of(excludedTags.begin(), excludedTags.end(),
[&deckTags](const QString &tag) { return deckTags.contains(tag); });
// Filter out if any excluded tag is present or if any selected tag is missing
deckPreview->filteredByTags = !(hasAllSelected && !hasAnyExcluded);
}
return allTags;
}
void VisualDeckStorageTagFilterWidget::refreshTags()
@ -80,8 +61,6 @@ void VisualDeckStorageTagFilterWidget::refreshTags()
void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet<QString> &tags)
{
auto *flowWidget = findChild<FlowWidget *>();
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
const QString &tagName = tagWidget->getTagName();
@ -116,20 +95,12 @@ void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag)
auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag);
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent,
&VisualDeckStorageWidget::updateTagFilter);
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this,
&VisualDeckStorageTagFilterWidget::refreshTags);
auto *flowWidget = findChild<FlowWidget *>();
flowWidget->addWidget(newTagWidget);
}
}
void VisualDeckStorageTagFilterWidget::sortTags()
{
auto *flowWidget = findChild<FlowWidget *>();
if (!flowWidget) {
return;
}
// Get all tag widgets
QList<DeckPreviewTagDisplayWidget *> tagWidgets = findChildren<DeckPreviewTagDisplayWidget *>();
@ -147,19 +118,26 @@ void VisualDeckStorageTagFilterWidget::sortTags()
}
}
QSet<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
QStringList VisualDeckStorageTagFilterWidget::selectedTags() const
{
QSet<QString> allTags;
QList<DeckPreviewWidget *> deckWidgets = parent->findChildren<DeckPreviewWidget *>();
for (DeckPreviewWidget *widget : deckWidgets) {
if (widget->checkVisibility()) {
for (const QString &tag : widget->deckLoader->getDeck().deckList.getTags()) {
allTags.insert(tag);
}
QStringList selected;
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
if (tagWidget->getState() == TagState::Selected) {
selected.append(tagWidget->getTagName());
}
}
return allTags;
return selected;
}
QStringList VisualDeckStorageTagFilterWidget::excludedTags() const
{
QStringList excluded;
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
if (tagWidget->getState() == TagState::Excluded) {
excluded.append(tagWidget->getTagName());
}
}
return excluded;
}
QStringList VisualDeckStorageTagFilterWidget::getAllKnownTags() const

View file

@ -2,21 +2,22 @@
* @file visual_deck_storage_tag_filter_widget.h
* @ingroup VisualDeckStorageWidgets
*/
//! \todo Document this file.
#ifndef VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H
#define VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H
#include "deck_preview/deck_preview_widget.h"
#include <QSet>
#include <QStringList>
#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);
@ -27,9 +28,21 @@ class VisualDeckStorageTagFilterWidget : public QWidget
public:
explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent);
[[nodiscard]] QStringList getAllKnownTags() const;
void filterDecksBySelectedTags(const QList<DeckPreviewWidget *> &deckPreviews) const;
/**
* @brief The tags currently in "selected" state.
*/
[[nodiscard]] QStringList selectedTags() const;
/**
* @brief The tags currently in "excluded" state.
*/
[[nodiscard]] QStringList excludedTags() const;
public slots:
/**
* @brief Rebuilds the tag chips from the tags of the currently visible decks.
*/
void refreshTags();
void showEvent(QShowEvent *event) override;
};

View file

@ -2,24 +2,28 @@
#include "../../../client/settings/cache_settings.h"
#include "../quick_settings/settings_button_widget.h"
#include "deck_preview/deck_preview_color_identity_filter_widget.h"
#include "deck_preview/deck_preview_widget.h"
#include "visual_deck_storage_folder_display_widget.h"
#include "visual_deck_storage_quick_settings_widget.h"
#include "visual_deck_storage_search_widget.h"
#include "visual_deck_storage_sort_widget.h"
#include "visual_deck_storage_tag_filter_widget.h"
#include <QComboBox>
#include <QDirIterator>
#include <QMouseEvent>
#include <QLabel>
#include <QTimer>
#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), folderWidget(nullptr)
VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent)
{
deckListModel = new DeckListModel(this);
deckListModel->setObjectName("visualDeckModel");
// The model and proxy own all deck data, sorting and filtering. The view widgets below only
// display the proxy's accepted rows, so nothing touches the filesystem outside the model.
model_ = new VisualDeckStorageModel(this);
proxyModel_ = new VisualDeckStorageSortFilterProxyModel(this);
proxyModel_->setSourceModel(model_);
layout = new QVBoxLayout(this);
layout->setSpacing(0);
@ -75,6 +79,25 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
layout->addWidget(tagFilterWidget);
layout->addWidget(scrollArea);
// The deck data changed (a load finished, or a mutation happened): re-evaluate the filters,
// since search/tag/color matches are computed against the data. Debounced so that a burst of
// load completions triggers a single re-application instead of one O(n) pass per deck.
refreshTimer = new QTimer(this);
refreshTimer->setSingleShot(true);
refreshTimer->setInterval(150);
connect(refreshTimer, &QTimer::timeout, this, [this] {
proxyModel_->reapplyFilters();
// A batch of decks finished loading: re-gather the tag chips from the visible decks once
// the burst settles instead of on every individual load.
tagFilterWidget->refreshTags();
});
connect(model_, &QAbstractItemModel::dataChanged, this, [this] { refreshTimer->start(); });
connect(model_, &VisualDeckStorageModel::deckLoaded, this, [this] { refreshTimer->start(); });
// A deck's file path changed: re-apply the sort, since orders like "filename" depend on it.
connect(model_, &VisualDeckStorageModel::deckFilePathChanged, this, [this] { proxyModel_->resort(); });
connect(sortWidget, &VisualDeckStorageSortWidget::sortOrderChanged, this,
&VisualDeckStorageWidget::updateSortOrder);
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&VisualDeckStorageWidget::createRootFolderWidget);
@ -123,6 +146,8 @@ void VisualDeckStorageWidget::retranslateUi()
refreshButton->setToolTip(tr("Refresh loaded files"));
quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings"));
sortWidget->retranslateUi();
}
/**
@ -134,72 +159,52 @@ const VisualDeckStorageQuickSettingsWidget *VisualDeckStorageWidget::settings()
}
/**
* Reapplies all sort and filter options by calling the appropriate update methods.
* Reapplies all sort and filter options by updating the proxy model.
*/
void VisualDeckStorageWidget::reapplySortAndFilters()
{
updateSortOrder();
updateTagFilter();
updateColorFilter();
updateSearchFilter();
proxyModel_->setSortOrder(sortWidget->currentSortOrder());
proxyModel_->reapplyFilters();
}
/**
* @brief Scans the deck folder and rebuilds the folder tree of deck previews.
*/
void VisualDeckStorageWidget::createRootFolderWidget()
{
folderWidget = new VisualDeckStorageFolderDisplayWidget(this, this, SettingsCache::instance().paths().getDeckPath(),
false, quickSettingsWidget->getShowFolders());
model_->setDeckPath(SettingsCache::instance().paths().getDeckPath());
folderWidget =
new VisualDeckStorageFolderDisplayWidget(this, this, QString(), false, quickSettingsWidget->getShowFolders());
scrollArea->setWidget(folderWidget); // this automatically destroys the old folderWidget
scrollArea->widget()->setMaximumWidth(scrollArea->viewport()->width());
scrollArea->widget()->adjustSize();
/* We have to schedule a QTimer here so that the sorting logic doesn't try to access widgets that haven't been
* processed by the event loop yet. Otherwise, deck sorting will intermittently segfault on some systems.
*/
QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters);
// Sort and filter runs against the model data, so it is safe to apply immediately.
reapplySortAndFilters();
}
void VisualDeckStorageWidget::updateShowFolders(bool enabled)
{
if (folderWidget) {
folderWidget->updateShowFolders(enabled);
QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters);
}
}
void VisualDeckStorageWidget::updateSortOrder()
{
if (folderWidget) {
sortWidget->sortFolder(folderWidget);
for (VisualDeckStorageFolderDisplayWidget *subFolderWidget :
folderWidget->findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
sortWidget->sortFolder(subFolderWidget);
}
}
proxyModel_->setSortOrder(sortWidget->currentSortOrder());
}
void VisualDeckStorageWidget::updateTagFilter()
{
if (folderWidget) {
tagFilterWidget->filterDecksBySelectedTags(folderWidget->findChildren<DeckPreviewWidget *>());
folderWidget->updateVisibility();
}
}
void VisualDeckStorageWidget::updateColorFilter()
{
if (folderWidget) {
deckPreviewColorIdentityFilterWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>());
folderWidget->updateVisibility();
}
}
void VisualDeckStorageWidget::updateSearchFilter()
{
if (folderWidget) {
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText());
folderWidget->updateVisibility();
}
const QStringList selected = tagFilterWidget->selectedTags();
const QStringList excluded = tagFilterWidget->excludedTags();
proxyModel_->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();
}
void VisualDeckStorageWidget::updateTagsVisibility(const bool visible)
@ -215,4 +220,4 @@ void VisualDeckStorageWidget::updateTagsVisibility(const bool visible)
void VisualDeckStorageWidget::updateSelectionAnimationEnabled(const bool enabled)
{
deckPreviewSelectionAnimationEnabled = enabled;
}
}

View file

@ -2,29 +2,30 @@
* @file visual_deck_storage_widget.h
* @ingroup VisualDeckStorageWidgets
*/
//! \todo Document this file.
#ifndef VISUAL_DECK_STORAGE_WIDGET_H
#define VISUAL_DECK_STORAGE_WIDGET_H
#include "../../deck_loader/deck_loader.h"
#include "../cards/card_size_widget.h"
#include "../quick_settings/settings_button_widget.h"
#include "deck_preview/deck_preview_color_identity_filter_widget.h"
#include "visual_deck_storage_folder_display_widget.h"
#include "visual_deck_storage_quick_settings_widget.h"
#include "visual_deck_storage_search_widget.h"
#include "visual_deck_storage_sort_widget.h"
#include "visual_deck_storage_tag_filter_widget.h"
#include "visual_deck_storage_model.h"
#include "visual_deck_storage_sort_filter_proxy_model.h"
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
class QSpinBox;
class QLabel;
class QResizeEvent;
class QShowEvent;
class QTimer;
class DeckPreviewColorIdentityFilterWidget;
class VisualDeckStorageFolderDisplayWidget;
class VisualDeckStorageQuickSettingsWidget;
class VisualDeckStorageSearchWidget;
class VisualDeckStorageSortWidget;
class VisualDeckStorageTagFilterWidget;
class VisualDeckStorageFolderDisplayWidget;
class DeckPreviewColorIdentityFilterWidget;
class VisualDeckStorageWidget final : public QWidget
{
Q_OBJECT
@ -37,29 +38,41 @@ public:
bool deckPreviewSelectionAnimationEnabled;
[[nodiscard]] const VisualDeckStorageQuickSettingsWidget *settings() const;
[[nodiscard]] VisualDeckStorageModel *model() const
{
return model_;
}
[[nodiscard]] VisualDeckStorageSortFilterProxyModel *proxyModel() const
{
return proxyModel_;
}
public slots:
void createRootFolderWidget(); // Refresh the display of cards based on the current sorting option
/**
* @brief Starts scanning the deck folder and rebuilds the folder tree and previews.
*/
void createRootFolderWidget();
void updateShowFolders(bool enabled);
void updateTagFilter();
void updateColorFilter();
void updateSearchFilter();
void updateTagsVisibility(bool visible);
void updateSelectionAnimationEnabled(bool enabled);
void updateSortOrder();
void updateTagFilter();
signals:
void deckLoadRequested(const QString &filePath);
void openDeckEditor(const LoadedDeck &deck);
protected:
void resizeEvent(QResizeEvent *event) override;
void showEvent(QShowEvent *event) override;
signals:
void bannerCardsRefreshed();
void deckLoadRequested(const QString &filePath);
void openDeckEditor(const LoadedDeck &deck);
private:
void reapplySortAndFilters();
private:
QVBoxLayout *layout;
QWidget *searchAndSortContainer;
QHBoxLayout *searchAndSortLayout;
DeckListModel *deckListModel;
QLabel *databaseLoadIndicator;
VisualDeckStorageSortWidget *sortWidget;
VisualDeckStorageSearchWidget *searchWidget;
@ -67,9 +80,10 @@ private:
QToolButton *refreshButton;
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
QScrollArea *scrollArea;
VisualDeckStorageFolderDisplayWidget *folderWidget;
void reapplySortAndFilters();
VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr;
VisualDeckStorageModel *model_ = nullptr;
VisualDeckStorageSortFilterProxyModel *proxyModel_ = nullptr;
QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads.
};
#endif // VISUAL_DECK_STORAGE_WIDGET_H