(.*?)",
- QRegularExpression::DotMatchesEverythingOption);
- const auto match = errorsRx.match(data);
- if (match.hasMatch()) {
- static const QRegularExpression itemRx("
", QRegularExpression::DotMatchesEverythingOption);
- auto items = itemRx.globalMatch(match.captured(1));
- while (items.hasNext()) {
- const QString error = QTextDocumentFragment::fromHtml(items.next().captured(1)).toPlainText().simplified();
- if (!error.isEmpty()) {
- errorMessageList.append(error);
+ int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (reply->hasRawHeader("Location")) {
+ /*
+ * If the reply contains a "Location" header, a relative URL to the deck on TappedOut
+ * can be extracted from the header. The http status is a 302 "redirect".
+ */
+ QString deckUrl = reply->rawHeader("Location");
+ qCInfo(TappedOutInterfaceLog) << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
+ QDesktopServices::openUrl("https://tappedout.net" + deckUrl);
+ } else {
+ /*
+ * Otherwise, the deck has not been parsed correctly. Error messages can be extracted
+ * from the html. Css pseudo selector for errors: $("div.alert-danger > ul > li")
+ */
+ QString data(reply->readAll());
+ QStringList errorMessageList = {tr("Unable to analyze the deck.")};
+
+ static const QRegularExpression rx("
(.*?)");
+ auto match = rx.match(data);
+ if (match.hasMatch()) {
+ QString errors = match.captured(1);
+ static const QRegularExpression rx2("
(.*?)");
+ static const QRegularExpression rxremove("<[^>]*>");
+ auto matchIterator = rx2.globalMatch(errors);
+ while (matchIterator.hasNext()) {
+ auto match2 = matchIterator.next();
+ errorMessageList.append(match2.captured(1).remove(rxremove).simplified());
}
}
+
+ QString errorMessage = errorMessageList.join("\n");
+ qCWarning(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size()
+ << "message" << errorMessage;
+
+ QMessageBox::critical(nullptr, tr("Error"), errorMessage);
}
- qCWarning(TappedOutInterfaceLog) << "TappedOut analysis failed, HTTP status" << httpStatus;
- showError(errorMessageList.join('\n'));
+
+ reply->deleteLater();
+ deleteLater();
}
void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
@@ -106,9 +90,13 @@ void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray
DeckList mainboard, sideboard;
copyDeckSplitMainAndSide(deck, mainboard, sideboard);
- data.append("name=" + QUrl::toPercentEncoding(deck.getName()));
- data.append("&mainboard=" + QUrl::toPercentEncoding(mainboard.writeToString_Plain(false, true)));
- data.append("&sideboard=" + QUrl::toPercentEncoding(sideboard.writeToString_Plain(false, true)));
+ QUrl params;
+ QUrlQuery urlQuery;
+ urlQuery.addQueryItem("name", deck.getName());
+ urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true));
+ urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true));
+ params.setQuery(urlQuery);
+ data.append(params.query(QUrl::EncodeReserved).toUtf8());
}
void TappedOutInterface::analyzeDeck(const DeckList &deck)
@@ -129,9 +117,7 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi
{
auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
- // Exclude confirmed tokens, but preserve cards missing from the local
- // database, including newly released cards in an imported deck.
- if ((dbCard && dbCard->getIsToken()) || node->getName() == DECK_ZONE_MAYBEBOARD) {
+ if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) {
return;
}
diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.h b/cockatrice/src/client/network/interfaces/tapped_out_interface.h
index 27e8c2273..615991b07 100644
--- a/cockatrice/src/client/network/interfaces/tapped_out_interface.h
+++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.h
@@ -14,7 +14,6 @@
inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface");
class QByteArray;
-class QString;
class QNetworkAccessManager;
class QNetworkReply;
class DeckList;
@@ -30,8 +29,6 @@ class TappedOutInterface : public QObject
Q_OBJECT
private:
QNetworkAccessManager *manager;
- bool replyHandled = false;
- void showError(const QString &message);
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
private slots:
diff --git a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp
index 2b307735d..7127b0a29 100644
--- a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp
+++ b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.cpp
@@ -15,18 +15,14 @@ static const QString ARCHIDEKT_SUFFIX = "/?format=json";
static const QString MOXFIELD_BASE = "https://api.moxfield.com/v2/decks/all/";
static const QString MOXFIELD_SUFFIX = "/";
-static const QString DECKSTATS_BASE = "https://deckstats.net/decks/";
static const QString DECKSTATS_SUFFIX = "?include_comments=1&export_mtgarena=1";
-static const QString MANABOX_BASE = "https://cloud.manabox.app/decks/";
-
bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo)
{
static QRegularExpression rxTappedOut("tappedout\\.net/(?:mtg-decks/)?([^/?#]+)");
static QRegularExpression rxArchidekt("archidekt\\.com/decks/(\\d+)");
static QRegularExpression rxMoxfield("moxfield\\.com/decks/([a-zA-Z0-9_-]+)");
static QRegularExpression rxDeckstats("deckstats\\.net/decks/(\\d+/[a-zA-Z0-9_-]+)");
- static QRegularExpression rxManabox("manabox\\.app/decks/([a-zA-Z0-9_-]+)");
QRegularExpressionMatch match;
@@ -53,18 +49,11 @@ bool parseDeckUrl(const QString &url, ParsedDeckInfo &outInfo)
return true;
} else if ((match = rxDeckstats.match(url)).hasMatch()) {
QString deckPath = match.captured(1);
- outInfo = ParsedDeckInfo{.baseUrl = DECKSTATS_BASE,
+ outInfo = ParsedDeckInfo{.baseUrl = "https://deckstats.net/decks/",
.deckID = deckPath,
- .fullUrl = DECKSTATS_BASE + deckPath + DECKSTATS_SUFFIX,
+ .fullUrl = "https://deckstats.net/decks/" + deckPath + DECKSTATS_SUFFIX,
.provider = DeckProvider::Deckstats};
return true;
- } else if ((match = rxManabox.match(url)).hasMatch()) {
- QString deckPath = match.captured(1);
- outInfo = ParsedDeckInfo{.baseUrl = MANABOX_BASE,
- .deckID = deckPath,
- .fullUrl = MANABOX_BASE + deckPath,
- .provider = DeckProvider::Manabox};
- return true;
}
return false;
diff --git a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h
index d3bff976f..bbdd70c97 100644
--- a/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h
+++ b/cockatrice/src/client/network/parsers/deck_link_to_api_transformer.h
@@ -15,7 +15,6 @@ enum class DeckProvider
Archidekt,
Moxfield,
Deckstats,
- Manabox,
Unknown
};
diff --git a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h
index 7e15d472f..87fde4d54 100644
--- a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h
+++ b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h
@@ -8,19 +8,12 @@
#define INTERFACE_JSON_DECK_PARSER_H
#include "../../../interface/deck_loader/card_node_function.h"
-#include "libcockatrice/deck_list/tree/inner_deck_list_node.h"
#include
#include
-#include
-#include
-#include
-#include
#include
#include
-inline Q_LOGGING_CATEGORY(InterfaceJsonDeckParserLog, "interface_json_deck_parser");
-
class IJsonDeckParser
{
public:
@@ -39,6 +32,9 @@ public:
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
+ deckList.setName(deckName);
+ deckList.setComments(deckDescription);
+
QString outputText;
QTextStream outStream(&outputText);
@@ -54,14 +50,8 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
- outStream.flush();
- outStream.seek(0);
deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
return deckList;
}
@@ -77,6 +67,9 @@ public:
QString deckName = obj.value("name").toString();
QString deckDescription = obj.value("description").toString();
+ deckList.setName(deckName);
+ deckList.setComments(deckDescription);
+
QString outputText;
QTextStream outStream(&outputText);
@@ -104,14 +97,8 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
}
- outStream.flush();
- outStream.seek(0);
deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
deckList.forEachCard(CardNodeFunction::ResolveProviderId());
- deckList.setName(deckName);
- deckList.setComments(deckDescription);
QJsonObject commandersObj = obj.value("commanders").toObject();
if (!commandersObj.isEmpty()) {
@@ -127,113 +114,6 @@ public:
}
}
- // Moxfield stores Oathbreaker signature spells in their own board,
- // separate from both mainboard and commanders. They are playable deck
- // cards, but must not replace the Oathbreaker as the banner card.
- const QJsonObject signatureSpells = obj.value("signatureSpells").toObject();
- for (auto it = signatureSpells.begin(); it != signatureSpells.end(); ++it) {
- const QJsonObject entry = it.value().toObject();
- const QJsonObject card = entry.value("card").toObject();
- const QString name = card.value("name").toString();
- const int quantity = entry.value("quantity").toInt();
- if (name.isEmpty() || quantity <= 0) {
- continue;
- }
- const QString set = card.value("set").toString().toUpper();
- const QString collectorNumber = card.value("cn").toString();
- const QString providerId = card.value("scryfall_id").toString();
- deckList.addCards(name, quantity, DECK_ZONE_MAIN, -1, set, collectorNumber, providerId);
- }
-
- return deckList;
- }
-};
-
-class ManaboxJsonParser : public IJsonDeckParser
-{
- enum BoardType : quint32
- {
- MainBoard = 0,
- SideBoard = 1,
- Commander = 2,
- MaybeBoard = 3,
- };
-
-public:
- DeckList parse(const QJsonObject &obj) override
- {
- DeckList deckList;
-
- QString deckName = obj.value("name").toString();
-
- QString outputText;
- QString mainboardText;
- QString sideboardText;
- QTextStream mainboardStream(&mainboardText);
- QTextStream sideboardStream(&sideboardText);
- QTextStream outStream(&outputText);
-
- auto commanderList = QList();
- auto maybeboardList = QList();
- for (auto entry : obj.value("cards").toArray()) {
- auto card = entry.toObject();
-
- auto collectorNumber = card.value("collectorNumber").toString();
- auto cardName = card.value("name").toString();
- auto boardType = static_cast(card.value("boardCategory").toInt());
- auto setName = card.value("setId").toString();
- auto quantity = card.value("quantity").toInt();
- auto providerId = card.value("scryfallId").toString();
-
- switch (boardType) {
- case MainBoard:
- mainboardStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber
- << '\n';
- break;
- case SideBoard:
- sideboardStream << "SB: " << quantity << ' ' << cardName << " (" << setName << ") "
- << collectorNumber << '\n';
- break;
- case MaybeBoard:
- maybeboardList.append(card);
- break;
- case Commander:
- commanderList.append(card);
- break;
- default:
- qCWarning(InterfaceJsonDeckParserLog)
- << "Unknown ManaBox Zone" << card.value("boardCategory").toInt() << "for" << cardName;
- break;
- }
- }
-
- outStream << mainboardText << sideboardText;
-
- deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
- deckList.setName(deckName);
- deckList.forEachCard(CardNodeFunction::ResolveProviderId());
-
- for (const auto &maybeboardCard : maybeboardList) {
- QString cardName = maybeboardCard.value("name").toString();
- QString setName = maybeboardCard.value("setId").toString().toUpper();
- QString collectorNumber = maybeboardCard.value("collectorNumber").toString();
- QString providerId = maybeboardCard.value("scryfallId").toString();
-
- for (int x = 0; x < maybeboardCard.value("quantity").toInt(); x++) {
- deckList.addCard(cardName, DECK_ZONE_MAYBEBOARD, -1, setName, collectorNumber, providerId);
- }
- }
- for (const auto &commanderCard : commanderList) {
-
- QString commanderName = commanderCard.value("name").toString();
- QString setName = commanderCard.value("setId").toString().toUpper();
- QString collectorNumber = commanderCard.value("collectorNumber").toString();
- QString providerId = commanderCard.value("scryfallId").toString();
-
- deckList.setBannerCard({commanderName, providerId});
- deckList.addCard(commanderName, DECK_ZONE_MAIN, -1, setName, collectorNumber, providerId);
- }
-
return deckList;
}
};
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
index d74414d4a..62668c4d9 100644
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
+++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.cpp
@@ -2,305 +2,253 @@
#include "../../../../main.h"
#include "../../../settings/cache_settings.h"
-#include "spoiler_file.h"
-#include
+#include
+#include
+#include
#include
+#include
#include
#include
#include
+#include
+#include
+#include
+#include
#include
#include
#include
+#include
#include
-#include
+#include
#include
#include
-#include
+#include
+#include
#include
#include
#include
#include
#include
+#include
+#include
#include
-namespace
-{
-const QUrl SPOILERS_STATUS_URL(
- QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"));
-const QUrl SPOILERS_URL(QStringLiteral("https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"));
-// Request coordination stays on the GUI thread; reload workers never access it.
-QPointer currentUpdater;
-QPointer> activeReload;
-quint64 newestOperation = 0;
+#define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
+#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
-bool removeSpoilerFile(const QString &path)
+SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
{
- QFile file(path);
- if (!file.exists()) {
- return false;
- }
- if (!file.remove()) {
- qCWarning(SpoilerBackgroundUpdaterLog) << "Could not remove spoiler database" << path << file.errorString();
- return false;
- }
- return true;
-}
-} // namespace
-
-SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *parent, bool forceReload)
- : QObject(parent), enabledAtStart(SettingsCache::instance().downloads().getDownloadSpoilersStatus()),
- forceReload(forceReload), targetPath(SettingsCache::instance().paths().getSpoilerCardDatabasePath()),
- operationId(++newestOperation), manager(NetworkManagerFactory::create(this)), notificationIcon(trayIcon)
-{
- connect(&SettingsCache::instance().downloads(), &DownloadSettings::downloadSpoilerStatusChanged, this,
- &SpoilerBackgroundUpdater::cancelIfSettingsChanged);
- connect(&SettingsCache::instance().paths(), &PathsSettings::cardDatabasePathChanged, this,
- &SpoilerBackgroundUpdater::cancelIfSettingsChanged);
- // Even a disabled/no-op operation completes after its caller can connect signals.
- QTimer::singleShot(0, this, &SpoilerBackgroundUpdater::begin);
-}
-
-void SpoilerBackgroundUpdater::begin()
-{
- if (completed) {
- return;
- }
- if (cancelled || operationId != newestOperation ||
- (reloadOnly ? SettingsCache::instance().downloads().getDownloadSpoilersStatus()
- : (!enabledAtStart || !SettingsCache::instance().downloads().getDownloadSpoilersStatus())) ||
- targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) {
- finish();
- return;
- }
- const auto previous = currentUpdater;
- if (activeReload) {
- // The existing database API stores its final load status after its parser
- // mutex is released. Do not overlap updater-owned calls to that API.
- connect(activeReload, &QFutureWatcher::finished, this, &SpoilerBackgroundUpdater::begin,
- static_cast(Qt::QueuedConnection | Qt::SingleShotConnection));
- if (previous && previous != this) {
- previous->cancel();
- }
- return;
- }
- currentUpdater = this;
- QPointer guard(this);
- if (previous && previous != this) {
- previous->cancel();
- }
- if (!guard) {
- return;
- }
- if (!isCurrent()) {
- finish();
- } else if (reloadOnly) {
- if (QFileInfo::exists(targetPath) && !removeSpoilerFile(targetPath)) {
- finish(tr("The spoiler database could not be removed. Check its file permissions and use Reload Card "
- "Database to retry."));
- return;
- }
- reloadDatabase(false);
+ isSpoilerDownloadEnabled = SettingsCache::instance().downloads().getDownloadSpoilersStatus();
+ if (isSpoilerDownloadEnabled) {
+ // Start the process of checking if we're in spoiler season
+ // File exists means we're in spoiler season
+ startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false);
} else {
- downloadFromURL(SPOILERS_STATUS_URL, false);
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled";
}
}
-bool SpoilerBackgroundUpdater::isCurrent() const
+void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool saveResults)
{
- return !completed && !cancelled && currentUpdater == this && operationId == newestOperation &&
- (reloadOnly != SettingsCache::instance().downloads().getDownloadSpoilersStatus()) &&
- targetPath == SettingsCache::instance().paths().getSpoilerCardDatabasePath();
+ auto spoilerURL = QUrl(url);
+ downloadFromURL(spoilerURL, saveResults);
}
-void SpoilerBackgroundUpdater::cancelIfSettingsChanged()
+void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
{
- if (reloadOnly == SettingsCache::instance().downloads().getDownloadSpoilersStatus() ||
- targetPath != SettingsCache::instance().paths().getSpoilerCardDatabasePath()) {
- cancel();
- }
-}
-
-void SpoilerBackgroundUpdater::cancel()
-{
- if (completed) {
- return;
- }
- cancelled = true;
- const auto reply = activeReply;
- activeReply.clear();
- QPointer guard(this);
- if (reply) {
- reply->abort();
- if (reply) {
- reply->deleteLater();
- }
- }
- // A running reload owns no updater state. Allow its completion to retire its
- // watcher before a heap-owned updater is released by the caller.
- if (guard && !reloading) {
- finish();
- }
-}
-
-void SpoilerBackgroundUpdater::downloadFromURL(const QUrl &url, bool saveResults)
-{
- QNetworkRequest request(url);
+ auto *nam = NetworkManagerFactory::create(this);
+ auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
- request.setTransferTimeout(60000);
- activeReply = manager->get(request);
- if (saveResults) {
- connect(activeReply, &QNetworkReply::finished, this,
- &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
- } else {
- connect(activeReply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
- }
-}
+ QNetworkReply *reply = nam->get(request);
-void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
-{
- auto *reply = qobject_cast(sender());
- if (!reply || reply != activeReply) {
- return;
- }
- activeReply.clear();
- reply->deleteLater();
- if (!isCurrent()) {
- finish();
- return;
- }
- if (reply->error() == QNetworkReply::NoError) {
- downloadFromURL(SPOILERS_URL, true);
- } else if (reply->error() == QNetworkReply::ContentNotFoundError) {
- const bool existed = QFileInfo::exists(targetPath);
- seasonEnded = existed;
- if (existed && !removeSpoilerFile(targetPath)) {
- finish(tr("The spoiler season has ended, but the old spoiler database could not be removed."));
- return;
- }
- if (existed || forceReload) {
- reloadDatabase(false);
- } else {
- finish();
- }
+ if (saveResults) {
+ // This will write out to the file (used for spoiler.xml)
+ connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
} else {
- finish(tr("Could not check spoiler availability: %1").arg(reply->errorString()));
+ // This will check the status (used to see if we're in spoiler season or not)
+ connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
}
}
void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
{
- auto *reply = qobject_cast(sender());
- if (!reply || reply != activeReply) {
- return;
- }
- activeReply.clear();
- reply->deleteLater();
- if (!isCurrent()) {
- finish();
- return;
- }
- if (reply->error() != QNetworkReply::NoError) {
- finish(tr("Could not download spoilers: %1").arg(reply->errorString()));
- return;
- }
- spoilerData = reply->readAll();
- QString error;
- const auto result = SpoilerFile::save(targetPath, spoilerData, [this] { return isCurrent(); }, error);
- if (result == SpoilerFile::SaveResult::Failed) {
- finish(isCurrent() ? error : QString());
- } else if (result == SpoilerFile::SaveResult::Written || forceReload) {
- reloadDatabase(result == SpoilerFile::SaveResult::Written);
+ // Check for server reply
+ auto *reply = dynamic_cast(sender());
+ QNetworkReply::NetworkError errorCode = reply->error();
+
+ if (errorCode == QNetworkReply::NoError) {
+ spoilerData = reply->readAll();
+
+ // Save the spoiler.xml file to the disk
+ saveDownloadedFile(spoilerData);
+
+ reply->deleteLater();
+ emit spoilerCheckerDone();
} else {
- finish();
- }
-}
-
-void SpoilerBackgroundUpdater::reloadDatabase(bool updated)
-{
- reloading = true;
- auto *database = CardDatabaseManager::getInstance();
- // The job outlives an individual request, so destroying that request cannot
- // let a newer updater overlap the worker that is still completing.
- auto *watcher = new QFutureWatcher(QCoreApplication::instance());
- activeReload = watcher;
- connect(watcher, &QFutureWatcher::finished, watcher, [watcher] {
- if (activeReload == watcher) {
- activeReload.clear();
- }
- watcher->deleteLater();
- });
- connect(watcher, &QFutureWatcher::finished, this, [this, watcher, database, updated] {
- bool loaded = false;
- try {
- loaded = watcher->result() && database->getLoadStatus() == Ok;
- } catch (const std::exception &error) {
- qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler database reload failed:" << error.what();
- }
- reloading = false;
- if (!isCurrent()) {
- finish();
- } else if (!loaded) {
- finish(reloadOnly
- ? tr("Could not reload card data after disabling spoilers. Use Reload Card Database to retry.")
- : tr("Could not reload card data after updating spoilers. Use Update Spoilers to retry the "
- "reload."));
- } else {
- finish({}, updated);
- }
- });
- watcher->setFuture(QtConcurrent::run([database] {
- database->reloadCardDatabasesAndNotify();
- return true;
- }));
-}
-
-void SpoilerBackgroundUpdater::finish(const QString &error, bool updated)
-{
- if (completed) {
- return;
- }
- completed = true;
- spoilerData = QByteArray();
- if (currentUpdater == this) {
- currentUpdater.clear();
- }
- QPointer guard(this);
- if (!error.isEmpty() && !cancelled) {
- qCWarning(SpoilerBackgroundUpdaterLog) << error;
- if (notificationIcon) {
- notificationIcon->showMessage(tr("Spoiler update failed"), error);
- }
- if (!guard) {
- return;
- }
- emit spoilerUpdateFailed(error);
- } else if (updated && !cancelled) {
- if (notificationIcon) {
- notificationIcon->showMessage(tr("Spoilers have been updated!"),
- tr("The card database now includes the downloaded spoilers."));
- }
- } else if (seasonEnded && !cancelled && notificationIcon) {
- notificationIcon->showMessage(tr("Spoiler season has ended"),
- tr("The spoiler database was removed. Please run Oracle to get released cards."));
- }
- if (guard) {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode;
emit spoilerCheckerDone();
}
}
-SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::current()
+bool SpoilerBackgroundUpdater::deleteSpoilerFile()
{
- return currentUpdater;
+ QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
+ QFileInfo fi(fileName);
+ QDir fileDir(fi.path());
+ QFile file(fileName);
+
+ // Delete the spoiler.xml file
+ if (file.exists() && file.remove()) {
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml";
+ return true;
+ }
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted";
+ return false;
}
-SpoilerBackgroundUpdater *SpoilerBackgroundUpdater::deleteSpoilerFile()
+void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
{
- // Deletion and the following reload share the operation lifetime. Callers can
- // observe either failure before the queued operation starts, and an explicit
- // retry still reloads when the file has already been removed.
- auto *updater = new SpoilerBackgroundUpdater(QCoreApplication::instance(), true);
- updater->reloadOnly = true;
- connect(updater, &SpoilerBackgroundUpdater::spoilerCheckerDone, updater, &QObject::deleteLater);
- return updater;
+ auto *response = dynamic_cast(sender());
+ QNetworkReply::NetworkError errorCode = response->error();
+
+ if (errorCode == QNetworkReply::ContentNotFoundError) {
+ // Spoiler season is offline at this point, so the spoiler.xml file can be safely deleted
+ // The user should run Oracle to get the latest card information
+ if (deleteSpoilerFile() && trayIcon) {
+ trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle"));
+ }
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline";
+ emit spoilerCheckerDone();
+ } else if (errorCode == QNetworkReply::NoError) {
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online";
+ startSpoilerDownloadProcess(SPOILERS_URL, true);
+ } else if (errorCode == QNetworkReply::HostNotFoundError) {
+ if (trayIcon) {
+ trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection"));
+ }
+
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection";
+ emit spoilerCheckerDone();
+ } else {
+ if (trayIcon) {
+ trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode);
+ }
+
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode;
+ emit spoilerCheckerDone();
+ }
+}
+
+bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
+{
+ QString fileName = SettingsCache::instance().paths().getSpoilerCardDatabasePath();
+ QFileInfo fi(fileName);
+ QDir fileDir(fi.path());
+
+ if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) {
+ return false;
+ }
+
+ // Check if the data matches. If it does, then spoilers are up to date.
+ if (getHash(fileName) == getHash(data)) {
+ if (trayIcon) {
+ trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added"));
+ }
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date";
+ return false;
+ }
+
+ QFile file(fileName);
+ if (!file.open(QIODevice::WriteOnly)) {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName;
+ file.close();
+ return false;
+ }
+
+ if (file.write(data) == -1) {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName;
+ file.close();
+ return false;
+ }
+
+ file.close();
+
+ // Data written, so reload the card database
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
+ // Spoilers can introduce sets the config has never seen; the loader never
+ // runs checkUnknownSets() itself, so run it once the finished snapshot has
+ // been swapped in.
+ auto *watcher = new QFutureWatcher(this);
+ connect(watcher, &QFutureWatcher::finished, this, [watcher] {
+ watcher->deleteLater();
+ CardDatabaseManager::getInstance()->checkUnknownSets();
+ });
+ watcher->setFuture(QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); }));
+
+ // If the user has notifications enabled, let them know
+ // when the database was last updated
+ if (trayIcon) {
+ QList lines = data.split('\n');
+
+ for (const QByteArray &line : lines) {
+ if (line.contains("Created At:")) {
+ QString timeStamp = QString(line).replace("Created At:", "").trimmed();
+ timeStamp.chop(6); // Remove " (UTC)"
+
+ auto utcTime = QLocale().toDateTime(timeStamp, "ddd, MMM dd yyyy, hh:mm:ss");
+#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
+ utcTime.setTimeZone(QTimeZone::UTC);
+#else
+ utcTime.setTimeSpec(Qt::UTC);
+#endif
+
+ QString localTime = utcTime.toLocalTime().toString("MMM d, hh:mm");
+
+ trayIcon->showMessage(tr("Spoilers have been updated!"), tr("Last change:") + " " + localTime);
+ emit spoilersUpdatedSuccessfully();
+ return true;
+ }
+ }
+ }
+
+ return true;
+}
+
+QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName)
+{
+ QFile file(fileName);
+
+ if (file.open(QFile::ReadOnly)) {
+ // Only read the first 512 bytes (enough to get the "created" tag)
+ const QByteArray bytes = file.read(512);
+
+ QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
+ hash.addData(bytes);
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result();
+
+ file.close();
+ return hash.result();
+ } else {
+ qCWarning(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!";
+ file.close();
+ return QByteArray();
+ }
+}
+
+QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data)
+{
+ // Only read the first 512 bytes (enough to get the "created" tag)
+ const QByteArray bytes = data.left(512);
+
+ QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
+ hash.addData(bytes);
+
+ qCInfo(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result();
+
+ return hash.result();
}
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h
index fd90eb688..a6c4ed00f 100644
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h
+++ b/cockatrice/src/client/network/update/card_spoiler/spoiler_background_updater.h
@@ -2,6 +2,7 @@
* @file spoiler_background_updater.h
* @ingroup Client
*/
+//! \todo Document this file.
#ifndef COCKATRICE_SPOILER_DOWNLOADER_H
#define COCKATRICE_SPOILER_DOWNLOADER_H
@@ -9,13 +10,10 @@
#include
#include
#include
-#include
#include
#include
-class QNetworkAccessManager;
-class QNetworkReply;
-class QSystemTrayIcon;
+class QProcess;
class QUrl;
inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater");
@@ -24,48 +22,30 @@ class SpoilerBackgroundUpdater : public QObject
{
Q_OBJECT
public:
- explicit SpoilerBackgroundUpdater(QObject *parent = nullptr, bool forceReload = false);
+ explicit SpoilerBackgroundUpdater(QObject *apParent = nullptr);
inline QString getCardUpdaterBinaryName()
{
return "oracle";
}
- // Queues removal and reload; the application owns the returned updater and
- // deletes it on completion. Callers can connect its failure/completion signals.
- static SpoilerBackgroundUpdater *deleteSpoilerFile();
- // The operation currently running, if any. The updater outlives the page that
- // started it, so a reopened page uses this to observe the in-flight update.
- static SpoilerBackgroundUpdater *current();
+ QByteArray getHash(const QString fileName);
+ QByteArray getHash(QByteArray data);
+ static bool deleteSpoilerFile();
private slots:
- void begin();
void actDownloadFinishedSpoilersFile();
void actCheckIfSpoilerSeasonEnabled();
- void cancelIfSettingsChanged();
private:
- const bool enabledAtStart;
- const bool forceReload;
- const QString targetPath;
- const quint64 operationId;
- QNetworkAccessManager *manager;
- QPointer activeReply;
- QPointer notificationIcon;
+ bool isSpoilerDownloadEnabled;
+ QProcess *cardUpdateProcess;
QByteArray spoilerData;
- bool completed = false;
- bool cancelled = false;
- bool reloading = false;
- bool seasonEnded = false;
- bool reloadOnly = false;
-
- bool isCurrent() const;
- void cancel();
- void finish(const QString &error = {}, bool updated = false);
- void downloadFromURL(const QUrl &url, bool saveResults);
- void reloadDatabase(bool updated);
+ void startSpoilerDownloadProcess(QString url, bool saveResults);
+ void downloadFromURL(QUrl url, bool saveResults);
+ bool saveDownloadedFile(QByteArray data);
signals:
+ void spoilersUpdatedSuccessfully();
void spoilerCheckerDone();
- void spoilerUpdateFailed(const QString &message);
};
-#endif // COCKATRICE_SPOILER_DOWNLOADER_H
+#endif // COCKATRICE_SPOILER_DOWNLOADER_H
\ No newline at end of file
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp
deleted file mode 100644
index 554ea1108..000000000
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp
+++ /dev/null
@@ -1,109 +0,0 @@
-#include "spoiler_file.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace
-{
-QByteArray hashFile(const QString &path)
-{
- QFile file(path);
- if (!file.open(QIODevice::ReadOnly)) {
- return {};
- }
- QCryptographicHash hash(QCryptographicHash::Sha256);
- return hash.addData(&file) ? hash.result() : QByteArray();
-}
-} // namespace
-
-namespace SpoilerFile
-{
-QString validate(const QByteArray &data)
-{
- QXmlStreamReader xml(data);
- if (!xml.readNextStartElement() || xml.name() != QStringLiteral("cockatrice_carddatabase")) {
- return QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "The download is not a Cockatrice card database. The previous spoiler file was preserved.");
- }
- bool versionOk = false;
- const int version = xml.attributes().value(QStringLiteral("version")).toInt(&versionOk);
- if (!versionOk || (version != 3 && version != 4)) {
- return QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "The spoiler database uses an unsupported format. The previous spoiler file was preserved.");
- }
- bool hasCards = false;
- int depth = 1;
- while (!xml.atEnd()) {
- const auto token = xml.readNext();
- if (token == QXmlStreamReader::StartElement) {
- ++depth;
- if (depth == 2 && xml.name() == QStringLiteral("cards")) {
- hasCards = true;
- }
- } else if (token == QXmlStreamReader::EndElement) {
- --depth;
- }
- }
- if (xml.hasError() || !hasCards) {
- return QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "The spoiler database is incomplete or invalid. The previous spoiler file was preserved.");
- }
- return {};
-}
-
-SaveResult
-save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error)
-{
- error = validate(data);
- if (!error.isEmpty()) {
- return SaveResult::Failed;
- }
- if (!isCurrent()) {
- error = QCoreApplication::translate("SpoilerBackgroundUpdater",
- "The spoiler update was cancelled because its settings changed.");
- return SaveResult::Failed;
- }
- if (hashFile(targetPath) == QCryptographicHash::hash(data, QCryptographicHash::Sha256)) {
- return SaveResult::Unchanged;
- }
- const QDir directory = QFileInfo(targetPath).dir();
- if (!directory.exists() && !directory.mkpath(QStringLiteral("."))) {
- error = QCoreApplication::translate("SpoilerBackgroundUpdater",
- "Could not create the spoiler database directory: %1")
- .arg(directory.absolutePath());
- return SaveResult::Failed;
- }
- QSaveFile file(targetPath);
- file.setDirectWriteFallback(false);
- if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.flush()) {
- error = QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "Could not save the spoiler database to %1: %2. The previous file was preserved.")
- .arg(targetPath, file.errorString());
- return SaveResult::Failed;
- }
- if (!isCurrent()) {
- file.cancelWriting();
- error = QCoreApplication::translate("SpoilerBackgroundUpdater",
- "The spoiler update was cancelled because its settings changed.");
- return SaveResult::Failed;
- }
- if (!file.commit()) {
- error = QCoreApplication::translate(
- "SpoilerBackgroundUpdater",
- "Could not replace the spoiler database at %1: %2. The previous file was preserved.")
- .arg(targetPath, file.errorString());
- return SaveResult::Failed;
- }
- return SaveResult::Written;
-}
-
-} // namespace SpoilerFile
diff --git a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h b/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h
deleted file mode 100644
index 3a138cdea..000000000
--- a/cockatrice/src/client/network/update/card_spoiler/spoiler_file.h
+++ /dev/null
@@ -1,24 +0,0 @@
-#ifndef COCKATRICE_SPOILER_FILE_H
-#define COCKATRICE_SPOILER_FILE_H
-
-#include
-#include
-#include
-
-namespace SpoilerFile
-{
-enum class SaveResult
-{
- Failed,
- Unchanged,
- Written
-};
-
-QString validate(const QByteArray &data);
-// Validate before opening the destination, compare complete files, and replace
-// atomically. Check operation ownership both before writing and before committing.
-SaveResult
-save(const QString &targetPath, const QByteArray &data, const std::function &isCurrent, QString &error);
-} // namespace SpoilerFile
-
-#endif // COCKATRICE_SPOILER_FILE_H
diff --git a/cockatrice/src/client/network/update/client/update_downloader.cpp b/cockatrice/src/client/network/update/client/update_downloader.cpp
index 8b5ef3d6c..8868876fa 100644
--- a/cockatrice/src/client/network/update/client/update_downloader.cpp
+++ b/cockatrice/src/client/network/update/client/update_downloader.cpp
@@ -1,16 +1,15 @@
#include "update_downloader.h"
+#include
+#include
#include
-#include
+#include
#include
#include
#include
-#include
-#include
#include
#include
#include
-#include
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
{
@@ -24,116 +23,48 @@ void UpdateDownloader::beginDownload(QUrl downloadUrl)
originalUrl = downloadUrl;
}
- if (auto *old = std::exchange(response, nullptr)) {
- old->abort();
- old->deleteLater();
- }
-
- errorReported = false;
- cancelRequested = false;
-
- // The transfer timeout comes from the manager (NetworkManagerFactory).
response = netMan->get(QNetworkRequest(downloadUrl));
connect(response, &QNetworkReply::finished, this, &UpdateDownloader::fileFinished);
- connect(response, &QNetworkReply::errorOccurred, this, &UpdateDownloader::downloadError);
connect(response, &QNetworkReply::downloadProgress, this, &UpdateDownloader::downloadProgress);
- connect(this, &UpdateDownloader::stopDownload, this, &UpdateDownloader::cancelDownload);
+ connect(this, &UpdateDownloader::stopDownload, response, &QNetworkReply::abort);
}
-void UpdateDownloader::downloadError(QNetworkReply::NetworkError networkError)
+void UpdateDownloader::downloadError(QNetworkReply::NetworkError)
{
- // A reply that was superseded by beginDownload() can still deliver a
- // queued error; it must not be reported against the current reply.
- if (response == nullptr || sender() != response || errorReported) {
+ if (response == nullptr) {
return;
}
- // A transfer timeout aborts the reply, so it arrives as OperationCanceledError.
- // Only an abort the user asked for is suppressed; every other cancel is a
- // stalled transfer and has to be reported.
- if (networkError == QNetworkReply::OperationCanceledError) {
- if (cancelRequested) {
- return;
- }
-
- errorReported = true;
- emit error(tr("The download timed out. Check your connection and try again."));
- return;
- }
-
- errorReported = true;
- emit error(response->errorString());
-}
-
-void UpdateDownloader::cancelDownload()
-{
- cancelRequested = true;
- if (response != nullptr) {
- response->abort();
- }
+ emit error(response->errorString().toUtf8());
}
void UpdateDownloader::fileFinished()
{
- // An aborted, superseded reply also emits finished(). Only the reply
- // currently stored in response may be consumed.
- if (response == nullptr || sender() != response) {
+ // If we finished but there's a redirect, follow it
+ QVariant redirect = response->attribute(QNetworkRequest::RedirectionTargetAttribute);
+ if (!redirect.isNull()) {
+ beginDownload(redirect.toUrl());
return;
}
- // errorOccurred normally reports this first; keep the check for errors that
- // only surface when the reply finishes.
- if (response->error() != QNetworkReply::NoError) {
- downloadError(response->error());
- response->deleteLater();
- response = nullptr;
+ // Handle any errors we had
+ if (response->error()) {
+ emit error(response->errorString());
return;
}
- // Give the update its own private directory. The name is unpredictable, so a
- // local user cannot pre-create or symlink the path, and the original file
- // name (including the installer suffix) is preserved inside it.
- QTemporaryDir downloadDir(QDir::temp().filePath(QStringLiteral("cockatrice-update-XXXXXX")));
- if (!downloadDir.isValid()) {
- emit error(tr("Could not create a temporary directory for the download."));
- response->deleteLater();
- response = nullptr;
- return;
- }
- downloadDir.setAutoRemove(false);
+ // Work out the file name of the download
+ QString fileName = QDir::temp().path() + QDir::separator() + originalUrl.toString().section('/', -1);
- const QString remoteName = QFileInfo(originalUrl.path()).fileName();
- const QString fileName = downloadDir.filePath(remoteName.isEmpty() ? QStringLiteral("update") : remoteName);
-
- QSaveFile file(fileName);
+ // Save the build in a temporary directory
+ QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
- downloadDir.remove();
- emit error(tr("Could not open the file for writing: %1").arg(file.errorString()));
- response->deleteLater();
- response = nullptr;
+ emit error(tr("Could not open the file for reading."));
return;
}
- const QByteArray data = response->readAll();
- if (file.write(data) != data.size()) {
- file.cancelWriting();
- downloadDir.remove();
- emit error(tr("Could not write the downloaded file to disk."));
- response->deleteLater();
- response = nullptr;
- return;
- }
-
- if (!file.commit()) {
- downloadDir.remove();
- emit error(tr("Could not save the downloaded file: %1").arg(file.errorString()));
- response->deleteLater();
- response = nullptr;
- return;
- }
-
- response->deleteLater();
- response = nullptr;
+ file.write(response->readAll());
+ file.close();
// Emit the success signal with a URL to the download file
emit downloadSuccessful(QUrl::fromLocalFile(fileName));
diff --git a/cockatrice/src/client/network/update/client/update_downloader.h b/cockatrice/src/client/network/update/client/update_downloader.h
index 7704eca38..8aeefb22a 100644
--- a/cockatrice/src/client/network/update/client/update_downloader.h
+++ b/cockatrice/src/client/network/update/client/update_downloader.h
@@ -32,13 +32,10 @@ private:
QUrl originalUrl;
QNetworkAccessManager *netMan;
QNetworkReply *response;
- bool errorReported = false;
- bool cancelRequested = false;
private slots:
void fileFinished();
void downloadProgress(qint64 bytesRead, qint64 totalBytes);
void downloadError(QNetworkReply::NetworkError);
- void cancelDownload();
};
#endif // COCKATRICE_UPDATEDOWNLOADER_H
diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp
index b40e26377..08eccf1e4 100644
--- a/cockatrice/src/client/settings/cache_settings.cpp
+++ b/cockatrice/src/client/settings/cache_settings.cpp
@@ -6,7 +6,6 @@
#include "shortcuts_settings.h"
#include
-#include
#include
#include
#include
@@ -48,32 +47,7 @@ Q_GLOBAL_STATIC(SettingsCache, settingsCache)
SettingsCache &SettingsCache::instance()
{
- SettingsCache &cache = *settingsCache; // returns a QT managed singleton reference
- // The release channels own QNetworkAccessManagers, which must be destroyed
- // while the application object is alive. ~SettingsCache() runs during
- // static teardown, after QApplication is gone, so hook the application's
- // shutdown instead. Retried here because the first access may predate the
- // application object.
- cache.installShutdownHook();
- return cache;
-}
-
-void SettingsCache::installShutdownHook()
-{
- if (shutdownHookInstalled) {
- return;
- }
- QCoreApplication *application = QCoreApplication::instance();
- if (application == nullptr) {
- return;
- }
- connect(application, &QCoreApplication::aboutToQuit, this, [this] {
- for (ReleaseChannel *releaseChannel : std::as_const(releaseChannels)) {
- delete releaseChannel;
- }
- releaseChannels.clear();
- });
- shutdownHookInstalled = true;
+ return *settingsCache; // returns a QT managed singleton reference
}
QString SettingsCache::getDataPath()
diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h
index 8ccf6f76b..9aeb88a18 100644
--- a/cockatrice/src/client/settings/cache_settings.h
+++ b/cockatrice/src/client/settings/cache_settings.h
@@ -90,13 +90,8 @@ private:
QString themeName;
QList releaseChannels;
- bool shutdownHookInstalled = false;
bool isPortableBuild;
- // Connects the aboutToQuit handler that deletes the release channels while
- // the application object is still alive; a no-op before it exists.
- void installShutdownHook();
-
[[nodiscard]] QString getSafeConfigPath(QString configEntry, QString defaultPath) const;
[[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const;
void loadPaths();
diff --git a/cockatrice/src/client/settings/shortcut_treeview.cpp b/cockatrice/src/client/settings/shortcut_treeview.cpp
index 2501798cb..4a5bafb01 100644
--- a/cockatrice/src/client/settings/shortcut_treeview.cpp
+++ b/cockatrice/src/client/settings/shortcut_treeview.cpp
@@ -1,6 +1,5 @@
#include "shortcut_treeview.h"
-#include "../../interface/theme_manager.h"
#include "cache_settings.h"
#include "shortcuts_settings.h"
@@ -79,18 +78,6 @@ ShortcutTreeView::ShortcutTreeView(QWidget *parent) : QTreeView(parent)
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&ShortcutTreeView::refreshShortcuts);
-
- // Group headers store their font on the item; re-apply the role family on
- // font changes (setFont emits itemChanged, which repaints the rows).
- connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
- QFont groupFont = themeManager->appFont(AppFont::InterfaceDefault);
- groupFont.setBold(true);
- for (int row = 0; row < shortcutsModel->rowCount(); ++row) {
- if (auto *groupItem = shortcutsModel->item(row)) {
- groupItem->setFont(groupFont);
- }
- }
- });
}
void ShortcutTreeView::populateShortcutsModel()
@@ -106,8 +93,7 @@ void ShortcutTreeView::populateShortcutsModel()
curParent = parentItems.value(group);
} else {
curParent = new QStandardItem(group);
- // Family comes from the InterfaceDefault theme role; bold stays caller-side.
- QFont font = themeManager->appFont(AppFont::InterfaceDefault);
+ static QFont font = curParent->font();
font.setBold(true);
curParent->setFont(font);
parentItems.insert(group, curParent);
diff --git a/cockatrice/src/filters/filter_builder.cpp b/cockatrice/src/filters/filter_builder.cpp
index 9c2a91a5b..22679abb1 100644
--- a/cockatrice/src/filters/filter_builder.cpp
+++ b/cockatrice/src/filters/filter_builder.cpp
@@ -1,6 +1,6 @@
#include "filter_builder.h"
-#include "../interface/theme_icon.h"
+#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/utility/custom_line_edit.h"
#include
@@ -30,7 +30,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
typeCombo->addItem(CardFilter::typeName(static_cast(i)), QVariant(i));
}
- QPushButton *ok = new QPushButton(loadThemedIcon(QStringLiteral("icons/increment")), QString());
+ QPushButton *ok = new QPushButton(themePixmap(QStringLiteral("icons/increment")), QString());
ok->setObjectName("ok");
ok->setMaximumSize(20, 20);
diff --git a/cockatrice/src/filters/filter_tree_model.cpp b/cockatrice/src/filters/filter_tree_model.cpp
index 66fe39821..9b55ce2fd 100644
--- a/cockatrice/src/filters/filter_tree_model.cpp
+++ b/cockatrice/src/filters/filter_tree_model.cpp
@@ -1,7 +1,5 @@
#include "filter_tree_model.h"
-#include "../interface/theme_manager.h"
-
#include
#include
#include
@@ -41,12 +39,6 @@ FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
connect(fTree, &FilterTree::postInsertRow, this, &FilterTreeModel::proxyEndInsertRow);
connect(fTree, &FilterTree::preRemoveRow, this, &FilterTreeModel::proxyBeginRemoveRow);
connect(fTree, &FilterTree::postRemoveRow, this, &FilterTreeModel::proxyEndRemoveRow);
- // FontRole data reads the DeckList role live; rebroadcast so attached
- // views re-query instead of waiting for an incidental repaint.
- connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
- emit layoutAboutToBeChanged();
- emit layoutChanged();
- });
}
FilterTreeModel::~FilterTreeModel()
@@ -210,8 +202,7 @@ QVariant FilterTreeModel::data(const QModelIndex &index, int role) const
switch (role) {
case Qt::FontRole:
if (!node->isLeaf()) {
- // Family comes from the DeckList theme role; bold stays caller-side.
- QFont f = themeManager->appFont(AppFont::DeckList);
+ QFont f;
f.setBold(true);
return f;
}
diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp
index 84e062cd7..0e8b6ced3 100644
--- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp
+++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp
@@ -3,7 +3,6 @@
#include "../../client/settings/cache_settings.h"
#include "../../interface/card_localization.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
-#include "../../interface/theme_manager.h"
#include "../card_dimensions.h"
#include "../game_scene.h"
#include "../z_values.h"
@@ -155,7 +154,7 @@ void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &transla
painter->rotate(angle);
painter->translate(-translatedSize.width() / 2, -translatedSize.height() / 2);
- QFont f = themeManager->appFont(AppFont::CardText);
+ QFont f;
f.setPixelSize(fontSize * scale);
painter->setFont(f);
diff --git a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp
index 3a740aa98..50e80d359 100644
--- a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp
+++ b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp
@@ -1,7 +1,5 @@
#include "abstract_graphics_item.h"
-#include "../../interface/theme_manager.h"
-
#include
#include
#include
@@ -22,7 +20,7 @@ void AbstractGraphicsItem::paintNumberEllipse(int number,
painter->save();
QString numStr = QString::number(number);
- QFont font = themeManager->cardTextFont();
+ QFont font("Serif");
font.setPixelSize(fontSize);
font.setWeight(QFont::Bold);
diff --git a/cockatrice/src/game_graphics/board/counter_general.cpp b/cockatrice/src/game_graphics/board/counter_general.cpp
index 23618e6bf..4a8bbd6a4 100644
--- a/cockatrice/src/game_graphics/board/counter_general.cpp
+++ b/cockatrice/src/game_graphics/board/counter_general.cpp
@@ -1,7 +1,6 @@
#include "counter_general.h"
#include "../../interface/pixel_map_generator.h"
-#include "../../interface/theme_manager.h"
#include "abstract_graphics_item.h"
#include
@@ -42,7 +41,7 @@ void GeneralCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /
painter->drawPixmap(QPoint(0, 0), pixmap);
if (value) {
- QFont f = themeManager->cardTextFont();
+ QFont f("Serif");
f.setPixelSize(qMax((int)(radius * scaleFactor), 10));
f.setWeight(QFont::Bold);
painter->setPen(Qt::black);
diff --git a/cockatrice/src/game_graphics/deckview/deck_view.cpp b/cockatrice/src/game_graphics/deckview/deck_view.cpp
index f62d33335..6e0b0eaaa 100644
--- a/cockatrice/src/game_graphics/deckview/deck_view.cpp
+++ b/cockatrice/src/game_graphics/deckview/deck_view.cpp
@@ -240,8 +240,8 @@ void DeckViewCardContainer::paint(QPainter *painter, const QStyleOptionGraphicsI
painter->drawLine(QPointF(0, separatorY), QPointF(width, separatorY));
painter->setPen(QColor(Qt::white));
- // Family comes from the DeckList theme role; pixel sizes/weights unchanged.
- QFont f = themeManager->deckListFont();
+ QFont f("Serif");
+ f.setStyleHint(QFont::Serif);
f.setPixelSize(24);
f.setWeight(QFont::Bold);
painter->setFont(f);
@@ -292,8 +292,8 @@ QList> DeckViewCardContainer::getRowsAndCols() const
int DeckViewCardContainer::getCardTypeTextWidth() const
{
- // Family comes from the DeckList theme role; pixel size/weight unchanged.
- QFont f = themeManager->deckListFont();
+ QFont f("Serif");
+ f.setStyleHint(QFont::Serif);
f.setPixelSize(16);
f.setWeight(QFont::Bold);
QFontMetrics fm(f);
@@ -366,15 +366,6 @@ void DeckViewCardContainer::setWidth(qreal _width)
DeckViewScene::DeckViewScene(QObject *parent) : QGraphicsScene(parent), locked(true), deck(0), optimalAspectRatio(1.0)
{
- // Zone headers read the DeckList role at paint time and cache their
- // rendering, so touch every item to invalidate the caches and pick up the
- // new family on font changes (containers cannot connect this themselves:
- // QGraphicsItem is not a QObject).
- connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
- for (auto *item : items()) {
- item->update();
- }
- });
}
DeckViewScene::~DeckViewScene()
diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp
index fe63d7a6f..3bef9e59e 100644
--- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp
+++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp
@@ -71,8 +71,6 @@ void ToggleButton::setState(bool _state)
DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
: QWidget(nullptr), visualDeckStorageWidget(nullptr), parentGame(parent), playerId(_playerId)
{
- setObjectName("deckViewContainer");
-
loadLocalButton = new QPushButton;
loadRemoteButton = new QPushButton;
loadFromClipboardButton = new QPushButton;
@@ -82,14 +80,6 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
forceStartGameButton = new QPushButton;
sideboardLockButton = new ToggleButton;
- loadLocalButton->setObjectName("loadLocalButton");
- loadRemoteButton->setObjectName("loadRemoteButton");
- loadFromClipboardButton->setObjectName("loadFromClipboardButton");
- readyStartButton->setObjectName("readyStartButton");
- forceStartGameButton->setObjectName("forceStartGameButton");
- unloadDeckButton->setObjectName("unloadDeckButton");
- sideboardLockButton->setObjectName("sideboardLockButton");
-
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);
diff --git a/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp
index 75ede0ca8..90cbed9a1 100644
--- a/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp
+++ b/cockatrice/src/game_graphics/deckview/tabbed_deck_view_container.cpp
@@ -14,7 +14,6 @@ class TabGame;
TabbedDeckViewContainer::TabbedDeckViewContainer(int _playerId, TabGame *parent)
: QTabWidget(nullptr), playerId(_playerId), parentGame(parent)
{
- setObjectName(QString("tabbedDeckView_%1").arg(_playerId));
setTabsClosable(true);
connect(this, &QTabWidget::tabCloseRequested, this, &TabbedDeckViewContainer::closeTab);
diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp
index 128899a13..db1040006 100644
--- a/cockatrice/src/game_graphics/game_scene.cpp
+++ b/cockatrice/src/game_graphics/game_scene.cpp
@@ -1,7 +1,6 @@
#include "game_scene.h"
#include "../client/settings/cache_settings.h"
-#include "../interface/theme_manager.h"
#include "animated_item.h"
#include "board/abstract_card_item.h"
#include "board/arrow_item.h"
@@ -36,7 +35,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -66,14 +64,6 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
addItem(phasesToolbar);
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::minPlayersForMultiColumnLayoutChanged, this,
&GameScene::rearrange);
- // CardText items (P/T boxes, counters, player names) read the role at
- // paint time and cache their rendering, so touch every item to invalidate
- // the caches and pick up the new family on font changes.
- connect(themeManager, &ThemeManager::fontsChanged, this, [this] {
- for (auto *item : items()) {
- item->update();
- }
- });
rearrange();
}
@@ -442,37 +432,6 @@ QList GameScene::collectActivePlayers(int &firstPlayerIndex) cons
}
}
}
-
- // Lay out the table in seat order rather than id order. Players missing
- // from the tracked order keep id order at the end.
- QList seatOrder;
- for (auto *pgItem : playerViews.values()) {
- if (PlayerLogic *p = pgItem->getLogic()) {
- seatOrder = p->getGame()->getGameState()->getSeatOrder();
- break;
- }
- }
- if (!seatOrder.isEmpty()) {
- std::sort(activePlayers.begin(), activePlayers.end(), [&](PlayerLogic *a, PlayerLogic *b) {
- int indexA = seatOrder.indexOf(a->getPlayerInfo()->getId());
- int indexB = seatOrder.indexOf(b->getPlayerInfo()->getId());
- if (indexA == -1) {
- indexA = seatOrder.size() + a->getPlayerInfo()->getId();
- }
- if (indexB == -1) {
- indexB = seatOrder.size() + b->getPlayerInfo()->getId();
- }
- return indexA < indexB;
- });
- // Recompute the local-player index after sorting.
- firstPlayerIndex = 0;
- for (int i = 0; i < activePlayers.size(); ++i) {
- if (activePlayers[i]->getPlayerInfo()->getLocal()) {
- firstPlayerIndex = i;
- break;
- }
- }
- }
return activePlayers;
}
diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp
index 7a5aa073a..cb59b3d7d 100644
--- a/cockatrice/src/game_graphics/game_view.cpp
+++ b/cockatrice/src/game_graphics/game_view.cpp
@@ -2,14 +2,12 @@
#include "../client/settings/cache_settings.h"
#include "../client/settings/shortcuts_settings.h"
-#include "../interface/theme_manager.h"
#include "game_scene.h"
#include
#include
#include
#include
-#include
#include
#include
#include
@@ -26,21 +24,6 @@
#include
#include
-namespace
-{
-QFont deckOverlayFont(int pixelSize, bool bold)
-{
- // The drag/total counters and the tally columns are in-game overlays, not
- // deck list text: keep the monospace family the stylesheets used to set so
- // digit widths stay stable as counts tick and `white-space: pre` lines up.
- QFont font(QStringLiteral("monospace"));
- font.setStyleHint(QFont::Monospace);
- font.setPixelSize(pixelSize);
- font.setBold(bold);
- return font;
-}
-} // namespace
-
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
// This subclass disables that behavior so dragCountLabel can appear above it.
class SelectionRubberBand : public QRubberBand
@@ -65,7 +48,6 @@ protected:
GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0)
{
- setObjectName("gameView");
setBackgroundBrush(QBrush(QColor(0, 0, 0)));
setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing);
setViewportUpdateMode(BoundingRectViewportUpdate);
@@ -93,14 +75,15 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this);
const QString baseProperties = "color: white; "
+ "font-family: monospace; "
"background-color: rgba(0, 0, 0, 160); "
"border-radius: 3px; "
"padding: 1px 2px; "
"white-space: pre;";
- const QString dragCountLabelStyle = baseProperties;
- const QString totalCountLabelStyle = baseProperties;
- const QString subtypeTallyLabelStyle = baseProperties;
+ const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;";
+ const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;";
+ const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;";
dragCountLabel = new QLabel(this);
dragCountLabel->setStyleSheet(dragCountLabelStyle);
@@ -117,27 +100,6 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
tallyLayout->setContentsMargins(2, 2, 2, 2);
tallyLayout->setSpacing(2);
tallyContainer->hide();
-
- applyDeckOverlayFonts();
-}
-
-void GameView::applyDeckOverlayFonts()
-{
- dragCountLabel->setFont(deckOverlayFont(14, true));
- totalCountLabel->setFont(deckOverlayFont(16, true));
- tallyContainer->setFont(deckOverlayFont(12, false));
- for (int i = 0; i < tallyLayout->count(); ++i) {
- if (auto *label = qobject_cast(tallyLayout->itemAt(i)->widget())) {
- // Count column (odd positions) keeps the bold emphasis.
- const bool count = i % 2 == 1;
- label->setFont(deckOverlayFont(count ? 14 : 12, count));
- }
- }
-
- // The container keeps the size computed from the previous fonts, so force
- // rebuildTallyLabels() to re-measure it instead of reusing cachedTallyRows.
- cachedTallyRows.clear();
- updateTotalSelectionCount();
}
void GameView::resizeEvent(QResizeEvent *event)
@@ -257,8 +219,9 @@ QSize GameView::rebuildTallyLabels(const QList &entries)
{
clearTallyLabels();
- const QString nameStyle = QStringLiteral("color: white; background: transparent;");
- const QString countStyle = QStringLiteral("color: white; background: transparent;");
+ const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;");
+ const QString countStyle =
+ QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;");
int totalHeight = 0;
int maxNameWidth = 0;
@@ -268,13 +231,11 @@ QSize GameView::rebuildTallyLabels(const QList &entries)
for (const TallyRow &entry : entries) {
auto *nameLabel = new QLabel(entry.name, tallyContainer);
nameLabel->setStyleSheet(nameStyle);
- nameLabel->setFont(deckOverlayFont(12, false));
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
tallyLayout->addWidget(nameLabel, row, 0);
auto *countLabel = new QLabel(entry.value, tallyContainer);
countLabel->setStyleSheet(countStyle);
- countLabel->setFont(deckOverlayFont(14, true));
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
tallyLayout->addWidget(countLabel, row, 1);
diff --git a/cockatrice/src/game_graphics/game_view.h b/cockatrice/src/game_graphics/game_view.h
index 99a73a0a8..e57267734 100644
--- a/cockatrice/src/game_graphics/game_view.h
+++ b/cockatrice/src/game_graphics/game_view.h
@@ -50,7 +50,6 @@ private slots:
void refreshShortcuts();
void updateTotalSelectionCount(const QSize &viewSize = QSize());
void setFocusDisabled(bool disabled);
- void applyDeckOverlayFonts();
public slots:
void updateSceneRect(const QRectF &rect);
diff --git a/cockatrice/src/game_graphics/log/message_log_widget.cpp b/cockatrice/src/game_graphics/log/message_log_widget.cpp
index d526766af..046a1ae90 100644
--- a/cockatrice/src/game_graphics/log/message_log_widget.cpp
+++ b/cockatrice/src/game_graphics/log/message_log_widget.cpp
@@ -4,8 +4,6 @@
#include "../../client/settings/card_counter_settings.h"
#include "../../client/sound_engine.h"
#include "../../interface/card_localization.h"
-#include "../../interface/theme_icon.h"
-#include "../../interface/theme_manager.h"
#include "../../interface/widgets/server/chat_view/chat_view.h"
#include "../board/card_item.h"
#include "../board/translate_counter_name.h"
@@ -14,12 +12,8 @@
#include
#include
#include
-#include
-#include
#include
#include
-#include
-#include
#include
#include
#include
@@ -126,7 +120,6 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position
void MessageLogWidget::containerProcessingDone()
{
currentContext = MessageContext_None;
- contextJudgeName.clear();
messageSuffix = messagePrefix = QString();
}
@@ -612,20 +605,6 @@ void MessageLogWidget::logReverseTurn(PlayerLogic *player, bool reversed)
.arg(reversed ? tr("reversed") : tr("normal")));
}
-void MessageLogWidget::logTurnOrderChanged(QStringList playerNames, bool randomized)
-{
- QStringList sanitizedNames;
- for (const QString &name : playerNames) {
- sanitizedNames.append(sanitizeHtml(name));
- }
- const QString order = sanitizedNames.join(" -> ");
- if (randomized) {
- appendHtmlServerMessage(tr("Randomized turn order: %1.").arg(order));
- } else {
- appendHtmlServerMessage(tr("New turn order set: %1.").arg(order));
- }
-}
-
void MessageLogWidget::logRollDie(PlayerLogic *player, int sides, const QList &rolls)
{
if (rolls.length() == 1) {
@@ -862,27 +841,8 @@ void MessageLogWidget::logUndoDrawFailed(PlayerLogic *player)
void MessageLogWidget::setContextJudgeName(QString name)
{
- contextJudgeName = std::move(name);
- refreshJudgeSuffix();
-}
-
-void MessageLogWidget::refreshJudgeSuffix()
-{
- const QPixmap pixmap = loadThemedIcon(QStringLiteral("icons/scales")).pixmap(QSize(24, 24));
- if (!pixmap.isNull()) {
- // Rich text cannot resolve `currentColor`; register a palette-rendered
- // image so already-appended judge tags repaint on a theme switch.
- document()->addResource(QTextDocument::ImageResource, QUrl(QStringLiteral("cockatrice:scales")), pixmap);
- document()->markContentsDirty(0, document()->characterCount());
- }
-
- if (contextJudgeName.isEmpty()) {
- return;
- }
-
- messagePrefix = QStringLiteral("").arg(palette().color(QPalette::WindowText).name());
- messageSuffix =
- QStringLiteral(" [
%1]").arg(sanitizeHtml(contextJudgeName));
+ messagePrefix = QString("");
+ messageSuffix = QString(" [
%1]").arg(sanitizeHtml(name));
}
void MessageLogWidget::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor)
@@ -931,6 +891,4 @@ void MessageLogWidget::connectToPlayerEventHandler(PlayerEventHandler *playerEve
MessageLogWidget::MessageLogWidget(TabSupervisor *_tabSupervisor, AbstractGame *_game, QWidget *parent)
: ChatView(_tabSupervisor, _game, true, parent), currentContext(MessageContext_None)
{
- connect(themeManager, &ThemeManager::paletteChanged, this, &MessageLogWidget::refreshJudgeSuffix);
- connect(themeManager, &ThemeManager::themeChanged, this, &MessageLogWidget::refreshJudgeSuffix);
}
diff --git a/cockatrice/src/game_graphics/log/message_log_widget.h b/cockatrice/src/game_graphics/log/message_log_widget.h
index 3ec5ff93c..5642a76e9 100644
--- a/cockatrice/src/game_graphics/log/message_log_widget.h
+++ b/cockatrice/src/game_graphics/log/message_log_widget.h
@@ -39,10 +39,8 @@ private:
MessageContext currentContext;
QString messagePrefix, messageSuffix;
- QString contextJudgeName;
static QPair getFromStr(CardZoneLogic *zone, QString cardName, int position, bool ownerChange);
- void refreshJudgeSuffix();
public:
void connectToPlayerEventHandler(PlayerEventHandler *player);
@@ -96,7 +94,6 @@ public slots:
int amount,
bool isLentToAnotherPlayer);
void logReverseTurn(PlayerLogic *player, bool reversed);
- void logTurnOrderChanged(QStringList playerNames, bool randomized);
void logRollDie(PlayerLogic *player, int sides, const QList &rolls);
void logSay(PlayerLogic *player, QString message);
void logSetActivePhase(int phase);
diff --git a/cockatrice/src/game_graphics/player/player_list_widget.cpp b/cockatrice/src/game_graphics/player/player_list_widget.cpp
index 6895e7db8..7b358010f 100644
--- a/cockatrice/src/game_graphics/player/player_list_widget.cpp
+++ b/cockatrice/src/game_graphics/player/player_list_widget.cpp
@@ -1,7 +1,6 @@
#include "player_list_widget.h"
#include "../../interface/pixel_map_generator.h"
-#include "../../interface/theme_icon.h"
#include "../../interface/widgets/server/user/user_context_menu.h"
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
#include "user_level.h"
@@ -68,13 +67,13 @@ PlayerListWidget::PlayerListWidget(TabSupervisor *_tabSupervisor,
QWidget *parent)
: QTreeWidget(parent), tabSupervisor(_tabSupervisor), client(_client), game(_game), gameStarted(false)
{
- readyIcon = loadThemedIcon(QStringLiteral("icons/ready_start"));
- notReadyIcon = loadThemedIcon(QStringLiteral("icons/not_ready_start"));
- concededIcon = loadThemedIcon(QStringLiteral("icons/conceded"));
- playerIcon = loadThemedIcon(QStringLiteral("icons/player"));
- judgeIcon = loadThemedIcon(QStringLiteral("icons/scales"));
- spectatorIcon = loadThemedIcon(QStringLiteral("icons/spectator"));
- lockIcon = loadThemedIcon(QStringLiteral("icons/lock"));
+ readyIcon = themePixmap(QStringLiteral("icons/ready_start"));
+ notReadyIcon = themePixmap(QStringLiteral("icons/not_ready_start"));
+ concededIcon = themePixmap(QStringLiteral("icons/conceded"));
+ playerIcon = loadColorAdjustedPixmap("theme:icons/player");
+ judgeIcon = loadColorAdjustedPixmap("theme:icons/scales");
+ spectatorIcon = loadColorAdjustedPixmap("theme:icons/spectator");
+ lockIcon = themePixmap(QStringLiteral("icons/lock"));
if (tabSupervisor) {
itemDelegate = new PlayerListItemDelegate(this);
diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp
index 0d10ba4ab..1cd97e7b5 100644
--- a/cockatrice/src/game_graphics/player/player_target.cpp
+++ b/cockatrice/src/game_graphics/player/player_target.cpp
@@ -2,7 +2,6 @@
#include "../../client/settings/cache_settings.h"
#include "../../interface/pixel_map_generator.h"
-#include "../../interface/theme_manager.h"
#include "../board/abstract_counter.h"
#include "../board/abstract_graphics_item.h"
#include "../board/arrow_target.h"
@@ -77,7 +76,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
QRectF translatedRect = path.controlPointRect();
QSize translatedSize = translatedRect.size().toSize();
- QFont font = themeManager->cardTextFont();
+ QFont font("Serif");
font.setWeight(QFont::Bold);
font.setPixelSize(qMax(qRound(translatedSize.height() / 1.3), 9));
painter->setFont(font);
@@ -227,7 +226,7 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o
name = name.mid(0, 10) + "...";
}
- QFont font = themeManager->appFont(AppFont::CardText);
+ QFont font;
font.setPixelSize(qMax(qRound(translatedNameRect.height() / 1.5), 9));
painter->setFont(font);
painter->setPen(Qt::white);
diff --git a/cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp b/cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp
deleted file mode 100644
index c6376ef75..000000000
--- a/cockatrice/src/game_graphics/pq_cockatrice_event_player.cpp
+++ /dev/null
@@ -1,628 +0,0 @@
-#include "pq_cockatrice_event_player.h"
-
-#include "../../interface/widgets/tabs/tab_game.h"
-#include "board/card_item.h"
-#include "game_scene.h"
-#include "game_view.h"
-#include "zones/card_zone.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-Q_LOGGING_CATEGORY(PQ_COCKATRICE_EVENT_PLAYER, "cockatrice.guiTest.eventPlayer")
-
-namespace
-{
-/**
- * Strips Qt's mnemonic markers so a test can name a menu entry by the label the
- * user sees ("Turn Over" rather than "T&urn Over").
- */
-QString normalizeActionText(const QString &text)
-{
- QString normalized = text;
- normalized.remove('&');
- return normalized.simplified();
-}
-
-/**
- * Maps a printable character onto the key a US keyboard would report for it, so
- * typing produces the same QKeyEvents a recording would.
- */
-Qt::Key keyForCharacter(QChar character)
-{
- if (character.isLetter()) {
- return static_cast(Qt::Key_A + (character.toUpper().unicode() - 'A'));
- }
- if (character.isDigit()) {
- return static_cast(Qt::Key_0 + (character.unicode() - '0'));
- }
- switch (character.unicode()) {
- case ' ':
- return Qt::Key_Space;
- case '\n':
- return Qt::Key_Return;
- case '\t':
- return Qt::Key_Tab;
- case '-':
- return Qt::Key_Minus;
- case '_':
- return Qt::Key_Underscore;
- case '/':
- return Qt::Key_Slash;
- case ',':
- return Qt::Key_Comma;
- case '.':
- return Qt::Key_Period;
- case '\'':
- return Qt::Key_Apostrophe;
- case ';':
- return Qt::Key_Semicolon;
- case ':':
- return Qt::Key_Colon;
- case '"':
- return Qt::Key_QuoteDbl;
- case '&':
- return Qt::Key_Ampersand;
- case '(':
- return Qt::Key_ParenLeft;
- case ')':
- return Qt::Key_ParenRight;
- case '[':
- return Qt::Key_BracketLeft;
- case ']':
- return Qt::Key_BracketRight;
- default:
- return Qt::Key_unknown;
- }
-}
-
-/** Depth-first search for the first model index whose label matches @p text. */
-QModelIndex findModelIndexByText(const QAbstractItemModel *model, const QString &text, const QModelIndex &parent = {})
-{
- for (int row = 0; row < model->rowCount(parent); ++row) {
- const QModelIndex index = model->index(row, 0, parent);
- if (index.data(Qt::DisplayRole).toString() == text) {
- return index;
- }
- const QModelIndex child = findModelIndexByText(model, text, index);
- if (child.isValid()) {
- return child;
- }
- }
- return {};
-}
-
-bool needsShift(QChar character)
-{
- if (character.isUpper()) {
- return true;
- }
- return QStringLiteral("_:\"&()").contains(character);
-}
-} // namespace
-
-pqCockatriceEventPlayer::pqCockatriceEventPlayer(QObject *parent) : pqWidgetEventPlayer(parent)
-{
-}
-
-bool pqCockatriceEventPlayer::playEvent(QObject *object, const QString &command, const QString &arguments, bool &error)
-{
- // A known command that fails is a handled-but-failed event: QtTesting logs
- // "Unhandled event" when playEvent returns false, so report through `error`
- // (the contract playExpectProperty already follows) and still return true.
- const auto run = [&error](bool ok) {
- if (!ok) {
- error = true;
- }
- return true;
- };
-
- if (command == "clickCard") {
- return run(playClickCard(object, arguments));
- } else if (command == "flipCard") {
- return run(playFlipCard(object, arguments));
- } else if (command == "cardAction") {
- return run(playCardAction(object, arguments));
- } else if (command == "moveCard") {
- return run(playMoveCard(object, arguments));
- } else if (command == "drawArrow") {
- return run(playDrawArrow(object, arguments));
- } else if (command == "createToken") {
- return run(playCreateToken(object, arguments));
- } else if (command == "selectItem") {
- return run(playSelectItem(object, arguments));
- } else if (command == "typeText") {
- return run(playTypeText(object, arguments));
- } else if (command == "expectProperty") {
- return playExpectProperty(object, arguments, error);
- }
- return false;
-}
-
-QGraphicsItem *
-pqCockatriceEventPlayer::findCardInScene(QGraphicsScene *scene, const QString &cardName, const QString &zoneName)
-{
- for (QGraphicsItem *gi : scene->items()) {
- if (gi->type() != typeCard) {
- continue;
- }
- auto *card = static_cast(gi);
- if (!card->getZone() || card->getZone()->getName() != zoneName) {
- continue;
- }
- QString actualName = card->getName();
- if (actualName == cardName || actualName.endsWith(" " + cardName)) {
- return card;
- }
- }
- return nullptr;
-}
-
-QGraphicsItem *pqCockatriceEventPlayer::findZoneInScene(QGraphicsScene *scene, const QString &zoneName)
-{
- for (QGraphicsItem *gi : scene->items()) {
- if (gi->type() != typeZone) {
- continue;
- }
- auto *zone = static_cast(gi);
- if (zone->getLogic() && zone->getLogic()->getName() == zoneName) {
- return zone;
- }
- }
- return nullptr;
-}
-
-QAction *pqCockatriceEventPlayer::findActionInMenu(QMenu *menu, const QString &actionText)
-{
- if (!menu || actionText.isEmpty()) {
- return nullptr;
- }
- for (QAction *action : menu->actions()) {
- if (action->isSeparator()) {
- continue;
- }
- if (normalizeActionText(action->text()).contains(actionText, Qt::CaseInsensitive)) {
- return action;
- }
- if (QMenu *subMenu = action->menu()) {
- if (QAction *found = findActionInMenu(subMenu, actionText)) {
- return found;
- }
- }
- }
- return nullptr;
-}
-
-QMenu *pqCockatriceEventPlayer::visiblePopupMenu()
-{
- for (QWidget *w : QApplication::topLevelWidgets()) {
- auto *menu = qobject_cast(w);
- if (menu && menu->isVisible()) {
- return menu;
- }
- }
- return nullptr;
-}
-
-QPoint pqCockatriceEventPlayer::viewportPoint(QGraphicsView *view, const QGraphicsItem *item)
-{
- return view->mapFromScene(item->sceneBoundingRect().center());
-}
-void pqCockatriceEventPlayer::sendMouseEvent(QGraphicsView *view,
- QEvent::Type type,
- const QPoint &pos,
- Qt::MouseButton button,
- Qt::MouseButtons buttons)
-{
- QMouseEvent event(type, pos, view->viewport()->mapToGlobal(pos), button, buttons, Qt::NoModifier);
- qApp->notify(view->viewport(), &event);
-}
-
-bool pqCockatriceEventPlayer::playClickCard(QObject *object, const QString &arguments)
-{
- auto *gv = qobject_cast(object);
- if (!gv) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: target widget is not a QGraphicsView";
- return false;
- }
-
- QStringList parts = arguments.split(',');
- if (parts.size() < 2) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: arguments must be 'cardName,zoneName', got:" << arguments;
- return false;
- }
-
- QGraphicsScene *scene = gv->scene();
- if (!scene) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "clickCard: no scene on the QGraphicsView";
- return false;
- }
-
- auto *target = findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed());
- if (!target) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "clickCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
- return false;
- }
-
- const QPoint pos = viewportPoint(gv, target);
- // A double click is the play gesture from hand: press/release, then the
- // double-click event and its release, exactly as a recording produces it.
- sendMouseEvent(gv, QEvent::MouseButtonPress, pos, Qt::LeftButton, Qt::LeftButton);
- sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton);
- sendMouseEvent(gv, QEvent::MouseButtonDblClick, pos, Qt::LeftButton, Qt::LeftButton);
- sendMouseEvent(gv, QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton);
-
- return true;
-}
-
-bool pqCockatriceEventPlayer::playFlipCard(QObject *object, const QString &arguments)
-{
- auto *gv = qobject_cast(object);
- if (!gv) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: target widget is not a QGraphicsView";
- return false;
- }
-
- QStringList parts = arguments.split(',');
- if (parts.size() < 2) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: arguments must be 'cardName,zoneName', got:" << arguments;
- return false;
- }
-
- QGraphicsScene *scene = gv->scene();
- if (!scene) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "flipCard: no scene";
- return false;
- }
-
- auto *card = qgraphicsitem_cast(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed()));
- if (!card) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "flipCard: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
- return false;
- }
-
- return invokeCardMenuAction("flipCard", gv, card, QStringLiteral("Turn Over"));
-}
-
-bool pqCockatriceEventPlayer::playCardAction(QObject *object, const QString &arguments)
-{
- auto *gv = qobject_cast(object);
- if (!gv) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: target widget is not a QGraphicsView";
- return false;
- }
-
- QStringList parts = arguments.split(',');
- if (parts.size() < 3) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "cardAction: arguments must be 'cardName,zoneName,actionText', got:" << arguments;
- return false;
- }
-
- QGraphicsScene *scene = gv->scene();
- if (!scene) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "cardAction: no scene";
- return false;
- }
-
- auto *card = qgraphicsitem_cast(findCardInScene(scene, parts[0].trimmed(), parts[1].trimmed()));
- if (!card) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "cardAction: card" << parts[0].trimmed() << "not found in zone" << parts[1].trimmed();
- return false;
- }
-
- return invokeCardMenuAction("cardAction", gv, card, parts.mid(2).join(',').trimmed());
-}
-
-bool pqCockatriceEventPlayer::invokeCardMenuAction(const char *command,
- QGraphicsView *view,
- CardItem *card,
- const QString &actionText)
-{
- const QPoint pos = viewportPoint(view, card);
- sendMouseEvent(view, QEvent::MouseButtonPress, pos, Qt::RightButton, Qt::RightButton);
- sendMouseEvent(view, QEvent::MouseButtonRelease, pos, Qt::RightButton, Qt::NoButton);
- qApp->processEvents();
-
- QMenu *menu = visiblePopupMenu();
- if (!menu) {
- qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no popup menu appeared after right-click";
- return false;
- }
-
- QAction *action = findActionInMenu(menu, actionText);
- if (!action) {
- qCWarning(PQ_COCKATRICE_EVENT_PLAYER) << command << ": no context menu action matching" << actionText;
- menu->close();
- return false;
- }
-
- action->trigger();
- menu->close();
- qApp->processEvents();
- return true;
-}
-
-bool pqCockatriceEventPlayer::playMoveCard(QObject *object, const QString &arguments)
-{
- auto *gv = qobject_cast(object);
- if (!gv) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: target widget is not a QGraphicsView";
- return false;
- }
-
- QStringList parts = arguments.split(',');
- if (parts.size() < 3) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "moveCard: arguments must be 'cardName,srcZoneName,dstZoneName', got:" << arguments;
- return false;
- }
-
- const QString cardName = parts[0].trimmed();
- const QString srcZoneName = parts[1].trimmed();
- const QString dstZoneName = parts[2].trimmed();
-
- QGraphicsScene *scene = gv->scene();
- if (!scene) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: no scene";
- return false;
- }
-
- auto *card = findCardInScene(scene, cardName, srcZoneName);
- if (!card) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: card" << cardName << "not found in zone" << srcZoneName;
- return false;
- }
-
- auto *dstZone = findZoneInScene(scene, dstZoneName);
- if (!dstZone) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "moveCard: destination zone" << dstZoneName << "not found";
- return false;
- }
-
- const QPoint srcPos = viewportPoint(gv, card);
- const QPoint dstPos = viewportPoint(gv, dstZone);
-
- // A drag has to travel past Qt's start distance before the scene turns it
- // into a card drag item, otherwise the move is interpreted as a click.
- const int dragDistance = 2 * QApplication::startDragDistance() + 10;
- const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0);
-
- sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::LeftButton, Qt::LeftButton);
- sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::LeftButton, Qt::LeftButton);
- qApp->processEvents();
- sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::LeftButton, Qt::LeftButton);
- qApp->processEvents();
- sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::LeftButton, Qt::NoButton);
- qApp->processEvents();
-
- return true;
-}
-
-bool pqCockatriceEventPlayer::playDrawArrow(QObject *object, const QString &arguments)
-{
- auto *gv = qobject_cast(object);
- if (!gv) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: target widget is not a QGraphicsView";
- return false;
- }
-
- QStringList parts = arguments.split(',');
- if (parts.size() < 4) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "drawArrow: arguments must be 'srcCard,srcZone,dstCard,dstZone', got:" << arguments;
- return false;
- }
-
- const QString srcCardName = parts[0].trimmed();
- const QString srcZoneName = parts[1].trimmed();
- const QString dstCardName = parts[2].trimmed();
- const QString dstZoneName = parts[3].trimmed();
-
- QGraphicsScene *scene = gv->scene();
- if (!scene) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "drawArrow: no scene";
- return false;
- }
-
- auto *srcCard = findCardInScene(scene, srcCardName, srcZoneName);
- if (!srcCard) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "drawArrow: source card" << srcCardName << "not found in zone" << srcZoneName;
- return false;
- }
-
- auto *dstCard = findCardInScene(scene, dstCardName, dstZoneName);
- if (!dstCard) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "drawArrow: dest card" << dstCardName << "not found in zone" << dstZoneName;
- return false;
- }
-
- const QPoint srcPos = viewportPoint(gv, srcCard);
- const QPoint dstPos = viewportPoint(gv, dstCard);
-
- const int dragDistance = 2 * QApplication::startDragDistance() + 10;
- const QPoint pastDragStart = srcPos + QPoint(dragDistance, 0);
-
- sendMouseEvent(gv, QEvent::MouseButtonPress, srcPos, Qt::RightButton, Qt::RightButton);
- sendMouseEvent(gv, QEvent::MouseMove, pastDragStart, Qt::RightButton, Qt::RightButton);
- qApp->processEvents();
- sendMouseEvent(gv, QEvent::MouseMove, dstPos, Qt::RightButton, Qt::RightButton);
- sendMouseEvent(gv, QEvent::MouseButtonRelease, dstPos, Qt::RightButton, Qt::NoButton);
- qApp->processEvents();
-
- return true;
-}
-
-bool pqCockatriceEventPlayer::playCreateToken(QObject *object, const QString &arguments)
-{
- auto *gv = qobject_cast(object);
- if (!gv) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: target widget is not a QGraphicsView";
- return false;
- }
-
- QStringList parts = arguments.split(',');
- if (parts.size() < 3) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: arguments must be 'name,color,pt', got:" << arguments;
- return false;
- }
-
- QString tokenName = parts[0].trimmed();
- QString tokenColor = parts[1].trimmed();
- QString tokenPt = parts[2].trimmed();
-
- TabGame *tabGame = nullptr;
- QWidget *w = gv;
- while (w) {
- tabGame = qobject_cast(w);
- if (tabGame) {
- break;
- }
- w = w->parentWidget();
- }
- if (!tabGame) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: TabGame not found";
- return false;
- }
-
- AbstractGame *game = tabGame->getGame();
- if (!game) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no game";
- return false;
- }
-
- int activePlayerId = game->getGameState()->getActivePlayer();
- PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(activePlayerId);
- if (!player) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "createToken: no active local player";
- return false;
- }
-
- TokenInfo info;
- info.name = tokenName;
- info.color = tokenColor;
- info.pt = tokenPt;
- info.destroy = true;
- info.faceDown = false;
-
- player->getPlayerActions()->actCreateToken(info);
-
- for (int i = 0; i < 10; ++i) {
- qApp->processEvents();
- }
-
- return true;
-}
-
-bool pqCockatriceEventPlayer::playSelectItem(QObject *object, const QString &arguments)
-{
- const QString text = arguments.trimmed();
- if (text.isEmpty()) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: needs the label of the item to select";
- return false;
- }
-
- if (auto *tree = qobject_cast(object)) {
- QTreeWidgetItemIterator item(tree);
- while (*item) {
- if ((*item)->text(0) == text) {
- tree->setCurrentItem(*item);
- qApp->processEvents();
- return true;
- }
- ++item;
- }
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no tree item labelled" << text;
- return false;
- }
-
- if (auto *view = qobject_cast(object)) {
- const QModelIndex index = findModelIndexByText(view->model(), text);
- if (!index.isValid()) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: no item labelled" << text;
- return false;
- }
- view->setCurrentIndex(index);
- qApp->processEvents();
- return true;
- }
-
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "selectItem: target is neither a tree widget nor an item view";
- return false;
-}
-
-bool pqCockatriceEventPlayer::playTypeText(QObject *object, const QString &arguments)
-{
- auto *widget = qobject_cast(object);
- if (!widget) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "typeText: target is not a QWidget";
- return false;
- }
-
- for (const QChar character : arguments) {
- const Qt::Key key = keyForCharacter(character);
- const Qt::KeyboardModifiers modifiers = needsShift(character) ? Qt::ShiftModifier : Qt::NoModifier;
- QKeyEvent press(QEvent::KeyPress, key, modifiers, QString(character));
- QKeyEvent release(QEvent::KeyRelease, key, modifiers, QString(character));
- qApp->notify(widget, &press);
- qApp->notify(widget, &release);
- }
- qApp->processEvents();
-
- return true;
-}
-
-bool pqCockatriceEventPlayer::playExpectProperty(QObject *object, const QString &arguments, bool &error)
-{
- if (!object) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty: no target object";
- error = true;
- return true;
- }
-
- QStringList parts = arguments.split(',');
- if (parts.size() < 2) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "expectProperty: arguments must be 'propertyName,expectedValue', got:" << arguments;
- error = true;
- return true;
- }
-
- const QString propertyName = parts[0].trimmed();
- const QString expected = parts.mid(1).join(',');
-
- const QVariant actual = object->property(propertyName.toUtf8().constData());
- if (!actual.isValid()) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER)
- << "expectProperty:" << object->objectName() << "has no property" << propertyName;
- error = true;
- return true;
- }
-
- if (actual.toString() != expected) {
- qCCritical(PQ_COCKATRICE_EVENT_PLAYER) << "expectProperty:" << object->objectName() << propertyName << "is"
- << actual.toString() << "but the test expected" << expected;
- error = true;
- return true;
- }
-
- return true;
-}
diff --git a/cockatrice/src/game_graphics/pq_cockatrice_event_player.h b/cockatrice/src/game_graphics/pq_cockatrice_event_player.h
deleted file mode 100644
index 89d88af77..000000000
--- a/cockatrice/src/game_graphics/pq_cockatrice_event_player.h
+++ /dev/null
@@ -1,91 +0,0 @@
-#ifndef PQ_COCKATRICE_EVENT_PLAYER_H
-#define PQ_COCKATRICE_EVENT_PLAYER_H
-
-#include "pqWidgetEventPlayer.h"
-
-#include