mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-16 07:22:16 -07:00
Merge branch 'master' into cccolors
This commit is contained in:
commit
f022ee793c
78 changed files with 2299 additions and 1203 deletions
|
|
@ -67,7 +67,10 @@ set(cockatrice_SOURCES
|
|||
src/client/ui/line_edit_completer.cpp
|
||||
src/client/ui/phases_toolbar.cpp
|
||||
src/client/ui/picture_loader/picture_loader.cpp
|
||||
src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
|
||||
src/client/ui/picture_loader/picture_loader_status_bar.cpp
|
||||
src/client/ui/picture_loader/picture_loader_worker.cpp
|
||||
src/client/ui/picture_loader/picture_loader_worker_work.cpp
|
||||
src/client/ui/picture_loader/picture_to_load.cpp
|
||||
src/client/ui/pixel_map_generator.cpp
|
||||
src/client/ui/theme_manager.cpp
|
||||
|
|
@ -190,6 +193,7 @@ set(cockatrice_SOURCES
|
|||
src/game/cards/card_search_model.cpp
|
||||
src/game/deckview/deck_view.cpp
|
||||
src/game/deckview/deck_view_container.cpp
|
||||
src/game/filters/deck_filter_string.cpp
|
||||
src/game/filters/filter_builder.cpp
|
||||
src/game/filters/filter_card.cpp
|
||||
src/game/filters/filter_string.cpp
|
||||
|
|
|
|||
|
|
@ -379,5 +379,6 @@
|
|||
<file>resources/tips/tips_of_the_day.xml</file>
|
||||
|
||||
<file>resources/help/search.md</file>
|
||||
<file>resources/help/deck_search.md</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
|||
|
|
@ -60,4 +60,6 @@
|
|||
|
||||
# pixel_map_generator = false
|
||||
|
||||
# deck_filter_string = false
|
||||
# filter_string = false
|
||||
# syntax_help = false
|
||||
|
|
|
|||
40
cockatrice/resources/help/deck_search.md
Normal file
40
cockatrice/resources/help/deck_search.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
## Deck Search Syntax Help
|
||||
-----
|
||||
The search bar recognizes a set of special commands.<br>
|
||||
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all
|
||||
searches are case insensitive.
|
||||
<dl>
|
||||
<dt>Display Name (The deck name, or the filename if the deck name isn't set):</dt>
|
||||
<dd>[red deck wins](#red deck wins) <small>(Any deck with a display name containing the words red, deck, and wins)</small></dd>
|
||||
<dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck with a display name containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt>Deck <u>N</u>ame:</dt>
|
||||
<dd>[n:aggro](#n:aggro) <small>(Any deck with a name containing the word aggro)</small></dd>
|
||||
<dd>[n:red n:deck n:wins](#n:red n:deck n:wins) <small>(Any deck with a name containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[n:"red deck wins"](#n:%22red deck wins%22) <small>(Any deck with a name containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt><u>F</u>ile Name:</dt>
|
||||
<dd>[f:aggro](#f:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
|
||||
<dd>[f:red f:deck f:wins](#f:red f:deck f:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
|
||||
<dd>[f:"red deck wins"](#f:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt>Relative <u>P</u>ath (starting from the deck folder):</dt>
|
||||
<dd>[p:aggro](#p:aggro) <small>(Any deck that has "aggro" somewhere in its relative path)</small></dd>
|
||||
<dd>[p:edh/](#p:edh/) <small>(Any deck with "edh/" in its relative path, A.K.A. decks in the "edh" folder)</small></dd>
|
||||
|
||||
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
|
||||
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
|
||||
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>
|
||||
<dd><a href="#[[t:legendary]]>5">[[t:legendary]]>5</a> <small>(Any card that contains at least 5 legendaries)</small></dd>
|
||||
<dd><a href="#[[]]:100">[[]]:100</a> <small>(Any deck that contains exactly 100 cards)</small></dd>
|
||||
|
||||
<dt>Negate:</dt>
|
||||
<dd>[soldier -aggro](#soldier -aggro) <small>(Any deck filename that contains "soldier", but not "aggro")</small></dd>
|
||||
|
||||
<dt>Branching:</dt>
|
||||
<dd>[t:aggro OR o:control](#t:aggro OR o:control) <small>(Any deck filename that contains either aggro or control)</small></dd>
|
||||
|
||||
<dt>Grouping:</dt>
|
||||
<dd><a href="#red -([[]]:100 or aggro)">red -([[]]:100 or aggro)</a> <small>(Any deck that has red in its filename but is not 100 cards or has aggro in its filename)</small></dd>
|
||||
|
||||
</dl>
|
||||
|
|
@ -58,4 +58,9 @@ In this list of examples below, each entry has an explanation and can be clicked
|
|||
<dt>Grouping:</dt>
|
||||
<dd><a href="#t:angel -(angel or c:w)">t:angel -(angel or c:w)</a> <small>(Any angel that doesn't have angel in its name and isn't white)</small></dd>
|
||||
|
||||
<dt>Regular Expression:</dt>
|
||||
<dd>[/^fell/](#/^fell/) <small>(Any card name that begins with "fell")</small></dd>
|
||||
<dd>[o:/counter target .* spell/](#o:/counter target .* spell/) <small>(Any card text with "counter target *something* spell")</small></dd>
|
||||
<dd>[o:/for each .* and\/or .*/](#o:/for each .* and\/or .*/) <small>(/'s can be escaped with a \)</small></dd>
|
||||
|
||||
</dl>
|
||||
|
|
|
|||
|
|
@ -1538,7 +1538,7 @@ void TabGame::createPlayAreaWidget(bool bReplay)
|
|||
scene = new GameScene(phasesToolbar, this);
|
||||
gameView = new GameView(scene);
|
||||
|
||||
gamePlayAreaVBox = new QVBoxLayout;
|
||||
auto gamePlayAreaVBox = new QVBoxLayout;
|
||||
gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0);
|
||||
gamePlayAreaVBox->addWidget(gameView);
|
||||
|
||||
|
|
@ -1594,12 +1594,12 @@ void TabGame::createReplayDock()
|
|||
connect(replayFastForwardButton, &QToolButton::toggled, this, &TabGame::replayFastForwardButtonToggled);
|
||||
|
||||
// putting everything together
|
||||
replayControlLayout = new QHBoxLayout;
|
||||
auto replayControlLayout = new QHBoxLayout;
|
||||
replayControlLayout->addWidget(timelineWidget, 10);
|
||||
replayControlLayout->addWidget(replayPlayButton);
|
||||
replayControlLayout->addWidget(replayFastForwardButton);
|
||||
|
||||
replayControlWidget = new QWidget();
|
||||
auto replayControlWidget = new QWidget();
|
||||
replayControlWidget->setObjectName("replayControlWidget");
|
||||
replayControlWidget->setLayout(replayControlLayout);
|
||||
|
||||
|
|
@ -1635,13 +1635,13 @@ void TabGame::createCardInfoDock(bool bReplay)
|
|||
Q_UNUSED(bReplay);
|
||||
|
||||
cardInfoFrameWidget = new CardInfoFrameWidget();
|
||||
cardHInfoLayout = new QHBoxLayout;
|
||||
cardVInfoLayout = new QVBoxLayout;
|
||||
auto cardHInfoLayout = new QHBoxLayout;
|
||||
auto cardVInfoLayout = new QVBoxLayout;
|
||||
cardVInfoLayout->setContentsMargins(0, 0, 0, 0);
|
||||
cardVInfoLayout->addWidget(cardInfoFrameWidget);
|
||||
cardVInfoLayout->addLayout(cardHInfoLayout);
|
||||
|
||||
cardBoxLayoutWidget = new QWidget;
|
||||
auto cardBoxLayoutWidget = new QWidget;
|
||||
cardBoxLayoutWidget->setLayout(cardVInfoLayout);
|
||||
|
||||
cardInfoDock = new QDockWidget(this);
|
||||
|
|
@ -1679,6 +1679,22 @@ void TabGame::createPlayerListDock(bool bReplay)
|
|||
|
||||
void TabGame::createMessageDock(bool bReplay)
|
||||
{
|
||||
auto messageLogLayout = new QVBoxLayout;
|
||||
messageLogLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// clock
|
||||
if (!bReplay) {
|
||||
timeElapsedLabel = new QLabel;
|
||||
timeElapsedLabel->setAlignment(Qt::AlignCenter);
|
||||
gameTimer = new QTimer(this);
|
||||
gameTimer->setInterval(1000);
|
||||
connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime);
|
||||
gameTimer->start();
|
||||
|
||||
messageLogLayout->addWidget(timeElapsedLabel);
|
||||
}
|
||||
|
||||
// message log
|
||||
messageLog = new MessageLogWidget(tabSupervisor, this);
|
||||
connect(messageLog, &MessageLogWidget::cardNameHovered, cardInfoFrameWidget,
|
||||
qOverload<const QString &>(&CardInfoFrameWidget::setCard));
|
||||
|
|
@ -1690,14 +1706,12 @@ void TabGame::createMessageDock(bool bReplay)
|
|||
connect(messageLog, &MessageLogWidget::addMentionTag, this, &TabGame::addMentionTag);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::chatMentionCompleterChanged, this,
|
||||
&TabGame::actCompleterChanged);
|
||||
}
|
||||
|
||||
timeElapsedLabel = new QLabel;
|
||||
timeElapsedLabel->setAlignment(Qt::AlignCenter);
|
||||
gameTimer = new QTimer(this);
|
||||
gameTimer->setInterval(1000);
|
||||
connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime);
|
||||
gameTimer->start();
|
||||
messageLogLayout->addWidget(messageLog);
|
||||
|
||||
// chat entry
|
||||
if (!bReplay) {
|
||||
sayLabel = new QLabel;
|
||||
sayEdit = new LineEditCompleter;
|
||||
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
||||
|
|
@ -1726,20 +1740,15 @@ void TabGame::createMessageDock(bool bReplay)
|
|||
connect(tabSupervisor, &TabSupervisor::adminLockChanged, this, &TabGame::adminLockChanged);
|
||||
connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabGame::actSay);
|
||||
|
||||
sayHLayout = new QHBoxLayout;
|
||||
auto sayHLayout = new QHBoxLayout;
|
||||
sayHLayout->addWidget(sayLabel);
|
||||
sayHLayout->addWidget(sayEdit);
|
||||
|
||||
messageLogLayout->addLayout(sayHLayout);
|
||||
}
|
||||
|
||||
messageLogLayout = new QVBoxLayout;
|
||||
messageLogLayout->setContentsMargins(0, 0, 0, 0);
|
||||
if (!bReplay)
|
||||
messageLogLayout->addWidget(timeElapsedLabel);
|
||||
messageLogLayout->addWidget(messageLog);
|
||||
if (!bReplay)
|
||||
messageLogLayout->addLayout(sayHLayout);
|
||||
|
||||
messageLogLayoutWidget = new QWidget;
|
||||
// dock
|
||||
auto messageLogLayoutWidget = new QWidget;
|
||||
messageLogLayoutWidget->setLayout(messageLogLayout);
|
||||
|
||||
messageLayoutDock = new QDockWidget(this);
|
||||
|
|
|
|||
|
|
@ -111,10 +111,8 @@ private:
|
|||
GameScene *scene;
|
||||
GameView *gameView;
|
||||
QMap<int, DeckViewContainer *> deckViewContainers;
|
||||
QVBoxLayout *cardVInfoLayout, *messageLogLayout, *gamePlayAreaVBox, *deckViewContainerLayout;
|
||||
QHBoxLayout *cardHInfoLayout, *sayHLayout, *mainHLayout, *replayControlLayout;
|
||||
QWidget *cardBoxLayoutWidget, *messageLogLayoutWidget, *gamePlayAreaWidget, *deckViewContainerWidget,
|
||||
*replayControlWidget;
|
||||
QVBoxLayout *deckViewContainerLayout;
|
||||
QWidget *gamePlayAreaWidget, *deckViewContainerWidget;
|
||||
QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock;
|
||||
QAction *playersSeparator;
|
||||
QMenu *gameMenu, *viewMenu, *cardInfoDockMenu, *messageLayoutDockMenu, *playerListDockMenu, *replayDockMenu;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@
|
|||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QMainWindow>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
#include <QScreen>
|
||||
#include <QStatusBar>
|
||||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
|
@ -27,6 +29,16 @@ PictureLoader::PictureLoader() : QObject(nullptr)
|
|||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
|
||||
|
||||
connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded);
|
||||
|
||||
statusBar = new PictureLoaderStatusBar(nullptr);
|
||||
QMainWindow *mainWindow = qobject_cast<QMainWindow *>(QApplication::activeWindow());
|
||||
if (mainWindow) {
|
||||
mainWindow->statusBar()->addPermanentWidget(statusBar);
|
||||
}
|
||||
|
||||
connect(worker, &PictureLoaderWorker::imageLoadQueued, statusBar, &PictureLoaderStatusBar::addQueuedImageLoad);
|
||||
connect(worker, &PictureLoaderWorker::imageLoadSuccessful, statusBar,
|
||||
&PictureLoaderStatusBar::addSuccessfulImageLoad);
|
||||
}
|
||||
|
||||
PictureLoader::~PictureLoader()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define PICTURELOADER_H
|
||||
|
||||
#include "../../../game/cards/card_info.h"
|
||||
#include "picture_loader_status_bar.h"
|
||||
#include "picture_loader_worker.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
|
@ -27,6 +28,7 @@ private:
|
|||
void operator=(PictureLoader const &);
|
||||
|
||||
PictureLoaderWorker *worker;
|
||||
PictureLoaderStatusBar *statusBar;
|
||||
|
||||
public:
|
||||
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
#include "picture_loader_request_status_display_widget.h"
|
||||
|
||||
PictureLoaderRequestStatusDisplayWidget::PictureLoaderRequestStatusDisplayWidget(QWidget *parent,
|
||||
const QUrl &_url,
|
||||
PictureLoaderWorkerWork *worker)
|
||||
: QWidget(parent)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
|
||||
if (worker->cardToDownload.getCard()) {
|
||||
name = new QLabel(this);
|
||||
name->setText(worker->cardToDownload.getCard()->getName());
|
||||
setShortname = new QLabel(this);
|
||||
setShortname->setText(worker->cardToDownload.getSetName());
|
||||
providerId = new QLabel(this);
|
||||
providerId->setText(worker->cardToDownload.getCard()->getProperty("uuid"));
|
||||
|
||||
layout->addWidget(name);
|
||||
layout->addWidget(setShortname);
|
||||
layout->addWidget(providerId);
|
||||
}
|
||||
|
||||
startTime = new QLabel(QDateTime::currentDateTime().toString(), this);
|
||||
elapsedTime = new QLabel("0", this);
|
||||
finished = new QLabel("False", this);
|
||||
url = new QLabel(_url.toString(), this);
|
||||
|
||||
layout->addWidget(startTime);
|
||||
layout->addWidget(elapsedTime);
|
||||
layout->addWidget(finished);
|
||||
layout->addWidget(url);
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#ifndef PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
|
||||
#define PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
|
||||
#include "picture_loader_worker_work.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QWidget>
|
||||
|
||||
class PictureLoaderRequestStatusDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
PictureLoaderRequestStatusDisplayWidget(QWidget *parent, const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||
PictureLoaderWorkerWork *worker;
|
||||
|
||||
void setFinished()
|
||||
{
|
||||
finished->setText("True");
|
||||
update();
|
||||
repaint();
|
||||
}
|
||||
|
||||
bool getFinished() const
|
||||
{
|
||||
return finished->text() == "True";
|
||||
}
|
||||
|
||||
void setElapsedTime(const QString &_elapsedTime) const
|
||||
{
|
||||
elapsedTime->setText(_elapsedTime);
|
||||
}
|
||||
|
||||
int queryElapsedSeconds()
|
||||
{
|
||||
if (!finished) {
|
||||
int elapsedSeconds = QDateTime::fromString(startTime->text()).secsTo(QDateTime::currentDateTime());
|
||||
elapsedTime->setText(QString::number(elapsedSeconds));
|
||||
update();
|
||||
repaint();
|
||||
return elapsedSeconds;
|
||||
}
|
||||
return elapsedTime->text().toInt();
|
||||
}
|
||||
|
||||
QString getStartTime() const
|
||||
{
|
||||
return startTime->text();
|
||||
}
|
||||
|
||||
QString getUrl() const
|
||||
{
|
||||
return url->text();
|
||||
}
|
||||
|
||||
private:
|
||||
QHBoxLayout *layout;
|
||||
QLabel *name;
|
||||
QLabel *setShortname;
|
||||
QLabel *providerId;
|
||||
QLabel *startTime;
|
||||
QLabel *elapsedTime;
|
||||
QLabel *finished;
|
||||
QLabel *url;
|
||||
};
|
||||
|
||||
#endif // PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#include "picture_loader_status_bar.h"
|
||||
|
||||
#include "picture_loader_request_status_display_widget.h"
|
||||
|
||||
PictureLoaderStatusBar::PictureLoaderStatusBar(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
progressBar = new QProgressBar(this);
|
||||
progressBar->setMaximum(0);
|
||||
progressBar->setFormat("%v/%m");
|
||||
layout->addWidget(progressBar);
|
||||
|
||||
loadLog = new SettingsButtonWidget(this);
|
||||
layout->addWidget(loadLog);
|
||||
|
||||
cleaner = new QTimer(this);
|
||||
cleaner->setInterval(1000);
|
||||
connect(cleaner, &QTimer::timeout, this, &PictureLoaderStatusBar::cleanOldEntries);
|
||||
cleaner->start();
|
||||
|
||||
setLayout(layout);
|
||||
}
|
||||
|
||||
void PictureLoaderStatusBar::cleanOldEntries()
|
||||
{
|
||||
if (!loadLog || !loadLog->popup) {
|
||||
return;
|
||||
}
|
||||
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
|
||||
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
|
||||
statusDisplayWidget->queryElapsedSeconds();
|
||||
if (statusDisplayWidget->getFinished() &&
|
||||
QDateTime::fromString(statusDisplayWidget->getStartTime()).secsTo(QDateTime::currentDateTime()) > 10) {
|
||||
loadLog->removeSettingsWidget(statusDisplayWidget);
|
||||
progressBar->setMaximum(progressBar->maximum() - 1);
|
||||
progressBar->setValue(progressBar->value() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoaderStatusBar::addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||
{
|
||||
loadLog->addSettingsWidget(new PictureLoaderRequestStatusDisplayWidget(loadLog, url, worker));
|
||||
progressBar->setMaximum(progressBar->maximum() + 1);
|
||||
}
|
||||
|
||||
void PictureLoaderStatusBar::addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||
{
|
||||
Q_UNUSED(worker)
|
||||
progressBar->setValue(progressBar->value() + 1);
|
||||
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
|
||||
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
|
||||
if (statusDisplayWidget->getUrl() == url.toString()) {
|
||||
statusDisplayWidget->queryElapsedSeconds();
|
||||
statusDisplayWidget->setFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#ifndef PICTURE_LOADER_STATUS_BAR_H
|
||||
#define PICTURE_LOADER_STATUS_BAR_H
|
||||
|
||||
#include "../widgets/quick_settings/settings_button_widget.h"
|
||||
#include "picture_loader_worker_work.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QProgressBar>
|
||||
#include <QWidget>
|
||||
|
||||
class PictureLoaderStatusBar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PictureLoaderStatusBar(QWidget *parent);
|
||||
|
||||
public slots:
|
||||
void addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||
void addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||
void cleanOldEntries();
|
||||
|
||||
private:
|
||||
QHBoxLayout *layout;
|
||||
QProgressBar *progressBar;
|
||||
SettingsButtonWidget *loadLog;
|
||||
QTimer *cleaner;
|
||||
};
|
||||
|
||||
#endif // PICTURE_LOADER_STATUS_BAR_H
|
||||
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
#include "../../../game/cards/card_database_manager.h"
|
||||
#include "../../../settings/cache_settings.h"
|
||||
#include "picture_loader_worker_work.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDirIterator>
|
||||
#include <QJsonDocument>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
#include <utility>
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||
|
|
@ -19,11 +21,8 @@ PictureLoaderWorker::PictureLoaderWorker()
|
|||
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
|
||||
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
|
||||
{
|
||||
connect(this, &PictureLoaderWorker::startLoadQueue, this, &PictureLoaderWorker::processLoadQueue,
|
||||
Qt::QueuedConnection);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &PictureLoaderWorker::picsPathChanged);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
|
||||
&PictureLoaderWorker::picDownloadChanged);
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
|
||||
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
|
||||
|
||||
|
|
@ -34,18 +33,17 @@ PictureLoaderWorker::PictureLoaderWorker()
|
|||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
networkManager->setTransferTimeout();
|
||||
#endif
|
||||
auto cache = new QNetworkDiskCache(this);
|
||||
cache = new QNetworkDiskCache(this);
|
||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
cache->setMaximumCacheSize(1024L * 1024L *
|
||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
||||
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
|
||||
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, this,
|
||||
[this](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||
networkManager->setCache(cache);
|
||||
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
connect(networkManager, &QNetworkAccessManager::finished, this, &PictureLoaderWorker::picDownloadFinished);
|
||||
|
||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
loadRedirectCache();
|
||||
|
|
@ -57,244 +55,104 @@ PictureLoaderWorker::PictureLoaderWorker()
|
|||
pictureLoaderThread = new QThread;
|
||||
pictureLoaderThread->start(QThread::LowPriority);
|
||||
moveToThread(pictureLoaderThread);
|
||||
|
||||
connect(&requestTimer, &QTimer::timeout, this, &PictureLoaderWorker::processQueuedRequests);
|
||||
requestTimer.setInterval(1000);
|
||||
requestTimer.start();
|
||||
}
|
||||
|
||||
PictureLoaderWorker::~PictureLoaderWorker()
|
||||
{
|
||||
saveRedirectCache();
|
||||
pictureLoaderThread->deleteLater();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::processLoadQueue()
|
||||
void PictureLoaderWorker::queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||
{
|
||||
if (loadQueueRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadQueueRunning = true;
|
||||
while (true) {
|
||||
mutex.lock();
|
||||
if (loadQueue.isEmpty()) {
|
||||
mutex.unlock();
|
||||
loadQueueRunning = false;
|
||||
return;
|
||||
}
|
||||
cardBeingLoaded = loadQueue.takeFirst();
|
||||
mutex.unlock();
|
||||
|
||||
QString setName = cardBeingLoaded.getSetName();
|
||||
QString cardName = cardBeingLoaded.getCard()->getName();
|
||||
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
|
||||
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardName << " set: " << setName << "]: Trying to load picture";
|
||||
|
||||
// FIXME: This is a hack so that to keep old Cockatrice behavior
|
||||
// (ignoring provider ID) when the "override all card art with personal
|
||||
// preference" is set.
|
||||
//
|
||||
// Figure out a proper way to integrate the two systems at some point.
|
||||
//
|
||||
// Note: need to go through a member for
|
||||
// overrideAllCardArtWithPersonalPreference as reading from the
|
||||
// SettingsCache instance from the PictureLoaderWorker thread could
|
||||
// cause race conditions.
|
||||
//
|
||||
// XXX: Reading from the CardDatabaseManager instance from the
|
||||
// PictureLoaderWorker thread might not be safe either
|
||||
bool searchCustomPics = overrideAllCardArtWithPersonalPreference ||
|
||||
CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
|
||||
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey());
|
||||
if (searchCustomPics && cardImageExistsOnDisk(setName, correctedCardName, searchCustomPics)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardName << " set: " << setName << "]: No custom picture, trying to download";
|
||||
cardsToDownload.append(cardBeingLoaded);
|
||||
cardBeingLoaded.clear();
|
||||
if (!downloadRunning) {
|
||||
startNextPicDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname, bool searchCustomPics)
|
||||
{
|
||||
QImage image;
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
QList<QString> picsPaths = QList<QString>();
|
||||
|
||||
if (searchCustomPics) {
|
||||
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||
|
||||
// Recursively check all subdirectories of the CUSTOM folder
|
||||
while (it.hasNext()) {
|
||||
QString thisPath(it.next());
|
||||
QFileInfo thisFileInfo(thisPath);
|
||||
|
||||
if (thisFileInfo.isFile() &&
|
||||
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
|
||||
thisFileInfo.baseName() == correctedCardname)) {
|
||||
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!setName.isEmpty()) {
|
||||
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
|
||||
// We no longer store downloaded images there, but don't just ignore
|
||||
// stuff that old versions have put there.
|
||||
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
|
||||
}
|
||||
|
||||
// Iterates through the list of paths, searching for images with the desired
|
||||
// name with any QImageReader-supported
|
||||
// extension
|
||||
for (const auto &_picsPath : picsPaths) {
|
||||
imgReader.setFileName(_picsPath);
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".full");
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.full found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".xlhq");
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.xlhq found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::startNextPicDownload()
|
||||
{
|
||||
if (cardsToDownload.isEmpty()) {
|
||||
cardBeingDownloaded.clear();
|
||||
downloadRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
downloadRunning = true;
|
||||
|
||||
cardBeingDownloaded = cardsToDownload.takeFirst();
|
||||
|
||||
QString picUrl = cardBeingDownloaded.getCurrentUrl();
|
||||
|
||||
if (picUrl.isEmpty()) {
|
||||
downloadRunning = false;
|
||||
picDownloadFailed();
|
||||
} else {
|
||||
QUrl url(picUrl);
|
||||
qCDebug(PictureLoaderWorkerLog).nospace() << "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Trying to fetch picture from url " << url.toDisplayString();
|
||||
makeRequest(url);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadFailed()
|
||||
{
|
||||
/* Take advantage of short circuiting here to call the nextUrl until one
|
||||
is not available. Only once nextUrl evaluates to false will this move
|
||||
on to nextSet. If the Urls for a particular card are empty, this will
|
||||
effectively go through the sets for that card. */
|
||||
if (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
|
||||
mutex.lock();
|
||||
loadQueue.prepend(cardBeingDownloaded);
|
||||
mutex.unlock();
|
||||
} else {
|
||||
qCWarning(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
|
||||
<< (picDownload ? "download failed" : "downloads disabled")
|
||||
<< ", no more url combinations to try: BAILING OUT";
|
||||
imageLoaded(cardBeingDownloaded.getCard(), QImage());
|
||||
cardBeingDownloaded.clear();
|
||||
}
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
|
||||
{
|
||||
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
|
||||
return md5Blacklist.contains(md5sum);
|
||||
}
|
||||
|
||||
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
|
||||
{
|
||||
// Check if the redirect is cached
|
||||
QUrl cachedRedirect = getCachedRedirect(url);
|
||||
if (!cachedRedirect.isEmpty()) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString()
|
||||
<< " to " << cachedRedirect.toDisplayString();
|
||||
return makeRequest(cachedRedirect); // Use the cached redirect
|
||||
queueRequest(cachedRedirect, worker);
|
||||
}
|
||||
if (cache->metaData(url).isValid()) {
|
||||
makeRequest(url, worker);
|
||||
} else {
|
||||
requestLoadQueue.append(qMakePair(url, worker));
|
||||
emit imageLoadQueued(url, worker);
|
||||
}
|
||||
}
|
||||
|
||||
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||
{
|
||||
// Check for cached redirects
|
||||
QUrl cachedRedirect = getCachedRedirect(url);
|
||||
if (!cachedRedirect.isEmpty()) {
|
||||
emit imageLoadSuccessful(url, worker);
|
||||
return makeRequest(cachedRedirect, worker);
|
||||
}
|
||||
|
||||
QNetworkRequest req(url);
|
||||
|
||||
// QNetworkDiskCache leaks file descriptors when downloading compressed
|
||||
// files, see https://bugreports.qt.io/browse/QTBUG-135641
|
||||
//
|
||||
// We can set the Accept-Encoding header manually to disable Qt's automatic
|
||||
// response decompression, but then we would have to deal with decompression
|
||||
// ourselves.
|
||||
//
|
||||
// Since we are dowloading images that are usually stored in a
|
||||
// compressed format (e.g. jpeg or webp), it's not clear compression at the
|
||||
// HTTP level helps; in fact, images are typically returned without
|
||||
// compression. Redirects, on the other hand, are compressed and cause file
|
||||
// descriptor leaks -- but since redirects have no payload, we don't really
|
||||
// care either.
|
||||
//
|
||||
// In the end, just do the simple thing and disable HTTP compression.
|
||||
req.setRawHeader("accept-encoding", "identity");
|
||||
|
||||
if (!picDownload) {
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||
}
|
||||
|
||||
QNetworkReply *reply = networkManager->get(req);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
|
||||
// Connect reply handling
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url, worker]() {
|
||||
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
||||
|
||||
if (redirectTarget.isValid()) {
|
||||
QUrl redirectUrl = redirectTarget.toUrl();
|
||||
if (redirectUrl.isRelative()) {
|
||||
redirectUrl = url.resolved(redirectUrl);
|
||||
}
|
||||
|
||||
cacheRedirect(url, redirectUrl);
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from " << url.toDisplayString()
|
||||
<< " to " << redirectUrl.toDisplayString();
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
worker->picDownloadFinished(reply);
|
||||
emit imageLoadSuccessful(url, worker);
|
||||
|
||||
// If we hit a cached image, we get to make another request for free.
|
||||
if (reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) {
|
||||
if (!requestLoadQueue.isEmpty()) {
|
||||
auto request = requestLoadQueue.takeFirst();
|
||||
makeRequest(request.first, request.second);
|
||||
}
|
||||
}
|
||||
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
|
||||
qInfo() << "Too many requests.";
|
||||
}
|
||||
reply->deleteLater();
|
||||
});
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::processQueuedRequests()
|
||||
{
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (!requestLoadQueue.isEmpty()) {
|
||||
auto request = requestLoadQueue.takeFirst();
|
||||
makeRequest(request.first, request.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::enqueueImageLoad(const CardInfoPtr &card)
|
||||
{
|
||||
auto worker = new PictureLoaderWorkerWork(this, card);
|
||||
Q_UNUSED(worker);
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::imageLoadedSuccessfully(CardInfoPtr card, const QImage &image)
|
||||
{
|
||||
emit imageLoaded(std::move(card), image);
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
||||
{
|
||||
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
||||
saveRedirectCache();
|
||||
// saveRedirectCache();
|
||||
}
|
||||
|
||||
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
||||
|
|
@ -353,129 +211,6 @@ void PictureLoaderWorker::cleanStaleEntries()
|
|||
}
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
||||
{
|
||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
|
||||
if (reply->error()) {
|
||||
if (isFromCache) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString() << " and retrying ("
|
||||
<< reply->errorString() << ")";
|
||||
|
||||
networkManager->cache()->remove(reply->url());
|
||||
|
||||
makeRequest(reply->url());
|
||||
} else {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
|
||||
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
|
||||
|
||||
picDownloadFailed();
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
|
||||
statusCode == 308) {
|
||||
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: following " << (isFromCache ? "cached redirect" : "redirect") << " to "
|
||||
<< redirectUrl.toDisplayString();
|
||||
makeRequest(redirectUrl);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// peek is used to keep the data in the buffer for use by QImageReader
|
||||
const QByteArray &picData = reply->peek(reply->size());
|
||||
|
||||
if (imageIsBlackListed(picData)) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Picture found, but blacklisted, will consider it as not found";
|
||||
|
||||
picDownloadFailed();
|
||||
reply->deleteLater();
|
||||
startNextPicDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
QImage testImage;
|
||||
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
imgReader.setDevice(reply);
|
||||
|
||||
bool logSuccessMessage = false;
|
||||
|
||||
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
|
||||
auto replyHeader = reply->peek(riffHeaderSize);
|
||||
|
||||
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
|
||||
auto imgBuf = QBuffer(this);
|
||||
imgBuf.setData(reply->readAll());
|
||||
|
||||
auto movie = QMovie(&imgBuf);
|
||||
movie.start();
|
||||
movie.stop();
|
||||
|
||||
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
|
||||
logSuccessMessage = true;
|
||||
} else if (imgReader.read(&testImage)) {
|
||||
imageLoaded(cardBeingDownloaded.getCard(), testImage);
|
||||
logSuccessMessage = true;
|
||||
} else {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Possible " << (isFromCache ? "cached" : "downloaded") << " picture at "
|
||||
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
|
||||
|
||||
picDownloadFailed();
|
||||
}
|
||||
|
||||
if (logSuccessMessage) {
|
||||
qCInfo(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
|
||||
<< reply->url().toDisplayString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
// avoid queueing the same card more than once
|
||||
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : loadQueue) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : cardsToDownload) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
loadQueue.append(PictureToLoad(card));
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadChanged()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
#ifndef PICTURE_LOADER_WORKER_H
|
||||
#define PICTURE_LOADER_WORKER_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
#include "../../../game/cards/card_info.h"
|
||||
#include "picture_loader_worker_work.h"
|
||||
#include "picture_to_load.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
#include <QTimer>
|
||||
|
||||
#define REDIRECT_HEADER_NAME "redirects"
|
||||
#define REDIRECT_ORIGINAL_URL "original"
|
||||
|
|
@ -17,16 +22,23 @@
|
|||
|
||||
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
|
||||
|
||||
class PictureLoaderWorkerWork;
|
||||
class PictureLoaderWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PictureLoaderWorker();
|
||||
~PictureLoaderWorker() override;
|
||||
void queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||
|
||||
void enqueueImageLoad(CardInfoPtr card);
|
||||
void enqueueImageLoad(const CardInfoPtr &card);
|
||||
void clearNetworkCache();
|
||||
|
||||
public slots:
|
||||
QNetworkReply *makeRequest(const QUrl &url, PictureLoaderWorkerWork *workThread);
|
||||
void processQueuedRequests();
|
||||
void imageLoadedSuccessfully(CardInfoPtr card, const QImage &image);
|
||||
|
||||
private:
|
||||
static QStringList md5Blacklist;
|
||||
|
||||
|
|
@ -35,6 +47,7 @@ private:
|
|||
QList<PictureToLoad> loadQueue;
|
||||
QMutex mutex;
|
||||
QNetworkAccessManager *networkManager;
|
||||
QNetworkDiskCache *cache;
|
||||
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
||||
QString cacheFilePath; // Path to persistent storage
|
||||
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
||||
|
|
@ -42,18 +55,11 @@ private:
|
|||
PictureToLoad cardBeingLoaded;
|
||||
PictureToLoad cardBeingDownloaded;
|
||||
bool picDownload, downloadRunning, loadQueueRunning;
|
||||
QQueue<QPair<QUrl, PictureLoaderWorkerWork *>> requestLoadQueue;
|
||||
QList<QPair<QUrl, PictureLoaderWorkerWork *>> requestQueue;
|
||||
QTimer requestTimer; // Timer for processing delayed requests
|
||||
bool overrideAllCardArtWithPersonalPreference;
|
||||
void startNextPicDownload();
|
||||
|
||||
/** Emit the `imageLoaded` signal and return `true` if a picture is found on
|
||||
disk, return `false` otherwise.
|
||||
|
||||
If `searchCustomPics` is `true`, the CUSTOM folder is searched for a
|
||||
matching image first; otherwise, only the set-based folders are used. */
|
||||
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName, bool searchCustomPics);
|
||||
|
||||
bool imageIsBlackListed(const QByteArray &);
|
||||
QNetworkReply *makeRequest(const QUrl &url);
|
||||
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
||||
void loadRedirectCache();
|
||||
|
|
@ -61,18 +67,15 @@ private:
|
|||
void cleanStaleEntries();
|
||||
|
||||
private slots:
|
||||
void picDownloadFinished(QNetworkReply *reply);
|
||||
void picDownloadFailed();
|
||||
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
|
||||
public slots:
|
||||
void processLoadQueue();
|
||||
|
||||
signals:
|
||||
void startLoadQueue();
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
void imageLoadQueued(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||
void imageLoadSuccessful(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||
};
|
||||
|
||||
#endif // PICTURE_LOADER_WORKER_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
#include "picture_loader_worker_work.h"
|
||||
|
||||
#include "../../../game/cards/card_database_manager.h"
|
||||
#include "../../../settings/cache_settings.h"
|
||||
#include "picture_loader_worker.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDirIterator>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
QStringList PictureLoaderWorkerWork::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||
|
||||
PictureLoaderWorkerWork::PictureLoaderWorkerWork(PictureLoaderWorker *_worker, const CardInfoPtr &toLoad)
|
||||
: QThread(nullptr), worker(_worker), cardToDownload(toLoad)
|
||||
{
|
||||
connect(this, &PictureLoaderWorkerWork::requestImageDownload, worker, &PictureLoaderWorker::queueRequest,
|
||||
Qt::QueuedConnection);
|
||||
connect(this, &PictureLoaderWorkerWork::imageLoaded, worker, &PictureLoaderWorker::imageLoadedSuccessfully,
|
||||
Qt::QueuedConnection);
|
||||
pictureLoaderThread = new QThread;
|
||||
pictureLoaderThread->start(QThread::LowPriority);
|
||||
moveToThread(pictureLoaderThread);
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
PictureLoaderWorkerWork::~PictureLoaderWorkerWork()
|
||||
{
|
||||
pictureLoaderThread->deleteLater();
|
||||
}
|
||||
|
||||
bool PictureLoaderWorkerWork::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
|
||||
{
|
||||
QImage image;
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
QList<QString> picsPaths = QList<QString>();
|
||||
QDirIterator it(SettingsCache::instance().getCustomPicsPath(),
|
||||
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||
|
||||
// Recursively check all subdirectories of the CUSTOM folder
|
||||
while (it.hasNext()) {
|
||||
QString thisPath(it.next());
|
||||
QFileInfo thisFileInfo(thisPath);
|
||||
|
||||
if (thisFileInfo.isFile() &&
|
||||
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
|
||||
thisFileInfo.baseName() == correctedCardname)) {
|
||||
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
|
||||
}
|
||||
}
|
||||
|
||||
if (!setName.isEmpty()) {
|
||||
picsPaths << SettingsCache::instance().getPicsPath() + "/" + setName + "/" + correctedCardname
|
||||
// We no longer store downloaded images there, but don't just ignore
|
||||
// stuff that old versions have put there.
|
||||
<< SettingsCache::instance().getPicsPath() + "/downloadedPics/" + setName + "/" + correctedCardname;
|
||||
}
|
||||
|
||||
// Iterates through the list of paths, searching for images with the desired
|
||||
// name with any QImageReader-supported
|
||||
// extension
|
||||
for (const auto &_picsPath : picsPaths) {
|
||||
imgReader.setFileName(_picsPath);
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
|
||||
imageLoaded(cardToDownload.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".full");
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
|
||||
<< " set: " << setName << "]: Picture.full found on disk.";
|
||||
imageLoaded(cardToDownload.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".xlhq");
|
||||
if (imgReader.read(&image)) {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
|
||||
<< " set: " << setName << "]: Picture.xlhq found on disk.";
|
||||
imageLoaded(cardToDownload.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PictureLoaderWorkerWork::startNextPicDownload()
|
||||
{
|
||||
QString picUrl = cardToDownload.getCurrentUrl();
|
||||
|
||||
if (picUrl.isEmpty()) {
|
||||
downloadRunning = false;
|
||||
picDownloadFailed();
|
||||
} else {
|
||||
QUrl url(picUrl);
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Trying to fetch picture from url "
|
||||
<< url.toDisplayString();
|
||||
emit requestImageDownload(url, this);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoaderWorkerWork::picDownloadFailed()
|
||||
{
|
||||
/* Take advantage of short-circuiting here to call the nextUrl until one
|
||||
is not available. Only once nextUrl evaluates to false will this move
|
||||
on to nextSet. If the Urls for a particular card are empty, this will
|
||||
effectively go through the sets for that card. */
|
||||
if (cardToDownload.nextUrl() || cardToDownload.nextSet()) {
|
||||
startNextPicDownload();
|
||||
} else {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Picture NOT found, "
|
||||
<< (picDownload ? "download failed" : "downloads disabled")
|
||||
<< ", no more url combinations to try: BAILING OUT";
|
||||
imageLoaded(cardToDownload.getCard(), QImage());
|
||||
}
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
void PictureLoaderWorkerWork::picDownloadFinished(QNetworkReply *reply)
|
||||
{
|
||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
|
||||
if (reply->error()) {
|
||||
if (isFromCache) {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Removing corrupted cache file for url "
|
||||
<< reply->url().toDisplayString() << " and retrying (" << reply->errorString() << ")";
|
||||
|
||||
networkManager->cache()->remove(reply->url());
|
||||
|
||||
requestImageDownload(reply->url(), this);
|
||||
} else {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: " << (picDownload ? "Download" : "Cache search")
|
||||
<< " failed for url " << reply->url().toDisplayString() << " (" << reply->errorString() << ")";
|
||||
|
||||
picDownloadFailed();
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
|
||||
statusCode == 308) {
|
||||
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: following "
|
||||
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
|
||||
requestImageDownload(redirectUrl, this);
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// peek is used to keep the data in the buffer for use by QImageReader
|
||||
const QByteArray &picData = reply->peek(reply->size());
|
||||
|
||||
if (imageIsBlackListed(picData)) {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||
<< " set: " << cardToDownload.getSetName()
|
||||
<< "]: Picture found, but blacklisted, will consider it as not found";
|
||||
|
||||
picDownloadFailed();
|
||||
reply->deleteLater();
|
||||
startNextPicDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
QImage testImage;
|
||||
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
imgReader.setDevice(reply);
|
||||
|
||||
bool logSuccessMessage = false;
|
||||
|
||||
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
|
||||
auto replyHeader = reply->peek(riffHeaderSize);
|
||||
|
||||
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
|
||||
auto imgBuf = QBuffer(this);
|
||||
imgBuf.setData(reply->readAll());
|
||||
|
||||
auto movie = QMovie(&imgBuf);
|
||||
movie.start();
|
||||
movie.stop();
|
||||
|
||||
imageLoaded(cardToDownload.getCard(), movie.currentImage());
|
||||
logSuccessMessage = true;
|
||||
} else if (imgReader.read(&testImage)) {
|
||||
imageLoaded(cardToDownload.getCard(), testImage);
|
||||
logSuccessMessage = true;
|
||||
} else {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Possible " << (isFromCache ? "cached" : "downloaded")
|
||||
<< " picture at " << reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
|
||||
|
||||
picDownloadFailed();
|
||||
}
|
||||
|
||||
if (logSuccessMessage) {
|
||||
qCDebug(PictureLoaderWorkerWorkLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
|
||||
<< " set: " << cardToDownload.getSetName() << "]: Image successfully "
|
||||
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url " << reply->url().toDisplayString();
|
||||
} else {
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
bool PictureLoaderWorkerWork::imageIsBlackListed(const QByteArray &picData)
|
||||
{
|
||||
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
|
||||
return md5Blacklist.contains(md5sum);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
#ifndef PICTURE_LOADER_WORKER_WORK_H
|
||||
#define PICTURE_LOADER_WORKER_WORK_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
#include "picture_loader_worker.h"
|
||||
#include "picture_to_load.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
|
||||
#define REDIRECT_HEADER_NAME "redirects"
|
||||
#define REDIRECT_ORIGINAL_URL "original"
|
||||
#define REDIRECT_URL "redirect"
|
||||
#define REDIRECT_TIMESTAMP "timestamp"
|
||||
#define REDIRECT_CACHE_FILENAME "cache.ini"
|
||||
|
||||
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerWorkLog, "picture_loader.worker");
|
||||
|
||||
class PictureLoaderWorker;
|
||||
class PictureLoaderWorkerWork : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PictureLoaderWorkerWork(PictureLoaderWorker *worker, const CardInfoPtr &toLoad);
|
||||
~PictureLoaderWorkerWork() override;
|
||||
PictureLoaderWorker *worker;
|
||||
PictureToLoad cardToDownload;
|
||||
public slots:
|
||||
void picDownloadFinished(QNetworkReply *reply);
|
||||
void picDownloadFailed();
|
||||
|
||||
private:
|
||||
static QStringList md5Blacklist;
|
||||
QThread *pictureLoaderThread;
|
||||
QNetworkAccessManager *networkManager;
|
||||
bool picDownload, downloadRunning, loadQueueRunning;
|
||||
void startNextPicDownload();
|
||||
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
|
||||
bool imageIsBlackListed(const QByteArray &);
|
||||
|
||||
signals:
|
||||
void startLoadQueue();
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
void requestImageDownload(const QUrl &url, PictureLoaderWorkerWork *instance);
|
||||
};
|
||||
|
||||
#endif // PICTURE_LOADER_WORKER_WORK_H
|
||||
|
|
@ -12,12 +12,13 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
|||
QString _zoneName,
|
||||
QString _activeGroupCriteria,
|
||||
QStringList _activeSortCriteria,
|
||||
DisplayType _displayType,
|
||||
int bannerOpacity,
|
||||
int subBannerOpacity,
|
||||
CardSizeWidget *_cardSizeWidget)
|
||||
: QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), activeGroupCriteria(_activeGroupCriteria),
|
||||
activeSortCriteria(_activeSortCriteria), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity),
|
||||
cardSizeWidget(_cardSizeWidget)
|
||||
activeSortCriteria(_activeSortCriteria), displayType(_displayType), bannerOpacity(bannerOpacity),
|
||||
subBannerOpacity(subBannerOpacity), cardSizeWidget(_cardSizeWidget)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
|
@ -60,7 +61,7 @@ void DeckCardZoneDisplayWidget::displayCards()
|
|||
deleteCardGroupIfItDoesNotExist();
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::refreshDisplayType(const QString &_displayType)
|
||||
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
|
||||
{
|
||||
displayType = _displayType;
|
||||
QLayoutItem *item;
|
||||
|
|
@ -102,7 +103,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
|
|||
continue;
|
||||
}
|
||||
|
||||
if (displayType == "overlap") {
|
||||
if (displayType == DisplayType::Overlap) {
|
||||
auto *display_widget = new OverlappedCardGroupDisplayWidget(
|
||||
cardGroupContainer, deckListModel, zoneName, cardGroup, activeGroupCriteria, activeSortCriteria,
|
||||
subBannerOpacity, cardSizeWidget);
|
||||
|
|
@ -112,7 +113,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
|
|||
connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget,
|
||||
&CardGroupDisplayWidget::onActiveSortCriteriaChanged);
|
||||
cardGroupLayout->addWidget(display_widget);
|
||||
} else if (displayType == "flat") {
|
||||
} else if (displayType == DisplayType::Flat) {
|
||||
auto *display_widget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, zoneName,
|
||||
cardGroup, activeGroupCriteria, activeSortCriteria,
|
||||
subBannerOpacity, cardSizeWidget);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "../../../../game/cards/card_info.h"
|
||||
#include "../general/display/banner_widget.h"
|
||||
#include "../general/layout_containers/overlap_widget.h"
|
||||
#include "../visual_deck_editor/visual_deck_editor_widget.h"
|
||||
#include "card_info_picture_with_text_overlay_widget.h"
|
||||
#include "card_size_widget.h"
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ public:
|
|||
QString zoneName,
|
||||
QString activeGroupCriteria,
|
||||
QStringList activeSortCriteria,
|
||||
DisplayType displayType,
|
||||
int bannerOpacity,
|
||||
int subBannerOpacity,
|
||||
CardSizeWidget *_cardSizeWidget);
|
||||
|
|
@ -33,7 +35,7 @@ public slots:
|
|||
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
|
||||
void onHover(CardInfoPtr card);
|
||||
void displayCards();
|
||||
void refreshDisplayType(const QString &displayType);
|
||||
void refreshDisplayType(const DisplayType &displayType);
|
||||
void addCardGroupIfItDoesNotExist();
|
||||
void deleteCardGroupIfItDoesNotExist();
|
||||
void onActiveGroupCriteriaChanged(QString activeGroupCriteria);
|
||||
|
|
@ -48,6 +50,7 @@ signals:
|
|||
private:
|
||||
QString activeGroupCriteria;
|
||||
QStringList activeSortCriteria;
|
||||
DisplayType displayType = DisplayType::Overlap;
|
||||
int bannerOpacity = 20;
|
||||
int subBannerOpacity = 10;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
|
|
@ -55,7 +58,6 @@ private:
|
|||
BannerWidget *banner;
|
||||
QWidget *cardGroupContainer;
|
||||
QVBoxLayout *cardGroupLayout;
|
||||
QString displayType = "flat";
|
||||
OverlapWidget *overlapWidget;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,20 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
|
||||
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
|
||||
|
||||
activeGroupCriteriaLabel = new QLabel(this);
|
||||
|
||||
activeGroupCriteriaComboBox = new QComboBox(this);
|
||||
activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE);
|
||||
activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST);
|
||||
activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR);
|
||||
connect(activeGroupCriteriaComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() {
|
||||
deckModel->setActiveGroupCriteria(
|
||||
static_cast<DeckListModelGroupCriteria>(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
|
||||
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
|
||||
deckView->expandAll();
|
||||
deckView->expandAll();
|
||||
});
|
||||
|
||||
aIncrement = new QAction(QString(), this);
|
||||
aIncrement->setIcon(QPixmap("theme:icons/increment"));
|
||||
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement);
|
||||
|
|
@ -144,6 +158,9 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
auto *hashSizePolicy = new QSizePolicy();
|
||||
|
|
@ -286,11 +303,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
|||
});
|
||||
|
||||
for (const auto &pair : pairList) {
|
||||
QVariantMap dataMap;
|
||||
dataMap["name"] = pair.first;
|
||||
dataMap["uuid"] = pair.second;
|
||||
|
||||
bannerCardComboBox->addItem(pair.first, dataMap);
|
||||
bannerCardComboBox->addItem(pair.first, QVariant::fromValue(pair));
|
||||
}
|
||||
|
||||
// Try to restore the previous selection by finding the currentText
|
||||
|
|
@ -298,7 +311,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
|||
if (restoredIndex != -1) {
|
||||
bannerCardComboBox->setCurrentIndex(restoredIndex);
|
||||
if (deckModel->getDeckList()->getBannerCard().second !=
|
||||
bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap()["uuid"].toString()) {
|
||||
bannerCardComboBox->currentData().value<QPair<QString, QString>>().second) {
|
||||
setBannerCard(restoredIndex);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -318,9 +331,8 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
|||
|
||||
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
|
||||
{
|
||||
QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap();
|
||||
deckModel->getDeckList()->setBannerCard(
|
||||
QPair<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
|
||||
auto cardAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
deckModel->getDeckList()->setBannerCard(cardAndId);
|
||||
emit deckModified();
|
||||
}
|
||||
|
||||
|
|
@ -578,6 +590,7 @@ void DeckEditorDeckDockWidget::retranslateUi()
|
|||
showBannerCardCheckBox->setText(tr("Show banner card selection menu"));
|
||||
showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
|
||||
commentsLabel->setText(tr("&Comments:"));
|
||||
activeGroupCriteriaLabel->setText(tr("Group by:"));
|
||||
hashLabel1->setText(tr("Hash:"));
|
||||
|
||||
aIncrement->setText(tr("&Increment number"));
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ private:
|
|||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||
QLabel *hashLabel1;
|
||||
LineEditUnfocusable *hashLabel;
|
||||
QLabel *activeGroupCriteriaLabel;
|
||||
QComboBox *activeGroupCriteriaComboBox;
|
||||
|
||||
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ void SettingsButtonWidget::addSettingsWidget(QWidget *toAdd) const
|
|||
popup->addSettingsWidget(toAdd);
|
||||
}
|
||||
|
||||
void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||
{
|
||||
popup->removeSettingsWidget(toRemove);
|
||||
}
|
||||
|
||||
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
|
||||
{
|
||||
button->setIcon(iconMap);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class SettingsButtonWidget : public QWidget
|
|||
public:
|
||||
explicit SettingsButtonWidget(QWidget *parent = nullptr);
|
||||
void addSettingsWidget(QWidget *toAdd) const;
|
||||
void removeSettingsWidget(QWidget *toRemove) const;
|
||||
void setButtonIcon(QPixmap iconMap);
|
||||
|
||||
protected:
|
||||
|
|
@ -25,6 +26,8 @@ private slots:
|
|||
private:
|
||||
QHBoxLayout *layout;
|
||||
QToolButton *button;
|
||||
|
||||
public:
|
||||
SettingsPopupWidget *popup;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ void SettingsPopupWidget::addSettingsWidget(QWidget *toAdd) const
|
|||
containerLayout->addWidget(toAdd); // Add to containerWidget's layout
|
||||
}
|
||||
|
||||
void SettingsPopupWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||
{
|
||||
containerLayout->removeWidget(toRemove);
|
||||
toRemove->deleteLater();
|
||||
}
|
||||
|
||||
void SettingsPopupWidget::adjustSizeToFitScreen()
|
||||
{
|
||||
QScreen *screen = QApplication::screenAt(this->pos());
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class SettingsPopupWidget : public QWidget
|
|||
public:
|
||||
explicit SettingsPopupWidget(QWidget *parent = nullptr);
|
||||
void addSettingsWidget(QWidget *toAdd) const;
|
||||
void removeSettingsWidget(QWidget *toRemove) const;
|
||||
void adjustSizeToFitScreen();
|
||||
|
||||
signals:
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList()
|
|||
|
||||
// Refresh the filter file list
|
||||
QDir dir(SettingsCache::instance().getFiltersPath());
|
||||
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Time);
|
||||
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
|
||||
|
||||
// Loop through the filter files and create widgets for them
|
||||
for (const QString &filename : filterFiles) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "visual_database_display_name_filter_widget.h"
|
||||
|
||||
#include "../../../../dialogs/dlg_load_deck_from_clipboard.h"
|
||||
#include "../../../tabs/abstract_tab_deck_editor.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -35,6 +36,13 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi
|
|||
layout->addWidget(loadFromDeckButton);
|
||||
|
||||
connect(loadFromDeckButton, &QPushButton::clicked, this, &VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck);
|
||||
|
||||
loadFromClipboardButton = new QPushButton(this);
|
||||
layout->addWidget(loadFromClipboardButton);
|
||||
|
||||
connect(loadFromClipboardButton, &QPushButton::clicked, this,
|
||||
&VisualDatabaseDisplayNameFilterWidget::actLoadFromClipboard);
|
||||
|
||||
connect(filterModel, &FilterTreeModel::layoutChanged, this,
|
||||
[this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); });
|
||||
|
||||
|
|
@ -46,6 +54,8 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi()
|
|||
searchBox->setPlaceholderText(tr("Filter by name..."));
|
||||
loadFromDeckButton->setText(tr("Load from Deck"));
|
||||
loadFromDeckButton->setToolTip(tr("Apply all card names in currently loaded deck as exact match name filters"));
|
||||
loadFromClipboardButton->setText(tr("Load from Clipboard"));
|
||||
loadFromClipboardButton->setToolTip(tr("Apply all card names in clipboard as exact match name filters"));
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
||||
|
|
@ -75,6 +85,20 @@ void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
|||
updateFilterModel();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayNameFilterWidget::actLoadFromClipboard()
|
||||
{
|
||||
DlgLoadDeckFromClipboard dlg(this);
|
||||
if (!dlg.exec())
|
||||
return;
|
||||
|
||||
QStringList cardsInClipboard = dlg.getDeckList()->getCardList();
|
||||
for (QString cardName : cardsInClipboard) {
|
||||
createNameFilter(cardName);
|
||||
}
|
||||
|
||||
updateFilterModel();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name)
|
||||
{
|
||||
if (activeFilters.contains(name))
|
||||
|
|
@ -85,7 +109,11 @@ void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name
|
|||
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
|
||||
"QPushButton:hover { background-color: red; color: white; }");
|
||||
|
||||
connect(button, &QPushButton::clicked, this, [this, name]() { removeNameFilter(name); });
|
||||
connect(button, &QPushButton::clicked, this, [this, name]() {
|
||||
removeNameFilter(name);
|
||||
QTimer::singleShot(0, this,
|
||||
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
|
||||
});
|
||||
|
||||
flowWidget->addWidget(button);
|
||||
activeFilters[name] = button;
|
||||
|
|
@ -96,9 +124,6 @@ void VisualDatabaseDisplayNameFilterWidget::removeNameFilter(const QString &name
|
|||
if (activeFilters.contains(name)) {
|
||||
activeFilters[name]->deleteLater(); // Safe deletion
|
||||
activeFilters.remove(name);
|
||||
|
||||
QTimer::singleShot(0, this,
|
||||
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,11 +35,13 @@ private:
|
|||
QLineEdit *searchBox;
|
||||
FlowWidget *flowWidget;
|
||||
QPushButton *loadFromDeckButton;
|
||||
QPushButton *loadFromClipboardButton;
|
||||
|
||||
QMap<QString, QPushButton *> activeFilters; // Store active name filter buttons
|
||||
|
||||
private slots:
|
||||
void actLoadFromDeck();
|
||||
void actLoadFromClipboard();
|
||||
};
|
||||
|
||||
#endif // VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -95,7 +95,15 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
clearFilterWidget = new QToolButton();
|
||||
clearFilterWidget->setFixedSize(32, 32);
|
||||
clearFilterWidget->setIcon(QPixmap("theme:icons/delete"));
|
||||
connect(clearFilterWidget, &QToolButton::clicked, this, [this] { filterModel->clear(); });
|
||||
connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
|
||||
filterModel->blockSignals(true);
|
||||
filterModel->filterTree()->blockSignals(true);
|
||||
filterModel->clear();
|
||||
filterModel->blockSignals(false);
|
||||
filterModel->filterTree()->blockSignals(false);
|
||||
emit filterModel->filterTree()->changed();
|
||||
emit filterModel->layoutChanged();
|
||||
});
|
||||
|
||||
quickFilterSaveLoadWidget = new SettingsButtonWidget(this);
|
||||
quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/lock"));
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ void VisualDeckEditorWidget::retranslateUi()
|
|||
searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add "
|
||||
"preferred printing to the deck on pressing enter"));
|
||||
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
|
||||
displayTypeButton->setText(tr("Flat Layout"));
|
||||
displayTypeButton->setText(tr("Overlap Layout"));
|
||||
displayTypeButton->setToolTip(
|
||||
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)"));
|
||||
}
|
||||
|
|
@ -212,14 +212,13 @@ void VisualDeckEditorWidget::updateDisplayType()
|
|||
// Update UI and emit signal
|
||||
switch (currentDisplayType) {
|
||||
case DisplayType::Flat:
|
||||
emit displayTypeChanged("flat");
|
||||
displayTypeButton->setText(tr("Flat Layout"));
|
||||
break;
|
||||
case DisplayType::Overlap:
|
||||
emit displayTypeChanged("overlap");
|
||||
displayTypeButton->setText(tr("Overlap Layout"));
|
||||
break;
|
||||
}
|
||||
emit displayTypeChanged(currentDisplayType);
|
||||
}
|
||||
|
||||
void VisualDeckEditorWidget::addZoneIfDoesNotExist()
|
||||
|
|
@ -239,8 +238,9 @@ void VisualDeckEditorWidget::addZoneIfDoesNotExist()
|
|||
if (found) {
|
||||
continue;
|
||||
}
|
||||
DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget(
|
||||
zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria, 20, 10, cardSizeWidget);
|
||||
DeckCardZoneDisplayWidget *zoneDisplayWidget =
|
||||
new DeckCardZoneDisplayWidget(zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria,
|
||||
currentDisplayType, 20, 10, cardSizeWidget);
|
||||
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover);
|
||||
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick);
|
||||
connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ signals:
|
|||
void activeSortCriteriaChanged(QStringList activeSortCriteria);
|
||||
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
|
||||
void cardAdditionRequested(CardInfoPtr card);
|
||||
void displayTypeChanged(QString displayType);
|
||||
void displayTypeChanged(DisplayType displayType);
|
||||
|
||||
protected slots:
|
||||
void onHover(CardInfoPtr hoveredCard);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <QDirIterator>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
|
||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
|
||||
: QWidget(_parent), deckList(nullptr)
|
||||
|
|
@ -77,6 +78,21 @@ static QStringList getAllFiles(const QString &filePath)
|
|||
return allFiles;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||
{
|
||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||
|
|
@ -91,6 +107,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
|||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
||||
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
|
|
@ -100,6 +120,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
|||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||
if (conversionDialog.exec() == QDialog::Accepted) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event)
|
|||
{
|
||||
switch (event->button()) {
|
||||
case Qt::LeftButton:
|
||||
setState(TagState::Selected);
|
||||
setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected);
|
||||
break;
|
||||
case Qt::RightButton:
|
||||
setState(TagState::Excluded);
|
||||
setState(state != TagState::Excluded ? TagState::Excluded : TagState::NotSelected);
|
||||
break;
|
||||
case Qt::MiddleButton:
|
||||
setState(TagState::NotSelected);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
&DeckPreviewWidget::updateTagsVisibility);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageShowBannerCardComboBoxChanged, this,
|
||||
&DeckPreviewWidget::updateBannerCardComboBoxVisibility);
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::deckPreviewTooltipChanged, this,
|
||||
&DeckPreviewWidget::refreshBannerCardToolTip);
|
||||
|
||||
layout->addWidget(bannerCardDisplayWidget);
|
||||
}
|
||||
|
|
@ -79,7 +81,6 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
|||
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
bannerCardDisplayWidget->setFontSize(24);
|
||||
refreshBannerCardText();
|
||||
setFilePath(deckLoader->getLastFileName());
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
|
||||
|
|
@ -104,6 +105,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
|||
layout->addWidget(bannerCardLabel);
|
||||
layout->addWidget(bannerCardComboBox);
|
||||
|
||||
refreshBannerCardText();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
|
|
@ -179,15 +182,42 @@ QString DeckPreviewWidget::getColorIdentity()
|
|||
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
|
||||
{
|
||||
return deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName()
|
||||
: deckLoader->getName();
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
void DeckPreviewWidget::refreshBannerCardText()
|
||||
{
|
||||
bannerCardDisplayWidget->setOverlayText(
|
||||
deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() : deckLoader->getName());
|
||||
bannerCardDisplayWidget->setOverlayText(getDisplayName());
|
||||
|
||||
refreshBannerCardToolTip();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::refreshBannerCardToolTip()
|
||||
{
|
||||
auto type = visualDeckStorageWidget->settings()->getDeckPreviewTooltip();
|
||||
switch (type) {
|
||||
case VisualDeckStorageQuickSettingsWidget::TooltipType::None:
|
||||
bannerCardDisplayWidget->setToolTip("");
|
||||
break;
|
||||
case VisualDeckStorageQuickSettingsWidget::TooltipType::Filepath:
|
||||
bannerCardDisplayWidget->setToolTip(filePath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::updateBannerCardComboBox()
|
||||
|
|
@ -260,11 +290,11 @@ void DeckPreviewWidget::updateBannerCardComboBox()
|
|||
|
||||
void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
||||
{
|
||||
QVariant itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex());
|
||||
deckLoader->setBannerCard(QPair<QString, QString>(bannerCardComboBox->currentText(), itemData.toString()));
|
||||
auto nameAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
deckLoader->setBannerCard(nameAndId);
|
||||
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
|
||||
bannerCardDisplayWidget->setCard(CardDatabaseManager::getInstance()->getCardByNameAndProviderId(
|
||||
bannerCardComboBox->currentText(), itemData.toString()));
|
||||
bannerCardDisplayWidget->setCard(
|
||||
CardDatabaseManager::getInstance()->getCardByNameAndProviderId(nameAndId.first, nameAndId.second));
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public:
|
|||
const QString &_filePath);
|
||||
void retranslateUi();
|
||||
QString getColorIdentity();
|
||||
QString getDisplayName() const;
|
||||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
QVBoxLayout *layout;
|
||||
|
|
@ -49,6 +50,7 @@ signals:
|
|||
public slots:
|
||||
void setFilePath(const QString &filePath);
|
||||
void refreshBannerCardText();
|
||||
void refreshBannerCardToolTip();
|
||||
void updateBannerCardComboBox();
|
||||
void setBannerCard(int);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QSpinBox>
|
||||
|
||||
VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidget *parent)
|
||||
|
|
@ -41,14 +42,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
connect(showBannerCardComboBoxCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
|
||||
&SettingsCache::setVisualDeckStorageShowBannerCardComboBox);
|
||||
|
||||
// search folder names checkbox
|
||||
searchFolderNamesCheckBox = new QCheckBox(this);
|
||||
searchFolderNamesCheckBox->setChecked(SettingsCache::instance().getVisualDeckStorageSearchFolderNames());
|
||||
connect(searchFolderNamesCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&VisualDeckStorageQuickSettingsWidget::searchFolderNamesChanged);
|
||||
connect(searchFolderNamesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
|
||||
&SettingsCache::setVisualDeckStorageSearchFolderNames);
|
||||
|
||||
// draw unused color identities checkbox
|
||||
drawUnusedColorIdentitiesCheckBox = new QCheckBox(this);
|
||||
drawUnusedColorIdentitiesCheckBox->setChecked(
|
||||
|
|
@ -80,6 +73,26 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel);
|
||||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox);
|
||||
|
||||
// tooltip selector
|
||||
auto deckPreviewTooltipWidget = new QWidget(this);
|
||||
|
||||
deckPreviewTooltipLabel = new QLabel(deckPreviewTooltipWidget);
|
||||
deckPreviewTooltipComboBox = new QComboBox(deckPreviewTooltipWidget);
|
||||
deckPreviewTooltipComboBox->setFocusPolicy(Qt::StrongFocus);
|
||||
deckPreviewTooltipComboBox->addItem("", TooltipType::None);
|
||||
deckPreviewTooltipComboBox->addItem("", TooltipType::Filepath);
|
||||
|
||||
deckPreviewTooltipComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageTooltipType());
|
||||
connect(deckPreviewTooltipComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
[this] { emit deckPreviewTooltipChanged(getDeckPreviewTooltip()); });
|
||||
connect(deckPreviewTooltipComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(),
|
||||
&SettingsCache::setVisualDeckStorageTooltipType);
|
||||
|
||||
auto deckPreviewTooltipLayout = new QHBoxLayout(deckPreviewTooltipWidget);
|
||||
deckPreviewTooltipLayout->setContentsMargins(11, 0, 11, 0);
|
||||
deckPreviewTooltipLayout->addWidget(deckPreviewTooltipLabel);
|
||||
deckPreviewTooltipLayout->addWidget(deckPreviewTooltipComboBox);
|
||||
|
||||
// card size slider
|
||||
cardSizeWidget = new CardSizeWidget(this, nullptr, SettingsCache::instance().getVisualDeckStorageCardSize());
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
|
||||
|
|
@ -92,9 +105,9 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
this->addSettingsWidget(showTagFilterCheckBox);
|
||||
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
|
||||
this->addSettingsWidget(showBannerCardComboBoxCheckBox);
|
||||
this->addSettingsWidget(searchFolderNamesCheckBox);
|
||||
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
|
||||
this->addSettingsWidget(unusedColorIdentityOpacityWidget);
|
||||
this->addSettingsWidget(deckPreviewTooltipWidget);
|
||||
this->addSettingsWidget(cardSizeWidget);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::langChanged, this,
|
||||
|
|
@ -108,10 +121,13 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi()
|
|||
showTagFilterCheckBox->setText(tr("Show Tag Filter"));
|
||||
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
|
||||
showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option"));
|
||||
searchFolderNamesCheckBox->setText(tr("Include Folder Names in Search"));
|
||||
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
|
||||
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
|
||||
unusedColorIdentitiesOpacitySpinBox->setSuffix("%");
|
||||
|
||||
deckPreviewTooltipLabel->setText(tr("Deck tooltip:"));
|
||||
deckPreviewTooltipComboBox->setItemText(0, tr("None"));
|
||||
deckPreviewTooltipComboBox->setItemText(1, tr("Filepath"));
|
||||
}
|
||||
|
||||
bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const
|
||||
|
|
@ -139,16 +155,16 @@ bool VisualDeckStorageQuickSettingsWidget::getShowTagsOnDeckPreviews() const
|
|||
return showTagsOnDeckPreviewsCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool VisualDeckStorageQuickSettingsWidget::getSearchFolderNames() const
|
||||
{
|
||||
return searchFolderNamesCheckBox->isChecked();
|
||||
}
|
||||
|
||||
int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
|
||||
{
|
||||
return unusedColorIdentitiesOpacitySpinBox->value();
|
||||
}
|
||||
|
||||
VisualDeckStorageQuickSettingsWidget::TooltipType VisualDeckStorageQuickSettingsWidget::getDeckPreviewTooltip() const
|
||||
{
|
||||
return deckPreviewTooltipComboBox->currentData().value<TooltipType>();
|
||||
}
|
||||
|
||||
int VisualDeckStorageQuickSettingsWidget::getCardSize() const
|
||||
{
|
||||
return cardSizeWidget->getSlider()->value();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class CardSizeWidget;
|
|||
class QLabel;
|
||||
class QSpinBox;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
|
||||
/**
|
||||
* The VDS's quick settings menu.
|
||||
|
|
@ -22,12 +23,23 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget
|
|||
QCheckBox *showBannerCardComboBoxCheckBox;
|
||||
QCheckBox *showTagFilterCheckBox;
|
||||
QCheckBox *showTagsOnDeckPreviewsCheckBox;
|
||||
QCheckBox *searchFolderNamesCheckBox;
|
||||
QLabel *unusedColorIdentitiesOpacityLabel;
|
||||
QSpinBox *unusedColorIdentitiesOpacitySpinBox;
|
||||
QLabel *deckPreviewTooltipLabel;
|
||||
QComboBox *deckPreviewTooltipComboBox;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
|
||||
public:
|
||||
/**
|
||||
* The info to display in the deck preview's banner card tooltip.
|
||||
*/
|
||||
enum TooltipType
|
||||
{
|
||||
None,
|
||||
Filepath
|
||||
};
|
||||
Q_ENUM(TooltipType)
|
||||
|
||||
explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr);
|
||||
|
||||
void retranslateUi();
|
||||
|
|
@ -37,8 +49,8 @@ public:
|
|||
bool getShowBannerCardComboBox() const;
|
||||
bool getShowTagFilter() const;
|
||||
bool getShowTagsOnDeckPreviews() const;
|
||||
bool getSearchFolderNames() const;
|
||||
int getUnusedColorIdentitiesOpacity() const;
|
||||
TooltipType getDeckPreviewTooltip() const;
|
||||
int getCardSize() const;
|
||||
|
||||
signals:
|
||||
|
|
@ -47,8 +59,8 @@ signals:
|
|||
void showBannerCardComboBoxChanged(bool enabled);
|
||||
void showTagFilterChanged(bool enabled);
|
||||
void showTagsOnDeckPreviewsChanged(bool enabled);
|
||||
void searchFolderNamesChanged(bool enabled);
|
||||
void unusedColorIdentitiesOpacityChanged(int opacity);
|
||||
void deckPreviewTooltipChanged(TooltipType tooltip);
|
||||
void cardSizeChanged(int scale);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
#include "visual_deck_storage_search_widget.h"
|
||||
|
||||
#include "../../../../game/filters/deck_filter_string.h"
|
||||
#include "../../../../game/filters/syntax_help.h"
|
||||
#include "../../../../settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
|
||||
#include <QAction>
|
||||
|
||||
/**
|
||||
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
|
||||
|
|
@ -17,7 +22,13 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi
|
|||
setLayout(layout);
|
||||
|
||||
searchBar = new QLineEdit(this);
|
||||
searchBar->setPlaceholderText(tr("Search by filename"));
|
||||
searchBar->setPlaceholderText(tr("Search by filename (or search expression)"));
|
||||
searchBar->setClearButtonEnabled(true);
|
||||
searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||
|
||||
auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
||||
connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); });
|
||||
|
||||
layout->addWidget(searchBar);
|
||||
|
||||
// Add a debounce timer for the search bar to limit frequent updates
|
||||
|
|
@ -41,37 +52,29 @@ QString VisualDeckStorageSearchWidget::getSearchText()
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the filename used for the search.
|
||||
* 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.
|
||||
*
|
||||
* if includeFolderName is true, then this returns the relative filepath starting from the deck folder.
|
||||
* If the file isn't in the deck folder, or includeFolderName is false, then this will just return the filename.
|
||||
*
|
||||
* @param filePath The filePath to convert into a search name
|
||||
* @param filePath The filepath to convert into a relative filepath
|
||||
*/
|
||||
static QString getFileSearchName(const QString &filePath, bool includeFolderName)
|
||||
static QString toRelativeFilepath(const QString &filePath)
|
||||
{
|
||||
QString deckPath = SettingsCache::instance().getDeckPath();
|
||||
if (includeFolderName && filePath.startsWith(deckPath)) {
|
||||
return filePath.mid(deckPath.length()).toLower();
|
||||
if (filePath.startsWith(deckPath)) {
|
||||
return filePath.mid(deckPath.length());
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString fileName = fileInfo.fileName().toLower();
|
||||
QString fileName = fileInfo.fileName();
|
||||
return fileName;
|
||||
}
|
||||
|
||||
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets,
|
||||
const QString &searchText,
|
||||
bool includeFolderName)
|
||||
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText)
|
||||
{
|
||||
if (searchText.isEmpty() || searchText.isNull()) {
|
||||
for (auto widget : widgets) {
|
||||
widget->filteredBySearch = false;
|
||||
}
|
||||
}
|
||||
auto filterString = DeckFilterString(searchText);
|
||||
|
||||
for (auto file : widgets) {
|
||||
QString fileSearchName = getFileSearchName(file->filePath, includeFolderName);
|
||||
file->filteredBySearch = !fileSearchName.contains(searchText.toLower());
|
||||
for (auto widget : widgets) {
|
||||
QString relativeFilePath = toRelativeFilepath(widget->filePath);
|
||||
widget->filteredBySearch = !filterString.check(widget, {relativeFilePath});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class VisualDeckStorageSearchWidget : public QWidget
|
|||
public:
|
||||
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
|
||||
QString getSearchText();
|
||||
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText, bool includeFolderName);
|
||||
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText);
|
||||
|
||||
private:
|
||||
QHBoxLayout *layout;
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
&VisualDeckStorageWidget::updateShowFolders);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this,
|
||||
&VisualDeckStorageWidget::updateTagsVisibility);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::searchFolderNamesChanged, this,
|
||||
&VisualDeckStorageWidget::updateSearchFilter);
|
||||
|
||||
searchAndSortLayout->addWidget(deckPreviewColorIdentityFilterWidget);
|
||||
searchAndSortLayout->addWidget(sortWidget);
|
||||
|
|
@ -196,8 +194,7 @@ void VisualDeckStorageWidget::updateColorFilter()
|
|||
void VisualDeckStorageWidget::updateSearchFilter()
|
||||
{
|
||||
if (folderWidget) {
|
||||
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText(),
|
||||
quickSettingsWidget->getSearchFolderNames());
|
||||
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText());
|
||||
folderWidget->updateVisibility();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1099,7 +1099,7 @@ void MainWindow::cardDatabaseLoadingFailed()
|
|||
|
||||
void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
|
||||
{
|
||||
QMessageBox msgBox;
|
||||
QMessageBox msgBox(this);
|
||||
msgBox.setWindowTitle(tr("New sets found"));
|
||||
msgBox.setIcon(QMessageBox::Question);
|
||||
msgBox.setText(tr("%n new set(s) found in the card database\n"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,24 @@ DeckListModel::~DeckListModel()
|
|||
delete root;
|
||||
}
|
||||
|
||||
QString DeckListModel::getSortCriteriaForCard(CardInfoPtr info)
|
||||
{
|
||||
if (!info) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
switch (activeGroupCriteria) {
|
||||
case DeckListModelGroupCriteria::MAIN_TYPE:
|
||||
return info->getMainCardType();
|
||||
case DeckListModelGroupCriteria::MANA_COST:
|
||||
return info->getCmc();
|
||||
case DeckListModelGroupCriteria::COLOR:
|
||||
return info->getColors() == "" ? "Colorless" : info->getColors();
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void DeckListModel::rebuildTree()
|
||||
{
|
||||
beginResetModel();
|
||||
|
|
@ -49,7 +67,7 @@ void DeckListModel::rebuildTree()
|
|||
}
|
||||
|
||||
CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName());
|
||||
QString cardType = info ? info->getMainCardType() : "unknown";
|
||||
QString cardType = getSortCriteriaForCard(info);
|
||||
|
||||
auto *cardTypeNode = dynamic_cast<InnerDecklistNode *>(node->findChild(cardType));
|
||||
|
||||
|
|
@ -467,6 +485,12 @@ void DeckListModel::sort(int column, Qt::SortOrder order)
|
|||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria)
|
||||
{
|
||||
activeGroupCriteria = newCriteria;
|
||||
rebuildTree();
|
||||
}
|
||||
|
||||
void DeckListModel::cleanList()
|
||||
{
|
||||
setDeckList(new DeckLoader);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ class CardDatabase;
|
|||
class QPrinter;
|
||||
class QTextCursor;
|
||||
|
||||
enum DeckListModelGroupCriteria
|
||||
{
|
||||
MAIN_TYPE,
|
||||
MANA_COST,
|
||||
COLOR
|
||||
};
|
||||
|
||||
class DecklistModelCardNode : public AbstractDecklistCardNode
|
||||
{
|
||||
private:
|
||||
|
|
@ -85,6 +92,7 @@ signals:
|
|||
public:
|
||||
explicit DeckListModel(QObject *parent = nullptr);
|
||||
~DeckListModel() override;
|
||||
QString getSortCriteriaForCard(CardInfoPtr info);
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
|
@ -113,10 +121,12 @@ public:
|
|||
QList<CardInfoPtr> getCardsAsCardInfoPtrs() const;
|
||||
QList<CardInfoPtr> getCardsAsCardInfoPtrsForZone(QString zoneName) const;
|
||||
QList<QString> *getZones() const;
|
||||
void setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria);
|
||||
|
||||
private:
|
||||
DeckLoader *deckList;
|
||||
InnerDecklistNode *root;
|
||||
DeckListModelGroupCriteria activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
|
||||
int lastKnownColumn;
|
||||
Qt::SortOrder lastKnownOrder;
|
||||
InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "../../client/ui/picture_loader/picture_loader.h"
|
||||
#include "../../deck/deck_loader.h"
|
||||
#include "../../dialogs/dlg_load_deck.h"
|
||||
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
|
||||
#include "../../dialogs/dlg_load_remote_deck.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
|
|
@ -52,6 +53,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
{
|
||||
loadLocalButton = new QPushButton;
|
||||
loadRemoteButton = new QPushButton;
|
||||
loadFromClipboardButton = new QPushButton;
|
||||
unloadDeckButton = new QPushButton;
|
||||
readyStartButton = new ToggleButton;
|
||||
forceStartGameButton = new QPushButton;
|
||||
|
|
@ -59,6 +61,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
|
||||
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
|
||||
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
|
||||
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);
|
||||
connect(readyStartButton, &QPushButton::clicked, this, &DeckViewContainer::readyStart);
|
||||
connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck);
|
||||
connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart);
|
||||
|
|
@ -68,6 +71,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
auto *buttonHBox = new QHBoxLayout;
|
||||
buttonHBox->addWidget(loadLocalButton);
|
||||
buttonHBox->addWidget(loadRemoteButton);
|
||||
buttonHBox->addWidget(loadFromClipboardButton);
|
||||
buttonHBox->addWidget(unloadDeckButton);
|
||||
buttonHBox->addWidget(readyStartButton);
|
||||
buttonHBox->addWidget(sideboardLockButton);
|
||||
|
|
@ -118,6 +122,7 @@ void DeckViewContainer::retranslateUi()
|
|||
{
|
||||
loadLocalButton->setText(tr("Load deck..."));
|
||||
loadRemoteButton->setText(tr("Load remote deck..."));
|
||||
loadFromClipboardButton->setText(tr("Load from clipboard..."));
|
||||
unloadDeckButton->setText(tr("Unload deck"));
|
||||
readyStartButton->setText(tr("Ready to start"));
|
||||
forceStartGameButton->setText(tr("Force start"));
|
||||
|
|
@ -148,6 +153,7 @@ void DeckViewContainer::switchToDeckSelectView()
|
|||
|
||||
setVisibility(loadLocalButton, true);
|
||||
setVisibility(loadRemoteButton, !parentGame->getIsLocalGame());
|
||||
setVisibility(loadFromClipboardButton, true);
|
||||
setVisibility(unloadDeckButton, false);
|
||||
setVisibility(readyStartButton, false);
|
||||
setVisibility(sideboardLockButton, false);
|
||||
|
|
@ -172,6 +178,7 @@ void DeckViewContainer::switchToDeckLoadedView()
|
|||
|
||||
setVisibility(loadLocalButton, false);
|
||||
setVisibility(loadRemoteButton, false);
|
||||
setVisibility(loadFromClipboardButton, false);
|
||||
setVisibility(unloadDeckButton, true);
|
||||
setVisibility(readyStartButton, true);
|
||||
setVisibility(sideboardLockButton, true);
|
||||
|
|
@ -198,8 +205,11 @@ void DeckViewContainer::refreshShortcuts()
|
|||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
|
||||
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
|
||||
loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton"));
|
||||
unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton"));
|
||||
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
|
||||
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
|
||||
forceStartGameButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/forceStartGameButton"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -247,19 +257,27 @@ void DeckViewContainer::loadLocalDeck()
|
|||
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
||||
{
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
|
||||
QString deckString;
|
||||
DeckLoader deck;
|
||||
|
||||
bool error = !deck.loadFromFile(filePath, fmt, true);
|
||||
if (!error) {
|
||||
deckString = deck.writeToString_Native();
|
||||
error = deckString.length() > MAX_FILE_LENGTH;
|
||||
}
|
||||
if (error) {
|
||||
bool success = deck.loadFromFile(filePath, fmt, true);
|
||||
|
||||
if (!success) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
|
||||
return;
|
||||
}
|
||||
|
||||
loadDeckFromDeckLoader(&deck);
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadDeckFromDeckLoader(const DeckLoader *deck)
|
||||
{
|
||||
QString deckString = deck->writeToString_Native();
|
||||
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size."));
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckSelect cmd;
|
||||
cmd.set_deck(deckString.toStdString());
|
||||
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
|
||||
|
|
@ -279,6 +297,18 @@ void DeckViewContainer::loadRemoteDeck()
|
|||
}
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadFromClipboard()
|
||||
{
|
||||
auto dlg = DlgLoadDeckFromClipboard(this);
|
||||
|
||||
if (!dlg.exec()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DeckLoader *deck = dlg.getDeckList();
|
||||
loadDeckFromDeckLoader(deck);
|
||||
}
|
||||
|
||||
void DeckViewContainer::deckSelectFinished(const Response &r)
|
||||
{
|
||||
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ class DeckViewContainer : public QWidget
|
|||
Q_OBJECT
|
||||
private:
|
||||
QVBoxLayout *deckViewLayout;
|
||||
QPushButton *loadLocalButton, *loadRemoteButton, *unloadDeckButton, *forceStartGameButton;
|
||||
QPushButton *loadLocalButton, *loadRemoteButton, *loadFromClipboardButton;
|
||||
QPushButton *unloadDeckButton, *forceStartGameButton;
|
||||
ToggleButton *readyStartButton, *sideboardLockButton;
|
||||
DeckView *deckView;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
|
|
@ -58,6 +59,7 @@ private slots:
|
|||
void switchToDeckLoadedView();
|
||||
void loadLocalDeck();
|
||||
void loadRemoteDeck();
|
||||
void loadFromClipboard();
|
||||
void unloadDeck();
|
||||
void readyStart();
|
||||
void forceStart();
|
||||
|
|
@ -81,6 +83,7 @@ public:
|
|||
|
||||
public slots:
|
||||
void loadDeckFromFile(const QString &filePath);
|
||||
void loadDeckFromDeckLoader(const DeckLoader *deck);
|
||||
};
|
||||
|
||||
#endif // DECK_VIEW_CONTAINER_H
|
||||
|
|
|
|||
193
cockatrice/src/game/filters/deck_filter_string.cpp
Normal file
193
cockatrice/src/game/filters/deck_filter_string.cpp
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
#include "deck_filter_string.h"
|
||||
|
||||
#include "../cards/card_database_manager.h"
|
||||
#include "filter_string.h"
|
||||
#include "lib/peglib.h"
|
||||
|
||||
static peg::parser search(R"(
|
||||
Start <- QueryPartList
|
||||
~ws <- [ ]+
|
||||
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
||||
|
||||
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
|
||||
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
|
||||
|
||||
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / GenericQuery
|
||||
|
||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||
|
||||
DeckContentQuery <- CardSearch NumericExpression?
|
||||
CardSearch <- '[[' CardFilterString ']]'
|
||||
CardFilterString <- (!']]'.)*
|
||||
|
||||
DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
|
||||
FileNameQuery <- [Ff] ('ile' 'name'?)? [:] String
|
||||
PathQuery <- [Pp] 'ath'? [:] String
|
||||
|
||||
GenericQuery <- String
|
||||
|
||||
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
|
||||
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
|
||||
UnescapedStringListPart <- !['":<>=! ].
|
||||
SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)*
|
||||
|
||||
String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuoteUnlessEscaped*> ["] / ['] <NonSingleQuoteUnlessEscaped*> [']
|
||||
|
||||
NumericExpression <- NumericOperator ws? NumericValue
|
||||
NumericOperator <- [=:] / <[><!][=]?>
|
||||
NumericValue <- [0-9]+
|
||||
)");
|
||||
|
||||
static std::once_flag init;
|
||||
|
||||
static void setupParserRules()
|
||||
{
|
||||
// plumbing
|
||||
auto passthru = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
return !sv.empty() ? std::any_cast<DeckFilter>(sv[0]) : nullptr;
|
||||
};
|
||||
|
||||
search["Start"] = passthru;
|
||||
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
||||
};
|
||||
return std::all_of(sv.begin(), sv.end(), matchesFilter);
|
||||
};
|
||||
};
|
||||
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
|
||||
auto matchesFilter = [&deck, &info](const std::any &query) {
|
||||
return std::any_cast<DeckFilter>(query)(deck, info);
|
||||
};
|
||||
return std::any_of(sv.begin(), sv.end(), matchesFilter);
|
||||
};
|
||||
};
|
||||
search["SomewhatComplexQueryPart"] = passthru;
|
||||
search["QueryPart"] = passthru;
|
||||
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool {
|
||||
return !dependent(deck, info);
|
||||
};
|
||||
};
|
||||
|
||||
search["String"] = [](const peg::SemanticValues &sv) -> QString {
|
||||
if (sv.choice() == 0) {
|
||||
return QString::fromStdString(std::string(sv.sv()));
|
||||
}
|
||||
|
||||
return QString::fromStdString(std::string(sv.token(0)));
|
||||
};
|
||||
|
||||
search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher {
|
||||
const auto arg = std::any_cast<int>(sv[1]);
|
||||
const auto op = std::any_cast<QString>(sv[0]);
|
||||
|
||||
if (op == ">")
|
||||
return [=](const int s) { return s > arg; };
|
||||
if (op == ">=")
|
||||
return [=](const int s) { return s >= arg; };
|
||||
if (op == "<")
|
||||
return [=](const int s) { return s < arg; };
|
||||
if (op == "<=")
|
||||
return [=](const int s) { return s <= arg; };
|
||||
if (op == "=")
|
||||
return [=](const int s) { return s == arg; };
|
||||
if (op == ":")
|
||||
return [=](const int s) { return s == arg; };
|
||||
if (op == "!=")
|
||||
return [=](const int s) { return s != arg; };
|
||||
return [](int) { return false; };
|
||||
};
|
||||
|
||||
search["NumericValue"] = [](const peg::SemanticValues &sv) -> int {
|
||||
return QString::fromStdString(std::string(sv.sv())).toInt();
|
||||
};
|
||||
|
||||
search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString {
|
||||
return QString::fromStdString(std::string(sv.sv()));
|
||||
};
|
||||
|
||||
// actual functionality
|
||||
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
|
||||
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
|
||||
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
|
||||
int count = 0;
|
||||
deck->deckLoader->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
|
||||
auto cardInfoPtr = CardDatabaseManager::getInstance()->getCard(node->getName());
|
||||
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
|
||||
count += node->getNumber();
|
||||
}
|
||||
});
|
||||
return numberMatcher(count);
|
||||
};
|
||||
};
|
||||
|
||||
search["CardSearch"] = [](const peg::SemanticValues &sv) -> QString { return std::any_cast<QString>(sv[0]); };
|
||||
|
||||
search["CardFilterString"] = [](const peg::SemanticValues &sv) -> QString {
|
||||
return QString::fromStdString(std::string(sv.sv()));
|
||||
};
|
||||
|
||||
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
return deck->deckLoader->getName().contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
auto filename = QFileInfo(deck->filePath).fileName();
|
||||
return filename.contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
|
||||
return info.relativeFilePath.contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
|
||||
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
|
||||
return deck->getDisplayName().contains(name, Qt::CaseInsensitive);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
DeckFilterString::DeckFilterString()
|
||||
{
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
||||
_error = "Not initialized";
|
||||
}
|
||||
|
||||
DeckFilterString::DeckFilterString(const QString &expr)
|
||||
{
|
||||
QByteArray ba = expr.simplified().toUtf8();
|
||||
|
||||
std::call_once(init, setupParserRules);
|
||||
|
||||
_error = QString();
|
||||
|
||||
if (ba.isEmpty()) {
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; };
|
||||
return;
|
||||
}
|
||||
|
||||
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
|
||||
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
|
||||
});
|
||||
|
||||
if (!search.parse(ba.data(), filter)) {
|
||||
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
|
||||
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
|
||||
}
|
||||
}
|
||||
51
cockatrice/src/game/filters/deck_filter_string.h
Normal file
51
cockatrice/src/game/filters/deck_filter_string.h
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#ifndef DECK_FILTER_STRING_H
|
||||
#define DECK_FILTER_STRING_H
|
||||
|
||||
#include "../../client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
|
||||
|
||||
/**
|
||||
* Extra info relevant to filtering that isn't present in the DeckPreviewWidget
|
||||
*/
|
||||
struct ExtraDeckSearchInfo
|
||||
{
|
||||
/**
|
||||
* The relative filepath starting from the deck folder
|
||||
*/
|
||||
QString relativeFilePath;
|
||||
};
|
||||
|
||||
typedef std::function<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> DeckFilter;
|
||||
|
||||
class DeckFilterString
|
||||
{
|
||||
public:
|
||||
DeckFilterString();
|
||||
explicit DeckFilterString(const QString &expr);
|
||||
bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const
|
||||
{
|
||||
return filter(deck, info);
|
||||
}
|
||||
|
||||
bool valid() const
|
||||
{
|
||||
return _error.isEmpty();
|
||||
}
|
||||
|
||||
QString error()
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
private:
|
||||
QString _error;
|
||||
DeckFilter filter;
|
||||
};
|
||||
#endif // DECK_FILTER_STRING_H
|
||||
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
peg::parser search(R"(
|
||||
static peg::parser search(R"(
|
||||
Start <- QueryPartList
|
||||
~ws <- [ ]+
|
||||
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
||||
|
|
@ -20,7 +21,7 @@ QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQ
|
|||
|
||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||
SetQuery <- ('e'/'set') [:] FlexStringValue
|
||||
OracleQuery <- 'o' [:] RegexString
|
||||
OracleQuery <- 'o' [:] MatcherString
|
||||
|
||||
|
||||
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
|
||||
|
|
@ -42,7 +43,7 @@ ColorEx <- Color / [mc]
|
|||
|
||||
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
|
||||
|
||||
FieldQuery <- String [:] RegexString / String ws? NumericExpression
|
||||
FieldQuery <- String [:] MatcherString / String ws? NumericExpression
|
||||
|
||||
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
|
||||
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
|
||||
|
|
@ -52,8 +53,14 @@ String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuot
|
|||
StringValue <- String / [(] StringList [)]
|
||||
StringList <- StringListString (ws? [,] ws? StringListString)*
|
||||
StringListString <- UnescapedStringListPart+
|
||||
GenericQuery <- RegexString
|
||||
RegexString <- String
|
||||
GenericQuery <- MatcherString
|
||||
|
||||
# A String that can either be a normal string or a regex search string
|
||||
MatcherString <- RegexMatcher / NormalMatcher
|
||||
|
||||
NormalMatcher <- String
|
||||
RegexMatcher <- '/' RegexMatcherString '/'
|
||||
RegexMatcherString <- ('\\/' / !'/' .)+
|
||||
|
||||
FlexStringValue <- CompactStringSet / String / [(] StringList [)]
|
||||
CompactStringSet <- StringListString ([,+] StringListString)+
|
||||
|
|
@ -63,7 +70,7 @@ NumericOperator <- [=:] / <[><!][=]?>
|
|||
NumericValue <- [0-9]+
|
||||
)");
|
||||
|
||||
std::once_flag init;
|
||||
static std::once_flag init;
|
||||
|
||||
static void setupParserRules()
|
||||
{
|
||||
|
|
@ -261,14 +268,22 @@ static void setupParserRules()
|
|||
return QString::fromStdString(std::string(sv.sv()));
|
||||
};
|
||||
|
||||
search["RegexString"] = [](const peg::SemanticValues &sv) -> StringMatcher {
|
||||
search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
|
||||
auto target = std::any_cast<QString>(sv[0]);
|
||||
return [=](const QString &s) {
|
||||
auto sanitizedTarget = QString(target);
|
||||
sanitizedTarget.replace("\\\"", "\"");
|
||||
sanitizedTarget.replace("\\'", "'");
|
||||
return s.contains(sanitizedTarget, Qt::CaseInsensitive);
|
||||
};
|
||||
auto sanitizedTarget = QString(target);
|
||||
sanitizedTarget.replace("\\\"", "\"");
|
||||
sanitizedTarget.replace("\\'", "'");
|
||||
return [=](const QString &s) { return s.contains(sanitizedTarget, Qt::CaseInsensitive); };
|
||||
};
|
||||
|
||||
search["RegexMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
|
||||
auto target = std::any_cast<QString>(sv[0]);
|
||||
auto regex = QRegularExpression(target, QRegularExpression::CaseInsensitiveOption);
|
||||
return [=](const QString &s) { return regex.match(s).hasMatch(); };
|
||||
};
|
||||
|
||||
search["RegexMatcherString"] = [](const peg::SemanticValues &sv) -> QString {
|
||||
return QString::fromStdString(sv.token_to_string());
|
||||
};
|
||||
|
||||
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
*
|
||||
* @return the QTextBrowser
|
||||
*/
|
||||
static QTextBrowser *createBrowser()
|
||||
static QTextBrowser *createBrowser(const QString &helpFile)
|
||||
{
|
||||
QFile file("theme:help/search.md");
|
||||
QFile file(helpFile);
|
||||
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||
qCWarning(SyntaxHelpLog) << "Could not open syntax help file: " << helpFile;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -54,9 +55,29 @@ static QTextBrowser *createBrowser()
|
|||
*/
|
||||
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit)
|
||||
{
|
||||
auto browser = createBrowser();
|
||||
auto browser = createBrowser("theme:help/search.md");
|
||||
QObject::connect(browser, &QTextBrowser::anchorClicked,
|
||||
[lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); });
|
||||
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the deck search syntax help window and connects its anchorClicked signal to the given QLineEdit.
|
||||
* The window will automatically close when the QLineEdit is destroyed.
|
||||
*
|
||||
* @return the QTextBrowser
|
||||
*/
|
||||
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit)
|
||||
{
|
||||
auto browser = createBrowser("theme:help/deck_search.md");
|
||||
QObject::connect(browser, &QTextBrowser::anchorClicked, [lineEdit](const QUrl &link) {
|
||||
if (link.fragment() == "cardSearchSyntaxHelp") {
|
||||
createSearchSyntaxHelpWindow(lineEdit);
|
||||
} else {
|
||||
lineEdit->setText(link.fragment());
|
||||
}
|
||||
});
|
||||
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
|
||||
return browser;
|
||||
}
|
||||
|
|
@ -2,8 +2,13 @@
|
|||
#define SEARCH_SYNTAX_HELP_H
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QLoggingCategory>
|
||||
#include <QTextBrowser>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help");
|
||||
|
||||
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit);
|
||||
|
||||
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit);
|
||||
|
||||
#endif // SEARCH_SYNTAX_HELP_H
|
||||
|
|
|
|||
|
|
@ -275,7 +275,6 @@ SettingsCache::SettingsCache()
|
|||
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
|
||||
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
|
||||
visualDeckStorageShowTagFilter = settings->value("interface/visualdeckstorageshowtagfilter", true).toBool();
|
||||
visualDeckStorageSearchFolderNames = settings->value("interface/visualdeckstoragesearchfoldernames", true).toBool();
|
||||
visualDeckStorageShowBannerCardComboBox =
|
||||
settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool();
|
||||
visualDeckStorageShowTagsOnDeckPreviews =
|
||||
|
|
@ -284,6 +283,7 @@ SettingsCache::SettingsCache()
|
|||
settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool();
|
||||
visualDeckStorageUnusedColorIdentitiesOpacity =
|
||||
settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt();
|
||||
visualDeckStorageTooltipType = settings->value("interface/visualdeckstoragetooltiptype", 0).toInt();
|
||||
visualDeckStoragePromptForConversion =
|
||||
settings->value("interface/visualdeckstoragepromptforconversion", true).toBool();
|
||||
visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool();
|
||||
|
|
@ -734,12 +734,6 @@ void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTa
|
|||
emit visualDeckStorageShowTagFilterChanged(visualDeckStorageShowTagFilter);
|
||||
}
|
||||
|
||||
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value)
|
||||
{
|
||||
visualDeckStorageSearchFolderNames = value;
|
||||
settings->setValue("interface/visualdeckstoragesearchfoldernames", visualDeckStorageSearchFolderNames);
|
||||
}
|
||||
|
||||
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox)
|
||||
{
|
||||
visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox;
|
||||
|
|
@ -778,6 +772,12 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visual
|
|||
emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(visualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
}
|
||||
|
||||
void SettingsCache::setVisualDeckStorageTooltipType(int value)
|
||||
{
|
||||
visualDeckStorageTooltipType = value;
|
||||
settings->setValue("interface/visualdeckstoragetooltiptype", visualDeckStorageTooltipType);
|
||||
}
|
||||
|
||||
void SettingsCache::setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion)
|
||||
{
|
||||
visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion;
|
||||
|
|
|
|||
|
|
@ -148,10 +148,10 @@ private:
|
|||
bool visualDeckStorageShowBannerCardComboBox;
|
||||
bool visualDeckStorageShowTagsOnDeckPreviews;
|
||||
bool visualDeckStorageShowTagFilter;
|
||||
bool visualDeckStorageSearchFolderNames;
|
||||
int visualDeckStorageCardSize;
|
||||
bool visualDeckStorageDrawUnusedColorIdentities;
|
||||
int visualDeckStorageUnusedColorIdentitiesOpacity;
|
||||
int visualDeckStorageTooltipType;
|
||||
bool visualDeckStoragePromptForConversion;
|
||||
bool visualDeckStorageAlwaysConvert;
|
||||
bool visualDeckStorageInGame;
|
||||
|
|
@ -454,10 +454,6 @@ public:
|
|||
{
|
||||
return visualDeckStorageShowTagFilter;
|
||||
}
|
||||
bool getVisualDeckStorageSearchFolderNames() const
|
||||
{
|
||||
return visualDeckStorageSearchFolderNames;
|
||||
}
|
||||
bool getVisualDeckStorageShowBannerCardComboBox() const
|
||||
{
|
||||
return visualDeckStorageShowBannerCardComboBox;
|
||||
|
|
@ -478,6 +474,10 @@ public:
|
|||
{
|
||||
return visualDeckStorageUnusedColorIdentitiesOpacity;
|
||||
}
|
||||
int getVisualDeckStorageTooltipType() const
|
||||
{
|
||||
return visualDeckStorageTooltipType;
|
||||
}
|
||||
bool getVisualDeckStoragePromptForConversion() const
|
||||
{
|
||||
return visualDeckStoragePromptForConversion;
|
||||
|
|
@ -850,12 +850,12 @@ public slots:
|
|||
void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder);
|
||||
void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value);
|
||||
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);
|
||||
void setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value);
|
||||
void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox);
|
||||
void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags);
|
||||
void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize);
|
||||
void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities);
|
||||
void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
void setVisualDeckStorageTooltipType(int value);
|
||||
void setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion);
|
||||
void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert);
|
||||
void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value);
|
||||
|
|
|
|||
|
|
@ -267,6 +267,13 @@ private:
|
|||
{"DeckViewContainer/loadRemoteButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Remote Deck..."),
|
||||
parseSequenceString("Ctrl+Alt+O"),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
{"DeckViewContainer/loadFromClipboardButton",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."),
|
||||
parseSequenceString("Ctrl+Shift+V"),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
{"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"),
|
||||
parseSequenceString("Ctrl+Alt+U"),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
{"DeckViewContainer/readyStartButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Ready to Start"),
|
||||
parseSequenceString("Ctrl+Shift+S"),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
|
|
@ -274,6 +281,9 @@ private:
|
|||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Sideboard Lock"),
|
||||
parseSequenceString("Ctrl+Shift+B"),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
{"DeckViewContainer/forceStartGameButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Force Start"),
|
||||
parseSequenceString(""),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
{"Player/aCCMagenta", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Card Counter (F)"),
|
||||
parseSequenceString(""),
|
||||
ShortcutGroup::Card_Counters)},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue