mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
[VDS] Drive folder and preview widgets from the model (MVC views)
Took 16 minutes Took 8 minutes
This commit is contained in:
parent
c4f4c57722
commit
09d2270d45
21 changed files with 752 additions and 886 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ¤tText)
|
|||
// 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 ¤tText)
|
|||
bannerCardComboBox->setCurrentIndex(restoredIndex);
|
||||
} else {
|
||||
// Add a placeholder "-" and set it as the current selection
|
||||
int bannerIndex = bannerCardComboBox->findText(deckLoader->getDeck().deckList.getBannerCard().name);
|
||||
const QString currentBannerCardName =
|
||||
r == -1 ? QString() : model_->dataForRow(r).deck.deckList.getBannerCard().name;
|
||||
int bannerIndex = bannerCardComboBox->findText(currentBannerCardName);
|
||||
if (bannerIndex != -1) {
|
||||
bannerCardComboBox->setCurrentIndex(bannerIndex);
|
||||
} else {
|
||||
|
|
@ -362,8 +302,11 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
|||
{
|
||||
auto [name, id] = bannerCardComboBox->currentData().value<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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ¤tText);
|
||||
void setBannerCard(int);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void initializeUi(bool deckLoadSuccess);
|
||||
void resyncWidgets();
|
||||
/**
|
||||
* @brief Re-reads the row's data from the model and syncs every child widget.
|
||||
* Connected to the model's dataChanged signal.
|
||||
*/
|
||||
void syncFromModel();
|
||||
|
||||
/**
|
||||
* @brief Reloads the deck file if its modification time is newer than the stored one.
|
||||
*/
|
||||
void reloadIfModified();
|
||||
void updateVisibility();
|
||||
void refreshBannerCardToolTip();
|
||||
void updateColorIdentityVisibility(bool visible);
|
||||
void updateBannerCardComboBoxVisibility(bool visible);
|
||||
void updateTagsVisibility(bool visible);
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void setBannerCard(int);
|
||||
void setTags(const QStringList &tags);
|
||||
|
||||
protected:
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
void updateLastModifiedTime();
|
||||
void writeDeckToFile();
|
||||
[[nodiscard]] int row() const;
|
||||
[[nodiscard]] QString getDisplayName() const;
|
||||
void refreshBannerCardText();
|
||||
void updateBannerCardComboBox(const QString ¤tText);
|
||||
bool promptFileConversionIfRequired();
|
||||
QMenu *createRightClickMenu();
|
||||
void addSetBannerCardMenu(QMenu *menu);
|
||||
|
||||
private slots:
|
||||
void setTags(const QStringList &tags);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
||||
void actRenameDeck();
|
||||
void actRenameFile();
|
||||
void actDeleteFile();
|
||||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
VisualDeckStorageModel *model_;
|
||||
QString filePath;
|
||||
QVBoxLayout *layout;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||
QLabel *bannerCardLabel;
|
||||
QComboBox *bannerCardComboBox;
|
||||
};
|
||||
|
||||
class NoScrollFilter : public QObject
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()); });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue