mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 10:23:02 -07:00
Compare commits
No commits in common. "dba7cc73a440b6bf77444f5d7db2c8c29de97737" and "e589429bd987d5b76789363380a7fa3a35b5826e" have entirely different histories.
dba7cc73a4
...
e589429bd9
35 changed files with 939 additions and 1238 deletions
|
|
@ -230,7 +230,6 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/general/display/charts/bars/segmented_bar_widget.cpp
|
||||
src/interface/widgets/general/display/charts/pies/color_pie.cpp
|
||||
src/interface/widgets/general/home_styled_button.cpp
|
||||
src/interface/widgets/general/home_tab_button_color.h
|
||||
src/interface/widgets/general/home_widget.cpp
|
||||
src/interface/widgets/general/layout_containers/flow_widget.cpp
|
||||
src/interface/widgets/general/layout_containers/overlap_control_widget.cpp
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
|
||||
#include "../../interface/widgets/dialogs/dlg_load_remote_deck.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "deck_view.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
{
|
||||
QWidget::resizeEvent(event);
|
||||
|
||||
// Layout passes resize this widget repeatedly with identical sizes, so bail out before
|
||||
// touching the children when neither the width nor the resulting symbol size changed.
|
||||
const int totalWidth = event->size().width();
|
||||
if (totalWidth == lastWidth && lastIconSize != -1) {
|
||||
return;
|
||||
|
|
@ -88,12 +90,13 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
|
||||
const int count = layout->count();
|
||||
if (count == 0) {
|
||||
QList<ManaSymbolWidget *> manaSymbols = findChildren<ManaSymbolWidget *>();
|
||||
if (manaSymbols.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int spacing = layout->spacing();
|
||||
const int count = manaSymbols.size();
|
||||
const int availableWidth = totalWidth - (spacing * (count - 1));
|
||||
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||
|
||||
|
|
@ -102,10 +105,8 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
}
|
||||
lastIconSize = iconSize;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (auto *w = qobject_cast<ManaSymbolWidget *>(layout->itemAt(i)->widget())) {
|
||||
w->setFixedSize(iconSize, iconSize);
|
||||
}
|
||||
for (ManaSymbolWidget *manaSymbol : manaSymbols) {
|
||||
manaSymbol->setFixedSize(iconSize, iconSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,8 @@
|
|||
#include "deck_list_history_manager_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeView>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ BannerWidget::BannerWidget(QWidget *parent, const QString &text, Qt::Orientation
|
|||
|
||||
// Set minimum height for the widget
|
||||
setMinimumHeight(50);
|
||||
setMaximumHeight(100);
|
||||
connect(this, &BannerWidget::buddyVisibilityChanged, this, &BannerWidget::toggleBuddyVisibility);
|
||||
|
||||
updateDropdownIconState();
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
#ifndef COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
#define COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
|
||||
#include <QList>
|
||||
|
||||
namespace HomeTabButtonColor
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Where to get the colors for the home tab buttons from
|
||||
*/
|
||||
enum Source
|
||||
{
|
||||
Automatic, ///< Extract color from background, or use theme color if no background
|
||||
FromBackground, ///< Always extract color from background
|
||||
};
|
||||
|
||||
struct Entry
|
||||
{
|
||||
Source source;
|
||||
const char *trKey; ///< key for translation
|
||||
};
|
||||
|
||||
inline QList<Entry> all()
|
||||
{
|
||||
static QList<Entry> entries = {{Automatic, QT_TR_NOOP("Automatic")},
|
||||
{FromBackground, QT_TR_NOOP("Extract from background")}};
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts an int into the corresponding Source.
|
||||
*
|
||||
* @param value The int value
|
||||
* @return The Source. Returns Source::Automatic if the value is not within range
|
||||
*/
|
||||
inline Source intToSource(int value)
|
||||
{
|
||||
if (value > FromBackground) {
|
||||
return Automatic; // default
|
||||
}
|
||||
|
||||
return static_cast<Source>(value);
|
||||
}
|
||||
|
||||
} // namespace HomeTabButtonColor
|
||||
|
||||
#endif // COCKATRICE_HOME_TAB_BUTTON_COLOR_H
|
||||
|
|
@ -7,7 +7,6 @@
|
|||
#include "../cards/art_crop_attribution.h"
|
||||
#include "background_sources.h"
|
||||
#include "home_styled_button.h"
|
||||
#include "home_tab_button_color.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QPainter>
|
||||
|
|
@ -26,7 +25,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
|
||||
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
||||
|
||||
gradientColors = determineButtonColor();
|
||||
gradientColors = extractDominantColors(background);
|
||||
|
||||
layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter);
|
||||
|
||||
|
|
@ -56,8 +55,6 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
||||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
}
|
||||
|
||||
void HomeWidget::initializeBackgroundFromSource()
|
||||
|
|
@ -100,34 +97,6 @@ void HomeWidget::loadBackgroundSourceDeck()
|
|||
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
||||
}
|
||||
|
||||
static bool isDefaultBackgroundAndTheme()
|
||||
{
|
||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
|
||||
}
|
||||
|
||||
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
|
||||
{
|
||||
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
|
||||
|
||||
auto colorSource =
|
||||
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
|
||||
|
||||
switch (colorSource) {
|
||||
case HomeTabButtonColor::Automatic: {
|
||||
if (isDefaultBackgroundAndTheme()) {
|
||||
return defaultColor;
|
||||
} else {
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
}
|
||||
case HomeTabButtonColor::FromBackground:
|
||||
return extractDominantColors(background);
|
||||
}
|
||||
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
void HomeWidget::setRandomCard(ExactCard &newCard)
|
||||
{
|
||||
static constexpr int ATTEMPTS = 10;
|
||||
|
|
@ -202,7 +171,7 @@ void HomeWidget::updateBackgroundProperties()
|
|||
|
||||
void HomeWidget::updateButtonsToBackgroundColor()
|
||||
{
|
||||
gradientColors = determineButtonColor();
|
||||
gradientColors = extractDominantColors(background);
|
||||
for (HomeStyledButton *button : findChildren<HomeStyledButton *>()) {
|
||||
button->updateStylesheet(gradientColors);
|
||||
button->update();
|
||||
|
|
@ -297,6 +266,11 @@ void HomeWidget::updateConnectButton(const ClientStatus status)
|
|||
|
||||
QPair<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap)
|
||||
{
|
||||
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
|
||||
if (themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme) {
|
||||
return QPair<QColor, QColor>(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80));
|
||||
}
|
||||
|
||||
// Step 1: Downscale image for performance
|
||||
QImage image = pixmap.toImage()
|
||||
.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class HomeWidget : public QWidget
|
|||
public:
|
||||
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
|
||||
void updateRandomCard();
|
||||
static QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
|
||||
public slots:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
|
@ -47,7 +47,6 @@ private:
|
|||
|
||||
void setRandomCard(ExactCard &newCard);
|
||||
void loadBackgroundSourceDeck();
|
||||
QPair<QColor, QColor> determineButtonColor() const;
|
||||
};
|
||||
|
||||
#endif // HOME_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "printing_selector_card_overlay_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "printing_selector_card_display_widget.h"
|
||||
|
||||
#include <QImageReader>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
#include "../../client/settings/card_counter_settings.h"
|
||||
#include "../../palette_editor/palette_editor_dialog.h"
|
||||
#include "../dialogs/override_printing_warning.h"
|
||||
#include "../general/home_tab_button_color.h"
|
||||
#include "../interface/theme_manager.h"
|
||||
#include "../interface/widgets/general/background_sources.h"
|
||||
#include "../playmat/playmat_collection_dialog.h"
|
||||
|
|
@ -132,14 +131,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabDisplayCardName);
|
||||
|
||||
for (const auto &entry : HomeTabButtonColor::all()) {
|
||||
homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey));
|
||||
}
|
||||
|
||||
homeTabButtonColorSourceBox.setCurrentIndex(settings.appearance().getHomeTabButtonColorSourceIndex());
|
||||
connect(&homeTabButtonColorSourceBox, QOverload<int>::of(&QComboBox::currentIndexChanged), &settings.appearance(),
|
||||
&AppearanceSettings::setHomeTabButtonColorSourceIndex);
|
||||
|
||||
updateHomeTabSettingsVisibility();
|
||||
|
||||
auto *homeTabGrid = new QGridLayout;
|
||||
|
|
@ -148,8 +139,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 1, 0);
|
||||
homeTabGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 1, 1);
|
||||
homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0);
|
||||
homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1);
|
||||
|
||||
homeTabGroupBox = new QGroupBox;
|
||||
homeTabGroupBox->setLayout(homeTabGrid);
|
||||
|
|
@ -508,9 +497,6 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
|
||||
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
|
||||
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
|
||||
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
|
||||
homeTabButtonColorSourceBox.setToolTip(
|
||||
tr("Automatic: extract from background if present, otherwise use theme default"));
|
||||
|
||||
stylingGroupBox->setTitle(tr("Styling settings"));
|
||||
styleUserListCheckBox.setText(tr("Style user list"));
|
||||
|
|
|
|||
|
|
@ -35,15 +35,11 @@ private:
|
|||
QLabel styleComboLabel;
|
||||
QComboBox styleCombo;
|
||||
QPushButton editPaletteButton;
|
||||
|
||||
QLabel homeTabBackgroundSourceLabel;
|
||||
QComboBox homeTabBackgroundSourceBox;
|
||||
QLabel homeTabBackgroundShuffleFrequencyLabel;
|
||||
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
|
||||
QCheckBox homeTabDisplayCardNameCheckBox;
|
||||
QLabel homeTabButtonColorSourceLabel;
|
||||
QComboBox homeTabButtonColorSourceBox;
|
||||
|
||||
QCheckBox styleUserListCheckBox;
|
||||
QCheckBox showShortcutsCheckBox;
|
||||
QCheckBox showGameSelectorFilterToolbarCheckBox;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
#ifndef TAB_GENERIC_DECK_EDITOR_H
|
||||
#define TAB_GENERIC_DECK_EDITOR_H
|
||||
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h"
|
||||
#include "../interface/widgets/deck_editor/deck_editor_card_info_dock_widget.h"
|
||||
#include "../interface/widgets/deck_editor/deck_editor_database_display_widget.h"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#include "deck_preview_color_identity_filter_widget.h"
|
||||
|
||||
#include "../../cards/additional_info/mana_symbol_widget.h"
|
||||
#include "../visual_deck_storage_widget.h"
|
||||
#include "deck_preview_widget.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <QMouseEvent>
|
||||
|
||||
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent)
|
||||
: QWidget(parent), layout(new QHBoxLayout(this))
|
||||
|
|
@ -32,6 +32,10 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(Visua
|
|||
|
||||
// Connect the button's clicked signal
|
||||
connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode);
|
||||
connect(this, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, parent,
|
||||
&VisualDeckStorageWidget::updateColorFilter);
|
||||
connect(this, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, parent,
|
||||
&VisualDeckStorageWidget::updateColorFilter);
|
||||
|
||||
// Call retranslateUi to set the initial text
|
||||
retranslateUi();
|
||||
|
|
@ -41,33 +45,19 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi()
|
|||
{
|
||||
// Set the toggle button text based on the current mode
|
||||
switch (filterMode) {
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
|
||||
case ExactMatch:
|
||||
toggleButton->setText(tr("Mode: Exact Match"));
|
||||
break;
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes:
|
||||
case Includes:
|
||||
toggleButton->setText(tr("Mode: Includes"));
|
||||
break;
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes:
|
||||
case Excludes:
|
||||
toggleButton->setText(tr("Mode: Excludes"));
|
||||
break;
|
||||
}
|
||||
toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The colors that are currently toggled on.
|
||||
*/
|
||||
QSet<QChar> DeckPreviewColorIdentityFilterWidget::getActiveColors() const
|
||||
{
|
||||
QSet<QChar> activeColorSet;
|
||||
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
|
||||
if (it.value()) {
|
||||
activeColorSet.insert(it.key());
|
||||
}
|
||||
}
|
||||
return activeColorSet;
|
||||
}
|
||||
|
||||
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active)
|
||||
{
|
||||
activeColors[color] = active;
|
||||
|
|
@ -78,17 +68,88 @@ void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
|
|||
{
|
||||
// Cycle through the modes
|
||||
switch (filterMode) {
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
|
||||
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes;
|
||||
case ExactMatch:
|
||||
filterMode = Includes;
|
||||
break;
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::Includes:
|
||||
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes;
|
||||
case Includes:
|
||||
filterMode = Excludes;
|
||||
break;
|
||||
case VisualDeckStorageSortFilterProxyModel::FilterMode::Excludes:
|
||||
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch;
|
||||
case Excludes:
|
||||
filterMode = ExactMatch;
|
||||
break;
|
||||
}
|
||||
|
||||
retranslateUi(); // Update the button text
|
||||
emit filterModeChanged(filterMode);
|
||||
}
|
||||
|
||||
void DeckPreviewColorIdentityFilterWidget::filterWidgets(QList<DeckPreviewWidget *> widgets)
|
||||
{
|
||||
// Check if no colors are active
|
||||
bool noColorsActive = true;
|
||||
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
|
||||
if (it.value()) {
|
||||
noColorsActive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no colors are active, return the unfiltered list of widgets
|
||||
if (noColorsActive) {
|
||||
for (DeckPreviewWidget *previewWidget : widgets) {
|
||||
previewWidget->filteredByColor = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &widget : widgets) {
|
||||
QString colorIdentity = widget->getColorIdentity();
|
||||
|
||||
bool matchesFilter = true;
|
||||
switch (filterMode) {
|
||||
case ExactMatch: {
|
||||
// Exact match mode: active colors must exactly match colorIdentity
|
||||
|
||||
// Create a set of active colors
|
||||
QSet<QChar> activeColorSet;
|
||||
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
|
||||
if (it.value()) {
|
||||
activeColorSet.insert(it.key().toUpper()); // Use uppercase for uniformity
|
||||
}
|
||||
}
|
||||
|
||||
// Create a set of colors from the color identity string
|
||||
QSet<QChar> colorIdentitySet;
|
||||
for (const QChar &color : colorIdentity) {
|
||||
colorIdentitySet.insert(color.toUpper()); // Ensure case uniformity
|
||||
}
|
||||
|
||||
// Compare the sets: the sets must match exactly
|
||||
if (activeColorSet != colorIdentitySet) {
|
||||
matchesFilter = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Includes:
|
||||
// Includes mode: colorIdentity must contain all active colors
|
||||
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
|
||||
if (it.value() && !colorIdentity.contains(it.key())) {
|
||||
matchesFilter = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Excludes:
|
||||
// Excludes mode: colorIdentity must contain none of the active colors
|
||||
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
|
||||
if (it.value() && colorIdentity.contains(it.key())) {
|
||||
matchesFilter = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
widget->filteredByColor = !matchesFilter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@
|
|||
* @file deck_preview_color_identity_filter_widget.h
|
||||
* @ingroup VisualDeckPreviewWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H
|
||||
#define DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H
|
||||
|
||||
#include "../visual_deck_storage_sort_filter_proxy_model.h"
|
||||
#include "../visual_deck_storage_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMap>
|
||||
#include <QPushButton>
|
||||
#include <QSet>
|
||||
#include <QWidget>
|
||||
|
||||
class DeckPreviewWidget;
|
||||
class VisualDeckStorageWidget;
|
||||
|
||||
class DeckPreviewColorIdentityFilterWidget : public QWidget
|
||||
|
|
@ -21,34 +21,25 @@ class DeckPreviewColorIdentityFilterWidget : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* How the active colors are matched against a deck's color identity.
|
||||
*/
|
||||
enum FilterMode
|
||||
{
|
||||
ExactMatch, ///< The color identity consists of exactly the active colors.
|
||||
Includes, ///< The color identity contains all of the active colors.
|
||||
Excludes ///< The color identity contains none of the active colors.
|
||||
};
|
||||
Q_ENUM(FilterMode)
|
||||
|
||||
explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent);
|
||||
void retranslateUi();
|
||||
|
||||
/**
|
||||
* @brief The currently active color identity filter mode.
|
||||
*/
|
||||
[[nodiscard]] VisualDeckStorageSortFilterProxyModel::FilterMode getFilterMode() const
|
||||
{
|
||||
return filterMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The colors that are currently toggled on.
|
||||
*/
|
||||
[[nodiscard]] QSet<QChar> getActiveColors() const;
|
||||
void filterWidgets(QList<DeckPreviewWidget *> widgets);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted when the set of active colors changed due to user interaction.
|
||||
*/
|
||||
void filterModeChanged(FilterMode mode);
|
||||
void activeColorsChanged();
|
||||
|
||||
/**
|
||||
* Emitted when the user cycles the color identity filter mode.
|
||||
* @param mode The new filter mode.
|
||||
*/
|
||||
void filterModeChanged(VisualDeckStorageSortFilterProxyModel::FilterMode mode);
|
||||
|
||||
private slots:
|
||||
void handleColorToggled(QChar color, bool active);
|
||||
void updateFilterMode();
|
||||
|
|
@ -57,7 +48,7 @@ private:
|
|||
QHBoxLayout *layout;
|
||||
QPushButton *toggleButton;
|
||||
QMap<QChar, bool> activeColors;
|
||||
VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes;
|
||||
FilterMode filterMode = Includes; // Default to "includes" mode
|
||||
};
|
||||
|
||||
#endif // DECK_PREVIEW_COLOR_IDENTITY_FILTER_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
#include "deck_preview_deck_tags_display_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../../deck_loader/deck_loader.h"
|
||||
#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h"
|
||||
#include "../../../../interface/widgets/tabs/tab_deck_editor.h"
|
||||
#include "../../general/layout_containers/flow_widget.h"
|
||||
#include "deck_preview_tag_addition_widget.h"
|
||||
#include "deck_preview_tag_dialog.h"
|
||||
#include "deck_preview_tag_display_widget.h"
|
||||
#include "deck_preview_widget.h"
|
||||
|
||||
#include <QDirIterator>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags)
|
||||
: QWidget(_parent), currentTags(_tags)
|
||||
|
|
@ -50,16 +54,6 @@ void DeckPreviewDeckTagsDisplayWidget::refreshTags()
|
|||
flowWidget->addWidget(tagAdditionWidget);
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::setKnownTagsProvider(const std::function<QStringList()> &provider)
|
||||
{
|
||||
knownTagsProvider = provider;
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::setConversionPromptHandler(const std::function<bool()> &handler)
|
||||
{
|
||||
conversionPromptHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filepath of all files (no directories) in target directory and all subdirectories
|
||||
*/
|
||||
|
|
@ -98,13 +92,93 @@ static QStringList findAllKnownTags()
|
|||
|
||||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||
{
|
||||
// The deck editor path has no conversion prompt; the VDS path registers one.
|
||||
if (conversionPromptHandler && !conversionPromptHandler()) {
|
||||
return;
|
||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||
// If we're the child of a DeckPreviewWidget, then we need to handle conversion
|
||||
auto *deckPreviewWidget = qobject_cast<DeckPreviewWidget *>(parentWidget());
|
||||
|
||||
bool canAddTags = promptFileConversionIfRequired(deckPreviewWidget);
|
||||
|
||||
if (canAddTags) {
|
||||
QStringList knownTags = deckPreviewWidget->visualDeckStorageWidget->tagFilterWidget->getAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
}
|
||||
} else {
|
||||
// If we're the child of an AbstractTabDeckEditor, then we don't bother with conversion
|
||||
QStringList knownTags = findAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
}
|
||||
}
|
||||
|
||||
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||
|
||||
if (QFile::exists(newFileName)) {
|
||||
QMessageBox::StandardButton reply =
|
||||
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
|
||||
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
return reply == QMessageBox::Yes;
|
||||
}
|
||||
return true; // Safe to proceed
|
||||
}
|
||||
|
||||
static void convertFileToCockatriceFormat(DeckPreviewWidget *deckPreviewWidget)
|
||||
{
|
||||
DeckLoader::convertToCockatriceFormat(deckPreviewWidget->deckLoader->getDeck());
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getDeck().lastLoadInfo.fileName;
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the deck's file format supports tags.
|
||||
* If not, then prompt the user for file conversion.
|
||||
* @return whether the resulting file can support adding tags
|
||||
*/
|
||||
bool DeckPreviewDeckTagsDisplayWidget::promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget)
|
||||
{
|
||||
if (DeckFileFormat::getFormatFromName(deckPreviewWidget->filePath) == DeckFileFormat::Cockatrice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QStringList knownTags = knownTagsProvider ? knownTagsProvider() : findAllKnownTags();
|
||||
execTagDialog(knownTags);
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
convertFileToCockatriceFormat(deckPreviewWidget);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
|
||||
!conversionDialog.dontAskAgain());
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to convert file
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
convertFileToCockatriceFormat(deckPreviewWidget);
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTags)
|
||||
|
|
@ -117,4 +191,4 @@ void DeckPreviewDeckTagsDisplayWidget::execTagDialog(const QStringList &knownTag
|
|||
emit tagsChanged(updatedTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,53 +2,41 @@
|
|||
* @file deck_preview_deck_tags_display_widget.h
|
||||
* @ingroup VisualDeckPreviewWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H
|
||||
#define DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H
|
||||
|
||||
#include <QStringList>
|
||||
#include "../../../deck_loader/deck_loader.h"
|
||||
#include "deck_preview_widget.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <functional>
|
||||
|
||||
class FlowWidget;
|
||||
|
||||
class DeckPreviewWidget;
|
||||
class DeckPreviewDeckTagsDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QStringList currentTags;
|
||||
FlowWidget *flowWidget;
|
||||
std::function<QStringList()> knownTagsProvider;
|
||||
std::function<bool()> conversionPromptHandler;
|
||||
|
||||
public:
|
||||
explicit DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, const QStringList &_tags = {});
|
||||
void setTags(const QStringList &_tags);
|
||||
void refreshTags();
|
||||
|
||||
/**
|
||||
* @brief Sets a provider for the tags shown in the edit dialog.
|
||||
* Defaults to scanning all deck files in the deck folder.
|
||||
*/
|
||||
void setKnownTagsProvider(const std::function<QStringList()> &provider);
|
||||
|
||||
/**
|
||||
* @brief Sets a handler run before opening the tag dialog. Returning false
|
||||
* cancels the dialog. Defaults to no handler (the deck editor path).
|
||||
*/
|
||||
void setConversionPromptHandler(const std::function<bool()> &handler);
|
||||
|
||||
public slots:
|
||||
void openTagEditDlg();
|
||||
|
||||
private:
|
||||
bool promptFileConversionIfRequired(DeckPreviewWidget *deckPreviewWidget);
|
||||
void execTagDialog(const QStringList &knownTags);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted when the tags have changed due to user interaction.
|
||||
* @param tags The new list of tags.
|
||||
*/
|
||||
void tagsChanged(const QStringList &tags);
|
||||
|
||||
private:
|
||||
void execTagDialog(const QStringList &knownTags);
|
||||
};
|
||||
#endif // DECK_PREVIEW_DECK_TAGS_DISPLAY_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -1,69 +1,47 @@
|
|||
#include "deck_preview_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h"
|
||||
#include "../../../deck_loader/deck_loader.h"
|
||||
#include "../../cards/additional_info/color_identity_widget.h"
|
||||
#include "../../cards/deck_preview_card_picture_widget.h"
|
||||
#include "../visual_deck_storage_quick_settings_widget.h"
|
||||
#include "../visual_deck_storage_tag_filter_widget.h"
|
||||
#include "../visual_deck_storage_widget.h"
|
||||
#include "deck_preview_deck_tags_display_widget.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QMouseEvent>
|
||||
#include <QSet>
|
||||
#include <QStandardItemModel>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
||||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
VisualDeckStorageModel *_model,
|
||||
const QString &_filePath)
|
||||
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model), filePath(_filePath)
|
||||
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath),
|
||||
colorIdentityWidget(nullptr), deckTagsDisplayWidget(nullptr)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
auto *pictureWidget =
|
||||
deckLoader = new DeckLoader(this);
|
||||
connect(deckLoader, &DeckLoader::loadFinished, this, &DeckPreviewWidget::initializeUi);
|
||||
//! \todo Batch tag refresh: count finished deck loads and refresh tags once all decks are loaded.
|
||||
// Currently expensive: refreshes on each individual deck load instead of once at the end.
|
||||
connect(deckLoader, &DeckLoader::loadFinished, visualDeckStorageWidget->tagFilterWidget,
|
||||
&VisualDeckStorageTagFilterWidget::refreshTags);
|
||||
deckLoader->loadFromFileAsync(filePath, DeckFileFormat::getFormatFromName(filePath), false);
|
||||
|
||||
bannerCardDisplayWidget =
|
||||
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
|
||||
pictureWidget->setFontSize(24);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this,
|
||||
&DeckPreviewWidget::imageClickedEvent);
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
&DeckPreviewWidget::imageDoubleClickedEvent);
|
||||
bannerCardDisplayWidget = pictureWidget;
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this);
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this);
|
||||
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags);
|
||||
deckTagsDisplayWidget->setKnownTagsProvider(
|
||||
[this] { return visualDeckStorageWidget->tagFilterWidget->getAllKnownTags(); });
|
||||
deckTagsDisplayWidget->setConversionPromptHandler([this] { return promptFileConversionIfRequired(); });
|
||||
|
||||
bannerCardLabel = new QLabel(this);
|
||||
bannerCardLabel->setObjectName("bannerCardLabel");
|
||||
bannerCardComboBox = new QComboBox(this);
|
||||
bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
bannerCardComboBox->setObjectName("bannerCardComboBox");
|
||||
bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox));
|
||||
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&DeckPreviewWidget::setBannerCard);
|
||||
|
||||
// Apply the initial visibility settings and keep them in sync while they change.
|
||||
updateColorIdentityVisibility(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity());
|
||||
updateBannerCardComboBoxVisibility(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowBannerCardComboBox());
|
||||
updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews());
|
||||
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageShowColorIdentityChanged, this,
|
||||
|
|
@ -78,29 +56,6 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
&DeckPreviewWidget::refreshBannerCardToolTip);
|
||||
|
||||
layout->addWidget(bannerCardDisplayWidget);
|
||||
layout->addWidget(colorIdentityWidget);
|
||||
layout->addWidget(deckTagsDisplayWidget);
|
||||
layout->addWidget(bannerCardLabel);
|
||||
layout->addWidget(bannerCardComboBox);
|
||||
|
||||
// Only re-sync when this widget's own row changed. Without the row check, every
|
||||
// finished deck load would trigger a full resync (card db lookup + combo rebuild)
|
||||
// in every preview widget.
|
||||
connect(model, &QAbstractItemModel::dataChanged, this,
|
||||
[this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
|
||||
const int r = row();
|
||||
if (r >= topLeft.row() && r <= bottomRight.row()) {
|
||||
syncFromModel();
|
||||
}
|
||||
});
|
||||
|
||||
retranslateUi();
|
||||
syncFromModel();
|
||||
|
||||
// resizeEvent clamps every child to the picture's width, so collect them once here
|
||||
// to keep the resize handler from searching the widget tree on every layout pass.
|
||||
fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel,
|
||||
bannerCardComboBox};
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::retranslateUi()
|
||||
|
|
@ -114,15 +69,9 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
|||
if (bannerCardDisplayWidget == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int width = bannerCardDisplayWidget->width();
|
||||
if (width == lastKnownBannerWidth) {
|
||||
return;
|
||||
}
|
||||
lastKnownBannerWidth = width;
|
||||
|
||||
for (QWidget *widget : fixedWidthChildren) {
|
||||
widget->setMaximumWidth(width);
|
||||
QList<QWidget *> widgets = findChildren<QWidget *>();
|
||||
for (QWidget *widget : widgets) {
|
||||
widget->setMaximumWidth(bannerCardDisplayWidget->width());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,28 +79,83 @@ void DeckPreviewWidget::enterEvent(QEnterEvent *event)
|
|||
{
|
||||
QWidget::enterEvent(event);
|
||||
|
||||
// Don't do reloads until the deck has actually been loaded once.
|
||||
reloadIfModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The row of this deck in the source model, or -1 if it no longer exists.
|
||||
*/
|
||||
int DeckPreviewWidget::row() const
|
||||
{
|
||||
return model->rowForFilePath(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The display name is given by the deck name, or the filename if the deck name is not set.
|
||||
*/
|
||||
QString DeckPreviewWidget::getDisplayName() const
|
||||
{
|
||||
const int r = row();
|
||||
if (r == -1) {
|
||||
return {};
|
||||
// don't do reloads until widgets have been created
|
||||
if (bannerCardComboBox != nullptr) {
|
||||
reloadIfModified();
|
||||
}
|
||||
return model->dataForRow(r).displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the lastModifiedTime to the value given by the file.
|
||||
*/
|
||||
void DeckPreviewWidget::updateLastModifiedTime()
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
lastModifiedTime = fileInfo.lastModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Writes the current contents of the deck to file. Updates the lastModifiedTime afterward.
|
||||
*/
|
||||
void DeckPreviewWidget::writeDeckToFile()
|
||||
{
|
||||
DeckLoader::saveToFile(deckLoader->getDeck());
|
||||
updateLastModifiedTime();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
||||
{
|
||||
if (!deckLoadSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(filePath);
|
||||
lastModifiedTime = fileInfo.lastModified();
|
||||
|
||||
bannerCardDisplayWidget->setFontSize(24);
|
||||
setFilePath(deckLoader->getDeck().lastLoadInfo.fileName);
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this);
|
||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this);
|
||||
connect(deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::tagsChanged, this, &DeckPreviewWidget::setTags);
|
||||
|
||||
bannerCardLabel = new QLabel(this);
|
||||
bannerCardLabel->setObjectName("bannerCardLabel");
|
||||
bannerCardComboBox = new QComboBox(this);
|
||||
bannerCardComboBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||
bannerCardComboBox->setObjectName("bannerCardComboBox");
|
||||
bannerCardComboBox->installEventFilter(new NoScrollFilter(bannerCardComboBox));
|
||||
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&DeckPreviewWidget::setBannerCard);
|
||||
|
||||
updateColorIdentityVisibility(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity());
|
||||
updateBannerCardComboBoxVisibility(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowBannerCardComboBox());
|
||||
updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews());
|
||||
|
||||
layout->addWidget(colorIdentityWidget);
|
||||
layout->addWidget(deckTagsDisplayWidget);
|
||||
layout->addWidget(bannerCardLabel);
|
||||
layout->addWidget(bannerCardComboBox);
|
||||
|
||||
retranslateUi();
|
||||
resyncWidgets();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Syncs the contents of the child widgets with the current deck.
|
||||
*/
|
||||
void DeckPreviewWidget::resyncWidgets()
|
||||
{
|
||||
auto bannerCardRef = deckLoader->getDeck().deckList.getBannerCard();
|
||||
auto bannerCard = bannerCardRef.name.isEmpty() ? ExactCard() : CardDatabaseManager::query()->getCard(bannerCardRef);
|
||||
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
refreshBannerCardText();
|
||||
updateBannerCardComboBox(bannerCardRef.name);
|
||||
colorIdentityWidget->setColorIdentity(getColorIdentity());
|
||||
deckTagsDisplayWidget->setTags(deckLoader->getDeck().deckList.getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -159,36 +163,33 @@ QString DeckPreviewWidget::getDisplayName() const
|
|||
*/
|
||||
void DeckPreviewWidget::reloadIfModified()
|
||||
{
|
||||
const int r = row();
|
||||
if (r == -1 || !model->dataForRow(r).loadSucceeded) {
|
||||
QFileInfo fileInfo(filePath);
|
||||
QDateTime newLastModifiedTime = fileInfo.lastModified();
|
||||
|
||||
if (!newLastModifiedTime.isValid() || newLastModifiedTime <= lastModifiedTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
model->reloadIfModified(r);
|
||||
bool success = deckLoader->reload();
|
||||
|
||||
if (success) {
|
||||
fileInfo.refresh();
|
||||
lastModifiedTime = fileInfo.lastModified();
|
||||
resyncWidgets();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Syncs the contents of the child widgets with the current row's data.
|
||||
*/
|
||||
void DeckPreviewWidget::syncFromModel()
|
||||
void DeckPreviewWidget::updateVisibility()
|
||||
{
|
||||
const int r = row();
|
||||
if (r == -1) {
|
||||
return;
|
||||
setHidden(!checkVisibility());
|
||||
}
|
||||
|
||||
bool DeckPreviewWidget::checkVisibility() const
|
||||
{
|
||||
if (filteredBySearch || filteredByColor || filteredByTags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const DeckPreviewData &data = model->dataForRow(r);
|
||||
filePath = data.filePath;
|
||||
|
||||
const CardRef bannerCardRef = data.deck.deckList.getBannerCard();
|
||||
const ExactCard bannerCard =
|
||||
bannerCardRef.name.isEmpty() ? ExactCard() : CardDatabaseManager::query()->getCard(bannerCardRef);
|
||||
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
refreshBannerCardText();
|
||||
updateBannerCardComboBox(bannerCardRef.name);
|
||||
colorIdentityWidget->setColorIdentity(data.colorIdentity);
|
||||
deckTagsDisplayWidget->setTags(data.tags);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::updateColorIdentityVisibility(bool visible)
|
||||
|
|
@ -228,6 +229,51 @@ void DeckPreviewWidget::updateTagsVisibility(bool visible)
|
|||
}
|
||||
}
|
||||
|
||||
QString DeckPreviewWidget::getColorIdentity()
|
||||
{
|
||||
QStringList cardList = deckLoader->getDeck().deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
|
||||
if (cardList.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
||||
|
||||
for (const QString &cardName : cardList) {
|
||||
CardInfoPtr currentCard = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||
if (currentCard) {
|
||||
QString colors = currentCard->getColors(); // Assuming this returns something like "WUB"
|
||||
for (const QChar &color : colors) {
|
||||
colorSet.insert(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the color identity is in WUBRG order
|
||||
QString colorIdentity;
|
||||
const QString wubrgOrder = "WUBRG";
|
||||
for (const QChar &color : wubrgOrder) {
|
||||
if (colorSet.contains(color)) {
|
||||
colorIdentity.append(color);
|
||||
}
|
||||
}
|
||||
|
||||
return colorIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* The display name is given by the deck name, or the filename if the deck name is not set.
|
||||
*/
|
||||
QString DeckPreviewWidget::getDisplayName() const
|
||||
{
|
||||
QString deckName = deckLoader->getDeck().deckList.getName();
|
||||
return !deckName.isEmpty() ? deckName : QFileInfo(deckLoader->getDeck().lastLoadInfo.fileName).fileName();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::setFilePath(const QString &_filePath)
|
||||
{
|
||||
filePath = _filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the banner card text.
|
||||
* This also calls `refreshBannerCardToolTip`, since those two often need to be updated together.
|
||||
|
|
@ -264,15 +310,11 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText)
|
|||
// Prepare the new items with deduplication
|
||||
QSet<QPair<QString, QString>> bannerCardSet;
|
||||
|
||||
const int r = row();
|
||||
if (r != -1) {
|
||||
const DeckList &deckList = model->dataForRow(r).deck.deckList;
|
||||
const QList<const DecklistCardNode *> cardsInDeck = deckList.getCardNodes();
|
||||
QList<const DecklistCardNode *> cardsInDeck = deckLoader->getDeck().deckList.getCardNodes();
|
||||
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
bannerCardSet.insert(QPair<QString, QString>(currentCard->getName(), currentCard->getCardProviderId()));
|
||||
}
|
||||
for (auto currentCard : cardsInDeck) {
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
bannerCardSet.insert(QPair<QString, QString>(currentCard->getName(), currentCard->getCardProviderId()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,16 +327,16 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText)
|
|||
|
||||
// This is *slightly* more performant than using addItem in a loop.
|
||||
|
||||
QStandardItemModel *comboModel = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox);
|
||||
QStandardItemModel *model = new QStandardItemModel(pairList.size(), 1, bannerCardComboBox);
|
||||
|
||||
int row = 0;
|
||||
for (const auto &pair : pairList) {
|
||||
QStandardItem *item = new QStandardItem(pair.first);
|
||||
item->setData(QVariant::fromValue(pair), Qt::UserRole);
|
||||
comboModel->setItem(row++, 0, item);
|
||||
model->setItem(row++, 0, item);
|
||||
}
|
||||
|
||||
bannerCardComboBox->setModel(comboModel);
|
||||
bannerCardComboBox->setModel(model);
|
||||
|
||||
// Try to restore the previous selection by finding the currentText
|
||||
int restoredIndex = bannerCardComboBox->findText(currentText);
|
||||
|
|
@ -302,9 +344,7 @@ void DeckPreviewWidget::updateBannerCardComboBox(const QString ¤tText)
|
|||
bannerCardComboBox->setCurrentIndex(restoredIndex);
|
||||
} else {
|
||||
// Add a placeholder "-" and set it as the current selection
|
||||
const QString currentBannerCardName =
|
||||
r == -1 ? QString() : model->dataForRow(r).deck.deckList.getBannerCard().name;
|
||||
int bannerIndex = bannerCardComboBox->findText(currentBannerCardName);
|
||||
int bannerIndex = bannerCardComboBox->findText(deckLoader->getDeck().deckList.getBannerCard().name);
|
||||
if (bannerIndex != -1) {
|
||||
bannerCardComboBox->setCurrentIndex(bannerIndex);
|
||||
} else {
|
||||
|
|
@ -322,11 +362,8 @@ void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
|||
{
|
||||
auto [name, id] = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
CardRef cardRef = {name, id};
|
||||
const int r = row();
|
||||
if (r == -1) {
|
||||
return;
|
||||
}
|
||||
model->setBannerCard(r, cardRef);
|
||||
deckLoader->getDeck().deckList.setBannerCard(cardRef);
|
||||
writeDeckToFile();
|
||||
bannerCardDisplayWidget->setCard(CardDatabaseManager::query()->getCard(cardRef));
|
||||
}
|
||||
|
||||
|
|
@ -348,24 +385,17 @@ void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewC
|
|||
|
||||
void DeckPreviewWidget::setTags(const QStringList &tags)
|
||||
{
|
||||
const int r = row();
|
||||
if (r != -1) {
|
||||
model->setTags(r, tags);
|
||||
}
|
||||
deckLoader->getDeck().deckList.setTags(tags);
|
||||
writeDeckToFile();
|
||||
}
|
||||
|
||||
QMenu *DeckPreviewWidget::createRightClickMenu()
|
||||
{
|
||||
const int r = row();
|
||||
|
||||
auto *menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
emit openDeckEditor(model->deckForRow(r));
|
||||
}
|
||||
});
|
||||
connect(menu->addAction(tr("Open in deck editor")), &QAction::triggered, this,
|
||||
[this] { emit openDeckEditor(deckLoader->getDeck()); });
|
||||
|
||||
connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget,
|
||||
&DeckPreviewDeckTagsDisplayWidget::openTagEditDlg);
|
||||
|
|
@ -378,26 +408,14 @@ QMenu *DeckPreviewWidget::createRightClickMenu()
|
|||
|
||||
auto saveToClipboardMenu = menu->addMenu(tr("Save Deck to Clipboard"));
|
||||
|
||||
connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, true, true);
|
||||
}
|
||||
});
|
||||
connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, true, false);
|
||||
}
|
||||
});
|
||||
connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, false, true);
|
||||
}
|
||||
});
|
||||
connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this, [this, r] {
|
||||
if (r != -1) {
|
||||
DeckLoader::saveToClipboard(model->dataForRow(r).deck.deckList, false, false);
|
||||
}
|
||||
});
|
||||
connect(saveToClipboardMenu->addAction(tr("Annotated")), &QAction::triggered, this,
|
||||
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, true); });
|
||||
connect(saveToClipboardMenu->addAction(tr("Annotated (No set info)")), &QAction::triggered, this,
|
||||
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, true, false); });
|
||||
connect(saveToClipboardMenu->addAction(tr("Not Annotated")), &QAction::triggered, this,
|
||||
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, true); });
|
||||
connect(saveToClipboardMenu->addAction(tr("Not Annotated (No set info)")), &QAction::triggered, this,
|
||||
[this] { DeckLoader::saveToClipboard(deckLoader->getDeck().deckList, false, false); });
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
|
|
@ -432,58 +450,57 @@ void DeckPreviewWidget::addSetBannerCardMenu(QMenu *menu)
|
|||
|
||||
void DeckPreviewWidget::actRenameDeck()
|
||||
{
|
||||
const int r = row();
|
||||
if (r == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// read input
|
||||
const QString oldName = model->dataForRow(r).deckName;
|
||||
const QString oldName = deckLoader->getDeck().deckList.getName();
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(this, tr("Rename deck"), tr("New name:"), QLineEdit::Normal, oldName, &ok);
|
||||
QString newName = QInputDialog::getText(this, "Rename deck", tr("New name:"), QLineEdit::Normal, oldName, &ok);
|
||||
if (!ok || oldName == newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// write change
|
||||
model->renameDeck(r, newName);
|
||||
deckLoader->getDeck().deckList.setName(newName);
|
||||
writeDeckToFile();
|
||||
|
||||
// The banner card text updates via the model's dataChanged signal.
|
||||
// update VDS
|
||||
refreshBannerCardText();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::actRenameFile()
|
||||
{
|
||||
const int r = row();
|
||||
if (r == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// read input
|
||||
const auto info = QFileInfo(filePath);
|
||||
const QString oldName = info.baseName();
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(this, tr("Rename file"), tr("New name:"), QLineEdit::Normal, oldName, &ok);
|
||||
QString newName = QInputDialog::getText(this, "Rename file", tr("New name:"), QLineEdit::Normal, oldName, &ok);
|
||||
if (!ok || newName.isEmpty() || oldName == newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// write change
|
||||
if (!model->renameFile(r, newName)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
|
||||
QString newFileName = newName;
|
||||
if (!info.suffix().isEmpty()) {
|
||||
newFileName += "." + info.suffix();
|
||||
}
|
||||
|
||||
// The file path and banner card text update via the model's signals.
|
||||
// write change
|
||||
const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath();
|
||||
if (!QFile::rename(info.filePath(), newFilePath)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
deckLoader->getDeck().lastLoadInfo.fileName = newFilePath;
|
||||
setFilePath(newFilePath);
|
||||
|
||||
// update VDS
|
||||
updateLastModifiedTime();
|
||||
refreshBannerCardText();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::actDeleteFile()
|
||||
{
|
||||
const int r = row();
|
||||
if (r == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// read input
|
||||
auto res = QMessageBox::warning(this, tr("Delete file"), tr("Are you sure you want to delete the selected file?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
|
@ -492,74 +509,11 @@ void DeckPreviewWidget::actDeleteFile()
|
|||
}
|
||||
|
||||
// write change
|
||||
if (!model->deleteFile(r)) {
|
||||
if (!QFile::remove(QFileInfo(filePath).filePath())) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Delete failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// The folder widget removes this preview once the row is gone.
|
||||
}
|
||||
|
||||
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||
|
||||
if (QFile::exists(newFileName)) {
|
||||
QMessageBox::StandardButton reply =
|
||||
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
|
||||
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
return reply == QMessageBox::Yes;
|
||||
}
|
||||
return true; // Safe to proceed
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the deck's file format supports tags.
|
||||
* If not, then prompt the user for file conversion.
|
||||
* @return whether the resulting file can support adding tags
|
||||
*/
|
||||
bool DeckPreviewWidget::promptFileConversionIfRequired()
|
||||
{
|
||||
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirmOverwriteIfExists(this, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
model->convertToCockatriceFormat(row());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(this);
|
||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
|
||||
!conversionDialog.dontAskAgain());
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to convert file
|
||||
if (!confirmOverwriteIfExists(this, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
model->convertToCockatriceFormat(row());
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
// update VDS
|
||||
this->deleteLater();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@
|
|||
* @file deck_preview_widget.h
|
||||
* @ingroup VisualDeckPreviewWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DECK_PREVIEW_WIDGET_H
|
||||
#define DECK_PREVIEW_WIDGET_H
|
||||
|
||||
#include "../../../deck_loader/deck_loader.h"
|
||||
#include "../../cards/additional_info/color_identity_widget.h"
|
||||
#include "../../cards/deck_preview_card_picture_widget.h"
|
||||
#include "../visual_deck_storage_model.h"
|
||||
#include "../visual_deck_storage_widget.h"
|
||||
#include "deck_preview_deck_tags_display_widget.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
|
|
@ -16,82 +20,72 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
class QEnterEvent;
|
||||
class QLabel;
|
||||
class QMenu;
|
||||
class QMouseEvent;
|
||||
class ColorIdentityWidget;
|
||||
class DeckPreviewCardPictureWidget;
|
||||
class DeckPreviewDeckTagsDisplayWidget;
|
||||
class VisualDeckStorageModel;
|
||||
class VisualDeckStorageWidget;
|
||||
class DeckPreviewDeckTagsDisplayWidget;
|
||||
|
||||
class DeckPreviewWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DeckPreviewWidget(QWidget *parent,
|
||||
explicit DeckPreviewWidget(QWidget *_parent,
|
||||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
VisualDeckStorageModel *_model,
|
||||
const QString &_filePath);
|
||||
void retranslateUi();
|
||||
QString getColorIdentity();
|
||||
[[nodiscard]] QString getDisplayName() const;
|
||||
|
||||
/**
|
||||
* @brief The banner card picture; the parent widget wires its size to the card size setting.
|
||||
*/
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
QVBoxLayout *layout;
|
||||
QString filePath;
|
||||
QDateTime lastModifiedTime;
|
||||
DeckLoader *deckLoader;
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget = nullptr;
|
||||
ColorIdentityWidget *colorIdentityWidget = nullptr;
|
||||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget = nullptr;
|
||||
QLabel *bannerCardLabel = nullptr;
|
||||
QComboBox *bannerCardComboBox = nullptr;
|
||||
bool filteredBySearch = false;
|
||||
bool filteredByColor = false;
|
||||
bool filteredByTags = false;
|
||||
[[nodiscard]] bool checkVisibility() const;
|
||||
|
||||
signals:
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Re-reads the row's data from the model and syncs every child widget.
|
||||
* Connected to the model's dataChanged signal.
|
||||
*/
|
||||
void syncFromModel();
|
||||
|
||||
/**
|
||||
* @brief Reloads the deck file if its modification time is newer than the stored one.
|
||||
*/
|
||||
void reloadIfModified();
|
||||
void setFilePath(const QString &filePath);
|
||||
void refreshBannerCardText();
|
||||
void refreshBannerCardToolTip();
|
||||
void updateBannerCardComboBox(const QString ¤tText);
|
||||
void setBannerCard(int);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void initializeUi(bool deckLoadSuccess);
|
||||
void resyncWidgets();
|
||||
void reloadIfModified();
|
||||
void updateVisibility();
|
||||
void updateColorIdentityVisibility(bool visible);
|
||||
void updateBannerCardComboBoxVisibility(bool visible);
|
||||
void updateTagsVisibility(bool visible);
|
||||
void setBannerCard(int);
|
||||
void setTags(const QStringList &tags);
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
protected:
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] int row() const;
|
||||
[[nodiscard]] QString getDisplayName() const;
|
||||
void refreshBannerCardText();
|
||||
void updateBannerCardComboBox(const QString ¤tText);
|
||||
bool promptFileConversionIfRequired();
|
||||
void updateLastModifiedTime();
|
||||
void writeDeckToFile();
|
||||
QMenu *createRightClickMenu();
|
||||
void addSetBannerCardMenu(QMenu *menu);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
||||
private slots:
|
||||
void setTags(const QStringList &tags);
|
||||
|
||||
void actRenameDeck();
|
||||
void actRenameFile();
|
||||
void actDeleteFile();
|
||||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
VisualDeckStorageModel *model;
|
||||
QString filePath;
|
||||
QVBoxLayout *layout;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||
QLabel *bannerCardLabel;
|
||||
QComboBox *bannerCardComboBox;
|
||||
QList<QWidget *> fixedWidthChildren; ///< Children clamped to the picture width on resize.
|
||||
int lastKnownBannerWidth = -1; ///< The picture width last applied to the children.
|
||||
};
|
||||
|
||||
class NoScrollFilter : public QObject
|
||||
|
|
|
|||
|
|
@ -1,27 +1,20 @@
|
|||
#include "visual_deck_storage_folder_display_widget.h"
|
||||
|
||||
#include "../cards/card_info_picture_widget.h"
|
||||
#include "../general/display/banner_widget.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "deck_preview/deck_preview_widget.h"
|
||||
#include "visual_deck_storage_model.h"
|
||||
#include "visual_deck_storage_quick_settings_widget.h"
|
||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QDirIterator>
|
||||
#include <QMouseEvent>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
|
||||
QWidget *parent,
|
||||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
const QString &_folderPath,
|
||||
QString _filePath,
|
||||
bool canBeHidden,
|
||||
bool _showFolders)
|
||||
: QWidget(parent), showFolders(_showFolders), folderPath(_folderPath),
|
||||
visualDeckStorageWidget(_visualDeckStorageWidget)
|
||||
: QWidget(parent), showFolders(_showFolders), visualDeckStorageWidget(_visualDeckStorageWidget), filePath(_filePath)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
|
@ -29,9 +22,6 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
|
|||
header = new BannerWidget(this, "");
|
||||
header->setClickable(canBeHidden);
|
||||
header->setHidden(!showFolders);
|
||||
|
||||
const QString bannerText = folderPath.isEmpty() ? tr("Deck Storage") : folderPath;
|
||||
header->setText(bannerText);
|
||||
layout->addWidget(header);
|
||||
|
||||
container = new QWidget(this);
|
||||
|
|
@ -45,285 +35,192 @@ VisualDeckStorageFolderDisplayWidget::VisualDeckStorageFolderDisplayWidget(
|
|||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
|
||||
containerLayout->addWidget(flowWidget);
|
||||
|
||||
auto *proxy = visualDeckStorageWidget->proxyModel();
|
||||
// A burst of proxy changes (one dataChanged per finished deck load, plus the filter
|
||||
// invalidations) coalesces into a single reconcile, so a scan of many decks doesn't
|
||||
// rebuild the flow layout once per deck.
|
||||
connect(proxy, &QAbstractItemModel::modelReset, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
|
||||
connect(proxy, &QAbstractItemModel::rowsInserted, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
|
||||
connect(proxy, &QAbstractItemModel::rowsRemoved, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
|
||||
connect(proxy, &QAbstractItemModel::dataChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
|
||||
connect(proxy, &QAbstractItemModel::layoutChanged, this, &VisualDeckStorageFolderDisplayWidget::scheduleReconcile);
|
||||
createWidgetsForFiles();
|
||||
createWidgetsForFolders();
|
||||
|
||||
reconcileTimer = new QTimer(this);
|
||||
reconcileTimer->setSingleShot(true);
|
||||
reconcileTimer->setInterval(150);
|
||||
connect(reconcileTimer, &QTimer::timeout, this, &VisualDeckStorageFolderDisplayWidget::reconcile);
|
||||
|
||||
// Building the whole folder subtree synchronously here would stall the ui thread on large
|
||||
// collections, so the first reconcile runs as a chunked pass on later event loop turns.
|
||||
scheduleReconcile();
|
||||
refreshUi();
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::scheduleReconcile()
|
||||
void VisualDeckStorageFolderDisplayWidget::refreshUi()
|
||||
{
|
||||
if (deckPassActive) {
|
||||
// The active pass may be scanning stale model state, so restart it from a clean
|
||||
// slate once the current chunk yields.
|
||||
deckPassRestartRequested = true;
|
||||
return;
|
||||
QString bannerText = tr("Deck Storage");
|
||||
QString deckPath = SettingsCache::instance().paths().getDeckPath();
|
||||
if (filePath != deckPath) {
|
||||
QString relativePath = filePath;
|
||||
|
||||
if (filePath.startsWith(deckPath)) {
|
||||
relativePath = filePath.mid(deckPath.length()); // Remove the deckPath prefix
|
||||
if (relativePath.startsWith('/')) {
|
||||
relativePath.remove(0, 1); // Remove leading '/' if it exists
|
||||
}
|
||||
}
|
||||
|
||||
bannerText = relativePath;
|
||||
}
|
||||
reconcileTimer->start();
|
||||
header->setText(bannerText);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Starts a new chunked scan of the source model, yielding to the event loop between chunks.
|
||||
*/
|
||||
void VisualDeckStorageFolderDisplayWidget::reconcile()
|
||||
{
|
||||
beginDeckPass();
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::beginDeckPass()
|
||||
{
|
||||
deckPassActive = true;
|
||||
deckPassRestartRequested = false;
|
||||
deckPassRow = 0;
|
||||
visibleDeckCount = 0;
|
||||
deckPassPresentPaths.clear();
|
||||
|
||||
continueDeckPass();
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::continueDeckPass()
|
||||
{
|
||||
if (!deckPassActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
QElapsedTimer passTimer;
|
||||
passTimer.start();
|
||||
|
||||
auto *proxy = visualDeckStorageWidget->proxyModel();
|
||||
const int proxyRowCount = proxy->rowCount();
|
||||
|
||||
// Scan rows of this folder, creating missing previews, until the time budget for this
|
||||
// event loop turn runs out. The rest continues on the next turn.
|
||||
while (deckPassRow < proxyRowCount) {
|
||||
const int row = deckPassRow++;
|
||||
const QModelIndex index = proxy->index(row, 0);
|
||||
if (showFolders && index.data(VisualDeckStorageRoles::FolderPathRole).toString() != folderPath) {
|
||||
continue;
|
||||
}
|
||||
const QString filePath = index.data(VisualDeckStorageRoles::FilePathRole).toString();
|
||||
deckPassPresentPaths.insert(filePath);
|
||||
|
||||
DeckPreviewWidget *deckPreviewWidget = deckWidgets.value(filePath, nullptr);
|
||||
if (!deckPreviewWidget) {
|
||||
deckPreviewWidget = createDeckPreviewWidget(filePath);
|
||||
flowWidget->addWidget(deckPreviewWidget);
|
||||
}
|
||||
|
||||
const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool();
|
||||
if (matches == deckPreviewWidget->isHidden()) {
|
||||
deckPreviewWidget->setVisible(matches);
|
||||
}
|
||||
if (matches) {
|
||||
++visibleDeckCount;
|
||||
}
|
||||
|
||||
if (passTimer.elapsed() >= DECK_PASS_TIME_BUDGET_MS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (deckPassRestartRequested) {
|
||||
beginDeckPass();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deckPassRow < proxyRowCount) {
|
||||
QMetaObject::invokeMethod(this, &VisualDeckStorageFolderDisplayWidget::continueDeckPass, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
finishDeckPass();
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::finishDeckPass()
|
||||
{
|
||||
auto *proxy = visualDeckStorageWidget->proxyModel();
|
||||
|
||||
// Drop previews of decks that no longer exist in the model.
|
||||
for (auto it = deckWidgets.begin(); it != deckWidgets.end();) {
|
||||
if (!deckPassPresentPaths.contains(it.key())) {
|
||||
flowWidget->removeWidget(it.value());
|
||||
it.value()->deleteLater();
|
||||
it = deckWidgets.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// Order the flow layout like the proxy sorts its rows. Filtered-out decks stay part of
|
||||
// the layout, hidden in their sorted place until a filter lets them through again.
|
||||
QStringList orderedFilePaths;
|
||||
orderedFilePaths.reserve(proxy->rowCount());
|
||||
for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) {
|
||||
const QString filePath = proxy->index(proxyRow, 0).data(VisualDeckStorageRoles::FilePathRole).toString();
|
||||
if (deckWidgets.contains(filePath)) {
|
||||
orderedFilePaths.append(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-add all widgets so the flow layout order matches the proxy order. Skipped when the
|
||||
// order is unchanged so that data-only updates don't invalidate the flow layout.
|
||||
if (orderedFilePaths != lastOrderedFilePaths) {
|
||||
for (const QString &filePath : orderedFilePaths) {
|
||||
flowWidget->removeWidget(deckWidgets.value(filePath));
|
||||
}
|
||||
for (const QString &filePath : orderedFilePaths) {
|
||||
flowWidget->addWidget(deckWidgets.value(filePath));
|
||||
}
|
||||
lastOrderedFilePaths = orderedFilePaths;
|
||||
}
|
||||
|
||||
createSubFolderWidgets();
|
||||
|
||||
// Mark completion before evaluating visibility so this pass's own numbers decide whether
|
||||
// the folder has content. The flag only guards evaluations made *during* a build.
|
||||
deckPassActive = false;
|
||||
initialPassCompleted = true;
|
||||
|
||||
refreshVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a deck preview widget and wires it up to the storage widget.
|
||||
* Gets all files in the directory that have an accepted decklist file extension
|
||||
*
|
||||
* @param filePath The absolute path of the deck file to preview.
|
||||
* @param filePath The directory to search through
|
||||
* @param recursive Whether to search through subdirectories
|
||||
*/
|
||||
DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget(const QString &filePath)
|
||||
static QStringList getAllFiles(const QString &filePath, bool recursive)
|
||||
{
|
||||
auto *deckPreviewWidget =
|
||||
new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, visualDeckStorageWidget->model(), filePath);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::deckLoadRequested);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::openDeckEditor);
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
||||
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
||||
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
|
||||
deckWidgets.insert(filePath, deckPreviewWidget);
|
||||
return deckPreviewWidget;
|
||||
QStringList allFiles;
|
||||
|
||||
// QDirIterator with QDir::Files ensures only files are listed (no directories)
|
||||
auto flags =
|
||||
recursive ? QDirIterator::Subdirectories | QDirIterator::FollowSymlinks : QDirIterator::NoIteratorFlags;
|
||||
QDirIterator it(filePath, DeckLoader::ACCEPTED_FILE_EXTENSIONS, QDir::Files, flags);
|
||||
|
||||
while (it.hasNext()) {
|
||||
allFiles << it.next(); // Add each file path to the list
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::createWidgetsForFiles()
|
||||
{
|
||||
QList<DeckPreviewWidget *> allDecks;
|
||||
for (const QString &file : getAllFiles(filePath, !showFolders)) {
|
||||
auto *display = new DeckPreviewWidget(flowWidget, visualDeckStorageWidget, file);
|
||||
|
||||
connect(display, &DeckPreviewWidget::deckLoadRequested, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::deckLoadRequested);
|
||||
connect(display, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::openDeckEditor);
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
||||
display->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
||||
display->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
|
||||
allDecks.append(display);
|
||||
}
|
||||
|
||||
flowWidget->clearLayout(); // Clear existing widgets in the flow layout
|
||||
|
||||
for (DeckPreviewWidget *deck : allDecks) {
|
||||
flowWidget->addWidget(deck);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates, removes and keeps in sync the subfolder widgets of this folder.
|
||||
* Updates the visibility of this folder and all its DeckPreviewWidgets
|
||||
*
|
||||
* Only direct child folders are created here; each child manages its own
|
||||
* children, mirroring the folder tree on disk.
|
||||
* @param recursive Also update the visibility of all subfolders and their DeckPreviewWidgets.
|
||||
*/
|
||||
void VisualDeckStorageFolderDisplayWidget::createSubFolderWidgets()
|
||||
void VisualDeckStorageFolderDisplayWidget::updateVisibility(bool recursive)
|
||||
{
|
||||
bool atLeastOneWidgetVisible = checkVisibility();
|
||||
if (atLeastOneWidgetVisible) {
|
||||
setVisible(true);
|
||||
for (DeckPreviewWidget *display : flowWidget->findChildren<DeckPreviewWidget *>()) {
|
||||
display->updateVisibility();
|
||||
}
|
||||
if (recursive) {
|
||||
for (auto *subFolder : findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
|
||||
subFolder->updateVisibility(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool VisualDeckStorageFolderDisplayWidget::checkVisibility()
|
||||
{
|
||||
bool atLeastOneWidgetVisible = false;
|
||||
if (flowWidget) {
|
||||
// Iterate through all DeckPreviewWidgets
|
||||
for (DeckPreviewWidget *display : flowWidget->findChildren<DeckPreviewWidget *>()) {
|
||||
if (display->checkVisibility()) {
|
||||
atLeastOneWidgetVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (VisualDeckStorageFolderDisplayWidget *subFolder : findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
|
||||
if (subFolder->checkVisibility()) {
|
||||
atLeastOneWidgetVisible = true;
|
||||
}
|
||||
}
|
||||
return atLeastOneWidgetVisible;
|
||||
}
|
||||
|
||||
static QStringList getAllSubFolders(const QString &filePath)
|
||||
{
|
||||
QStringList allFolders;
|
||||
|
||||
// QDirIterator with QDir::Files ensures only files are listed (no directories)
|
||||
QDirIterator it(filePath, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
while (it.hasNext()) {
|
||||
allFolders << it.next(); // Add each file path to the list
|
||||
}
|
||||
|
||||
return allFolders;
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::createWidgetsForFolders()
|
||||
{
|
||||
if (!showFolders) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList children = childFolderPaths();
|
||||
|
||||
for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end();) {
|
||||
if (!children.contains(it.key())) {
|
||||
containerLayout->removeWidget(it.value());
|
||||
it.value()->deleteLater();
|
||||
it = subFolderWidgets.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &child : children) {
|
||||
if (subFolderWidgets.contains(child)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *subFolderWidget =
|
||||
new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, child, true, showFolders);
|
||||
connect(subFolderWidget, &VisualDeckStorageFolderDisplayWidget::contentVisibilityChanged, this,
|
||||
&VisualDeckStorageFolderDisplayWidget::refreshVisibility);
|
||||
containerLayout->addWidget(subFolderWidget);
|
||||
subFolderWidgets.insert(child, subFolderWidget);
|
||||
for (const QString &dir : getAllSubFolders(filePath)) {
|
||||
auto *display = new VisualDeckStorageFolderDisplayWidget(this, visualDeckStorageWidget, dir, true, showFolders);
|
||||
containerLayout->addWidget(display);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::updateShowFolders(bool enabled)
|
||||
{
|
||||
showFolders = enabled;
|
||||
header->setHidden(!showFolders);
|
||||
|
||||
if (!showFolders) {
|
||||
for (auto it = subFolderWidgets.begin(); it != subFolderWidgets.end(); ++it) {
|
||||
containerLayout->removeWidget(it.value());
|
||||
it.value()->deleteLater();
|
||||
}
|
||||
subFolderWidgets.clear();
|
||||
flattenFolderStructure();
|
||||
} else {
|
||||
// if setting was switched from disabled to enabled, we assume that there aren't any existing subfolders
|
||||
createWidgetsForFiles();
|
||||
createWidgetsForFolders();
|
||||
}
|
||||
|
||||
scheduleReconcile();
|
||||
header->setHidden(!showFolders);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Hides the folder when it contains nothing visible, and reports the change upward.
|
||||
* Steals all DeckPreviewWidgets from this widget's nested subfolders, and deletes those subfolders
|
||||
*/
|
||||
void VisualDeckStorageFolderDisplayWidget::refreshVisibility()
|
||||
void VisualDeckStorageFolderDisplayWidget::flattenFolderStructure()
|
||||
{
|
||||
const bool shouldBeVisible = hasContent();
|
||||
if (isHidden() == !shouldBeVisible) {
|
||||
return;
|
||||
for (auto *subFolder : findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
|
||||
// steal all DeckPreviewWidgets from the subfolder
|
||||
for (auto *deck : subFolder->getFlowWidget()->findChildren<DeckPreviewWidget *>()) {
|
||||
flowWidget->addWidget(deck);
|
||||
}
|
||||
|
||||
// delete the subfolder
|
||||
subFolder->deleteLater();
|
||||
}
|
||||
setHidden(!shouldBeVisible);
|
||||
emit contentVisibilityChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether this folder shows any deck previews or has any visible subfolder.
|
||||
*
|
||||
* While the first pass is still building, the folder counts as having content so it
|
||||
* doesn't flicker or hide prematurely before its previews have been created.
|
||||
*/
|
||||
bool VisualDeckStorageFolderDisplayWidget::hasContent() const
|
||||
QStringList VisualDeckStorageFolderDisplayWidget::gatherAllTagsFromFlowWidget() const
|
||||
{
|
||||
if (!initialPassCompleted || visibleDeckCount > 0) {
|
||||
return true;
|
||||
}
|
||||
QStringList allTags;
|
||||
|
||||
for (VisualDeckStorageFolderDisplayWidget *subFolderWidget : subFolderWidgets) {
|
||||
if (subFolderWidget->hasContent()) {
|
||||
return true;
|
||||
if (flowWidget) {
|
||||
// Iterate through all DeckPreviewWidgets
|
||||
for (DeckPreviewWidget *display : flowWidget->findChildren<DeckPreviewWidget *>()) {
|
||||
// Get tags from each DeckPreviewWidget
|
||||
QStringList tags = display->deckLoader->getDeck().deckList.getTags();
|
||||
|
||||
// Add tags to the list while avoiding duplicates
|
||||
allTags.append(tags);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
// Remove duplicates by calling 'removeDuplicates'
|
||||
allTags.removeDuplicates();
|
||||
|
||||
/**
|
||||
* @brief The direct child folder paths of this folder, sorted by name.
|
||||
*/
|
||||
QStringList VisualDeckStorageFolderDisplayWidget::childFolderPaths() const
|
||||
{
|
||||
QStringList children;
|
||||
const QString prefix = folderPath.isEmpty() ? QString() : folderPath + "/";
|
||||
|
||||
for (const QString &candidate : visualDeckStorageWidget->model()->getFolderPaths()) {
|
||||
if (!candidate.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const QString rest = candidate.mid(prefix.length());
|
||||
if (rest.isEmpty() || rest.contains('/')) {
|
||||
continue;
|
||||
}
|
||||
children.append(candidate);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
return allTags;
|
||||
}
|
||||
|
|
@ -1,105 +1,49 @@
|
|||
/**
|
||||
* @file visual_deck_storage_folder_display_widget.h
|
||||
* @ingroup VisualDeckStorageWidgets
|
||||
* @brief Renders the decks of one folder of the Visual Deck Storage.
|
||||
*
|
||||
* This is a pure view: it keeps one persistent DeckPreviewWidget alive per deck
|
||||
* in its folder, and shows or hides those widgets according to each row's
|
||||
* FilterMatchRole in the VisualDeckStorageSortFilterProxyModel. Subfolders are
|
||||
* shown as nested VisualDeckStorageFolderDisplayWidgets when the "show folders"
|
||||
* setting is enabled.
|
||||
*
|
||||
* Reconciling runs as a time-budgeted chunked pass that yields to the event loop
|
||||
* between chunks, so scanning a large collection never stalls the ui thread.
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
|
||||
#define VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
#include "../general/display/banner_widget.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
|
||||
class BannerWidget;
|
||||
class DeckPreviewWidget;
|
||||
class FlowWidget;
|
||||
class QTimer;
|
||||
class QVBoxLayout;
|
||||
class VisualDeckStorageWidget;
|
||||
|
||||
class VisualDeckStorageFolderDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
VisualDeckStorageFolderDisplayWidget(QWidget *parent,
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget,
|
||||
const QString &folderPath,
|
||||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
QString _filePath,
|
||||
bool canBeHidden,
|
||||
bool showFolders);
|
||||
bool _showFolders);
|
||||
void refreshUi();
|
||||
void createWidgetsForFiles();
|
||||
void createWidgetsForFolders();
|
||||
void flattenFolderStructure();
|
||||
[[nodiscard]] QStringList gatherAllTagsFromFlowWidget() const;
|
||||
[[nodiscard]] FlowWidget *getFlowWidget() const
|
||||
{
|
||||
return flowWidget;
|
||||
}
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Starts a new chunked reconcile pass that re-reads the proxy and rebuilds
|
||||
* the deck previews and subfolder widgets to match.
|
||||
*/
|
||||
void reconcile();
|
||||
|
||||
/**
|
||||
* @brief Coalesces proxy change signals (a burst of deck loads or filter
|
||||
* invalidations) into a single reconcile on the next event loop turn.
|
||||
*/
|
||||
void scheduleReconcile();
|
||||
void updateVisibility(bool recursive = true);
|
||||
bool checkVisibility();
|
||||
void updateShowFolders(bool enabled);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted whenever this folder's visible content changes, so parent
|
||||
* folders can re-evaluate their own visibility.
|
||||
*/
|
||||
void contentVisibilityChanged();
|
||||
|
||||
private:
|
||||
void beginDeckPass();
|
||||
void continueDeckPass();
|
||||
void finishDeckPass();
|
||||
[[nodiscard]] DeckPreviewWidget *createDeckPreviewWidget(const QString &filePath);
|
||||
void createSubFolderWidgets();
|
||||
void refreshVisibility();
|
||||
[[nodiscard]] bool hasContent() const;
|
||||
[[nodiscard]] QStringList childFolderPaths() const;
|
||||
|
||||
/**
|
||||
* @brief The maximum time in milliseconds spent creating deck previews per event loop turn.
|
||||
*
|
||||
* Creating all previews of a large folder at once blocks the ui thread for hundreds of
|
||||
* milliseconds, so the pass is split into chunks that yield to the event loop instead.
|
||||
*/
|
||||
static constexpr int DECK_PASS_TIME_BUDGET_MS = 20;
|
||||
|
||||
bool showFolders;
|
||||
QString folderPath; ///< Path relative to the deck folder, empty for the root folder.
|
||||
int visibleDeckCount = 0; ///< The number of this folder's deck previews not filtered out.
|
||||
QVBoxLayout *layout;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
QString filePath;
|
||||
BannerWidget *header;
|
||||
QWidget *container;
|
||||
QVBoxLayout *containerLayout;
|
||||
FlowWidget *flowWidget;
|
||||
BannerWidget *header;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
QHash<QString, DeckPreviewWidget *> deckWidgets; ///< Deck file path -> preview widget.
|
||||
QHash<QString, VisualDeckStorageFolderDisplayWidget *> subFolderWidgets; ///< Folder path -> subfolder widget.
|
||||
QTimer *reconcileTimer = nullptr; ///< Coalesces proxy change bursts.
|
||||
QStringList lastOrderedFilePaths; ///< The deck order last applied to the flow layout.
|
||||
|
||||
/// Whether a chunked reconcile pass is currently running.
|
||||
bool deckPassActive = false;
|
||||
/// Set when the model changes mid-pass. Discards progress and restarts the scan once
|
||||
/// the current chunk finishes so the pass always converges on the latest model state.
|
||||
bool deckPassRestartRequested = false;
|
||||
/// Whether the first reconcile pass has run to completion at least once.
|
||||
bool initialPassCompleted = false;
|
||||
int deckPassRow = 0; ///< Next proxy row to scan in the active pass.
|
||||
QSet<QString> deckPassPresentPaths; ///< File paths seen so far in the active pass.
|
||||
};
|
||||
|
||||
#endif // VISUAL_DECK_STORAGE_FOLDER_DISPLAY_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFutureWatcher>
|
||||
|
|
@ -33,18 +32,8 @@ struct DeckLoadResult
|
|||
{
|
||||
LoadedDeck deck; ///< The parsed deck.
|
||||
QDateTime lastModified; ///< File modification time at load.
|
||||
QString colorIdentity; ///< WUBRG color identity, computed off the UI thread.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief How long per event loop turn the pending-load drain applies finished loads.
|
||||
*
|
||||
* Applying a load updates the row and wakes up the proxies and views, which is
|
||||
* not free. A time budget instead of a fixed count lets fast machines apply
|
||||
* many loads in one turn while keeping the ui thread responsive everywhere.
|
||||
*/
|
||||
constexpr int LOAD_DRAIN_TIME_BUDGET_MS = 8;
|
||||
|
||||
/**
|
||||
* @brief The path of \a path relative to the deck root, or empty if \a path
|
||||
* is not below it.
|
||||
|
|
@ -119,8 +108,6 @@ DeckScanResult scanDeckDirectory(const QString &deckPath)
|
|||
}
|
||||
} // namespace
|
||||
|
||||
static QString computeColorIdentity(const LoadedDeck &deck);
|
||||
|
||||
VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
|
@ -195,22 +182,12 @@ const LoadedDeck &VisualDeckStorageModel::deckForRow(int row) const
|
|||
|
||||
int VisualDeckStorageModel::rowForFilePath(const QString &filePath) const
|
||||
{
|
||||
return rowByFilePath.value(filePath, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Rebuilds the file path -> row index from scratch after bulk changes.
|
||||
*/
|
||||
void VisualDeckStorageModel::reindexFilePaths()
|
||||
{
|
||||
rowByFilePath.clear();
|
||||
for (int row = 0; row < decks.size(); ++row) {
|
||||
const QString &filePath = decks.at(row).filePath;
|
||||
// First row wins, mirroring what a linear scan would return for duplicates.
|
||||
if (!rowByFilePath.contains(filePath)) {
|
||||
rowByFilePath.insert(filePath, row);
|
||||
for (int i = 0; i < decks.size(); ++i) {
|
||||
if (decks.at(i).filePath == filePath) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void VisualDeckStorageModel::startScan()
|
||||
|
|
@ -218,9 +195,7 @@ void VisualDeckStorageModel::startScan()
|
|||
++scanGeneration;
|
||||
beginResetModel();
|
||||
decks.clear();
|
||||
rowByFilePath.clear();
|
||||
folderPaths.clear();
|
||||
pendingLoads.clear();
|
||||
endResetModel();
|
||||
|
||||
if (deckPath.isEmpty()) {
|
||||
|
|
@ -249,7 +224,6 @@ void VisualDeckStorageModel::startScan()
|
|||
|
||||
beginInsertRows(QModelIndex(), 0, result.decks.size() - 1);
|
||||
decks = result.decks;
|
||||
reindexFilePaths();
|
||||
endInsertRows();
|
||||
|
||||
for (int row = 0; row < decks.size(); ++row) {
|
||||
|
|
@ -283,20 +257,26 @@ void VisualDeckStorageModel::beginLoad(int row)
|
|||
return; // The deck list was re-scanned while this load was running; drop the stale result.
|
||||
}
|
||||
|
||||
// Queue the result and apply a bounded number per event loop turn so that
|
||||
// finishing hundreds of loads at once cannot stall the UI thread.
|
||||
const std::optional<DeckLoadResult> result = watcher->result();
|
||||
PendingDeckLoad pending;
|
||||
pending.filePath = filePath;
|
||||
pending.generation = generation;
|
||||
if (result) {
|
||||
pending.ok = true;
|
||||
pending.deck = std::move(result->deck);
|
||||
pending.lastModified = result->lastModified;
|
||||
pending.colorIdentity = std::move(result->colorIdentity);
|
||||
const int row = rowForFilePath(filePath);
|
||||
if (row == -1) {
|
||||
return;
|
||||
}
|
||||
pendingLoads.append(std::move(pending));
|
||||
schedulePendingLoadDrain();
|
||||
|
||||
DeckPreviewData &data = decks[row];
|
||||
data.loadInProgress = false;
|
||||
|
||||
std::optional<DeckLoadResult> result = watcher->result();
|
||||
if (!result) {
|
||||
return; // Leave the row unloaded; it stays visible but without deck data.
|
||||
}
|
||||
|
||||
data.deck = std::move(result->deck);
|
||||
data.loadSucceeded = true;
|
||||
data.lastModified = result->lastModified;
|
||||
recomputeDeckMetadata(data);
|
||||
|
||||
emit dataChanged(index(row), index(row));
|
||||
emit deckLoaded(row);
|
||||
});
|
||||
|
||||
watcher->setFuture(QtConcurrent::run([filePath, fmt]() -> std::optional<DeckLoadResult> {
|
||||
|
|
@ -304,69 +284,10 @@ void VisualDeckStorageModel::beginLoad(int row)
|
|||
if (!deck) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// Color identity walks every card through the database, so compute it here to
|
||||
// keep the completion handler on the UI thread cheap.
|
||||
const QString colorIdentity = computeColorIdentity(*deck);
|
||||
return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity};
|
||||
return DeckLoadResult{*deck, QFileInfo(filePath).lastModified()};
|
||||
}));
|
||||
}
|
||||
|
||||
void VisualDeckStorageModel::schedulePendingLoadDrain()
|
||||
{
|
||||
if (drainScheduled) {
|
||||
return;
|
||||
}
|
||||
drainScheduled = true;
|
||||
QMetaObject::invokeMethod(this, &VisualDeckStorageModel::drainPendingLoads, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void VisualDeckStorageModel::drainPendingLoads()
|
||||
{
|
||||
drainScheduled = false;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
while (!pendingLoads.isEmpty()) {
|
||||
PendingDeckLoad pending = pendingLoads.takeFirst();
|
||||
|
||||
if (pending.generation != scanGeneration) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int row = rowForFilePath(pending.filePath);
|
||||
if (row == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DeckPreviewData &data = decks[row];
|
||||
data.loadInProgress = false;
|
||||
|
||||
if (!pending.ok) {
|
||||
continue; // Leave the row unloaded so it stays visible without deck data.
|
||||
}
|
||||
|
||||
data.deck = std::move(pending.deck);
|
||||
data.loadSucceeded = true;
|
||||
data.lastModified = pending.lastModified;
|
||||
recomputeDeckMetadata(data, false);
|
||||
data.colorIdentity = std::move(pending.colorIdentity);
|
||||
|
||||
emit dataChanged(index(row), index(row));
|
||||
emit deckLoaded(row);
|
||||
|
||||
// Checked after applying at least one load, so a single slow application
|
||||
// still makes progress instead of starving the queue.
|
||||
if (timer.elapsed() >= LOAD_DRAIN_TIME_BUDGET_MS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingLoads.isEmpty()) {
|
||||
schedulePendingLoadDrain();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the color identity of a deck in WUBRG order.
|
||||
*/
|
||||
|
|
@ -404,7 +325,7 @@ static QString computeColorIdentity(const LoadedDeck &deck)
|
|||
/**
|
||||
* @brief Recomputes all derived metadata of a row from its loaded deck.
|
||||
*/
|
||||
void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity)
|
||||
void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data)
|
||||
{
|
||||
const DeckList &deckList = data.deck.deckList;
|
||||
|
||||
|
|
@ -413,9 +334,7 @@ void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool r
|
|||
data.tags = deckList.getTags();
|
||||
data.lastLoaded = QDateTime::fromString(deckList.getLastLoadedTimestamp());
|
||||
data.bannerCard = deckList.getBannerCard();
|
||||
if (recomputeColorIdentity) {
|
||||
data.colorIdentity = computeColorIdentity(data.deck);
|
||||
}
|
||||
data.colorIdentity = computeColorIdentity(data.deck);
|
||||
}
|
||||
|
||||
void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePath)
|
||||
|
|
@ -425,13 +344,9 @@ void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePa
|
|||
}
|
||||
|
||||
DeckPreviewData &data = decks[row];
|
||||
rowByFilePath.remove(data.filePath);
|
||||
data.filePath = newFilePath;
|
||||
data.relativeFilePath = relativeFilePathFor(newFilePath, deckPath);
|
||||
data.folderPath = folderPathFor(newFilePath, deckPath);
|
||||
if (!rowByFilePath.contains(newFilePath)) {
|
||||
rowByFilePath.insert(newFilePath, row);
|
||||
}
|
||||
}
|
||||
|
||||
bool VisualDeckStorageModel::renameDeck(int row, const QString &newName)
|
||||
|
|
@ -495,14 +410,7 @@ bool VisualDeckStorageModel::deleteFile(int row)
|
|||
}
|
||||
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
rowByFilePath.remove(filePath);
|
||||
decks.removeAt(row);
|
||||
// Rows after the deleted one shift down by one.
|
||||
for (auto it = rowByFilePath.begin(); it != rowByFilePath.end(); ++it) {
|
||||
if (it.value() > row) {
|
||||
--it.value();
|
||||
}
|
||||
}
|
||||
endRemoveRows();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
|
@ -39,14 +38,7 @@ enum
|
|||
LastModifiedRole, /**< QDateTime of the deck file's last modification. */
|
||||
LastLoadedRole, /**< QDateTime when the deck was last loaded from the file. */
|
||||
BannerCardNameRole, /**< Name of the deck's banner card. */
|
||||
BannerCardProviderIdRole, /**< Provider id of the deck's banner card. */
|
||||
/**
|
||||
* @brief Whether the row passes the proxy's current search / tag / color filters.
|
||||
*
|
||||
* Not served by this model, but by VisualDeckStorageSortFilterProxyModel on top
|
||||
* of it. Declared here so every role read through a proxy index stays unique.
|
||||
*/
|
||||
FilterMatchRole
|
||||
BannerCardProviderIdRole /**< Provider id of the deck's banner card. */
|
||||
};
|
||||
} // namespace VisualDeckStorageRoles
|
||||
|
||||
|
|
@ -73,25 +65,11 @@ struct DeckPreviewData
|
|||
bool loadInProgress = false; ///< Whether the deck file is currently being loaded.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief One finished background deck load that has not been applied to the model yet.
|
||||
*/
|
||||
struct PendingDeckLoad
|
||||
{
|
||||
QString filePath; ///< Identifies the row the result belongs to.
|
||||
int generation; ///< Scan generation the load was started in.
|
||||
bool ok = false; ///< Whether the file parsed successfully.
|
||||
LoadedDeck deck; ///< The parsed deck, valid when ok.
|
||||
QDateTime lastModified; ///< File modification time at load, valid when ok.
|
||||
QString colorIdentity; ///< WUBRG color identity computed off the UI thread, valid when ok.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The list model backing the Visual Deck Storage widget tree.
|
||||
*
|
||||
* Rows are in filesystem scan order; ordering and filtering are handled by
|
||||
* VisualDeckStorageSortFilterProxyModel on top of this model. The proxy keeps
|
||||
* every row and exposes each row's filter result through its FilterMatchRole.
|
||||
* VisualDeckStorageSortFilterProxyModel on top of this model.
|
||||
*/
|
||||
class VisualDeckStorageModel : public QAbstractListModel
|
||||
{
|
||||
|
|
@ -166,23 +144,13 @@ signals:
|
|||
private:
|
||||
void startScan();
|
||||
void beginLoad(int row);
|
||||
static void recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity = true);
|
||||
void reindexFilePaths();
|
||||
void schedulePendingLoadDrain();
|
||||
|
||||
private slots:
|
||||
void drainPendingLoads();
|
||||
|
||||
private:
|
||||
static void recomputeDeckMetadata(DeckPreviewData &data);
|
||||
void setFilePathForRow(int row, const QString &newFilePath);
|
||||
|
||||
QString deckPath;
|
||||
QList<DeckPreviewData> decks;
|
||||
QHash<QString, int> rowByFilePath; ///< Maps each deck's file path to its row for O(1) lookups.
|
||||
QStringList folderPaths; ///< All subdirectories of the deck folder, sorted.
|
||||
int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored.
|
||||
QVector<PendingDeckLoad> pendingLoads; ///< Finished background loads waiting to be applied.
|
||||
bool drainScheduled = false; ///< Whether a queued drain pass is already pending.
|
||||
QStringList folderPaths; ///< All subdirectories of the deck folder, sorted.
|
||||
int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored.
|
||||
};
|
||||
|
||||
#endif // VISUAL_DECK_STORAGE_MODEL_H
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "visual_deck_storage_quick_settings_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
#include "visual_deck_storage_search_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../filters/deck_filter_string.h"
|
||||
#include "../../../filters/syntax_help.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QTimer>
|
||||
#include <QFileInfo>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
/**
|
||||
* @brief Constructs a search bar for filtering decks in the Visual Deck Storage.
|
||||
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
|
||||
*
|
||||
* Provides a search bar that allows users to search decks by filename or search
|
||||
* expression, with a debounced timer to trigger the search after the user stops typing.
|
||||
* This widget provides a search bar that allows users to search for cards by either their set name
|
||||
* or set code. It uses a debounced timer to trigger the search action after the user stops typing.
|
||||
*
|
||||
* @param parent The parent widget.
|
||||
* @param parent The parent PrintingSelector widget that will handle the search results.
|
||||
*/
|
||||
VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) : QWidget(parent)
|
||||
VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent) : parent(parent)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
|
@ -37,5 +40,47 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) :
|
|||
searchDebounceTimer->start(300); // 300ms debounce
|
||||
});
|
||||
|
||||
connect(searchDebounceTimer, &QTimer::timeout, this, [this] { emit searchTextChanged(searchBar->text()); });
|
||||
connect(searchDebounceTimer, &QTimer::timeout, parent, &VisualDeckStorageWidget::updateSearchFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieves the current text in the search bar.
|
||||
*
|
||||
* @return The text entered by the user in the search bar.
|
||||
*/
|
||||
QString VisualDeckStorageSearchWidget::getSearchText()
|
||||
{
|
||||
return searchBar->text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the filepath into a relative filepath starting from the deck folder.
|
||||
* If the file isn't in the deck folder, then this will just return the filename.
|
||||
*
|
||||
* @param filePath The filepath to convert into a relative filepath
|
||||
*/
|
||||
static QString toRelativeFilepath(const QString &filePath)
|
||||
{
|
||||
QString deckPath = SettingsCache::instance().paths().getDeckPath();
|
||||
if (filePath.startsWith(deckPath)) {
|
||||
return filePath.mid(deckPath.length());
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString fileName = fileInfo.fileName();
|
||||
return fileName;
|
||||
}
|
||||
|
||||
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText)
|
||||
{
|
||||
const auto filterString = DeckFilterString(searchText);
|
||||
|
||||
for (auto widget : widgets) {
|
||||
const DeckSearchData searchData{.deck = &widget->deckLoader->getDeck(),
|
||||
.filePath = widget->filePath,
|
||||
.displayName = widget->getDisplayName(),
|
||||
.relativeFilePath = toRelativeFilepath(widget->filePath)};
|
||||
|
||||
widget->filteredBySearch = !filterString.check(searchData);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,32 +2,30 @@
|
|||
* @file visual_deck_storage_search_widget.h
|
||||
* @ingroup VisualDeckStorageWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef VISUAL_DECK_STORAGE_SEARCH_WIDGET_H
|
||||
#define VISUAL_DECK_STORAGE_SEARCH_WIDGET_H
|
||||
|
||||
#include "deck_preview/deck_preview_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QWidget>
|
||||
|
||||
class QTimer;
|
||||
|
||||
class VisualDeckStorageWidget;
|
||||
class VisualDeckStorageSearchWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit VisualDeckStorageSearchWidget(QWidget *parent);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted once the debounce timer fires after the user stopped typing.
|
||||
* @param text The current contents of the search bar.
|
||||
*/
|
||||
void searchTextChanged(const QString &text);
|
||||
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
|
||||
QString getSearchText();
|
||||
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText);
|
||||
|
||||
private:
|
||||
QHBoxLayout *layout;
|
||||
VisualDeckStorageWidget *parent;
|
||||
QLineEdit *searchBar;
|
||||
QTimer *searchDebounceTimer;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -104,21 +104,12 @@ void VisualDeckStorageSortFilterProxyModel::resort()
|
|||
sort(0);
|
||||
}
|
||||
|
||||
QVariant VisualDeckStorageSortFilterProxyModel::data(const QModelIndex &index, int role) const
|
||||
bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
if (role == VisualDeckStorageRoles::FilterMatchRole) {
|
||||
if (!index.isValid()) {
|
||||
return true;
|
||||
}
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
return rowMatches(sourceIndex.row());
|
||||
if (sourceParent.isValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return QSortFilterProxyModel::data(index, role);
|
||||
}
|
||||
|
||||
bool VisualDeckStorageSortFilterProxyModel::rowMatches(int sourceRow) const
|
||||
{
|
||||
// If the match lists aren't sized to the current model yet, don't hide anything.
|
||||
if (sourceRow < 0 || sourceRow >= searchMatches.size() || sourceRow >= tagMatches.size() ||
|
||||
sourceRow >= colorMatches.size()) {
|
||||
|
|
@ -128,14 +119,6 @@ bool VisualDeckStorageSortFilterProxyModel::rowMatches(int sourceRow) const
|
|||
return searchMatches.at(sourceRow) && tagMatches.at(sourceRow) && colorMatches.at(sourceRow);
|
||||
}
|
||||
|
||||
bool VisualDeckStorageSortFilterProxyModel::filterAcceptsRow(int /*sourceRow*/,
|
||||
const QModelIndex & /*sourceParent*/) const
|
||||
{
|
||||
// Rows are never dropped: the filter result is exposed per row through
|
||||
// FilterMatchRole, so views can keep their widgets alive and just hide them.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VisualDeckStorageSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
|
||||
{
|
||||
const auto *source = deckSourceModel();
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
* Owns all search / tag / color filter state and the sort order. Filtering is
|
||||
* evaluated against the model's data (never against widgets), so it can run
|
||||
* before any view exists and re-evaluate whenever deck data finishes loading.
|
||||
*
|
||||
* Rows are never removed by filtering. Instead, every row carries the
|
||||
* FilterMatchRole, which views read to show or hide their widgets while keeping
|
||||
* them alive; all rows stay in the proxy so they keep their sorted position.
|
||||
*/
|
||||
|
||||
#ifndef VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H
|
||||
|
|
@ -51,8 +47,6 @@ public:
|
|||
|
||||
explicit VisualDeckStorageSortFilterProxyModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
void setSourceModel(QAbstractItemModel *model) override;
|
||||
|
||||
/// @name Filter input setters (each re-evaluates the affected matches)
|
||||
|
|
@ -83,7 +77,6 @@ protected:
|
|||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool rowMatches(int sourceRow) const;
|
||||
void resizeMatchLists();
|
||||
void updateSearchMatches();
|
||||
void updateTagMatches();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
#include "visual_deck_storage_sort_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent) : QWidget(parent)
|
||||
/**
|
||||
* @brief Constructs a PrintingSelectorCardSortWidget for searching cards by set name or set code.
|
||||
*
|
||||
* This widget provides a search bar that allows users to search for cards by either their set name
|
||||
* or set code. It uses a debounced timer to trigger the search action after the user stops typing.
|
||||
*
|
||||
* @param parent The parent PrintingSelector widget that will handle the search results.
|
||||
*/
|
||||
VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent)
|
||||
: parent(parent), sortOrder(Alphabetical)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
|
@ -21,10 +30,12 @@ VisualDeckStorageSortWidget::VisualDeckStorageSortWidget(VisualDeckStorageWidget
|
|||
|
||||
// Set the current sort order
|
||||
sortComboBox->setCurrentIndex(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSortingOrder());
|
||||
sortOrder = static_cast<SortOrder>(sortComboBox->currentIndex());
|
||||
|
||||
// Connect sorting change signal to persist the order and refresh the file list
|
||||
// Connect sorting change signal to refresh the file list
|
||||
connect(sortComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&VisualDeckStorageSortWidget::updateSortOrder);
|
||||
connect(this, &VisualDeckStorageSortWidget::sortOrderChanged, parent, &VisualDeckStorageWidget::updateSortOrder);
|
||||
}
|
||||
|
||||
void VisualDeckStorageSortWidget::retranslateUi()
|
||||
|
|
@ -36,13 +47,10 @@ void VisualDeckStorageSortWidget::retranslateUi()
|
|||
|
||||
// Clear and repopulate the ComboBox with translated items
|
||||
sortComboBox->clear();
|
||||
sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"),
|
||||
VisualDeckStorageSortFilterProxyModel::SortOrder::ByName);
|
||||
sortComboBox->addItem(tr("Sort Alphabetically (Filename)"),
|
||||
VisualDeckStorageSortFilterProxyModel::SortOrder::Alphabetical);
|
||||
sortComboBox->addItem(tr("Sort by Last Modified"),
|
||||
VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastModified);
|
||||
sortComboBox->addItem(tr("Sort by Last Loaded"), VisualDeckStorageSortFilterProxyModel::SortOrder::ByLastLoaded);
|
||||
sortComboBox->addItem(tr("Sort Alphabetically (Deck Name)"), ByName);
|
||||
sortComboBox->addItem(tr("Sort Alphabetically (Filename)"), Alphabetical);
|
||||
sortComboBox->addItem(tr("Sort by Last Modified"), ByLastModified);
|
||||
sortComboBox->addItem(tr("Sort by Last Loaded"), ByLastLoaded);
|
||||
|
||||
// Restore the current index
|
||||
sortComboBox->setCurrentIndex(oldIndex);
|
||||
|
|
@ -51,13 +59,60 @@ void VisualDeckStorageSortWidget::retranslateUi()
|
|||
sortComboBox->blockSignals(false);
|
||||
}
|
||||
|
||||
VisualDeckStorageSortFilterProxyModel::SortOrder VisualDeckStorageSortWidget::currentSortOrder() const
|
||||
{
|
||||
return static_cast<VisualDeckStorageSortFilterProxyModel::SortOrder>(sortComboBox->currentIndex());
|
||||
}
|
||||
|
||||
void VisualDeckStorageSortWidget::updateSortOrder()
|
||||
{
|
||||
sortOrder = static_cast<SortOrder>(sortComboBox->currentIndex());
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageSortingOrder(sortComboBox->currentIndex());
|
||||
emit sortOrderChanged();
|
||||
}
|
||||
|
||||
void VisualDeckStorageSortWidget::sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget)
|
||||
{
|
||||
auto children =
|
||||
folderWidget->getFlowWidget()->findChildren<QWidget *>(QString(), Qt::FindChildOption::FindDirectChildrenOnly);
|
||||
for (auto widget : children) {
|
||||
auto deckPreviewWidgets =
|
||||
widget->findChildren<DeckPreviewWidget *>(QString(), Qt::FindChildOption::FindDirectChildrenOnly);
|
||||
auto newOrder = filterFiles(deckPreviewWidgets);
|
||||
for (DeckPreviewWidget *previewWidget : newOrder) {
|
||||
folderWidget->getFlowWidget()->removeWidget(previewWidget);
|
||||
}
|
||||
for (DeckPreviewWidget *previewWidget : newOrder) {
|
||||
folderWidget->getFlowWidget()->addWidget(previewWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QList<DeckPreviewWidget *> VisualDeckStorageSortWidget::filterFiles(QList<DeckPreviewWidget *> widgets)
|
||||
{
|
||||
// Sort the widgets list based on the current sort order
|
||||
std::sort(widgets.begin(), widgets.end(), [this](DeckPreviewWidget *widget1, DeckPreviewWidget *widget2) {
|
||||
if (!widget1 || !widget2) {
|
||||
return false; // Handle null pointers gracefully
|
||||
}
|
||||
|
||||
QFileInfo info1(widget1->filePath);
|
||||
QFileInfo info2(widget2->filePath);
|
||||
|
||||
switch (sortOrder) {
|
||||
case ByName:
|
||||
return widget1->deckLoader->getDeck().deckList.getName() <
|
||||
widget2->deckLoader->getDeck().deckList.getName();
|
||||
case Alphabetical:
|
||||
return QString::localeAwareCompare(info1.fileName(), info2.fileName()) <= 0;
|
||||
case ByLastModified:
|
||||
return info1.lastModified() > info2.lastModified();
|
||||
case ByLastLoaded: {
|
||||
QDateTime time1 =
|
||||
QDateTime::fromString(widget1->deckLoader->getDeck().deckList.getLastLoadedTimestamp());
|
||||
QDateTime time2 =
|
||||
QDateTime::fromString(widget2->deckLoader->getDeck().deckList.getLastLoadedTimestamp());
|
||||
return time1 > time2;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Default case, no sorting applied
|
||||
});
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@
|
|||
* @file visual_deck_storage_sort_widget.h
|
||||
* @ingroup VisualDeckStorageWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef VISUAL_DECK_STORAGE_SORT_WIDGET_H
|
||||
#define VISUAL_DECK_STORAGE_SORT_WIDGET_H
|
||||
|
||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
class VisualDeckStorageWidget;
|
||||
class VisualDeckStorageFolderDisplayWidget;
|
||||
class VisualDeckStorageSortWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -20,23 +22,25 @@ class VisualDeckStorageSortWidget : public QWidget
|
|||
public:
|
||||
explicit VisualDeckStorageSortWidget(VisualDeckStorageWidget *parent);
|
||||
void retranslateUi();
|
||||
|
||||
/**
|
||||
* @brief The currently selected sort order.
|
||||
*/
|
||||
[[nodiscard]] VisualDeckStorageSortFilterProxyModel::SortOrder currentSortOrder() const;
|
||||
void updateSortOrder();
|
||||
void sortFolder(VisualDeckStorageFolderDisplayWidget *folderWidget);
|
||||
QString getSearchText();
|
||||
QList<DeckPreviewWidget *> filterFiles(QList<DeckPreviewWidget *> widgets);
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted when the user picks a different sort order.
|
||||
*/
|
||||
void sortOrderChanged();
|
||||
|
||||
private slots:
|
||||
void updateSortOrder();
|
||||
|
||||
private:
|
||||
enum SortOrder
|
||||
{
|
||||
ByName,
|
||||
Alphabetical,
|
||||
ByLastModified,
|
||||
ByLastLoaded,
|
||||
};
|
||||
QHBoxLayout *layout;
|
||||
VisualDeckStorageWidget *parent;
|
||||
SortOrder sortOrder; // Current sorting option
|
||||
QComboBox *sortComboBox;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "deck_preview/deck_preview_tag_display_widget.h"
|
||||
#include "visual_deck_storage_model.h"
|
||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -20,7 +18,7 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto
|
|||
|
||||
setFixedHeight(100);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
auto *flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
|
||||
layout->addWidget(flowWidget);
|
||||
}
|
||||
|
|
@ -31,26 +29,45 @@ void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event)
|
|||
refreshTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The tags of all decks currently accepted by the proxy model.
|
||||
*/
|
||||
QSet<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
|
||||
void VisualDeckStorageTagFilterWidget::filterDecksBySelectedTags(const QList<DeckPreviewWidget *> &deckPreviews) const
|
||||
{
|
||||
QSet<QString> allTags;
|
||||
auto *proxy = parent->proxyModel();
|
||||
QStringList selectedTags;
|
||||
QStringList excludedTags;
|
||||
|
||||
for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) {
|
||||
const QModelIndex index = proxy->index(proxyRow, 0);
|
||||
if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) {
|
||||
continue;
|
||||
}
|
||||
const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList();
|
||||
for (const QString &tag : deckTags) {
|
||||
allTags.insert(tag);
|
||||
// Collect selected and excluded tags
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
||||
switch (tagWidget->getState()) {
|
||||
case TagState::Selected:
|
||||
selectedTags.append(tagWidget->getTagName());
|
||||
break;
|
||||
case TagState::Excluded:
|
||||
excludedTags.append(tagWidget->getTagName());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allTags;
|
||||
// If no tags are selected or excluded, show all
|
||||
if (selectedTags.isEmpty() && excludedTags.isEmpty()) {
|
||||
for (DeckPreviewWidget *deckPreview : deckPreviews) {
|
||||
deckPreview->filteredByTags = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (DeckPreviewWidget *deckPreview : deckPreviews) {
|
||||
QStringList deckTags = deckPreview->deckLoader->getDeck().deckList.getTags();
|
||||
|
||||
bool hasAllSelected = std::all_of(selectedTags.begin(), selectedTags.end(),
|
||||
[&deckTags](const QString &tag) { return deckTags.contains(tag); });
|
||||
|
||||
bool hasAnyExcluded = std::any_of(excludedTags.begin(), excludedTags.end(),
|
||||
[&deckTags](const QString &tag) { return deckTags.contains(tag); });
|
||||
|
||||
// Filter out if any excluded tag is present or if any selected tag is missing
|
||||
deckPreview->filteredByTags = !(hasAllSelected && !hasAnyExcluded);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::refreshTags()
|
||||
|
|
@ -63,6 +80,8 @@ void VisualDeckStorageTagFilterWidget::refreshTags()
|
|||
|
||||
void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet<QString> &tags)
|
||||
{
|
||||
auto *flowWidget = findChild<FlowWidget *>();
|
||||
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
||||
const QString &tagName = tagWidget->getTagName();
|
||||
|
||||
|
|
@ -97,12 +116,20 @@ void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag)
|
|||
auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag);
|
||||
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent,
|
||||
&VisualDeckStorageWidget::updateTagFilter);
|
||||
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this,
|
||||
&VisualDeckStorageTagFilterWidget::refreshTags);
|
||||
auto *flowWidget = findChild<FlowWidget *>();
|
||||
flowWidget->addWidget(newTagWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageTagFilterWidget::sortTags()
|
||||
{
|
||||
auto *flowWidget = findChild<FlowWidget *>();
|
||||
if (!flowWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all tag widgets
|
||||
QList<DeckPreviewTagDisplayWidget *> tagWidgets = findChildren<DeckPreviewTagDisplayWidget *>();
|
||||
|
||||
|
|
@ -120,26 +147,19 @@ void VisualDeckStorageTagFilterWidget::sortTags()
|
|||
}
|
||||
}
|
||||
|
||||
QStringList VisualDeckStorageTagFilterWidget::selectedTags() const
|
||||
QSet<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
|
||||
{
|
||||
QStringList selected;
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
||||
if (tagWidget->getState() == TagState::Selected) {
|
||||
selected.append(tagWidget->getTagName());
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
QSet<QString> allTags;
|
||||
QList<DeckPreviewWidget *> deckWidgets = parent->findChildren<DeckPreviewWidget *>();
|
||||
|
||||
QStringList VisualDeckStorageTagFilterWidget::excludedTags() const
|
||||
{
|
||||
QStringList excluded;
|
||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
||||
if (tagWidget->getState() == TagState::Excluded) {
|
||||
excluded.append(tagWidget->getTagName());
|
||||
for (DeckPreviewWidget *widget : deckWidgets) {
|
||||
if (widget->checkVisibility()) {
|
||||
for (const QString &tag : widget->deckLoader->getDeck().deckList.getTags()) {
|
||||
allTags.insert(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
return allTags;
|
||||
}
|
||||
|
||||
QStringList VisualDeckStorageTagFilterWidget::getAllKnownTags() const
|
||||
|
|
|
|||
|
|
@ -2,22 +2,21 @@
|
|||
* @file visual_deck_storage_tag_filter_widget.h
|
||||
* @ingroup VisualDeckStorageWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H
|
||||
#define VISUAL_DECK_STORAGE_TAG_FILTER_WIDGET_H
|
||||
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include "deck_preview/deck_preview_widget.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class FlowWidget;
|
||||
class VisualDeckStorageWidget;
|
||||
class VisualDeckStorageTagFilterWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
VisualDeckStorageWidget *parent;
|
||||
FlowWidget *flowWidget;
|
||||
|
||||
[[nodiscard]] QSet<QString> gatherAllTags() const;
|
||||
void removeTagsNotInList(const QSet<QString> &tags);
|
||||
|
|
@ -28,21 +27,9 @@ class VisualDeckStorageTagFilterWidget : public QWidget
|
|||
public:
|
||||
explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent);
|
||||
[[nodiscard]] QStringList getAllKnownTags() const;
|
||||
|
||||
/**
|
||||
* @brief The tags currently in "selected" state.
|
||||
*/
|
||||
[[nodiscard]] QStringList selectedTags() const;
|
||||
|
||||
/**
|
||||
* @brief The tags currently in "excluded" state.
|
||||
*/
|
||||
[[nodiscard]] QStringList excludedTags() const;
|
||||
void filterDecksBySelectedTags(const QList<DeckPreviewWidget *> &deckPreviews) const;
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Rebuilds the tag chips from the tags of the currently visible decks.
|
||||
*/
|
||||
void refreshTags();
|
||||
void showEvent(QShowEvent *event) override;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,29 +2,24 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../quick_settings/settings_button_widget.h"
|
||||
#include "deck_preview/deck_preview_color_identity_filter_widget.h"
|
||||
#include "deck_preview/deck_preview_widget.h"
|
||||
#include "visual_deck_storage_folder_display_widget.h"
|
||||
#include "visual_deck_storage_quick_settings_widget.h"
|
||||
#include "visual_deck_storage_search_widget.h"
|
||||
#include "visual_deck_storage_sort_widget.h"
|
||||
#include "visual_deck_storage_tag_filter_widget.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
#include <QComboBox>
|
||||
#include <QDirIterator>
|
||||
#include <QMouseEvent>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent)
|
||||
VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent), folderWidget(nullptr)
|
||||
{
|
||||
// The model and proxy own all deck data, sorting and filtering. The view widgets below
|
||||
// only display the proxy's rows and their FilterMatchRole, so nothing touches the
|
||||
// filesystem outside the model.
|
||||
storageModel = new VisualDeckStorageModel(this);
|
||||
storageProxyModel = new VisualDeckStorageSortFilterProxyModel(this);
|
||||
storageProxyModel->setSourceModel(storageModel);
|
||||
deckListModel = new DeckListModel(this);
|
||||
deckListModel->setObjectName("visualDeckModel");
|
||||
|
||||
layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(0);
|
||||
|
|
@ -80,33 +75,6 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
layout->addWidget(tagFilterWidget);
|
||||
layout->addWidget(scrollArea);
|
||||
|
||||
// The deck data changed (a load finished, or a mutation happened): re-evaluate the filters,
|
||||
// since search/tag/color matches are computed against the data. Debounced so that a burst of
|
||||
// load completions triggers a single re-application instead of one O(n) pass per deck.
|
||||
refreshTimer = new QTimer(this);
|
||||
refreshTimer->setSingleShot(true);
|
||||
refreshTimer->setInterval(150);
|
||||
connect(refreshTimer, &QTimer::timeout, this, [this] {
|
||||
storageProxyModel->reapplyFilters();
|
||||
// A batch of decks finished loading: re-gather the tag chips from the visible decks once
|
||||
// the burst settles instead of on every individual load.
|
||||
tagFilterWidget->refreshTags();
|
||||
});
|
||||
connect(storageModel, &QAbstractItemModel::dataChanged, this, [this] { refreshTimer->start(); });
|
||||
connect(storageModel, &VisualDeckStorageModel::deckLoaded, this, [this] { refreshTimer->start(); });
|
||||
// A deck's file path changed: re-apply the sort, since orders like "filename" depend on it.
|
||||
connect(storageModel, &VisualDeckStorageModel::deckFilePathChanged, this, [this] { storageProxyModel->resort(); });
|
||||
connect(sortWidget, &VisualDeckStorageSortWidget::sortOrderChanged, this,
|
||||
&VisualDeckStorageWidget::updateSortOrder);
|
||||
// The filter widgets only own their ui state. Pushing it into the proxy model
|
||||
// happens here, so the children stay decoupled from the model layer.
|
||||
connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this,
|
||||
&VisualDeckStorageWidget::updateColorFilter);
|
||||
connect(deckPreviewColorIdentityFilterWidget, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this,
|
||||
&VisualDeckStorageWidget::updateColorFilter);
|
||||
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
|
||||
&VisualDeckStorageWidget::updateSearchFilter);
|
||||
|
||||
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
|
||||
&VisualDeckStorageWidget::createRootFolderWidget);
|
||||
|
||||
|
|
@ -155,8 +123,6 @@ void VisualDeckStorageWidget::retranslateUi()
|
|||
|
||||
refreshButton->setToolTip(tr("Refresh loaded files"));
|
||||
quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings"));
|
||||
|
||||
sortWidget->retranslateUi();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,69 +134,72 @@ const VisualDeckStorageQuickSettingsWidget *VisualDeckStorageWidget::settings()
|
|||
}
|
||||
|
||||
/**
|
||||
* Reapplies all sort and filter options by updating the proxy model.
|
||||
* Reapplies all sort and filter options by calling the appropriate update methods.
|
||||
*/
|
||||
void VisualDeckStorageWidget::reapplySortAndFilters()
|
||||
{
|
||||
storageProxyModel->setSortOrder(sortWidget->currentSortOrder());
|
||||
storageProxyModel->reapplyFilters();
|
||||
updateSortOrder();
|
||||
updateTagFilter();
|
||||
updateColorFilter();
|
||||
updateSearchFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scans the deck folder and rebuilds the folder tree of deck previews.
|
||||
*/
|
||||
void VisualDeckStorageWidget::createRootFolderWidget()
|
||||
{
|
||||
storageModel->setDeckPath(SettingsCache::instance().paths().getDeckPath());
|
||||
|
||||
folderWidget =
|
||||
new VisualDeckStorageFolderDisplayWidget(this, this, QString(), false, quickSettingsWidget->getShowFolders());
|
||||
folderWidget = new VisualDeckStorageFolderDisplayWidget(this, this, SettingsCache::instance().paths().getDeckPath(),
|
||||
false, quickSettingsWidget->getShowFolders());
|
||||
|
||||
scrollArea->setWidget(folderWidget); // this automatically destroys the old folderWidget
|
||||
scrollArea->widget()->setMaximumWidth(scrollArea->viewport()->width());
|
||||
scrollArea->widget()->adjustSize();
|
||||
|
||||
// Sort and filter runs against the model data, so it is safe to apply immediately.
|
||||
reapplySortAndFilters();
|
||||
/* We have to schedule a QTimer here so that the sorting logic doesn't try to access widgets that haven't been
|
||||
* processed by the event loop yet. Otherwise, deck sorting will intermittently segfault on some systems.
|
||||
*/
|
||||
QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters);
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::updateShowFolders(bool enabled)
|
||||
{
|
||||
if (folderWidget) {
|
||||
folderWidget->updateShowFolders(enabled);
|
||||
QTimer::singleShot(0, this, &VisualDeckStorageWidget::reapplySortAndFilters);
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::updateSortOrder()
|
||||
{
|
||||
storageProxyModel->setSortOrder(sortWidget->currentSortOrder());
|
||||
if (folderWidget) {
|
||||
sortWidget->sortFolder(folderWidget);
|
||||
for (VisualDeckStorageFolderDisplayWidget *subFolderWidget :
|
||||
folderWidget->findChildren<VisualDeckStorageFolderDisplayWidget *>()) {
|
||||
sortWidget->sortFolder(subFolderWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::updateTagFilter()
|
||||
{
|
||||
const QStringList selected = tagFilterWidget->selectedTags();
|
||||
const QStringList excluded = tagFilterWidget->excludedTags();
|
||||
storageProxyModel->setTagFilter(QSet<QString>(selected.cbegin(), selected.cend()),
|
||||
QSet<QString>(excluded.cbegin(), excluded.cend()));
|
||||
// The visible deck set changed, so the chips are re-gathered from it.
|
||||
tagFilterWidget->refreshTags();
|
||||
if (folderWidget) {
|
||||
tagFilterWidget->filterDecksBySelectedTags(folderWidget->findChildren<DeckPreviewWidget *>());
|
||||
folderWidget->updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the color identity filter widget's state into the proxy model.
|
||||
*/
|
||||
void VisualDeckStorageWidget::updateColorFilter()
|
||||
{
|
||||
storageProxyModel->setColorFilter(deckPreviewColorIdentityFilterWidget->getFilterMode(),
|
||||
deckPreviewColorIdentityFilterWidget->getActiveColors());
|
||||
if (folderWidget) {
|
||||
deckPreviewColorIdentityFilterWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>());
|
||||
folderWidget->updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the search bar's text into the proxy model.
|
||||
*/
|
||||
void VisualDeckStorageWidget::updateSearchFilter(const QString &text)
|
||||
void VisualDeckStorageWidget::updateSearchFilter()
|
||||
{
|
||||
storageProxyModel->setSearchText(text);
|
||||
if (folderWidget) {
|
||||
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText());
|
||||
folderWidget->updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::updateTagsVisibility(const bool visible)
|
||||
|
|
@ -246,4 +215,4 @@ void VisualDeckStorageWidget::updateTagsVisibility(const bool visible)
|
|||
void VisualDeckStorageWidget::updateSelectionAnimationEnabled(const bool enabled)
|
||||
{
|
||||
deckPreviewSelectionAnimationEnabled = enabled;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,30 +2,29 @@
|
|||
* @file visual_deck_storage_widget.h
|
||||
* @ingroup VisualDeckStorageWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef VISUAL_DECK_STORAGE_WIDGET_H
|
||||
#define VISUAL_DECK_STORAGE_WIDGET_H
|
||||
|
||||
#include "visual_deck_storage_model.h"
|
||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
#include "../quick_settings/settings_button_widget.h"
|
||||
#include "deck_preview/deck_preview_color_identity_filter_widget.h"
|
||||
#include "visual_deck_storage_folder_display_widget.h"
|
||||
#include "visual_deck_storage_quick_settings_widget.h"
|
||||
#include "visual_deck_storage_search_widget.h"
|
||||
#include "visual_deck_storage_sort_widget.h"
|
||||
#include "visual_deck_storage_tag_filter_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
|
||||
class QLabel;
|
||||
class QResizeEvent;
|
||||
class QShowEvent;
|
||||
class QTimer;
|
||||
class DeckPreviewColorIdentityFilterWidget;
|
||||
class VisualDeckStorageFolderDisplayWidget;
|
||||
class VisualDeckStorageQuickSettingsWidget;
|
||||
class QSpinBox;
|
||||
class VisualDeckStorageSearchWidget;
|
||||
class VisualDeckStorageSortWidget;
|
||||
class VisualDeckStorageTagFilterWidget;
|
||||
|
||||
class VisualDeckStorageFolderDisplayWidget;
|
||||
class DeckPreviewColorIdentityFilterWidget;
|
||||
class VisualDeckStorageWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -38,43 +37,29 @@ public:
|
|||
bool deckPreviewSelectionAnimationEnabled;
|
||||
|
||||
[[nodiscard]] const VisualDeckStorageQuickSettingsWidget *settings() const;
|
||||
[[nodiscard]] VisualDeckStorageModel *model() const
|
||||
{
|
||||
return storageModel;
|
||||
}
|
||||
[[nodiscard]] VisualDeckStorageSortFilterProxyModel *proxyModel() const
|
||||
{
|
||||
return storageProxyModel;
|
||||
}
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Starts scanning the deck folder and rebuilds the folder tree and previews.
|
||||
*/
|
||||
void createRootFolderWidget();
|
||||
void createRootFolderWidget(); // Refresh the display of cards based on the current sorting option
|
||||
void updateShowFolders(bool enabled);
|
||||
void updateTagFilter();
|
||||
void updateColorFilter();
|
||||
void updateSearchFilter();
|
||||
void updateTagsVisibility(bool visible);
|
||||
void updateSelectionAnimationEnabled(bool enabled);
|
||||
void updateSortOrder();
|
||||
void updateTagFilter();
|
||||
void updateColorFilter();
|
||||
void updateSearchFilter(const QString &text);
|
||||
|
||||
signals:
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
private:
|
||||
void reapplySortAndFilters();
|
||||
signals:
|
||||
void bannerCardsRefreshed();
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
QWidget *searchAndSortContainer;
|
||||
QHBoxLayout *searchAndSortLayout;
|
||||
DeckListModel *deckListModel;
|
||||
QLabel *databaseLoadIndicator;
|
||||
VisualDeckStorageSortWidget *sortWidget;
|
||||
VisualDeckStorageSearchWidget *searchWidget;
|
||||
|
|
@ -82,10 +67,9 @@ private:
|
|||
QToolButton *refreshButton;
|
||||
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
|
||||
QScrollArea *scrollArea;
|
||||
VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr;
|
||||
VisualDeckStorageModel *storageModel = nullptr;
|
||||
VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr;
|
||||
QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads.
|
||||
VisualDeckStorageFolderDisplayWidget *folderWidget;
|
||||
|
||||
void reapplySortAndFilters();
|
||||
};
|
||||
|
||||
#endif // VISUAL_DECK_STORAGE_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -69,14 +69,3 @@ void AppearanceSettings::setHomeTabDisplayCardName(bool _displayCardName)
|
|||
setValue(_displayCardName, "homeTabDisplayCardName");
|
||||
emit homeTabDisplayCardNameChanged();
|
||||
}
|
||||
|
||||
int AppearanceSettings::getHomeTabButtonColorSourceIndex() const
|
||||
{
|
||||
return getValue("homeTabButtonColorSource", "", "", 0).toInt();
|
||||
}
|
||||
|
||||
void AppearanceSettings::setHomeTabButtonColorSourceIndex(int index)
|
||||
{
|
||||
setValue(index, "homeTabButtonColorSource");
|
||||
emit homeTabButtonColorChanged();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ public:
|
|||
void setHomeTabBackgroundShuffleFrequency(int _frequency);
|
||||
[[nodiscard]] bool getHomeTabDisplayCardName() const;
|
||||
void setHomeTabDisplayCardName(bool _displayCardName);
|
||||
[[nodiscard]] int getHomeTabButtonColorSourceIndex() const;
|
||||
void setHomeTabButtonColorSourceIndex(int index);
|
||||
|
||||
signals:
|
||||
void themeNameChanged();
|
||||
|
|
@ -36,7 +34,6 @@ signals:
|
|||
void homeTabBackgroundSourceChanged();
|
||||
void homeTabBackgroundShuffleFrequencyChanged();
|
||||
void homeTabDisplayCardNameChanged();
|
||||
void homeTabButtonColorChanged();
|
||||
|
||||
public:
|
||||
explicit AppearanceSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue