New visual deck storage (#5290)

* Add TabDeckStorageVisual

* Visual Deck Storage

* Add BannerCard to .cod format, use it in the visual deck storage widget.

* Show filename instead of deckname if deck name is empty.

* Lint.

* Don't delint cmake list through hooks.

* Add deck loading functionality.

* Open Decks on double click, not single click.

* Void event for now.

* Fix build issue with overload?

* Fix build issue with overload?

* Include QDebug.

* Turn the tab into a widget.

* Move the signals down to the widget, move the connections and slots up to the parent widgets.

* No banner card equals an empty CardInfoPtr.

* Add an option to sort by filename or last modified.

* Flip last modified comparison.

* Lint.

* Don't open decks twice in the storage tab.

* Fix unload deck not working by showing/hiding widgets instead of adding/removing to layout.

* Add a search bar.

* Add a card size slider.

* Lint.

* Lint.

* Lint.

* Fix settings mocks.

* No need to QDebug.

* No need to QDebug.

* Member variable.

* Member variable.

* Non-lambda.

* Change set to list conversion.

* Specify overload.

* Include MouseEvent

* Adjust font size dynamically.

* Add an option to show the visual deck storage on database load.

* Fix the close button not working on the tab, add an option to launch the visual deck storage tab to Cockatrice menu.

* Override virtual functions.

* Correct tab text.

* Add a setting to remember last used sorting order for visual deck storage widget.

* Update banner card combo box correctly.

* Fix mocks.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: Zach H <zahalpern+github@gmail.com>
This commit is contained in:
BruebachL 2025-01-06 00:12:20 +01:00 committed by GitHub
parent 7496e79e8c
commit 62f7c7f9ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 834 additions and 31 deletions

View file

@ -96,16 +96,12 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
// Call the base class's paintEvent to draw the card image
CardInfoPictureWidget::paintEvent(event);
// Now add the custom text overlay on top of the image
// If no overlay text, skip drawing the text
if (overlayText.isEmpty()) {
return;
}
QStylePainter painter(this);
// Set text properties
QFont font = painter.font();
font.setPointSize(fontSize);
painter.setFont(font);
QStylePainter painter(this);
// Get the pixmap from the base class using the getter
const QPixmap &pixmap = getResizedPixmap();
@ -118,13 +114,34 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
const QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
const QRect pixmapRect(topLeft, scaledSize);
// Prepare text wrapping
const QFontMetrics fontMetrics(font);
const int lineHeight = fontMetrics.height();
const int textWidth = pixmapRect.width();
QString wrappedText;
// Calculate the optimal font size
QFont font = painter.font();
int optimalFontSize = fontSize; // Start with the user-defined font size
const QFontMetrics baseMetrics(font);
int textWidth = pixmapRect.width();
// Break the text into multiple lines to fit within the pixmap width
// Reduce the font size until the text fits within the pixmap's width
do {
font.setPointSize(optimalFontSize);
QFontMetrics fm(font);
int currentWidth = 0;
for (const QString &word : overlayText.split(' ')) {
currentWidth = std::max(currentWidth, fm.horizontalAdvance(word));
}
if (currentWidth <= textWidth) {
break;
}
--optimalFontSize;
} while (optimalFontSize > 1);
// Apply the calculated font size
painter.setFont(font);
// Wrap the text to fit within the pixmap width
const QFontMetrics fontMetrics(font);
QString wrappedText;
QString currentLine;
QStringList words = overlayText.split(' ');
for (const QString &word : words) {
@ -141,13 +158,22 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
wrappedText += currentLine;
// Calculate total text block height
const int totalTextHeight = static_cast<int>(wrappedText.count('\n')) * lineHeight + lineHeight;
int totalTextHeight = wrappedText.count('\n') * fontMetrics.height() + fontMetrics.height();
// Adjust font size if the total text height exceeds the pixmap height
while (totalTextHeight > pixmapRect.height() && optimalFontSize > 1) {
--optimalFontSize;
font.setPointSize(optimalFontSize);
painter.setFont(font);
const QFontMetrics newMetrics(font);
totalTextHeight = wrappedText.count('\n') * newMetrics.height() + newMetrics.height();
}
// Set up the text layout options
QTextOption textOption;
textOption.setAlignment(textAlignment);
// Create a text rectangle centered within the pixmap rect
// Create a text rectangle centered vertically within the pixmap rect
auto textRect = QRect(pixmapRect.left(), pixmapRect.top(), pixmapRect.width(), totalTextHeight);
textRect.moveTop((pixmapRect.height() - totalTextHeight) / 2 + pixmapRect.top());
@ -169,7 +195,6 @@ void CardInfoPictureWithTextOverlayWidget::drawOutlinedText(QPainter &painter,
const QString &text,
const QTextOption &textOption) const
{
// Draw the black outline (outlineColor)
painter.setPen(outlineColor);
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
@ -180,7 +205,7 @@ void CardInfoPictureWithTextOverlayWidget::drawOutlinedText(QPainter &painter,
}
}
// Draw the main text (textColor)
// Draw the main text
painter.setPen(textColor);
painter.drawText(textRect, text, textOption);
}

View file

@ -7,7 +7,7 @@
#include <QSize>
#include <QTextOption>
class CardInfoPictureWithTextOverlayWidget final : public CardInfoPictureWidget
class CardInfoPictureWithTextOverlayWidget : public CardInfoPictureWidget
{
Q_OBJECT

View file

@ -1,6 +1,8 @@
#include "card_size_widget.h"
#include "../../../../settings/cache_settings.h"
#include "../printing_selector/printing_selector.h"
#include "../visual_deck_storage/visual_deck_storage_widget.h"
/**
* @class CardSizeWidget
@ -40,7 +42,12 @@ CardSizeWidget::CardSizeWidget(QWidget *parent, FlowWidget *flowWidget, int defa
*/
void CardSizeWidget::updateCardSizeSetting(int newValue)
{
SettingsCache::instance().setPrintingSelectorCardSize(newValue);
// Check the type of the parent widget
if ((parent = qobject_cast<PrintingSelector *>(parentWidget()))) {
SettingsCache::instance().setPrintingSelectorCardSize(newValue);
} else if ((parent = qobject_cast<VisualDeckStorageWidget *>(parentWidget()))) {
SettingsCache::instance().setVisualDeckStorageCardSize(newValue);
}
}
/**

View file

@ -15,11 +15,11 @@ class CardSizeWidget : public QWidget
public:
explicit CardSizeWidget(QWidget *parent, FlowWidget *flowWidget = nullptr, int defaultValue = 100);
[[nodiscard]] QSlider *getSlider() const;
QWidget *parent;
public slots:
static void updateCardSizeSetting(int newValue);
void updateCardSizeSetting(int newValue);
private:
QWidget *parent;
FlowWidget *flowWidget;
QHBoxLayout *cardSizeLayout;
QLabel *cardSizeLabel;

View file

@ -0,0 +1,53 @@
#include "deck_preview_card_picture_widget.h"
#include <QApplication>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QPainterPath>
#include <QStylePainter>
#include <QTextOption>
/**
* @brief Constructs a CardPictureWithTextOverlay widget.
* @param parent The parent widget.
* @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
* @param textColor The color of the overlay text.
* @param outlineColor The color of the outline around the text.
* @param fontSize The font size of the overlay text.
* @param alignment The alignment of the text within the overlay.
*
* Sets the widget's size policy and default border style.
*/
DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
const bool hoverToZoomEnabled,
const QColor &textColor,
const QColor &outlineColor,
const int fontSize,
const Qt::Alignment alignment)
: CardInfoPictureWithTextOverlayWidget(parent, hoverToZoomEnabled, textColor, outlineColor, fontSize, alignment)
{
singleClickTimer = new QTimer(this);
singleClickTimer->setSingleShot(true);
connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); });
}
void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
lastMouseEvent = event;
singleClickTimer->start(QApplication::doubleClickInterval());
}
}
void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
singleClickTimer->stop(); // Prevent single-click logic
emit imageDoubleClicked(lastMouseEvent, this);
}
}
void DeckPreviewCardPictureWidget::setFilePath(const QString &_filePath)
{
filePath = _filePath;
}

View file

@ -0,0 +1,40 @@
#ifndef DECK_PREVIEW_CARD_PICTURE_WIDGET_H
#define DECK_PREVIEW_CARD_PICTURE_WIDGET_H
#include "card_info_picture_with_text_overlay_widget.h"
#include <QColor>
#include <QSize>
#include <QTextOption>
class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlayWidget
{
Q_OBJECT
public:
explicit DeckPreviewCardPictureWidget(QWidget *parent = nullptr,
bool hoverToZoomEnabled = false,
const QColor &textColor = Qt::white,
const QColor &outlineColor = Qt::black,
int fontSize = 12,
Qt::Alignment alignment = Qt::AlignCenter);
QString filePath;
signals:
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
public slots:
void setFilePath(const QString &filePath);
private:
QTimer *singleClickTimer;
QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
};
#endif // DECK_PREVIEW_CARD_PICTURE_WIDGET_H

View file

@ -0,0 +1,58 @@
#include "visual_deck_storage_search_widget.h"
/**
* @brief Constructs a PrintingSelectorCardSearchWidget 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.
*/
VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent) : parent(parent)
{
layout = new QHBoxLayout(this);
setLayout(layout);
searchBar = new QLineEdit(this);
searchBar->setPlaceholderText(tr("Search by filename"));
layout->addWidget(searchBar);
// Add a debounce timer for the search bar to limit frequent updates
searchDebounceTimer = new QTimer(this);
searchDebounceTimer->setSingleShot(true);
connect(searchBar, &QLineEdit::textChanged, this, [this]() {
searchDebounceTimer->start(300); // 300ms debounce
});
connect(searchDebounceTimer, &QTimer::timeout, parent, &VisualDeckStorageWidget::refreshBannerCards);
}
/**
* @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();
}
QStringList VisualDeckStorageSearchWidget::filterFiles(const QStringList &files, const QString &searchText)
{
if (searchText.isEmpty() || searchText.isNull()) {
return files;
}
QStringList filteredFiles;
for (const auto &file : files) {
QFileInfo fileInfo(file);
QString fileName = fileInfo.fileName().toLower();
if (fileName.contains(searchText.toLower())) {
filteredFiles << file;
}
}
return filteredFiles;
}

View file

@ -0,0 +1,28 @@
#ifndef VISUAL_DECK_STORAGE_SEARCH_WIDGET_H
#define VISUAL_DECK_STORAGE_SEARCH_WIDGET_H
#include "visual_deck_storage_widget.h"
#include <QHBoxLayout>
#include <QLineEdit>
#include <QTimer>
#include <QWidget>
class VisualDeckStorageWidget;
class VisualDeckStorageSearchWidget : public QWidget
{
Q_OBJECT
public:
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
QString getSearchText();
QStringList filterFiles(const QStringList &files, const QString &searchText);
private:
QHBoxLayout *layout;
VisualDeckStorageWidget *parent;
QLineEdit *searchBar;
QTimer *searchDebounceTimer;
};
#endif // VISUAL_DECK_STORAGE_SEARCH_WIDGET_H

View file

@ -0,0 +1,121 @@
#include "visual_deck_storage_widget.h"
#include "../../../../deck/deck_loader.h"
#include "../../../../game/cards/card_database_manager.h"
#include "../../../../settings/cache_settings.h"
#include "visual_deck_storage_search_widget.h"
#include <QComboBox>
#include <QDirIterator>
#include <QMouseEvent>
#include <QVBoxLayout>
VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(parent), sortOrder(Alphabetical)
{
deckListModel = new DeckListModel(this);
deckListModel->setObjectName("visualDeckModel");
layout = new QVBoxLayout();
setLayout(layout);
// ComboBox for sorting options
sortComboBox = new QComboBox(this);
sortComboBox->addItem("Sort Alphabetically (Filename)", Alphabetical);
sortComboBox->addItem("Sort by Last Modified", ByLastModified);
sortComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageSortingOrder());
searchWidget = new VisualDeckStorageSearchWidget(this);
// Add combo box to the main layout
layout->addWidget(sortComboBox);
layout->addWidget(searchWidget);
flowWidget = new FlowWidget(this, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
layout->addWidget(flowWidget);
cardSizeWidget = new CardSizeWidget(this, flowWidget, SettingsCache::instance().getVisualDeckStorageCardSize());
layout->addWidget(cardSizeWidget);
// Connect sorting change signal to refresh the file list
connect(sortComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&VisualDeckStorageWidget::updateSortOrder);
}
void VisualDeckStorageWidget::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
updateSortOrder();
}
void VisualDeckStorageWidget::updateSortOrder()
{
sortOrder = static_cast<SortOrder>(sortComboBox->currentData().toInt());
SettingsCache::instance().setVisualDeckStorageSortingOrder(sortComboBox->currentData().toInt());
refreshBannerCards(); // Refresh the banner cards with the new sort order
}
void VisualDeckStorageWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
{
emit imageClicked(event, instance);
}
void VisualDeckStorageWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
{
emit imageDoubleClicked(event, instance);
}
void VisualDeckStorageWidget::refreshBannerCards()
{
QStringList allFiles;
// QDirIterator with QDir::Files and QDir::NoSymLinks ensures only files are listed (no directories or symlinks)
QDirIterator it(SettingsCache::instance().getDeckPath(), QDir::Files | QDir::NoSymLinks,
QDirIterator::Subdirectories);
while (it.hasNext()) {
allFiles << it.next(); // Add each file path to the list
}
// Sort files based on the current sort order
std::sort(allFiles.begin(), allFiles.end(), [this](const QString &file1, const QString &file2) {
QFileInfo info1(file1);
QFileInfo info2(file2);
switch (sortOrder) {
case Alphabetical:
return info1.fileName().toLower() < info2.fileName().toLower();
case ByLastModified:
return info1.lastModified() > info2.lastModified();
}
return false; // Default case
});
auto filteredFiles = searchWidget->filterFiles(allFiles, searchWidget->getSearchText());
flowWidget->clearLayout(); // Clear existing widgets in the flow layout
foreach (const QString &file, filteredFiles) {
auto deckLoader = new DeckLoader();
deckLoader->loadFromFile(file, DeckLoader::CockatriceFormat);
deckListModel->setDeckList(new DeckLoader(*deckLoader));
auto *display = new DeckPreviewCardPictureWidget(flowWidget, false);
auto bannerCard = deckLoader->getBannerCard().isEmpty()
? CardInfoPtr()
: CardDatabaseManager::getInstance()->getCard(deckLoader->getBannerCard());
display->setCard(bannerCard);
display->setOverlayText(deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName()
: deckLoader->getName());
display->setFontSize(24);
display->setFilePath(deckLoader->getLastFileName());
connect(display, &DeckPreviewCardPictureWidget::imageClicked, this,
&VisualDeckStorageWidget::imageClickedEvent);
connect(display, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
&VisualDeckStorageWidget::imageDoubleClickedEvent);
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, display, &CardInfoPictureWidget::setScaleFactor);
display->setScaleFactor(cardSizeWidget->getSlider()->value());
flowWidget->addWidget(display);
}
}

View file

@ -0,0 +1,51 @@
#ifndef VISUAL_DECK_STORAGE_WIDGET_H
#define VISUAL_DECK_STORAGE_WIDGET_H
#include "../../../../deck/deck_list_model.h"
#include "../../../../deck/deck_view.h"
#include "../../../ui/widgets/cards/deck_preview_card_picture_widget.h"
#include "../../../ui/widgets/general/layout_containers/flow_widget.h"
#include "../cards/card_size_widget.h"
#include "visual_deck_storage_search_widget.h"
#include <QComboBox>
#include <QFileSystemModel>
class VisualDeckStorageSearchWidget;
class VisualDeckStorageWidget final : public QWidget
{
Q_OBJECT
public:
explicit VisualDeckStorageWidget(QWidget *parent);
void retranslateUi();
public slots:
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void refreshBannerCards(); // Refresh the display of cards based on the current sorting option
void showEvent(QShowEvent *event) override;
void updateSortOrder();
signals:
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
private:
enum SortOrder
{
Alphabetical,
ByLastModified
};
QVBoxLayout *layout;
FlowWidget *flowWidget;
DeckListModel *deckListModel;
QMap<QString, DeckViewCardContainer *> cardContainers;
SortOrder sortOrder; // Current sorting option
QComboBox *sortComboBox;
VisualDeckStorageSearchWidget *searchWidget;
CardSizeWidget *cardSizeWidget;
};
#endif // VISUAL_DECK_STORAGE_WIDGET_H

View file

@ -290,6 +290,11 @@ void MainWindow::actDeckEditor()
tabSupervisor->addDeckEditorTab(nullptr);
}
void MainWindow::actVisualDeckStorage()
{
tabSupervisor->addVisualDeckStorageTab();
}
void MainWindow::actFullScreen(bool checked)
{
if (checked)
@ -660,6 +665,7 @@ void MainWindow::retranslateUi()
aSinglePlayer->setText(tr("Start &local game..."));
aWatchReplay->setText(tr("&Watch replay..."));
aDeckEditor->setText(tr("&Deck editor"));
aVisualDeckStorage->setText(tr("&Visual Deck storage"));
aFullScreen->setText(tr("&Full screen"));
aRegister->setText(tr("&Register to server..."));
aForgotPassword->setText(tr("&Restore password..."));
@ -707,6 +713,8 @@ void MainWindow::createActions()
connect(aWatchReplay, SIGNAL(triggered()), this, SLOT(actWatchReplay()));
aDeckEditor = new QAction(this);
connect(aDeckEditor, SIGNAL(triggered()), this, SLOT(actDeckEditor()));
aVisualDeckStorage = new QAction(this);
connect(aVisualDeckStorage, SIGNAL(triggered()), this, SLOT(actVisualDeckStorage()));
aFullScreen = new QAction(this);
aFullScreen->setCheckable(true);
connect(aFullScreen, SIGNAL(toggled(bool)), this, SLOT(actFullScreen(bool)));
@ -794,6 +802,7 @@ void MainWindow::createMenus()
cockatriceMenu->addAction(aWatchReplay);
cockatriceMenu->addSeparator();
cockatriceMenu->addAction(aDeckEditor);
cockatriceMenu->addAction(aVisualDeckStorage);
cockatriceMenu->addSeparator();
cockatriceMenu->addAction(aFullScreen);
cockatriceMenu->addSeparator();
@ -881,6 +890,11 @@ MainWindow::MainWindow(QWidget *parent)
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
refreshShortcuts();
if (SettingsCache::instance().getVisualDeckStorageShowOnLoad()) {
connect(CardDatabaseManager::getInstance(), SIGNAL(cardDatabaseLoadingFinished()), tabSupervisor,
SLOT(addVisualDeckStorageTab()));
}
connect(CardDatabaseManager::getInstance(), SIGNAL(cardDatabaseLoadingFailed()), this,
SLOT(cardDatabaseLoadingFailed()));
connect(CardDatabaseManager::getInstance(), SIGNAL(cardDatabaseNewSetsFound(int, QStringList)), this,

View file

@ -74,6 +74,7 @@ private slots:
void actSinglePlayer();
void actWatchReplay();
void actDeckEditor();
void actVisualDeckStorage();
void actFullScreen(bool checked);
void actRegister();
void actSettings();
@ -131,10 +132,11 @@ private:
QList<QMenu *> tabMenus;
QMenu *cockatriceMenu, *dbMenu, *helpMenu, *trayIconMenu;
QAction *aConnect, *aDisconnect, *aSinglePlayer, *aWatchReplay, *aDeckEditor, *aFullScreen, *aSettings, *aExit,
*aAbout, *aTips, *aCheckCardUpdates, *aRegister, *aForgotPassword, *aUpdate, *aViewLog, *aManageSets,
*aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet, *aReloadCardDatabase, *aShow,
*aOpenSettingsFolder;
QAction *aConnect, *aDisconnect, *aSinglePlayer, *aWatchReplay, *aDeckEditor, *aVisualDeckStorage, *aFullScreen,
*aSettings, *aExit, *aAbout, *aTips, *aCheckCardUpdates, *aRegister, *aForgotPassword, *aUpdate, *aViewLog,
*aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet, *aReloadCardDatabase,
*aShow, *aOpenSettingsFolder;
TabSupervisor *tabSupervisor;
WndSets *wndSets;
RemoteClient *client;