Merge branch 'master' into master

This commit is contained in:
Jurriaan 2026-11-01 23:16:39 +01:00 • committed by GitHub
commit 9cfc1450cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
413 changed files with 12837 additions and 3590 deletions

View file

@ -7,6 +7,7 @@
#include <QProgressBar>
#include <QtGlobal>
#include <climits>
#include <libcockatrice/utility/qt_utils.h>
namespace
{
@ -32,8 +33,7 @@ QString stageName(CardUpdateStage stage, const QString &fallback)
CardDatabaseUpdateStatusBar::CardDatabaseUpdateStatusBar(QWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QHBoxLayout>(this);
stageLabel = new QLabel(this);
progressBar = new QProgressBar(this);

View file

@ -11,6 +11,7 @@
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QImageReader>
#include <QList>
#include <QMessageLogger>
@ -19,35 +20,62 @@
#include <QTimer>
#include <libcockatrice/settings/paths_settings.h>
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
static constexpr int REFRESH_DEBOUNCE_MS = 500;
// Watching can fail on network and FUSE mounts; the slow poll still picks those
// changes up without the old ten-second full rescan.
static constexpr int FALLBACK_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
: QObject(parent), picsPath(SettingsCache::instance().paths().getPicsPath()),
customPicsPath(SettingsCache::instance().paths().getCustomPicsPath())
: CardPictureLoaderLocal(parent,
SettingsCache::instance().paths().getPicsPath(),
SettingsCache::instance().paths().getCustomPicsPath())
{
// Hook up signals to settings
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
&CardPictureLoaderLocal::picsPathChanged);
}
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent,
const QString &_picsPath,
const QString &_customPicsPath)
: QObject(parent), picsPath(_picsPath), customPicsPath(_customPicsPath),
folderWatcher(new QFileSystemWatcher(this)), indexRefreshTimer(new QTimer(this)),
fallbackRefreshTimer(new QTimer(this))
{
indexRefreshTimer->setSingleShot(true);
indexRefreshTimer->setInterval(REFRESH_DEBOUNCE_MS);
connect(indexRefreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex);
connect(folderWatcher, &QFileSystemWatcher::directoryChanged, indexRefreshTimer, qOverload<>(&QTimer::start));
connect(folderWatcher, &QFileSystemWatcher::fileChanged, indexRefreshTimer, qOverload<>(&QTimer::start));
fallbackRefreshTimer->setInterval(FALLBACK_REFRESH_INTERVAL_MS);
connect(fallbackRefreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex);
fallbackRefreshTimer->start();
refreshIndex();
refreshTimer = new QTimer(this);
connect(refreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex);
refreshTimer->start(REFRESH_INTERVAL_MS);
}
void CardPictureLoaderLocal::refreshIndex()
{
customFolderIndex.clear();
QSet<QString> directories;
QDir customDir(customPicsPath);
if (customDir.exists()) {
directories.insert(customDir.absolutePath());
}
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);
const QString thisPath = it.next();
const QFileInfo thisFileInfo = it.fileInfo();
if (thisFileInfo.isFile()) {
if (thisFileInfo.isDir()) {
directories.insert(thisFileInfo.absoluteFilePath());
} else if (thisFileInfo.isFile()) {
// We don't know which name is the correctedName because there might be '.'s in the cardName.
// Just add all possibilities to be sure.
customFolderIndex.insert(thisFileInfo.baseName(), thisFileInfo.absoluteFilePath());
@ -55,8 +83,34 @@ void CardPictureLoaderLocal::refreshIndex()
}
}
updateWatchedDirectories(directories);
qCDebug(CardPictureLoaderLocalLog) << "Finished indexing local image folder CUSTOM; map now has"
<< customFolderIndex.size() << "entries.";
emit indexChanged();
}
void CardPictureLoaderLocal::updateWatchedDirectories(const QSet<QString> &directories)
{
const QSet<QString> stale = watchedDirectories - directories;
if (!stale.isEmpty()) {
folderWatcher->removePaths(QStringList(stale.begin(), stale.end()));
}
const QSet<QString> added = directories - watchedDirectories;
if (!added.isEmpty()) {
const QStringList failed = folderWatcher->addPaths(QStringList(added.begin(), added.end()));
if (!failed.isEmpty()) {
qCDebug(CardPictureLoaderLocalLog) << "Could not watch custom picture directories:" << failed;
}
}
watchedDirectories = directories;
}
QStringList CardPictureLoaderLocal::indexedPaths(const QString &baseName) const
{
return customFolderIndex.values(baseName);
}
QImage CardPictureLoaderLocal::tryLoad(const ExactCard &toLoad) const
@ -145,4 +199,5 @@ void CardPictureLoaderLocal::picsPathChanged()
{
picsPath = SettingsCache::instance().paths().getPicsPath();
customPicsPath = SettingsCache::instance().paths().getCustomPicsPath();
refreshIndex();
}

View file

@ -5,10 +5,12 @@
#include <QLoggingCategory>
#include <QMultiHash>
#include <QObject>
#include <QSet>
#include <QString>
#include <qtmetamacros.h>
class ExactCard;
class QFileSystemWatcher;
class QTimer;
inline Q_LOGGING_CATEGORY(CardPictureLoaderLocalLog, "card_picture_loader.local");
@ -40,6 +42,18 @@ public:
*/
explicit CardPictureLoaderLocal(QObject *parent);
/**
* @brief Constructs the loader against explicit folders without SettingsCache.
*
* Used by tests and by callers that already resolved the configured paths.
*/
CardPictureLoaderLocal(QObject *parent, const QString &picsPath, const QString &customPicsPath);
/**
* @brief Returns every indexed file whose base name matches @p baseName.
*/
[[nodiscard]] QStringList indexedPaths(const QString &baseName) const;
/**
* @brief Attempts to load a card image from local disk or custom folders.
* @param toLoad ExactCard object representing the card to load.
@ -54,7 +68,10 @@ private:
QString customPicsPath; ///< Path to custom card image folder
QMultiHash<QString, QString> customFolderIndex; ///< Multimap from cardName to file paths in CUSTOM folder
QTimer *refreshTimer; ///< Timer for periodic folder index refresh
QFileSystemWatcher *folderWatcher; ///< Watches the custom folder tree for changes
QTimer *indexRefreshTimer; ///< Debounces bursts of filesystem events
QTimer *fallbackRefreshTimer; ///< Slow poll for filesystems without change notification
QSet<QString> watchedDirectories; ///< Directories currently registered with folderWatcher
/**
* @brief Rebuilds the index of the custom image folder.
@ -65,6 +82,11 @@ private:
*/
void refreshIndex();
/**
* @brief Keeps the watcher registered on exactly @p directories.
*/
void updateWatchedDirectories(const QSet<QString> &directories);
/**
* @brief Attempts to load a card image from disk given its set and name info.
* @param setName Corrected short name of the card's set.
@ -82,6 +104,12 @@ private:
const QString &collectorNumber,
const QString &providerId) const;
signals:
/**
* @brief Emitted after the custom folder index has been rebuilt.
*/
void indexChanged();
private slots:
/**
* @brief Updates internal paths when the user changes picture settings.

View file

@ -28,6 +28,7 @@
#include <QVariant>
#include <QtGlobal>
#include <compare>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
@ -46,11 +47,10 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
hostRequestLimits(SettingsCache::instance().downloads().getHostRequestLimits())
{
networkManager = new QNetworkAccessManager(this);
networkManager = NetworkManagerFactory::create(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
// Use Qt's default timeout (30s, as of 2023-02-22)
networkManager->setTransferTimeout();
cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L *

View file

@ -21,6 +21,7 @@
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <utility>
CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
@ -74,19 +75,20 @@ QList<CardSetPtr> CardPictureToLoad::extractSetsSorted(const ExactCard &card)
//! \todo Check if going through all sets is still necessary after the ExactCard refactor.
static PrintingInfo findPrintingForSet(const ExactCard &card, const QString &setName)
{
SetToPrintingsMap setsToPrintings = card.getInfo().getSets();
const auto &setsToPrintings = card.getInfo().getSets();
if (!setsToPrintings.contains(setName)) {
const auto setPrintings = setsToPrintings.constFind(setName);
if (setPrintings == setsToPrintings.constEnd() || setPrintings.value().isEmpty()) {
return PrintingInfo();
}
for (const auto &printing : setsToPrintings[setName]) {
for (const auto &printing : setPrintings.value()) {
if (printing.getUuid() == card.getPrinting().getUuid()) {
return printing;
}
}
return setsToPrintings[setName][0];
return setPrintings.value().first();
}
void CardPictureToLoad::populateSetUrls()
@ -183,10 +185,14 @@ static int parse(const QString &urlTemplate,
{
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
static const QRegularExpression rxCardProp("!prop:([^!]+)!");
static const QRegularExpression rxCardSet("!set:([^!]+)!");
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
// The property type only takes two values, so keep the expression that was
// compiled for it instead of rebuilding one per URL template.
const QRegularExpression &rxCardPropSelector = propType == "prop" ? rxCardProp : rxCardSet;
auto matches = rxCardProp.globalMatch(urlTemplate);
auto matches = rxCardPropSelector.globalMatch(urlTemplate);
while (matches.hasNext()) {
auto match = matches.next();
QString templatePropertyName = match.captured(1);
@ -303,10 +309,10 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
}
QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) {
for (const auto &[prop, value] : std::as_const(transformMap).asKeyValueRange()) {
if (transformedUrl.contains(prop)) {
if (!transformMap[prop].isEmpty()) {
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
if (!value.isEmpty()) {
transformedUrl.replace(prop, QUrl::toPercentEncoding(value));
} else {
/* This means the template is requesting information that is not
* populated in this card, so it should return an empty string,

View file

@ -1,11 +0,0 @@
#include "deck_file_format.h"
#include <qnamespace.h>
DeckFileFormat::Format DeckFileFormat::getFormatFromName(const QString &fileName)
{
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
return Cockatrice;
}
return PlainText;
}

View file

@ -1,36 +0,0 @@
#ifndef COCKATRICE_DECK_FILE_FORMAT_H
#define COCKATRICE_DECK_FILE_FORMAT_H
#include <QString>
namespace DeckFileFormat
{
/**
* The deck file formats that Cockatrice supports.
*/
enum Format
{
/**
* Plaintext deck files, a format that is intended to be widely supported among different programs.
* This format does not support Cockatrice specific features such as banner cards or tags.
*/
PlainText,
/**
* This is Cockatrice's native deck file format, and supports deck metadata such as banner cards and tags.
* Stored as .cod files.
*/
Cockatrice
};
/**
* Determines what deck file format the given filename corresponds to.
*
* @param fileName The filename
* @return The deck format
*/
Format getFormatFromName(const QString &fileName);
} // namespace DeckFileFormat
#endif // COCKATRICE_DECK_FILE_FORMAT_H

View file

@ -7,13 +7,14 @@
#ifndef DECK_LOADER_H
#define DECK_LOADER_H
#include "deck_file_format.h"
#include "loaded_deck.h"
#include <QLoggingCategory>
#include <QObject>
#include <QPrinter>
#include <QString>
#include <QStringList>
#include <QTextCursor>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <optional>
#include <qtmetamacros.h>

View file

@ -1,11 +0,0 @@
#include "loaded_deck.h"
bool LoadedDeck::LoadInfo::isEmpty() const
{
return fileName.isEmpty() && remoteDeckId == NON_REMOTE_ID;
}
bool LoadedDeck::isEmpty() const
{
return deckList.isEmpty() && lastLoadInfo.isEmpty();
}

View file

@ -1,39 +0,0 @@
#ifndef COCKATRICE_LOADED_DECK_H
#define COCKATRICE_LOADED_DECK_H
#include "deck_file_format.h"
#include "libcockatrice/deck_list/deck_list.h"
#include <QString>
/**
* @brief Represents a deck that was loaded from somewhere.
* Contains the DeckList itself, as well as info about where it was loaded from.
*/
struct LoadedDeck
{
/**
* @brief Information about where the deck was loaded from.
*
* For local decks, the remoteDeckId field will always be -1.
* For remote decks, fileName will be empty and fileFormat will always be CockatriceFormat
*/
struct LoadInfo
{
static constexpr int NON_REMOTE_ID = -1;
QString fileName = "";
DeckFileFormat::Format fileFormat = DeckFileFormat::Cockatrice;
int remoteDeckId = NON_REMOTE_ID;
bool isEmpty() const;
};
DeckList deckList; ///< The decklist itself
LoadInfo lastLoadInfo = {}; ///< info about where the deck was loaded from
bool isEmpty() const;
};
#endif // COCKATRICE_LOADED_DECK_H

View file

@ -1,14 +1,14 @@
#include "intent_open_local_deck.h"
#include "../deck_loader/deck_file_format.h"
#include "../deck_loader/deck_loader.h"
#include "../deck_loader/loaded_deck.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "intent_wait_for_database_load.h"
#include "libcockatrice/card/database/card_database.h"
#include "libcockatrice/card/database/card_database_loader.h"
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_file_format.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <optional>
IntentOpenLocalDeck::IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file)

View file

@ -21,6 +21,7 @@
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/qt_utils.h>
#include <optional>
#include <qnamespace.h>
#include <string>
@ -184,13 +185,11 @@ void IntentOpenSharedDeck::onItemFailure(const QString &reason)
}
const int downloadedCount = loadedDecks.size();
const QMessageBox::StandardButton answer = QMessageBox::question(
previewDialog, tr("Open shared decks"),
tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount)
.arg(itemNames.value(currentItemId, tr("Unknown deck"))),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer == QMessageBox::Yes) {
if (QtUtils::confirm(previewDialog, tr("Open shared decks"),
tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "",
downloadedCount)
.arg(itemNames.value(currentItemId, tr("Unknown deck"))),
QMessageBox::Yes)) {
finishAll();
} else {
previewDialog->deleteLater();

View file

@ -1,7 +1,6 @@
#ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H
#define COCKATRICE_INTENT_OPEN_SHARED_DECK_H
#include "../deck_loader/loaded_deck.h"
#include "contexts/context_open_deck.h"
#include "intent.h"
@ -9,6 +8,7 @@
#include <QMap>
#include <QScopedPointer>
#include <QString>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <memory>
#include <qtmetamacros.h>

View file

@ -28,6 +28,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/settings/servers_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <memory>
#include <qlogging.h>
#include <qnamespace.h>
@ -115,9 +116,7 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
const QString message = generateJoinGameMessage(*ctx, gameDescription);
const QMessageBox::StandardButton answer = QMessageBox::question(
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
if (!QtUtils::confirm(mainWindow, tr("Join game"), message, QMessageBox::Yes)) {
return nullptr;
}
@ -231,11 +230,10 @@ Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingInt
if (client->getStatus() == StatusLoggedIn) {
const QString current =
QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort()));
const QMessageBox::StandardButton answer = QMessageBox::question(
mainWindow, tr("Open shared deck"),
tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
if (!QtUtils::confirm(
mainWindow, tr("Open shared deck"),
tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current),
QMessageBox::Yes)) {
return nullptr;
}
chain.migrationTargetHost = ctx->serverContext.hostname;

View file

@ -5,6 +5,8 @@
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QFlags>
#include <QIODevice>
#include <QLocale>
@ -18,8 +20,11 @@
#define LOGGER_MAX_ENTRIES 128
#define LOGGER_FILENAME "qdebug.txt"
#define LOGGER_SESSION_MARKER "Log session started at"
#define LOGGER_PERSISTENT_MAX_BYTES (512 * 1024)
#define LOGGER_PERSISTENT_KEEP_LINES 2000
Logger::Logger() : logToFileEnabled(false)
Logger::Logger() : logToFileEnabled(false), persistentLogEnabled(false)
{
logBuffer.append(getClientVersion());
logBuffer.append(getSystemArchitecture());
@ -35,6 +40,7 @@ Logger::Logger() : logToFileEnabled(false)
Logger::~Logger()
{
closeLogfileSession();
closePersistentLogSession();
logBuffer.clear();
}
@ -82,6 +88,137 @@ void Logger::closeLogfileSession()
fileHandle.close();
}
void Logger::setPersistentLogPath(const QString &path)
{
QMutexLocker locker(&mutex);
if (path.isEmpty() || (persistentLogEnabled && persistentLogPath == path)) {
return;
}
QDir().mkpath(QFileInfo(path).absolutePath());
persistentLogPath = path;
openPersistentLog(path);
}
void Logger::prunePersistentLog(const QString &path)
{
QFileInfo info(path);
if (!info.exists() || info.size() <= LOGGER_PERSISTENT_MAX_BYTES) {
return;
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
QList<QString> allLines;
QTextStream in(&file);
while (!in.atEnd()) {
allLines.append(in.readLine());
}
file.close();
const int start =
allLines.size() > LOGGER_PERSISTENT_KEEP_LINES ? allLines.size() - LOGGER_PERSISTENT_KEEP_LINES : 0;
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
return;
}
QTextStream out(&file);
for (int i = start; i < allLines.size(); ++i) {
out << allLines.at(i) << Qt::endl;
}
file.close();
}
void Logger::openPersistentLog(const QString &path)
{
prunePersistentLog(path);
persistentFileHandle.setFileName(path);
if (!persistentFileHandle.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
qWarning() << "Logger failed to open persistent log" << path << "for writing";
return;
}
persistentFileStream.setDevice(&persistentFileHandle);
persistentFileStream << LOGGER_SESSION_MARKER << " " << QDateTime::currentDateTime().toString() << Qt::endl;
persistentFileStream << getClientVersion() << Qt::endl;
persistentFileStream << getSystemArchitecture() << Qt::endl;
persistentFileStream << getClientInstallInfo() << Qt::endl;
persistentFileStream.flush();
persistentLogEnabled = true;
}
void Logger::closePersistentLogSession()
{
if (!persistentLogEnabled) {
return;
}
persistentLogEnabled = false;
persistentFileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << Qt::endl;
persistentFileStream.flush();
persistentFileHandle.close();
}
QList<QString> Logger::getPreviousSessionTail(int maxLines) const
{
QString path;
{
QMutexLocker locker(&mutex);
path = persistentLogPath;
}
if (path.isEmpty()) {
return {};
}
return extractPreviousSessionTail(path, maxLines);
}
QList<QString> Logger::extractPreviousSessionTail(const QString &filePath, int maxLines)
{
QList<QString> result;
if (maxLines <= 0) {
return result;
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return result;
}
QList<QString> allLines;
QTextStream in(&file);
while (!in.atEnd()) {
allLines.append(in.readLine());
}
file.close();
QList<int> markerIndexes;
for (int i = 0; i < allLines.size(); ++i) {
if (allLines.at(i).contains(QLatin1String(LOGGER_SESSION_MARKER))) {
markerIndexes.append(i);
}
}
QList<QString> tail;
if (markerIndexes.isEmpty()) {
tail = allLines;
} else if (markerIndexes.size() == 1) {
// Only the current session has been recorded; there is no previous session yet.
return result;
} else {
const int previousStart = markerIndexes.at(markerIndexes.size() - 2);
const int currentStart = markerIndexes.last();
for (int i = previousStart; i < currentStart; ++i) {
tail.append(allLines.at(i));
}
}
const int start = tail.size() > maxLines ? tail.size() - maxLines : 0;
for (int i = start; i < tail.size(); ++i) {
result.append(tail.at(i));
}
return result;
}
void Logger::log(QtMsgType /* type */, const QMessageLogContext & /* ctx */, const QString &message)
{
QMetaObject::invokeMethod(this, "internalLog", Qt::QueuedConnection, Q_ARG(const QString &, message));
@ -102,6 +239,11 @@ void Logger::internalLog(const QString &message)
if (logToFileEnabled) {
fileStream << message << Qt::endl; // Print to fileStream
}
if (persistentLogEnabled) {
persistentFileStream << message << Qt::endl;
persistentFileStream.flush();
}
}
QString Logger::getSystemArchitecture()

View file

@ -49,6 +49,9 @@ public:
{
return logBuffer;
}
void setPersistentLogPath(const QString &path);
QList<QString> getPreviousSessionTail(int maxLines = 60) const;
static QList<QString> extractPreviousSessionTail(const QString &filePath, int maxLines = 60);
private:
Logger();
@ -61,11 +64,18 @@ private:
QTextStream fileStream;
QFile fileHandle;
QList<QString> logBuffer;
QMutex mutex;
mutable QMutex mutex;
QString persistentLogPath;
bool persistentLogEnabled;
QTextStream persistentFileStream;
QFile persistentFileHandle;
protected:
void openLogfileSession();
void closeLogfileSession();
void openPersistentLog(const QString &path);
void prunePersistentLog(const QString &path);
void closePersistentLogSession();
protected slots:
void internalLog(const QString &message);

View file

@ -25,6 +25,7 @@
#include <QVBoxLayout>
#include <QWidget>
#include <initializer_list>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
@ -57,9 +58,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
void PaletteEditorDialog::setupUi()
{
auto *root = new QVBoxLayout(this);
root->setSpacing(0);
root->setContentsMargins(0, 0, 0, 0);
auto *root = QtUtils::makeLayout<QVBoxLayout>(this, 0, 0);
// Header
header = new QWidget;

View file

@ -18,6 +18,7 @@
#include <QTimer>
#include <QVBoxLayout>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <utility>
@ -65,8 +66,7 @@ static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
{
scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded);
scroll->setFrameShape(QFrame::NoFrame);
gridHost = new QWidget;
@ -74,8 +74,7 @@ PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
refreshChromePalettes();
scroll->setWidget(gridHost);
layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QVBoxLayout>(this);
layout->addWidget(scroll);
}
@ -102,9 +101,7 @@ void PaletteGridWidget::buildGrid(QWidget *host)
auto *label = new QLabel(groupHeaders[col], host);
label->setAlignment(Qt::AlignCenter);
label->setToolTip(groupTips[col]);
QFont f = label->font();
f.setBold(true);
label->setFont(f);
label->setFont(QtUtils::boldFont(label->font()));
label->setAutoFillBackground(true);
label->setContentsMargins(4, 4, 4, 4);
grid->addWidget(label, 0, col + 1);
@ -149,9 +146,7 @@ void PaletteGridWidget::buildGrid(QWidget *host)
auto *appHeader = new QLabel(tr("App colors"), host);
appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
QFont appHeaderFont = appHeader->font();
appHeaderFont.setBold(true);
appHeader->setFont(appHeaderFont);
appHeader->setFont(QtUtils::boldFont(appHeader->font()));
appHeader->setAutoFillBackground(true);
appHeader->setContentsMargins(4, 4, 4, 4);
grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);

View file

@ -558,6 +558,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
widget->update();
}
// Palette-only previews do not run themeChangedSlot(), so drop snapshots
// generated under the previous palette here as well.
clearPixmapGeneratorCaches();
emit paletteChanged();
}

View file

@ -23,9 +23,7 @@
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const QString &_colorIdentity)
: QWidget(parent), colorIdentity(_colorIdentity)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QHBoxLayout>(this, 5);
layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered
setLayout(layout);

View file

@ -11,12 +11,11 @@
#include <QResizeEvent>
#include <QSize>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
ManaCostWidget::ManaCostWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QHBoxLayout>(this, 5);
setLayout(layout);
setFixedHeight(50); // Fixed height

View file

@ -17,6 +17,7 @@
#include <QString>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/utility/qt_utils.h>
class QWidget;
@ -30,9 +31,8 @@ CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *pa
text->setObjectName("text");
connect(text, &CardInfoTextWidget::linkActivated, this, [this](const QString &card) { setCard({card}); });
auto *layout = new QVBoxLayout();
auto *layout = QtUtils::makeLayout<QVBoxLayout>();
layout->setObjectName("layout");
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(pic, 0, Qt::AlignCenter);
layout->addWidget(text, 0, Qt::AlignCenter);

View file

@ -19,6 +19,7 @@
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/card_ref.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
struct CardRef;
@ -49,15 +50,13 @@ CardInfoFrameWidget::CardInfoFrameWidget(QWidget *parent)
insertTab(ImageAndTextView, tab3, QString());
connect(this, &CardInfoFrameWidget::currentChanged, this, &CardInfoFrameWidget::setViewMode);
tab1Layout = new QVBoxLayout();
tab1Layout = QtUtils::makeLayout<QVBoxLayout>();
tab1Layout->setObjectName("tab1Layout");
tab1Layout->setContentsMargins(0, 0, 0, 0);
tab1Layout->setSpacing(0);
tab1->setLayout(tab1Layout);
tab2Layout = new QVBoxLayout();
tab2Layout = QtUtils::makeLayout<QVBoxLayout>();
tab2Layout->setObjectName("tab2Layout");
tab2Layout->setContentsMargins(0, 0, 0, 0);
tab2Layout->setSpacing(0);
tab2->setLayout(tab2Layout);
@ -65,9 +64,8 @@ CardInfoFrameWidget::CardInfoFrameWidget(QWidget *parent)
splitter->setObjectName("splitter");
splitter->setOrientation(Qt::Vertical);
tab3Layout = new QVBoxLayout();
tab3Layout = QtUtils::makeLayout<QVBoxLayout>();
tab3Layout->setObjectName("tab3Layout");
tab3Layout->setContentsMargins(0, 0, 0, 0);
tab3Layout->setSpacing(0);
tab3Layout->addWidget(splitter);
tab3->setLayout(tab3Layout);

View file

@ -491,7 +491,14 @@ void CardInfoPictureWidget::showEnlargedPixmap()
enlargedPixmapWidget->setCardPixmap(exactCard, enlargedSize);
const QPoint cursorPos = QCursor::pos();
const QRect screenGeometry = QGuiApplication::screenAt(cursorPos)->geometry();
QScreen *screen = QGuiApplication::screenAt(cursorPos);
if (!screen) {
screen = QGuiApplication::primaryScreen();
}
if (!screen) {
return;
}
const QRect screenGeometry = screen->geometry();
const QSize widgetSize = enlargedPixmapWidget->size();
int newX = cursorPos.x() + ENLARGED_PIXMAP_OFFSET;

View file

@ -19,6 +19,7 @@
#include <QWidget>
#include <libcockatrice/card/game_specific_terms.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <qobjectdefs.h>
@ -33,9 +34,7 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent)
textLabel->setReadOnly(true);
textLabel->setMinimumSize(35, 35);
propsScroll = new QScrollArea(this);
propsScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
propsScroll->setWidgetResizable(true);
propsScroll = QtUtils::makeScrollArea(this);
propsScroll->setAlignment(Qt::AlignLeft | Qt::AlignTop);
propsScroll->setContentsMargins(0, 0, 0, 0);
propsScroll->setFrameStyle(QFrame::NoFrame);

View file

@ -29,6 +29,7 @@
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class CardSizeWidget;
@ -179,15 +180,7 @@ void DeckCardZoneDisplayWidget::displayCards()
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
{
displayType = _displayType;
QLayoutItem *item;
while ((item = cardGroupLayout->takeAt(0)) != nullptr) {
if (item->widget()) {
item->widget()->deleteLater();
} else if (item->layout()) {
item->layout()->deleteLater();
}
delete item;
}
QtUtils::clearLayoutRec(cardGroupLayout);
indexToWidgetMap.clear();

View file

@ -22,6 +22,7 @@
#include <QWidget>
#include <algorithm>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <utility>
@ -47,12 +48,7 @@ ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *anal
void ManaBaseWidget::updateDisplay()
{
// Clear previous widgets
while (QLayoutItem *item = barLayout->takeAt(0)) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
QtUtils::clearLayoutRec(barLayout);
auto &pipCount = analyzer->getProductionPipCount();
auto &cardCount = analyzer->getProductionCardCount();

View file

@ -16,14 +16,13 @@
#include <QVBoxLayout>
#include <QVector>
#include <algorithm>
#include <libcockatrice/utility/qt_utils.h>
constexpr int MIN_ROW_HEIGHT = 100; // Minimum readable height per row
ManaCurveCategoryWidget::ManaCurveCategoryWidget(QWidget *parent) : QWidget(parent)
{
layout = new QVBoxLayout(this);
layout->setSpacing(4);
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QVBoxLayout>(this, 0, 4);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
}
@ -95,9 +94,7 @@ void ManaCurveCategoryWidget::updateDisplay(int minCmc,
row->setFixedHeight(MIN_ROW_HEIGHT);
QHBoxLayout *rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
rowLayout->setSpacing(4);
QHBoxLayout *rowLayout = QtUtils::makeLayout<QHBoxLayout>(row, 4);
QLabel *categoryLabel = new QLabel(cat, row);
categoryLabel->setFixedWidth(80);

View file

@ -21,6 +21,7 @@
#include <QVBoxLayout>
#include <QWidget>
#include <algorithm>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <utility>
@ -47,12 +48,7 @@ ManaDevotionWidget::ManaDevotionWidget(QWidget *parent, DeckListStatisticsAnalyz
void ManaDevotionWidget::updateDisplay()
{
// Clear previous widgets
while (QLayoutItem *item = barLayout->takeAt(0)) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
QtUtils::clearLayoutRec(barLayout);
auto &pipCount = analyzer->getDevotionPipCount();
auto &cardCount = analyzer->getDevotionCardCount();

View file

@ -22,6 +22,7 @@
#include <QSettings>
#include <QVBoxLayout>
#include <QVariant>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *_statsAnalyzer)
@ -54,8 +55,7 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
connect(includeSideboardCheckBox, &QCheckBox::clicked, this, &DeckAnalyticsWidget::includeSideboardChanged);
// Scroll area and container
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded);
scrollArea->setFrameShape(QFrame::NoFrame);
panelContainer = new QWidget(scrollArea);

View file

@ -28,6 +28,7 @@
#include <QTimer>
#include <QVBoxLayout>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWidget *analyticsPanel, QWidget *parent)
@ -35,18 +36,14 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid
{
setAcceptDrops(true);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
auto *mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 0);
// Frame for selection highlight
frame = new QFrame(this);
frame->setFrameShape(QFrame::Box);
frame->setLineWidth(2);
auto *frameLayout = new QVBoxLayout(frame);
frameLayout->setContentsMargins(0, 0, 0, 0);
frameLayout->setSpacing(0);
auto *frameLayout = QtUtils::makeLayout<QVBoxLayout>(frame, 0);
// Add the analytics panel
frameLayout->addWidget(analyticsPanel);
@ -63,9 +60,7 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid
// Bottom bar with drag button and resize handle
auto *bottomBar = new QWidget(frame);
auto *bottomLayout = new QHBoxLayout(bottomBar);
bottomLayout->setContentsMargins(0, 0, 0, 0);
bottomLayout->setSpacing(0);
auto *bottomLayout = QtUtils::makeLayout<QHBoxLayout>(bottomBar, 0);
// Drag button on the left
dragButton = new QPushButton("☰", bottomBar);

View file

@ -28,6 +28,7 @@
#include <QVBoxLayout>
#include <QVariant>
#include <QWidget>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
DeckEditorFilterDockWidget::DeckEditorFilterDockWidget(AbstractTabDeckEditor *parent)
@ -80,9 +81,8 @@ void DeckEditorFilterDockWidget::createFiltersDock()
filterDelAll->setDefaultAction(aClearFilterAll);
filterDelAll->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
auto *filterLayout = new QGridLayout;
auto *filterLayout = QtUtils::makeLayout<QGridLayout>();
filterLayout->setObjectName("filterLayout");
filterLayout->setContentsMargins(0, 0, 0, 0);
filterLayout->addWidget(filterBuilder, 0, 0, 1, 3);
filterLayout->addWidget(filterView, 1, 0, 1, 3);
filterLayout->addWidget(filterDelOne, 2, 0, 1, 1);

View file

@ -1,7 +1,6 @@
#include "deck_state_manager.h"
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/loaded_deck.h"
#include "deck_list_model.h"
#include "libcockatrice/card/card_info.h"
#include "libcockatrice/card/database/card_database_querier.h"
@ -18,6 +17,7 @@
#include <QVariant>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list_history_manager.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <qnamespace.h>

View file

@ -1,7 +1,7 @@
#ifndef COCKATRICE_DECK_STATE_MANAGER_H
#define COCKATRICE_DECK_STATE_MANAGER_H
#include "../../deck_loader/loaded_deck.h"
#include "deck_list_model.h"
#include <QModelIndex>
#include <QObject>
@ -10,6 +10,7 @@
#include <QStringList>
#include <functional>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <qtmetamacros.h>
class DeckListHistoryManager;

View file

@ -15,6 +15,7 @@
#include <QStringLiteral>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_querier.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent,
@ -62,8 +63,7 @@ SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent,
selectionRow->addWidget(selectionCheckBox);
selectionRow->addStretch(1);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
auto *layout = QtUtils::makeLayout<QVBoxLayout>(this);
layout->addLayout(selectionRow);
layout->addWidget(bannerCardDisplayWidget, 0, Qt::AlignHCenter);
layout->addWidget(colorIdentityWidget, 0, Qt::AlignHCenter);

View file

@ -295,9 +295,15 @@ void DlgConnect::updateDisplayInfo(const QString &saveName)
const QString stored = _data.at(4);
if (stored.startsWith("$")) {
storedVerifier = stored;
passwordEdit->clear();
passwordEdit->setPlaceholderText(tr("Password saved"));
} else {
passwordEdit->setText(stored);
passwordEdit->setPlaceholderText(QString());
}
} else {
passwordEdit->clear();
passwordEdit->setPlaceholderText(QString());
}
if (!_data.at(6).isEmpty()) {
@ -323,6 +329,7 @@ void DlgConnect::newHostSelected(bool state)
portEdit->setDisabled(false);
playernameEdit->clear();
passwordEdit->clear();
passwordEdit->setPlaceholderText(QString());
storedVerifier.clear();
saveEdit->clear();
saveEdit->setPlaceholderText(tr("Unique Server Name"));

View file

@ -1,7 +1,6 @@
#include "dlg_convert_deck_to_cod_format.h"
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_file_format.h"
#include <QCheckBox>
#include <QDialogButtonBox>
@ -12,7 +11,9 @@
#include <QMessageBox>
#include <QObject>
#include <QVBoxLayout>
#include <libcockatrice/deck_list/deck_file_format.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/qt_utils.h>
DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent)
{
@ -57,11 +58,8 @@ bool confirmOverwriteIfExists(QWidget *parent, const QString &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 QtUtils::confirm(parent, QObject::tr("Overwrite Existing File?"),
QObject::tr("A .cod version of this deck already exists. Overwrite it?"));
}
return true; // Safe to proceed
}

View file

@ -0,0 +1,245 @@
#include "dlg_crash_report.h"
#include "../../../client/crash/crash_reporter.h"
#include "../../../client/settings/cache_settings.h"
#include "../../logger.h"
#include <QCheckBox>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QEvent>
#include <QFile>
#include <QHBoxLayout>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QList>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <libcockatrice/settings/personal_settings.h>
DlgCrashReport::DlgCrashReport(QWidget *parent, bool _realSending, CrashReporter *_reporter)
: QDialog(parent), realSending(_realSending), reporter(_reporter)
{
infoLabel = new QLabel(this);
infoLabel->setWordWrap(true);
accountLabel = new QLabel(this);
accountLabel->setWordWrap(true);
// Constructed before previewArea: Qt derives the default Tab chain from construction
// order, and note sits visually above the log excerpt, so this keeps Tab moving downward.
noteArea = new QPlainTextEdit(this);
// Optional free text ("what I was doing when it crashed") — a few lines are
// enough for a description, leave the rest of the space to the log excerpt.
noteArea->setMaximumHeight(70);
previewArea = new QPlainTextEdit(this);
previewArea->setReadOnly(true);
previewArea->setPlainText(buildRedactedPreview());
rememberBox = new QCheckBox(this);
sendButton = new QPushButton(this);
sendButton->setDefault(true);
connect(sendButton, &QPushButton::clicked, this, &DlgCrashReport::actSend);
dontSendButton = new QPushButton(this);
dontSendButton->setAutoDefault(false);
connect(dontSendButton, &QPushButton::clicked, this, &DlgCrashReport::actDontSend);
auto *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(rememberBox);
buttonLayout->addStretch();
buttonLayout->addWidget(dontSendButton);
buttonLayout->addWidget(sendButton);
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(infoLabel);
mainLayout->addWidget(accountLabel);
mainLayout->addWidget(noteArea);
mainLayout->addWidget(previewArea);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
resize(700, 500);
retranslateUi();
}
void DlgCrashReport::changeEvent(QEvent *event)
{
if (event->type() == QEvent::LanguageChange) {
retranslateUi();
}
QDialog::changeEvent(event);
}
void DlgCrashReport::retranslateUi()
{
setWindowTitle(tr("Send crash report"));
if (realSending) {
// The preview below prefers the crashed session's persisted log tail
// (Logger::getPreviousSessionTail), falling back to this restart's in-memory buffer only
// when no previous session was saved. On send it is attached as crash-log.txt to the
// pending native crash event (dump + stack + log + note in one report); when no native
// crash event exists, it rides the fallback message event instead.
infoLabel->setText(tr("Oops, we crashed — and now we need your help to make Cockatrice better!\n\n"
"The good thing is, helping is easy and convenient. Sending uploads what the "
"previous run left behind when it crashed: the crash dump with the stack trace, "
"the app version and operating system info, the redacted log excerpt shown below, "
"and anything you write in the note box. If no log from the crashed session was "
"saved, this restart's early logs are shown instead.\n\n"
"Reports are what let us fix crashes like this one. Check \u201cRemember my choice\u201d "
"and Send, and future crash reports are sent automatically without interruption."));
} else {
infoLabel->setText(tr("(Mock preview — nothing will be uploaded.)\n\n"
"Cockatrice closed unexpectedly. A real report would contain what the previous run "
"left behind when it crashed: the crash dump with the stack trace, the app version "
"and operating system info, the redacted log excerpt shown below, and anything you "
"write in the note box. If no log from the crashed session was saved, this restart's "
"early logs are shown instead.\n\n"
"A mock file is written locally for inspection."));
}
accountLabel->setText(tr("No account needed, and no contact info is collected. Login links "
"(cockatrice://...) are scrubbed before sending."));
noteArea->setPlaceholderText(tr("(Optional) Describe what you were doing when it crashed"));
// Direction-neutral on purpose: the same box also remembers a "Don't send" decision
// (reject() persists NeverSend when it is checked), so it must not promise sending.
rememberBox->setText(tr("Remember my choice"));
sendButton->setText(tr("Send"));
dontSendButton->setText(tr("Don't send"));
}
QString DlgCrashReport::buildRedactedPreview()
{
Logger &logger = Logger::getInstance();
QStringList header;
header.append(logger.getClientVersion());
header.append(logger.getSystemArchitecture());
header.append(logger.getSystemLocale());
header.append(logger.getClientInstallInfo());
header.append(QStringLiteral("---"));
QList<QString> logLines = logger.getPreviousSessionTail();
if (logLines.isEmpty()) {
logLines = logger.getLogBuffer();
}
const int previewLines = 60;
int start = logLines.size() > previewLines ? logLines.size() - previewLines : 0;
QStringList lines = header;
for (int i = start; i < logLines.size(); ++i) {
lines.append(CrashReporter::redactCockatriceUrls(CrashReporter::sanitizeLogLine(logLines.at(i))));
}
return lines.join(QStringLiteral("\n"));
}
QString DlgCrashReport::writeMockReport(const QString &previewText, const QString &userNote)
{
Logger &logger = Logger::getInstance();
QJsonObject report;
report[QStringLiteral("mock")] = true;
report[QStringLiteral("timestamp")] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
report[QStringLiteral("clientVersion")] = logger.getClientVersion();
report[QStringLiteral("systemArchitecture")] = logger.getSystemArchitecture();
report[QStringLiteral("systemLocale")] = logger.getSystemLocale();
report[QStringLiteral("installInfo")] = logger.getClientInstallInfo();
report[QStringLiteral("logPreview")] = previewText;
report[QStringLiteral("userNote")] = userNote;
QString fileName = QStringLiteral("crash-report-mock-%1.json")
.arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd-HHmmss")));
QString filePath = QDir::tempPath() + QStringLiteral("/") + fileName;
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return QString();
}
file.write(QJsonDocument(report).toJson());
file.close();
return filePath;
}
void DlgCrashReport::actSend()
{
if (rememberBox->isChecked()) {
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::AlwaysSend);
}
const QString preview = previewArea->toPlainText();
const QString userNote = noteArea->toPlainText().trimmed();
if (realSending && reporter != nullptr) {
// Fold log and note into the pending native crash event(s) BEFORE consent:
// giveConsent() releases the held envelopes for upload, so dump, stack,
// log and note arrive as one Sentry event. Without a pending crash event
// (unclean exit that produced no native crash) this returns 0 and
// sendReport() falls back to a standalone message event with the same
// attachments.
const int attached = reporter->attachToPendingCrashEnvelopes(preview, userNote);
reporter->giveConsent();
if (attached > 0 || reporter->sendReport(preview, userNote)) {
accept();
return;
}
qCWarning(CrashReportLog) << "Failed to send crash report";
// Outcome mapping for the failure box: Save = store locally and close
// as accepted (the follow-up info box shows the path, so the user
// knows it was NOT sent); Discard = revoke consent and close as
// rejected (dropped); Cancel/X = back to the dialog, neither sent nor
// dropped. The Remember checkbox governs only the original
// Send/Don't-send decision, so the Discard path below revokes and
// closes via QDialog::reject() without touching the setting (calling
// our reject() override here would wrongly persist NeverSend over the
// AlwaysSend stored above when Remember was checked).
QMessageBox failureBox(QMessageBox::Warning, tr("Send crash report"),
tr("Sending the crash report failed. Save the report locally instead?"),
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, this);
failureBox.setDefaultButton(QMessageBox::Save);
failureBox.setEscapeButton(QMessageBox::Cancel);
const QMessageBox::StandardButton choice = static_cast<QMessageBox::StandardButton>(failureBox.exec());
if (choice == QMessageBox::Save) {
const QString savedPath = writeMockReport(preview, userNote);
if (savedPath.isEmpty()) {
QMessageBox::warning(this, tr("Send crash report"), tr("Could not save the crash report locally."));
} else {
QMessageBox::information(this, tr("Send crash report"),
tr("Crash report saved locally to:\n%1").arg(savedPath));
}
accept();
return;
}
if (choice == QMessageBox::Discard) {
reporter->revokeConsent();
QDialog::reject();
return;
}
return;
}
const QString savedPath = writeMockReport(preview, userNote);
if (savedPath.isEmpty()) {
qCWarning(CrashReportLog) << "Could not save the mock crash report locally";
} else {
qCInfo(CrashReportLog) << "Mock crash report saved locally to" << savedPath;
}
accept();
}
void DlgCrashReport::actDontSend()
{
reject();
}
void DlgCrashReport::reject()
{
if (rememberBox->isChecked()) {
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::NeverSend);
}
if (realSending && reporter != nullptr) {
reporter->revokeConsent();
}
QDialog::reject();
}

View file

@ -0,0 +1,63 @@
/**
* @file dlg_crash_report.h
* @ingroup Dialogs
*/
#ifndef DLG_CRASH_REPORT_H
#define DLG_CRASH_REPORT_H
#include <QDialog>
#include <QString>
#include <qtmetamacros.h>
class QCheckBox;
class QLabel;
class QPlainTextEdit;
class QPushButton;
class CrashReporter;
/**
* @brief Crash-report dialog: mock fallback or real Sentry send.
*
* Mock mode (default): the Send path writes a redacted JSON file to the temp
* dir instead of uploading anything. Real mode (realSending, with a live
* CrashReporter): Send first folds the redacted log and the optional user note
* into the pending native crash event(s), then gives consent so dump, stack,
* log and note upload as one Sentry event; when no pending crash event exists
* it falls back to a standalone message event with the same attachments.
* Failure offers a local save instead.
*/
class DlgCrashReport : public QDialog
{
Q_OBJECT
public:
explicit DlgCrashReport(QWidget *parent = nullptr, bool realSending = false, CrashReporter *_reporter = nullptr);
static QString buildRedactedPreview();
static QString writeMockReport(const QString &previewText, const QString &userNote = QString());
void reject() override;
protected:
void changeEvent(QEvent *event) override;
private:
QLabel *infoLabel;
QLabel *accountLabel;
QPlainTextEdit *previewArea;
QPlainTextEdit *noteArea;
QCheckBox *rememberBox;
QPushButton *sendButton;
QPushButton *dontSendButton;
bool realSending = false;
CrashReporter *reporter = nullptr;
void retranslateUi();
private slots:
void actSend();
void actDontSend();
};
#endif

View file

@ -17,6 +17,7 @@
#include <QVBoxLayout>
#include <QWidget>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/qt_utils.h>
DlgDefaultTagsEditor::DlgDefaultTagsEditor(QWidget *parent) : QDialog(parent)
{
@ -120,8 +121,7 @@ void DlgDefaultTagsEditor::addItem()
// Add new tag
auto *item = new QListWidgetItem(listWidget);
QWidget *widget = new QWidget(this);
QHBoxLayout *layout = new QHBoxLayout(widget);
layout->setContentsMargins(0, 0, 0, 0);
QHBoxLayout *layout = QtUtils::makeLayout<QHBoxLayout>(widget);
// Editable tag label
QLineEdit *lineEdit = new QLineEdit(newTag, this);

View file

@ -71,7 +71,21 @@ void DlgEditAvatar::actBrowse()
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
// Honour the EXIF orientation phone cameras write; otherwise portrait
// photos are shown and stored sideways.
imgReader.setAutoTransform(true);
imgReader.setFileName(fileName);
// Decode straight to the maximum avatar size. Scaling afterwards decodes
// the full-resolution image first, which can be tens of megabytes for a
// modern camera photo.
const QSize sourceSize = imgReader.size();
if (sourceSize.isValid() &&
(sourceSize.width() > MAX_AVATAR_DIMENSION || sourceSize.height() > MAX_AVATAR_DIMENSION)) {
const QSize targetSize = sourceSize.scaled(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, Qt::KeepAspectRatio);
imgReader.setScaledSize(targetSize);
}
if (!imgReader.read(&image)) {
qCWarning(DlgEditAvatarLog) << "Avatar image loading failed for file:" << fileName;
imageLabel->setText(tr("Invalid image chosen."));

View file

@ -10,8 +10,10 @@
#include "libcockatrice/deck_list/deck_list.h"
#include <QCheckBox>
#include <QDialog>
#include <QString>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <qtmetamacros.h>
class QPlainTextEdit;

View file

@ -27,6 +27,7 @@
#include <QVBoxLayout>
#include <functional>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/network/network_manager.h>
#include <qnamespace.h>
#include <version_string.h>
@ -34,7 +35,7 @@ class QWidget;
DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent)
{
nam = new QNetworkAccessManager(this);
nam = NetworkManagerFactory::create(this);
layout = new QVBoxLayout(this);
setLayout(layout);

View file

@ -6,8 +6,6 @@
#include "libcockatrice/protocol/pb/serverinfo_report.pb.h"
#include <QAbstractItemView>
#include <QFont>
#include <QFontDatabase>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
@ -26,6 +24,7 @@
#include <libcockatrice/protocol/pb/response_report_details.pb.h>
#include <libcockatrice/protocol/pb/response_report_my_list.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <string>
@ -70,13 +69,7 @@ DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent)
auto *chatGroup = new QGroupBox(tr("Chat Log Context"));
chatLogEdit = new QTextEdit;
chatLogEdit->setReadOnly(true);
QFont monoFont("monospace");
monoFont.setStyleHint(QFont::Monospace);
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
if (systemPointSize > 0) {
monoFont.setPointSize(systemPointSize);
}
chatLogEdit->setFont(monoFont);
chatLogEdit->setFont(QtUtils::monospaceFont());
auto *chatLayout = new QVBoxLayout(chatGroup);
chatLayout->setContentsMargins(4, 4, 4, 4);
chatLayout->addWidget(chatLogEdit);

View file

@ -5,8 +5,6 @@
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFont>
#include <QFontDatabase>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
@ -21,6 +19,7 @@
#include <QVariant>
#include <libcockatrice/protocol/pb/command_report.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/qt_utils.h>
#include <libcockatrice/utility/report_categories.h>
#include <qnamespace.h>
#include <string>
@ -92,13 +91,7 @@ DlgReportUser::DlgReportUser(AbstractClient *_client,
auto *chatLayout = new QVBoxLayout(chatGroup);
chatLogEdit = new QTextEdit;
chatLogEdit->setReadOnly(true);
QFont monoFont("monospace");
monoFont.setStyleHint(QFont::Monospace);
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
if (systemPointSize > 0) {
monoFont.setPointSize(systemPointSize);
}
chatLogEdit->setFont(monoFont);
chatLogEdit->setFont(QtUtils::monospaceFont());
if (!_autoChatLog.isEmpty()) {
chatLogEdit->setPlainText(_autoChatLog);
} else {
@ -135,12 +128,8 @@ void DlgReportUser::actSubmit()
return;
}
QMessageBox::StandardButton reply =
QMessageBox::question(this, tr("Confirm Report"),
tr("Submit report against %1 for %2?").arg(reportedUser, categoryBox->currentText()),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) {
if (!QtUtils::confirm(this, tr("Confirm Report"),
tr("Submit report against %1 for %2?").arg(reportedUser, categoryBox->currentText()))) {
return;
}

View file

@ -50,6 +50,7 @@
#include <algorithm>
#include <functional>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qalgorithms.h>
#include <qnamespace.h>
#include <qobjectdefs.h>
@ -71,10 +72,7 @@ DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *de
QSplitter *splitter = new QSplitter(Qt::Vertical, this);
// Top scroll area
scrollArea = new QScrollArea(this);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setWidgetResizable(true);
scrollArea = QtUtils::makeScrollArea(this);
listContainer = new QWidget(scrollArea);
listLayout = new QVBoxLayout(listContainer);
@ -86,14 +84,10 @@ DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *de
// Left container
QWidget *leftContainer = new QWidget(this);
QVBoxLayout *leftLayout = new QVBoxLayout(leftContainer);
leftLayout->setContentsMargins(0, 0, 0, 0);
QVBoxLayout *leftLayout = QtUtils::makeLayout<QVBoxLayout>(leftContainer);
uneditedCardsLabel = new QLabel(this);
uneditedCardsArea = new QScrollArea(this);
uneditedCardsArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
uneditedCardsArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
uneditedCardsArea->setWidgetResizable(true);
uneditedCardsArea = QtUtils::makeScrollArea(this);
uneditedCardsFlowWidget =
new FlowWidget(uneditedCardsArea, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
@ -105,14 +99,10 @@ DlgSelectSetForCards::DlgSelectSetForCards(QWidget *parent, DeckStateManager *de
// Right container
QWidget *rightContainer = new QWidget(this);
QVBoxLayout *rightLayout = new QVBoxLayout(rightContainer);
rightLayout->setContentsMargins(0, 0, 0, 0);
QVBoxLayout *rightLayout = QtUtils::makeLayout<QVBoxLayout>(rightContainer);
modifiedCardsLabel = new QLabel(this);
modifiedCardsArea = new QScrollArea(this);
modifiedCardsArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
modifiedCardsArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
modifiedCardsArea->setWidgetResizable(true);
modifiedCardsArea = QtUtils::makeScrollArea(this);
modifiedCardsFlowWidget =
new FlowWidget(modifiedCardsArea, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
@ -274,9 +264,9 @@ QMap<QString, int> DlgSelectSetForCards::getSetsForCards()
continue;
}
SetToPrintingsMap setMap = infoPtr->getSets();
for (auto &setName : setMap.keys()) {
setCounts[setName]++;
const auto &setMap = infoPtr->getSets();
for (auto it = setMap.constBegin(); it != setMap.constEnd(); ++it) {
setCounts[it.key()]++;
}
}
@ -393,8 +383,8 @@ QMap<QString, QStringList> DlgSelectSetForCards::getCardsForSets()
continue;
}
SetToPrintingsMap setMap = infoPtr->getSets();
for (auto it = setMap.begin(); it != setMap.end(); ++it) {
const auto &setMap = infoPtr->getSets();
for (auto it = setMap.constBegin(); it != setMap.constEnd(); ++it) {
setCards[it.key()].append(cardName);
}
}

View file

@ -13,6 +13,7 @@
#include <QVBoxLayout>
#include <google/protobuf/repeated_ptr_field.h>
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class QWidget;
@ -34,8 +35,7 @@ DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent,
// QLabels render AutoText and markup would otherwise be shown as rich text.
auto *titleLabel =
new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this);
QFont titleFont = titleLabel->font();
titleFont.setBold(true);
QFont titleFont = QtUtils::boldFont(titleLabel->font());
titleFont.setPointSize(titleFont.pointSize() + 2);
titleLabel->setFont(titleFont);
mainLayout->addWidget(titleLabel);

View file

@ -30,6 +30,7 @@
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <version_string.h>
@ -170,16 +171,13 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
publishDate = release->getPublishDate().toString(QLocale().dateFormat(QLocale::LongFormat));
if (isCompatible) {
int reply;
reply = QMessageBox::question(
window(), tr("Update Available"),
tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") +
"</a>)<br><br>" + tr("Do you want to update now?"),
QMessageBox::Yes | QMessageBox::No);
const QString message = tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" +
tr("New version") + QString(":</b> %1<br>").arg(release->getName()) + "<b>" +
tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) +
tr("Changelog") + "</a>)<br><br>" + tr("Do you want to update now?");
if (reply == QMessageBox::Yes) {
if (QtUtils::confirm(window(), tr("Update Available"), message)) {
downloadUpdate(release->getName());
} else {
closeDialog();

View file

@ -7,6 +7,7 @@
#include <QObject>
#include <QString>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/qt_utils.h>
class QWidget;
@ -30,10 +31,7 @@ bool OverridePrintingWarning::execMessageBox(QWidget *parent, bool enable)
"Are you sure you would like to disable this feature?");
}
QMessageBox::StandardButton result =
QMessageBox::question(parent, QObject::tr("Confirm Change"), message, QMessageBox::Yes | QMessageBox::No);
if (result == QMessageBox::Yes) {
if (QtUtils::confirm(parent, QObject::tr("Confirm Change"), message)) {
SettingsCache::instance().cardsDisplay().setOverrideAllCardArtWithPersonalPreference(enable);
// Caches are now invalid.
CardPictureLoader::clearPixmapCache();

View file

@ -60,6 +60,7 @@
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
/**
@ -71,10 +72,8 @@ static QScrollArea *makeScrollable(QWidget *widget)
{
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Maximum);
auto *scrollArea = new QScrollArea;
scrollArea->setWidgetResizable(true);
auto *scrollArea = QtUtils::makeScrollArea();
scrollArea->setContentsMargins(0, 0, 0, 0);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->horizontalScrollBar()->setEnabled(false);
scrollArea->setWidget(widget);
return scrollArea;
@ -625,13 +624,8 @@ void SettingsView::scrollToAndFlash(QWidget *target)
if (!target) {
return;
}
QWidget *widget = target;
while (widget) {
if (auto *scrollArea = qobject_cast<QScrollArea *>(widget)) {
scrollArea->ensureWidgetVisible(target);
break;
}
widget = widget->parentWidget();
if (auto *scrollArea = QtUtils::findParentOfType<QScrollArea>(target, true)) {
scrollArea->ensureWidgetVisible(target);
}
flashWidget(target);
}

View file

@ -2,9 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../deck_loader/deck_file_format.h"
#include "../../deck_loader/deck_loader.h"
#include "../../deck_loader/loaded_deck.h"
#include "../../pixel_map_generator.h"
#include "../../theme_config.h"
#include "../../theme_manager.h"
@ -57,6 +55,8 @@
#include <QtGlobal>
#include <algorithm>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_file_format.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/paths_settings.h>
@ -113,6 +113,32 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
&HomeWidget::updateButtonsToBackgroundColor);
}
void HomeWidget::retranslateUi()
{
visualDeckEditorButton->setText(tr("Create New Deck"));
visualDeckStorageButton->setText(tr("Browse Decks"));
visualDatabaseDisplayButton->setText(tr("Browse Card Database"));
edhrecButton->setText(tr("Browse EDHRec"));
archidektButton->setText(tr("Browse Archidekt"));
replayButton->setText(tr("View Replays"));
if (exitButton) {
exitButton->setText(tr("Quit"));
}
switch (connectionStatus) {
case StatusDisconnected:
connectButton->setText(tr("Connect"));
break;
case StatusLoggedIn:
connectButton->setText(tr("Play"));
break;
default:
// Connecting, registering, activating, logging in, etc.
connectButton->setText(tr("Connecting..."));
break;
}
}
void HomeWidget::initializeBackgroundFromSource()
{
// The featured logo is theme/scheme-derived too; reload it alongside the
@ -284,32 +310,32 @@ QGroupBox *HomeWidget::createButtons()
updateLogoOverlay();
boxLayout->addSpacing(25);
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
connectButton = new HomeStyledButton(QString(), gradientColors);
boxLayout->addWidget(connectButton);
auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
visualDeckEditorButton = new HomeStyledButton(QString(), gradientColors);
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
boxLayout->addWidget(visualDeckEditorButton);
auto visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
visualDeckStorageButton = new HomeStyledButton(QString(), gradientColors);
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
boxLayout->addWidget(visualDeckStorageButton);
auto visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
visualDatabaseDisplayButton = new HomeStyledButton(QString(), gradientColors);
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
&TabSupervisor::addVisualDatabaseDisplayTab);
boxLayout->addWidget(visualDatabaseDisplayButton);
auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
edhrecButton = new HomeStyledButton(QString(), gradientColors);
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
boxLayout->addWidget(edhrecButton);
auto archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
archidektButton = new HomeStyledButton(QString(), gradientColors);
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
boxLayout->addWidget(archidektButton);
auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
boxLayout->addWidget(replaybutton);
replayButton = new HomeStyledButton(QString(), gradientColors);
connect(replayButton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
boxLayout->addWidget(replayButton);
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
exitButton = new HomeStyledButton(QString(), gradientColors);
connect(exitButton, &QPushButton::clicked, qobject_cast<MainWindow *>(tabSupervisor->parentWidget()),
&MainWindow::actExit);
boxLayout->addWidget(exitButton);
@ -321,20 +347,18 @@ QGroupBox *HomeWidget::createButtons()
void HomeWidget::updateConnectButton(const ClientStatus status)
{
connectionStatus = status;
disconnect(connectButton, &QPushButton::clicked, nullptr, nullptr);
switch (status) {
case StatusConnecting:
connectButton->setText(tr("Connecting..."));
connectButton->setEnabled(false);
break;
case StatusDisconnected:
connectButton->setText(tr("Connect"));
connectButton->setEnabled(true);
connect(connectButton, &QPushButton::clicked, qobject_cast<MainWindow *>(tabSupervisor->parentWidget()),
&MainWindow::actConnect);
break;
case StatusLoggedIn:
connectButton->setText(tr("Play"));
connectButton->setEnabled(true);
connect(connectButton, &QPushButton::clicked, tabSupervisor,
&TabSupervisor::switchToFirstAvailableNetworkTab);
@ -342,6 +366,7 @@ void HomeWidget::updateConnectButton(const ClientStatus status)
default:
break;
}
retranslateUi();
}
QPair<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap)

View file

@ -33,6 +33,7 @@ class HomeWidget : public QWidget
public:
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
void updateRandomCard();
void retranslateUi();
static QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
public slots:
@ -53,7 +54,15 @@ private:
DeckList backgroundSourceDeck;
QLabel *logoLabel = nullptr;
QPair<QColor, QColor> gradientColors;
HomeStyledButton *connectButton;
HomeStyledButton *connectButton = nullptr;
HomeStyledButton *visualDeckEditorButton = nullptr;
HomeStyledButton *visualDeckStorageButton = nullptr;
HomeStyledButton *visualDatabaseDisplayButton = nullptr;
HomeStyledButton *edhrecButton = nullptr;
HomeStyledButton *archidektButton = nullptr;
HomeStyledButton *replayButton = nullptr;
HomeStyledButton *exitButton = nullptr;
ClientStatus connectionStatus = StatusDisconnected;
void setRandomCard(ExactCard &newCard);
void loadBackgroundSourceDeck();

View file

@ -19,6 +19,7 @@
#include <QSize>
#include <QSizePolicy>
#include <QWidget>
#include <libcockatrice/utility/qt_utils.h>
/**
* @brief Constructs a FlowWidget.
@ -47,21 +48,16 @@ FlowWidget::FlowWidget(QWidget *parent,
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
}
mainLayout = new QHBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout = QtUtils::makeLayout<QHBoxLayout>(this);
setLayout(mainLayout);
const bool useScrollArea = (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff);
// Scroll area (optional)
if (useScrollArea) {
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea = QtUtils::makeScrollArea(this, verticalPolicy, horizontalPolicy);
scrollArea->setMinimumSize(0, 0);
scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// Set scrollbar policies
scrollArea->setHorizontalScrollBarPolicy(horizontalPolicy);
scrollArea->setVerticalScrollBarPolicy(verticalPolicy);
} else {
scrollArea = nullptr;
}
@ -105,9 +101,9 @@ void FlowWidget::addWidget(QWidget *widget_to_add)
*
* The widget is filtered for arrow-key events so keyboard navigation between
* the flow items keeps working even when the flow sits inside a QScrollArea,
* which swallows arrow keys before they can reach FlowWidget::keyPressEvent.
* Only widgets added through this method are affected; anything that needs its
* own arrow keys should use plain addWidget.
* which swallows arrow keys before they can bubble up. Only widgets added
* through this method are affected; anything that needs its own arrow keys
* should use plain addWidget.
*
* @param widget_to_add The widget to add to the flow layout.
*/
@ -136,13 +132,7 @@ void FlowWidget::removeWidget(QWidget *widgetToRemove) const
void FlowWidget::clearLayout()
{
if (flowLayout) {
QLayoutItem *item;
while ((item = flowLayout->takeAt(0))) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
QtUtils::clearLayoutRec(flowLayout);
} else {
// Defensive fallback: recreate the layout if it was deleted externally.
flowLayout = new FlowLayout(container, flowDirection);
@ -207,17 +197,12 @@ QLayoutItem *FlowWidget::itemAt(int index) const
return flowLayout->itemAt(index);
}
void FlowWidget::keyPressEvent(QKeyEvent *event)
{
if (moveFocus(event)) {
event->accept();
return;
}
QWidget::keyPressEvent(event);
}
bool FlowWidget::eventFilter(QObject *watched, QEvent *event)
{
// Arrow-key navigation is deliberately scoped to widgets that opted in via
// addNavigableWidget(). Handling arrow keys in keyPressEvent() as well would
// also catch the events ordinary children (QLineEdit, ...) ignore and move
// focus away from the control the user is typing in.
if (event->type() == QEvent::KeyPress && moveFocus(static_cast<QKeyEvent *>(event))) {
return true;
}

View file

@ -51,7 +51,6 @@ public slots:
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private:
/// @brief Moves keyboard focus to an adjacent flow item for an arrow-key event.

View file

@ -4,6 +4,7 @@
#include <QLayoutItem>
#include <QWidget>
#include <libcockatrice/utility/qt_utils.h>
/**
* @class OverlapWidget
@ -80,11 +81,7 @@ void OverlapWidget::removeWidget(QWidget *widgetToRemove) const
void OverlapWidget::clearLayout()
{
if (overlapLayout != nullptr) {
QLayoutItem *item;
while ((item = overlapLayout->takeAt(0)) != nullptr) {
item->widget()->deleteLater();
delete item;
}
QtUtils::clearLayoutRec(overlapLayout);
}
// If layout is null, create a new layout; otherwise, reuse the existing one

View file

@ -17,6 +17,7 @@
#include <QPushButton>
#include <QStackedWidget>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <utility>
@ -31,9 +32,8 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent)
bannerHost = new BannerHost(this);
titleLabel = new QLabel(this);
QFont titleFont = titleLabel->font();
QFont titleFont = QtUtils::boldFont(titleLabel->font());
titleFont.setPointSizeF(titleFont.pointSizeF() * 1.4);
titleFont.setBold(true);
titleLabel->setFont(titleFont);
subtitleLabel = new QLabel(this);
@ -51,8 +51,7 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QDialog(parent)
connect(skipButton, &QPushButton::clicked, this, &FirstRunWizard::skip);
connect(nextButton, &QPushButton::clicked, this, &FirstRunWizard::goNext);
auto *headerLayout = new QVBoxLayout;
headerLayout->setContentsMargins(0, 0, 0, 0);
auto *headerLayout = QtUtils::makeLayout<QVBoxLayout>();
headerLayout->addWidget(bannerHost);
headerLayout->addSpacing(12);
headerLayout->addWidget(titleLabel);

View file

@ -20,6 +20,7 @@
#include <QVariant>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
namespace
{
@ -68,9 +69,8 @@ PreferencesSetupPage::PreferencesSetupPage(QWidget *parent) : FirstRunWizardPage
contentLayout->addStretch();
auto *scrollArea = new QScrollArea(this);
auto *scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded);
scrollArea->setWidget(content);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto *layout = new QVBoxLayout(this);

View file

@ -5,6 +5,7 @@
#include "../../settings_page/general_settings_page.h"
#include "../first_run_wizard_page.h"
#include "libcockatrice/settings/personal_settings.h"
#include "libcockatrice/utility/translation_loader.h"
#include <QApplication>
#include <QChar>
@ -30,7 +31,7 @@ WelcomePage::WelcomePage(QWidget *parent) : FirstRunWizardPage(parent)
languageLabel = new QLabel(this);
langCombo = new QComboBox(this);
for (const QString &code : GeneralSettingsPage::findQmFiles()) {
langCombo->addItem(GeneralSettingsPage::languageName(code), code);
langCombo->addItem(TranslationLoader::languageDisplayName(code), code);
}
QString current = SettingsCache::instance().personal().getLang();

View file

@ -19,6 +19,7 @@
#include <QString>
#include <QUrl>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
#include <qmath.h>
#include <qnamespace.h>
#include <qnumeric.h>
@ -151,8 +152,7 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
{
setFixedHeight(150);
stack = new QStackedLayout(this);
stack->setContentsMargins(0, 0, 0, 0);
stack = QtUtils::makeLayout<QStackedLayout>(this);
fallback = new GradientFallbackWidget(this);
stack->addWidget(fallback);

View file

@ -22,6 +22,7 @@
#include <QVariant>
#include <algorithm>
#include <functional>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class QModelIndex;
@ -42,9 +43,7 @@ CardAmountWidget::CardAmountWidget(QWidget *parent,
: QWidget(parent), deckStateManager(deckStateManager), cardSizeSlider(cardSizeSlider), rootCard(rootCard),
zoneName(zoneName), hovered(false)
{
layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(10);
layout = QtUtils::makeLayout<QHBoxLayout>(this, 10);
this->setLayout(layout);
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
layout->setAlignment(Qt::AlignHCenter);

View file

@ -32,6 +32,7 @@
#include <QVBoxLayout>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/macros.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
/**
@ -84,9 +85,7 @@ PrintingSelector::PrintingSelector(QWidget *parent, AbstractTabDeckEditor *_deck
displayOptionsWidget->addSettingsWidget(cardSizeWidget);
sortAndOptionsContainer = new QWidget(this);
sortAndOptionsLayout = new QHBoxLayout(sortAndOptionsContainer);
sortAndOptionsLayout->setSpacing(3);
sortAndOptionsLayout->setContentsMargins(0, 0, 0, 0);
sortAndOptionsLayout = QtUtils::makeLayout<QHBoxLayout>(sortAndOptionsContainer, 0, 3);
sortAndOptionsContainer->setLayout(sortAndOptionsLayout);
searchBar = new PrintingSelectorCardSearchWidget(this);
@ -229,7 +228,7 @@ void PrintingSelector::getAllSetsForCurrentCard()
return;
}
SetToPrintingsMap setMap = selectedCard->getSets();
const auto &setMap = selectedCard->getSets();
const QList<PrintingInfo> sortedPrintings = sortToolBar->sortSets(setMap);
const QList<PrintingInfo> filteredPrintings =
sortToolBar->filterSets(sortedPrintings, searchBar->getSearchText().trimmed().toLower());

View file

@ -40,6 +40,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class DeckStateManager;
@ -108,9 +109,7 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
: QWidget(parent), deckEditor(_deckEditor), rootCard(_rootCard)
{
// Set up the main layout
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
auto *mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 0);
setLayout(mainLayout);
// Add CardInfoPictureWidget

View file

@ -208,7 +208,7 @@ QList<PrintingInfo> PrintingSelectorCardSortingWidget::prependPrintingsInDeck(co
return {};
}
SetToPrintingsMap setMap = selectedCard->getSets();
const auto &setMap = selectedCard->getSets();
QList<QPair<PrintingInfo, int>> countList;
// Collect sets with their counts

View file

@ -5,6 +5,7 @@
#include <QRect>
#include <QSizePolicy>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
/**
@ -19,8 +20,7 @@ SetNameAndCollectorsNumberDisplayWidget::SetNameAndCollectorsNumberDisplayWidget
: QWidget(parent)
{
// Set up the layout for the widget
layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QVBoxLayout>(this);
layout->setSpacing(1);
setLayout(layout);

View file

@ -15,6 +15,7 @@
#include <QSizePolicy>
#include <QStringLiteral>
#include <QToolButton>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
@ -26,9 +27,8 @@ SettingsButtonWidget::SettingsButtonWidget(QWidget *parent)
connect(button, &QToolButton::clicked, this, &SettingsButtonWidget::togglePopup);
connect(popup, &SettingsPopupWidget::aboutToClose, this, &SettingsButtonWidget::onPopupClosed);
layout = new QHBoxLayout(this);
layout = QtUtils::makeLayout<QHBoxLayout>(this);
layout->addWidget(button);
layout->setContentsMargins(0, 0, 0, 0);
setLayout(layout);
}

View file

@ -8,6 +8,7 @@
#include <QScrollArea>
#include <QSize>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
SettingsPopupWidget::SettingsPopupWidget(QWidget *parent) : QWidget(parent, Qt::Popup | Qt::FramelessWindowHint)
@ -54,11 +55,8 @@ void SettingsPopupWidget::adjustSizeToFitScreen()
if (contentHeight > maxHeight) {
// Create a scroll area and add the container widget to it if not already created
if (!scrollArea) {
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea = QtUtils::makeScrollArea(this, Qt::ScrollBarAlwaysOn);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // No horizontal scrollbar
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); // Enable vertical scrollbar when needed
}
// Set maximum height for the scroll area and show vertical scrollbar

View file

@ -1,13 +1,13 @@
#include "replay_manager.h"
#include "../../../client/settings/cache_settings.h"
#include "../../../game/player/event_processing_options.h"
#include "libcockatrice/protocol/pb/game_event.pb.h"
#include "libcockatrice/protocol/pb/game_event_container.pb.h"
#include "libcockatrice/protocol/pb/game_replay.pb.h"
#include <QTimer>
#include <algorithm>
#include <libcockatrice/game/player/event_processing_options.h>
#include <libcockatrice/protocol/get_pb_extension.h>
#include <libcockatrice/settings/interface_settings.h>
#include <qnumeric.h>

View file

@ -1,11 +1,10 @@
#ifndef COCKATRICE_REPLAY_MANAGER_H
#define COCKATRICE_REPLAY_MANAGER_H
#include "../../../game/player/event_processing_options.h"
#include <QList>
#include <QObject>
#include <QtGlobal>
#include <libcockatrice/game/player/event_processing_options.h>
#include <qtmetamacros.h>
class GameReplay;

View file

@ -11,6 +11,7 @@
#include <QtGlobal>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/qt_utils.h>
ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : SettingsButtonWidget(parent)
{
@ -28,8 +29,7 @@ ReplayQuickSettingsWidget::ReplayQuickSettingsWidget(QWidget *parent) : Settings
// putting it all together
auto *widget = new QWidget;
auto *grid = new QGridLayout(widget);
grid->setContentsMargins(0, 0, 0, 0);
auto *grid = QtUtils::makeLayout<QGridLayout>(widget);
grid->addWidget(&fastForwardSpeedLabel, 0, 0, 1, 1);
grid->addWidget(&fastForwardSpeedBox, 0, 1, 1, 1);
grid->addWidget(&skipEmptyCheckBox, 1, 0, 1, 2);

View file

@ -8,9 +8,8 @@
#ifndef REPLAY_WIDGET_H
#define REPLAY_WIDGET_H
#include "../../../game/player/event_processing_options.h"
#include <QWidget>
#include <libcockatrice/game/player/event_processing_options.h>
#include <qtmetamacros.h>
class ReplayManager;

View file

@ -41,6 +41,7 @@
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <string>
@ -420,10 +421,8 @@ void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator,
// spectator join, so ask first instead of surprising the player.
const bool gameFull = game.player_count() == game.max_players();
if (gameFull && !asSpectator && !asJudge && !overrideRestrictions) {
const QMessageBox::StandardButton answer =
QMessageBox::question(this, tr("Join game"), tr("The game is full. Join as a spectator instead?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer != QMessageBox::Yes) {
if (!QtUtils::confirm(this, tr("Join game"), tr("The game is full. Join as a spectator instead?"),
QMessageBox::No)) {
return;
}
}

View file

@ -14,6 +14,7 @@
#include <QString>
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
class TabSupervisor;
template <class Key, class T> class QMap;
@ -24,8 +25,7 @@ GameSelectorQuickFilterToolBar::GameSelectorQuickFilterToolBar(QWidget *parent,
const QMap<int, QString> &allGameTypes)
: QWidget(parent), tabSupervisor(_tabSupervisor), model(_model)
{
mainLayout = new QHBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout = QtUtils::makeLayout<QHBoxLayout>(this);
mainLayout->setSpacing(5);
const GameFilterConfigs &filters = model->getFilters();

View file

@ -13,13 +13,14 @@
#include <QUrl>
#include <QVariant>
#include <QVariantMap>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/servers_settings.h>
#include <qlogging.h>
#define PUBLIC_SERVERS_JSON "https://cockatrice.github.io/public-servers.json"
HandlePublicServers::HandlePublicServers(QObject *parent)
: QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr)
: QObject(parent), nam(NetworkManagerFactory::create(this)), reply(nullptr)
{
}

View file

@ -1,8 +1,5 @@
#include "user_context_menu.h"
#include "../../../../game/abstract_game.h"
#include "../../../../game/game_event_handler.h"
#include "../../../../game/game_meta_info.h"
#include "../../dialogs/dlg_report_user.h"
#include "../../interface/widgets/tabs/tab_supervisor.h"
#include "../chat_view/chat_view.h"
@ -37,6 +34,9 @@
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QWidget>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/game_event_handler.h>
#include <libcockatrice/game/game_meta_info.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_kick_from_game.pb.h>
#include <libcockatrice/protocol/pb/commands.pb.h>
@ -49,6 +49,7 @@
#include <libcockatrice/protocol/pb/response_warn_list.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <string>
@ -685,10 +686,8 @@ void UserContextMenu::execRemoveFromIgnore(const QString &userName)
void UserContextMenu::execKick(int playerId)
{
auto result = QMessageBox::question(static_cast<QWidget *>(parent()), tr("Kick Player"),
tr("Are you sure you want to kick this player from the game?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (result == QMessageBox::Yes) {
if (QtUtils::confirm(static_cast<QWidget *>(parent()), tr("Kick Player"),
tr("Are you sure you want to kick this player from the game?"), QMessageBox::No)) {
Command_KickFromGame cmd;
cmd.set_player_id(playerId);

View file

@ -33,6 +33,7 @@
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/passwordhasher.h>
#include <libcockatrice/utility/qt_utils.h>
#include <string>
#include <sys/types.h>
@ -53,8 +54,7 @@ UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *paren
avatarPic.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
avatarPic.setAlignment(Qt::AlignCenter);
auto *avatarLayout = new QHBoxLayout;
avatarLayout->setContentsMargins(0, 0, 0, 0);
auto *avatarLayout = QtUtils::makeLayout<QHBoxLayout>();
avatarLayout->addStretch(1);
avatarLayout->addWidget(&avatarPic, 3);
avatarLayout->addStretch(1);

View file

@ -55,6 +55,7 @@
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/card_ref.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnumeric.h>
#include <string>
@ -402,9 +403,7 @@ UserInfoPopup::UserInfoPopup(TabSupervisor *_ts,
void UserInfoPopup::buildUi()
{
auto *root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
auto *root = QtUtils::makeLayout<QVBoxLayout>(this, 0);
// Header
header = new UserInfoHeaderWidget(this);

View file

@ -9,6 +9,7 @@
#include <QStringLiteral>
#include <QVBoxLayout>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/qt_utils.h>
namespace
{
@ -31,9 +32,7 @@ QString sectionKey(UserListWidget::Section section)
UserListPanelWidget::UserListPanelWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
: QWidget(parent)
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(2);
auto *mainLayout = QtUtils::makeLayout<QVBoxLayout>(this, 2);
searchBar = new QLineEdit(this);
searchBar->setClearButtonEnabled(true);

View file

@ -13,10 +13,10 @@
#include "user_list_painter.h"
#include "user_list_proxy.h"
#include <QAbstractAnimation>
#include <QAbstractItemModel>
#include <QApplication>
#include <QByteArray>
#include <QCollator>
#include <QColor>
#include <QContextMenuEvent>
#include <QCursor>
@ -41,7 +41,6 @@
#include <QPalette>
#include <QPoint>
#include <QPointF>
#include <QPropertyAnimation>
#include <QRect>
#include <QScreen>
#include <QScrollBar>
@ -57,7 +56,9 @@
#include <QVariant>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/models/natural_sort_collator.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class QPixmap;
@ -273,6 +274,11 @@ void UserListTWI::setOnline(bool online)
*/
bool UserListTWI::operator<(const QTreeWidgetItem &other) const
{
// QCollator caches the locale data, unlike QString::localeAwareCompare,
// which rebuilds it for every pair. The tree sorts one list at a time, so a
// single shared, immutable collator is enough.
static const QCollator collator = NaturalSort::makeNaturalSortCollator();
// Sort by online/offline
if (data(0, UserListRoles::Online) != other.data(0, UserListRoles::Online)) {
return data(0, UserListRoles::Online).toBool();
@ -288,8 +294,7 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {
return QString::localeAwareCompare(data(2, Qt::UserRole).toString(),
other.data(2, Qt::UserRole).toString()) < 0;
return collator.compare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
} else if (lhsUserLevelFlags.testFlag(userLevelEntry) != rhsUserLevelFlags.testFlag(userLevelEntry)) {
return lhsUserLevelFlags.testFlag(userLevelEntry) > rhsUserLevelFlags.testFlag(userLevelEntry);
}
@ -314,7 +319,7 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
}
// Sort by name
return QString::localeAwareCompare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
return collator.compare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
}
UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
@ -895,11 +900,7 @@ void UserListWidget::showPopupForUser(UserListTWI *item)
return;
}
auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup);
fade->setDuration(120);
fade->setStartValue(0.0);
fade->setEndValue(1.0);
fade->start(QAbstractAnimation::DeleteWhenStopped);
QtUtils::fadeWindow(userInfoPopup, 0.0, 1.0, 120);
}
void UserListWidget::positionPopup(UserListTWI *item)
@ -976,12 +977,7 @@ void UserListWidget::hidePopup(bool immediate)
}
// Fade out
auto *fade = new QPropertyAnimation(userInfoPopup, "windowOpacity", userInfoPopup);
fade->setDuration(100);
fade->setStartValue(userInfoPopup->windowOpacity());
fade->setEndValue(0.0);
connect(fade, &QPropertyAnimation::finished, userInfoPopup, &QWidget::hide);
fade->start(QAbstractAnimation::DeleteWhenStopped);
QtUtils::fadeWindow(userInfoPopup, userInfoPopup->windowOpacity(), 0.0, 100, true);
}
bool UserListWidget::isPressInsideListUi(const QWidget *widget) const

View file

@ -17,7 +17,6 @@
#include <QColor>
#include <QColorDialog>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QGridLayout>
#include <QGroupBox>
@ -30,7 +29,6 @@
#include <QStringLiteral>
#include <QStyleFactory>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
#include <QVariant>
#include <QtGlobal>
@ -39,6 +37,7 @@
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
AppearanceSettingsPage::AppearanceSettingsPage()
@ -446,7 +445,7 @@ void AppearanceSettingsPage::openThemeLocation()
dirDir.cdUp();
// open if dir exists, create if parent dir does exist
if (dirDir.exists() && dirDir.mkpath(dir)) {
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
QtUtils::openPathInFileManager(dir);
} else {
QMessageBox::critical(this, tr("Error"), tr("Could not create themes directory at '%1'.").arg(dir));
}

View file

@ -8,7 +8,6 @@
#include "update/client/release_channel.h"
#include <QByteArray>
#include <QCoreApplication>
#include <QDate>
#include <QDebug>
#include <QFile>
@ -22,7 +21,6 @@
#include <QPushButton>
#include <QSharedPointer>
#include <QSizePolicy>
#include <QTranslator>
#include <QVBoxLayout>
#include <QVariant>
#include <QVariantList>
@ -35,6 +33,7 @@
#include <libcockatrice/settings/tabs_settings.h>
#include <libcockatrice/settings/updates_settings.h>
#include <libcockatrice/utility/macros.h>
#include <libcockatrice/utility/qt_utils.h>
#include <libcockatrice/utility/translation_loader.h>
#include <qlogging.h>
#include <qnamespace.h>
@ -50,18 +49,24 @@ enum startupCardUpdateCheckBehaviorIndex
GeneralSettingsPage::GeneralSettingsPage()
{
// language settings
QStringList languageCodes = findQmFiles();
for (const QString &code : languageCodes) {
QString langName = languageName(code);
languageBox.addItem(langName, code);
for (const QString &code : findQmFiles()) {
languageBox.addItem(TranslationLoader::languageDisplayName(code), code);
}
QString setLanguage = QCoreApplication::translate("i18n", DEFAULT_LANG_NAME);
int index = languageBox.findText(setLanguage, Qt::MatchExactly);
if (index == -1) {
qWarning() << "could not find language" << setLanguage;
} else {
// Select the language the client runs with: its own entry, the base language for a variant without a
// file of its own (de_CH -> de), and the default language for an unset setting, which names no file.
const QString configuredLanguage = SettingsCache::instance().personal().getLang();
int index = languageBox.findData(configuredLanguage);
if (index < 0) {
index = languageBox.findData(configuredLanguage.section('_', 0, 0));
}
if (index < 0) {
index = languageBox.findData(TranslationLoader::defaultLanguageCode());
}
if (index >= 0) {
languageBox.setCurrentIndex(index);
} else {
qCWarning(GeneralSettingsPageLog) << "could not find language" << configuredLanguage;
}
advertiseTranslationPageLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse);
@ -95,12 +100,12 @@ GeneralSettingsPage::GeneralSettingsPage()
auto *languageGrid = new QGridLayout;
languageGrid->addWidget(&languageLabel, 0, 0);
languageGrid->addWidget(&languageBox, 0, 1);
languageGrid->addWidget(&cardLanguageLabel, 1, 0);
languageGrid->addWidget(&cardLanguageBox, 1, 1);
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
languageGrid->addWidget(&cardSearchLanguageLabel, 3, 0);
languageGrid->addWidget(&cardSearchLanguageBox, 3, 1);
languageGrid->addWidget(&advertiseTranslationPageLabel, 4, 1, Qt::AlignRight);
languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight);
languageGrid->addWidget(&cardLanguageLabel, 2, 0);
languageGrid->addWidget(&cardLanguageBox, 2, 1);
languageGrid->addWidget(&cardLanguageNoteLabel, 3, 1);
languageGrid->addWidget(&cardSearchLanguageLabel, 4, 0);
languageGrid->addWidget(&cardSearchLanguageBox, 4, 1);
cardLanguageNoteLabel.setWordWrap(true);
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
@ -133,6 +138,27 @@ GeneralSettingsPage::GeneralSettingsPage()
versionGroupBox = new QGroupBox;
versionGroupBox->setLayout(versionGrid);
// crash reporting consent (stored in personal.ini, not debug.ini)
crashReportingBox.addItem(""); // texts set in retranslateUi
crashReportingBox.addItem("");
crashReportingBox.addItem("");
crashReportingBox.setCurrentIndex(static_cast<int>(settings.personal().getCrashReportingMode()));
connect(&crashReportingBox, qOverload<int>(&QComboBox::currentIndexChanged), this, [](int index) {
if (index < 0 || index > static_cast<int>(PersonalSettings::CrashReportingMode::NeverSend)) {
return;
}
SettingsCache::instance().personal().setCrashReportingMode(
static_cast<PersonalSettings::CrashReportingMode>(index));
});
auto *crashReportingGrid = new QGridLayout;
crashReportingGrid->addWidget(&crashReportingLabel, 0, 0);
crashReportingGrid->addWidget(&crashReportingBox, 0, 1);
crashReportingGroupBox = new QGroupBox;
crashReportingGroupBox->setLayout(crashReportingGrid);
// card database settings
startupCardUpdateCheckBehaviorSelector.addItem(""); // these will be set in retranslateUI
startupCardUpdateCheckBehaviorSelector.addItem("");
@ -336,6 +362,7 @@ GeneralSettingsPage::GeneralSettingsPage()
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(languageGroupBox);
mainLayout->addWidget(versionGroupBox);
mainLayout->addWidget(crashReportingGroupBox);
mainLayout->addWidget(cardDatabaseGroupBox);
mainLayout->addWidget(startupGroupBox);
mainLayout->addWidget(pathsGroupBox);
@ -361,18 +388,6 @@ QStringList GeneralSettingsPage::findQmFiles()
TranslationLoader::applicationTranslationPaths(translationPath));
}
QString GeneralSettingsPage::languageName(const QString &lang)
{
QTranslator qTranslator;
// Only the language name is needed here, so the file is loaded without logging a line per
// language: findQmFiles() asked for languages whose files exist in the first place.
TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang,
TranslationLoader::applicationTranslationPaths(translationPath));
return qTranslator.translate("i18n", DEFAULT_LANG_NAME);
}
void GeneralSettingsPage::deckPathButtonClicked()
{
QString path = QFileDialog::getExistingDirectory(this, tr("Choose path"), deckPathEdit->text());
@ -525,12 +540,9 @@ void GeneralSettingsPage::cardLanguageBoxChanged(int index)
}
message += tr("<p>Run Oracle now?</p>");
const QMessageBox::StandardButton answer = QMessageBox::question(
this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
// The answer only controls whether Oracle starts right away; the caches stay
// cleared so the next import or launch rebuilds them in the new language.
if (answer == QMessageBox::Yes) {
if (QtUtils::confirm(this, tr("Card text & images language changed"), message, QMessageBox::Yes)) {
emit cardDatabaseUpdateRequested();
}
}
@ -556,7 +568,7 @@ void GeneralSettingsPage::retranslateUi()
const auto &settings = SettingsCache::instance();
languageGroupBox->setTitle(tr("Language settings"));
languageLabel.setText(tr("Language:"));
languageLabel.setText(tr("Interface language:"));
cardLanguageBox.setItemText(0, tr("English"));
cardLanguageLabel.setText(tr("Card text & images language:"));
cardLanguageNoteLabel.setText(
@ -569,13 +581,19 @@ void GeneralSettingsPage::retranslateUi()
tr("English and selected card language"));
advertiseTranslationPageLabel.setText(
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
versionGroupBox->setTitle(tr("Version settings"));
updateReleaseChannelLabel.setText(tr("Update channel"));
startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup"));
updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
crashReportingGroupBox->setTitle(tr("Crash reporting"));
crashReportingLabel.setText(tr("Send crash reports:"));
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::Ask), tr("Ask every time"));
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::AlwaysSend),
tr("Always send"));
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::NeverSend), tr("Never send"));
// We can't change the strings after they're put into the QComboBox, so this is our workaround
int oldIndex = updateReleaseChannelBox.currentIndex();
updateReleaseChannelBox.clear();

View file

@ -26,7 +26,6 @@ public:
void retranslateUi() override;
static QStringList findQmFiles();
static QString languageName(const QString &lang);
signals:
/// Request to re-import the card database with the newly selected card language
@ -49,6 +48,7 @@ private slots:
private:
QGroupBox *languageGroupBox;
QGroupBox *versionGroupBox;
QGroupBox *crashReportingGroupBox;
QGroupBox *cardDatabaseGroupBox;
QGroupBox *startupGroupBox;
QGroupBox *pathsGroupBox;
@ -70,6 +70,9 @@ private:
QCheckBox updateNotificationCheckBox;
QCheckBox newVersionOracleCheckBox;
QLabel crashReportingLabel;
QComboBox crashReportingBox;
QLabel startupCardUpdateCheckBehaviorLabel;
QComboBox startupCardUpdateCheckBehaviorSelector;
QLabel cardUpdateCheckIntervalLabel;

View file

@ -18,6 +18,7 @@
#include <QStringLiteral>
#include <QVBoxLayout>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
ShortcutSettingsPage::ShortcutSettingsPage()
@ -112,16 +113,15 @@ void ShortcutSettingsPage::currentItemChanged(const QString &key)
void ShortcutSettingsPage::resetShortcuts()
{
if (QMessageBox::question(this, tr("Restore all default shortcuts"),
tr("Do you really want to restore all default shortcuts?")) == QMessageBox::Yes) {
if (QtUtils::confirm(this, tr("Restore all default shortcuts"),
tr("Do you really want to restore all default shortcuts?"))) {
SettingsCache::instance().shortcuts().resetAllShortcuts();
}
}
void ShortcutSettingsPage::clearShortcuts()
{
if (QMessageBox::question(this, tr("Clear all default shortcuts"),
tr("Do you really want to clear all shortcuts?")) == QMessageBox::Yes) {
if (QtUtils::confirm(this, tr("Clear all default shortcuts"), tr("Do you really want to clear all shortcuts?"))) {
SettingsCache::instance().shortcuts().clearAllShortcuts();
}
}

View file

@ -18,6 +18,7 @@
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
enum visualDeckStoragePromptForConversionIndex
@ -248,8 +249,7 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
updateCommanderSpellbookUiState();
auto *labelLayout = new QHBoxLayout;
labelLayout->setContentsMargins(0, 0, 0, 0);
auto *labelLayout = QtUtils::makeLayout<QHBoxLayout>();
labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesLabel);
labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesExplainer);

View file

@ -11,9 +11,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../deck_loader/deck_file_format.h"
#include "../../deck_loader/deck_loader.h"
#include "../../deck_loader/loaded_deck.h"
#include "../cards/additional_info/deck_color_identity.h"
#include "../cards/card_info_frame_widget.h"
#include "../client/network/interfaces/deck_stats_interface.h"
@ -55,6 +53,8 @@
#include <QUrl>
#include <google/protobuf/stubs/port.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_file_format.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>

View file

@ -2,7 +2,6 @@
#include "../../../../../../client/settings/cache_settings.h"
#include "../../../../../deck_loader/card_node_function.h"
#include "../../../../../deck_loader/loaded_deck.h"
#include "../../../../cards/deck_card_zone_display_widget.h"
#include "../../../../visual_deck_editor/visual_deck_display_options_widget.h"
#include "../../../../visual_deck_editor/visual_deck_editor_widget.h"
@ -30,7 +29,9 @@
#include <QVariant>
#include <functional>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qlogging.h>
#include <qnamespace.h>
@ -66,10 +67,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
connect(displayOptionsWidget, &VisualDeckDisplayOptionsWidget::groupCriteriaChanged, this,
&ArchidektApiResponseDeckDisplayWidget::onGroupCriteriaChange);
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea = QtUtils::makeScrollArea(this);
layout->addWidget(scrollArea);
@ -240,7 +238,5 @@ void ArchidektApiResponseDeckDisplayWidget::constructZoneWidgetsFromDeckListMode
void ArchidektApiResponseDeckDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
layout->invalidate();
layout->activate();
layout->update();
QtUtils::relayout(layout);
}

View file

@ -24,6 +24,7 @@
#include <QVariant>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <version_string.h>
@ -241,9 +242,7 @@ void ArchidektApiResponseDeckEntryDisplayWidget::updateScaledPreview()
void ArchidektApiResponseDeckEntryDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
layout->invalidate();
layout->activate();
layout->update();
QtUtils::relayout(layout);
}
void ArchidektApiResponseDeckEntryDisplayWidget::actRequestNavigationToDeck()

View file

@ -11,6 +11,8 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QSlider>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsDisplayWidget(
@ -19,16 +21,14 @@ ArchidektApiResponseDeckListingsDisplayWidget::ArchidektApiResponseDeckListingsD
CardSizeWidget *_cardSizeSlider)
: QWidget(parent), cardSizeSlider(_cardSizeSlider)
{
layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QHBoxLayout>(this);
setLayout(layout);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
cardSizeSlider->enableCtrlScrollResize(flowWidget);
imageNetworkManager = new QNetworkAccessManager(this);
imageNetworkManager->setTransferTimeout(); // Use Qt's default timeout
imageNetworkManager = NetworkManagerFactory::create(this);
imageNetworkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
// Add widgets for deck listings
@ -64,7 +64,5 @@ void ArchidektApiResponseDeckListingsDisplayWidget::append(const ArchidektDeckLi
void ArchidektApiResponseDeckListingsDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
layout->invalidate();
layout->activate();
layout->update();
QtUtils::relayout(layout);
}

View file

@ -48,7 +48,9 @@
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qlogging.h>
#include <qnamespace.h>
#include <version_string.h>
@ -57,8 +59,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
: Tab(_tabSupervisor), currentPage(1), isLoadingMore(false), isListMode(true)
{
// Initialize network
networkManager = new QNetworkAccessManager(this);
networkManager->setTransferTimeout();
networkManager = NetworkManagerFactory::create(this);
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, &QNetworkAccessManager::finished, this, &TabArchidekt::processApiJson);
@ -80,9 +81,7 @@ void TabArchidekt::initializeUi()
{
// Main container
container = new QWidget(this);
mainLayout = new QVBoxLayout(container);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
mainLayout = QtUtils::makeLayout<QVBoxLayout>(container, 0);
// Primary toolbar (most important filters)
primaryToolbar = new QWidget(container);
@ -105,9 +104,7 @@ void TabArchidekt::initializeUi()
// Color filter (inline)
QWidget *colorWidget = new QWidget(primaryToolbar);
QHBoxLayout *colorLayout = new QHBoxLayout(colorWidget);
colorLayout->setContentsMargins(0, 0, 0, 0);
colorLayout->setSpacing(2);
QHBoxLayout *colorLayout = QtUtils::makeLayout<QHBoxLayout>(colorWidget, 2);
QString colorIdentity = "WUBRG";
for (const QChar &color : colorIdentity) {
@ -186,15 +183,10 @@ void TabArchidekt::initializeUi()
secondaryToolbarLayout->setSpacing(6);
// Scrollable results area
scrollArea = new QScrollArea(container);
scrollArea->setWidgetResizable(true);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea = QtUtils::makeScrollArea(container);
resultsContainer = new QWidget();
resultsLayout = new QVBoxLayout(resultsContainer);
resultsLayout->setContentsMargins(0, 0, 0, 0);
resultsLayout->setSpacing(0);
resultsLayout = QtUtils::makeLayout<QVBoxLayout>(resultsContainer, 0);
scrollArea->setWidget(resultsContainer);
@ -228,9 +220,9 @@ void TabArchidekt::setupFilterWidgets()
// Advanced filters (in secondary toolbar)
// EDH Bracket
auto *bracketLabel = new QLabel(tr("Bracket:"), secondaryToolbar);
bracketLabel = new QLabel(secondaryToolbar);
edhBracketCombo = new QComboBox(secondaryToolbar);
edhBracketCombo->addItem(tr("Any"));
edhBracketCombo->addItem(QString());
edhBracketCombo->addItems({"1", "2", "3", "4", "5"});
edhBracketCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
@ -284,7 +276,6 @@ void TabArchidekt::setupFilterWidgets()
sizeLayout->setContentsMargins(4, 4, 4, 4);
minDeckSizeSpin = new QSpinBox(sizeContainer);
minDeckSizeSpin->setSpecialValueText(tr("Any"));
minDeckSizeSpin->setRange(0, 200);
minDeckSizeSpin->setValue(0);
@ -292,7 +283,8 @@ void TabArchidekt::setupFilterWidgets()
minDeckSizeLogicCombo->addItems({"Exact", "≥", "≤"});
minDeckSizeLogicCombo->setCurrentIndex(1);
sizeLayout->addWidget(new QLabel(tr("Cards:"), sizeContainer));
cardsLabel = new QLabel(sizeContainer);
sizeLayout->addWidget(cardsLabel);
sizeLayout->addWidget(minDeckSizeSpin);
sizeLayout->addWidget(minDeckSizeLogicCombo);
@ -349,9 +341,9 @@ void TabArchidekt::connectSignals()
});
// Format filters trigger search
connect(edhBracketCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch);
connect(edhBracketCombo, &QComboBox::currentIndexChanged, this, &TabArchidekt::doSearch);
connect(minDeckSizeSpin, qOverload<int>(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch);
connect(minDeckSizeLogicCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch);
connect(minDeckSizeLogicCombo, &QComboBox::currentIndexChanged, this, &TabArchidekt::doSearch);
// Allow Enter key in text fields to trigger search
connect(nameField, &QLineEdit::returnPressed, this, &TabArchidekt::doSearchImmediate);
@ -401,8 +393,15 @@ void TabArchidekt::retranslateUi()
commandersField->setPlaceholderText(tr("Commander..."));
deckTagNameField->setPlaceholderText(tr("Tag..."));
bracketLabel->setText(tr("Bracket:"));
// Index 0 is the "Any" placeholder added in setupFilterWidgets()
edhBracketCombo->setItemText(0, tr("Any"));
formatButton->setButtonText(tr("Formats"));
deckSizeButton->setButtonText(tr("Deck Size"));
cardsLabel->setText(tr("Cards:"));
minDeckSizeSpin->setSpecialValueText(tr("Any"));
minDeckSizeLogicCombo->setItemText(0, tr("Exact"));
searchButton->setText(tr("Search"));
@ -492,13 +491,17 @@ QString TabArchidekt::buildSearchUrl()
query.addQueryItem("size", QString::number(minDeckSizeSpin->value()));
QString logic = "GTE";
QString selected = minDeckSizeLogicCombo->currentText();
if (selected == "≥") {
logic = "GTE";
} else if (selected == "≤") {
logic = "LTE";
} else {
logic = "";
// Index order matches setupFilterWidgets(): 0 = Exact, 1 = ≥, 2 = ≤
switch (minDeckSizeLogicCombo->currentIndex()) {
case 1:
logic = "GTE";
break;
case 2:
logic = "LTE";
break;
default:
logic = "";
break;
}
if (!logic.isEmpty()) {
@ -599,11 +602,7 @@ void TabArchidekt::processTopDecksResponse(QJsonObject reply)
// New search → clear everything
if (currentPage == 1) {
QLayoutItem *item;
while ((item = resultsLayout->takeAt(0)) != nullptr) {
delete item->widget();
delete item;
}
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider);
@ -629,11 +628,7 @@ void TabArchidekt::processDeckResponse(QJsonObject reply)
isListMode = false;
// Clear existing results for single deck view
QLayoutItem *item;
while ((item = resultsLayout->takeAt(0)) != nullptr) {
delete item->widget();
delete item;
}
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider);
connect(display, &ArchidektApiResponseDeckDisplayWidget::requestNavigation, this, &TabArchidekt::actNavigatePage);

View file

@ -287,9 +287,11 @@ private:
SettingsButtonWidget *formatButton; ///< Collapsible button for format filters
QVector<QCheckBox *> formatChecks; ///< Individual checkboxes for each format
QLabel *bracketLabel; ///< Label for EDH bracket selection
QComboBox *edhBracketCombo; ///< Dropdown for EDH bracket selection
SettingsButtonWidget *deckSizeButton; ///< Collapsible button for deck size filter
QLabel *cardsLabel; ///< Label for deck size selection
QSpinBox *minDeckSizeSpin; ///< Spinner to select minimum deck size
QComboBox *minDeckSizeLogicCombo; ///< Combo box for size comparison logic
@ -300,4 +302,4 @@ private:
CardInfoPtr cardToQuery; ///< Optional pre-selected card for initial filtering
};
#endif // COCKATRICE_TAB_ARCHIDEKT_H
#endif // COCKATRICE_TAB_ARCHIDEKT_H

View file

@ -21,6 +21,7 @@
#include <QToolButton>
#include <QVBoxLayout>
#include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
template <class T> class QSharedPointer;
@ -44,8 +45,7 @@ CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent
connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate);
auto *layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
auto *layout = QtUtils::makeLayout<QGridLayout>(this);
layout->addWidget(bracketLabel, 0, 0);
auto *bracketRow = new QHBoxLayout;
@ -110,8 +110,7 @@ bool CommanderBracketWidget::promptCommanderSpellbookIntegration()
// Create label + explainer button
auto *labelWidget = new QWidget(&dialog);
auto *labelLayout = new QHBoxLayout(labelWidget);
labelLayout->setContentsMargins(0, 0, 0, 0);
auto *labelLayout = QtUtils::makeLayout<QHBoxLayout>(labelWidget);
auto *namingLabel = new QLabel(tr("Bracket naming:"), labelWidget);
auto *explainerButton = new QToolButton(labelWidget);

View file

@ -12,12 +12,13 @@
#include <QStringLiteral>
#include <QUrl>
#include <QVariant>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/commander_bracket_settings.h>
static const QUrl COMMANDER_BRACKET_JSON_URL(QStringLiteral("https://cockatrice.github.io/commander-brackets.json"));
HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent)
: QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr)
: QObject(parent), nam(NetworkManagerFactory::create(this)), reply(nullptr)
{
}

View file

@ -15,6 +15,7 @@
#include <QSlider>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWidget(
@ -47,15 +48,7 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi
layout->addWidget(backgroundPlateWidget);
QWidget *currentParent = parentWidget();
TabEdhRecMain *parentTab = nullptr;
while (currentParent) {
if ((parentTab = qobject_cast<TabEdhRecMain *>(currentParent))) {
break;
}
currentParent = currentParent->parentWidget();
}
auto *parentTab = QtUtils::findParentOfType<TabEdhRecMain>(this);
if (parentTab) {
cardPictureWidget->setScaleFactor(parentTab->getCardSizeSlider()->getSlider()->value());

View file

@ -14,6 +14,7 @@
#include <QSlider>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCommanderDetailsDisplayWidget(
@ -33,15 +34,7 @@ EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCom
commanderPicture = new CardInfoPictureWidget(this);
commanderPicture->setCard(CardDatabaseManager::query()->getCard({commanderDetails.getName()}));
QWidget *currentParent = parentWidget();
TabEdhRecMain *parentTab = nullptr;
while (currentParent) {
if ((parentTab = qobject_cast<TabEdhRecMain *>(currentParent))) {
break;
}
currentParent = currentParent->parentWidget();
}
auto *parentTab = QtUtils::findParentOfType<TabEdhRecMain>(this);
if (parentTab) {
connect(parentTab->getCardSizeSlider()->getSlider(), &QSlider::valueChanged, commanderPicture,

View file

@ -21,6 +21,7 @@
#include <QStringList>
#include <QStringListModel>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qlogging.h>
#include <qnamespace.h>
@ -61,10 +62,7 @@ EdhrecCommanderApiResponseDisplayWidget::EdhrecCommanderApiResponseDisplayWidget
}
// Create a QScrollArea to hold the card display widgets
scrollArea = new QScrollArea(splitter);
scrollArea->setWidgetResizable(true);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea = QtUtils::makeScrollArea(splitter);
// Set the cardDisplayLayout inside the scroll area
auto scrollWidget = new QWidget(scrollArea);
@ -109,9 +107,7 @@ void EdhrecCommanderApiResponseDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
qDebug() << event->size();
layout->invalidate();
layout->activate();
layout->update();
QtUtils::relayout(layout);
if (scrollArea && scrollArea->widget()) {
scrollArea->widget()->resize(event->size());
}

View file

@ -7,6 +7,7 @@
#include <QGridLayout>
#include <QObject>
#include <QPushButton>
#include <libcockatrice/utility/qt_utils.h>
EdhrecCommanderApiResponseNavigationWidget::EdhrecCommanderApiResponseNavigationWidget(
QWidget *parent,
@ -40,15 +41,7 @@ EdhrecCommanderApiResponseNavigationWidget::EdhrecCommanderApiResponseNavigation
layout->addWidget(bracketNavigationWidget, 1, 0, 1, 2);
layout->addWidget(budgetNavigationWidget, 2, 0, 1, 2);
QWidget *currentParent = parentWidget();
TabEdhRecMain *parentTab = nullptr;
while (currentParent) {
if ((parentTab = qobject_cast<TabEdhRecMain *>(currentParent))) {
break;
}
currentParent = currentParent->parentWidget();
}
auto *parentTab = QtUtils::findParentOfType<TabEdhRecMain>(this);
if (parentTab) {
connect(comboPushButton, &QPushButton::clicked, this,

View file

@ -10,6 +10,7 @@
#include <QResizeEvent>
#include <QScrollArea>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
EdhrecTopCardsApiResponseDisplayWidget::EdhrecTopCardsApiResponseDisplayWidget(QWidget *parent,
@ -29,10 +30,7 @@ EdhrecTopCardsApiResponseDisplayWidget::EdhrecTopCardsApiResponseDisplayWidget(Q
}
// Create a QScrollArea to hold the card display widgets
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea = QtUtils::makeScrollArea(this);
// Set the cardDisplayLayout inside the scroll area
auto scrollWidget = new QWidget(scrollArea);
@ -45,9 +43,7 @@ EdhrecTopCardsApiResponseDisplayWidget::EdhrecTopCardsApiResponseDisplayWidget(Q
void EdhrecTopCardsApiResponseDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
layout->invalidate();
layout->activate();
layout->update();
QtUtils::relayout(layout);
if (scrollArea && scrollArea->widget()) {
scrollArea->widget()->resize(event->size());
}

View file

@ -12,6 +12,7 @@
#include <QScrollArea>
#include <QSize>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qlogging.h>
#include <qnamespace.h>
@ -33,10 +34,7 @@ EdhrecTopCommandersApiResponseDisplayWidget::EdhrecTopCommandersApiResponseDispl
}
// Create a QScrollArea to hold the card display widgets
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea = QtUtils::makeScrollArea(this);
// Set the cardDisplayLayout inside the scroll area
auto scrollWidget = new QWidget(scrollArea);
@ -50,9 +48,7 @@ void EdhrecTopCommandersApiResponseDisplayWidget::resizeEvent(QResizeEvent *even
{
QWidget::resizeEvent(event);
qDebug() << event->size();
layout->invalidate();
layout->activate();
layout->update();
QtUtils::relayout(layout);
if (scrollArea && scrollArea->widget()) {
scrollArea->widget()->resize(event->size());
}

View file

@ -10,6 +10,7 @@
#include <QResizeEvent>
#include <QScrollArea>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
EdhrecTopTagsApiResponseDisplayWidget::EdhrecTopTagsApiResponseDisplayWidget(QWidget *parent,
@ -29,10 +30,7 @@ EdhrecTopTagsApiResponseDisplayWidget::EdhrecTopTagsApiResponseDisplayWidget(QWi
}
// Create a QScrollArea to hold the card display widgets
scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea = QtUtils::makeScrollArea(this);
// Set the cardDisplayLayout inside the scroll area
auto scrollWidget = new QWidget(scrollArea);
@ -45,9 +43,7 @@ EdhrecTopTagsApiResponseDisplayWidget::EdhrecTopTagsApiResponseDisplayWidget(QWi
void EdhrecTopTagsApiResponseDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
layout->invalidate();
layout->activate();
layout->update();
QtUtils::relayout(layout);
if (scrollArea && scrollArea->widget()) {
scrollArea->widget()->resize(event->size());
}

View file

@ -18,14 +18,14 @@
#include <QRegularExpression>
#include <QSharedPointer>
#include <QUrl>
#include <libcockatrice/network/network_manager.h>
#include <qlogging.h>
class TabSupervisor;
TabEdhRec::TabEdhRec(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
{
networkManager = new QNetworkAccessManager(this);
networkManager->setTransferTimeout(); // Use Qt's default timeout
networkManager = NetworkManagerFactory::create(this);
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, &QNetworkAccessManager::finished, this, &TabEdhRec::processApiJson);
}

View file

@ -1,7 +1,6 @@
#include "tab_edhrec_main.h"
#include "../../../../../client/settings/cache_settings.h"
#include "../../../../deck_loader/loaded_deck.h"
#include "../../../cards/card_size_widget.h"
#include "../../../quick_settings/settings_button_widget.h"
#include "../../../utility/completer_utils.h"
@ -28,10 +27,12 @@
#include <QRegularExpression>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/database/card_database_querier.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/models/database/card/card_completer_proxy_model.h>
#include <libcockatrice/models/database/card/card_search_model.h>
#include <libcockatrice/models/database/card_database_display_model.h>
#include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <version_string.h>
@ -44,8 +45,7 @@ static bool canBeCommander(const CardInfoPtr &cardInfo)
TabEdhRecMain::TabEdhRecMain(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
{
networkManager = new QNetworkAccessManager(this);
networkManager->setTransferTimeout(); // Use Qt's default timeout
networkManager = NetworkManagerFactory::create(this);
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *)));

Some files were not shown because too many files have changed in this diff Show more