[Game] Follow the palette in judge message tags (#7597)

* [Settings] Delete update channels at shutdown

SettingsCache owns the stable and beta update channels as raw pointers, and
each owns a QNetworkAccessManager, so the process leaves QtNetwork objects
alive past QApplication teardown.

Delete them from a QCoreApplication::aboutToQuit handler instead of the
destructor: SettingsCache is a Q_GLOBAL_STATIC, so ~SettingsCache() runs
during static teardown, after QApplication is gone, and destroying the
network managers at that point is the post-application window that ASan
reports. The hook is installed on the first instance() call that has an
application object, since the first access may predate it.

* [Game] Follow the palette in judge message tags

Judge tags hardcoded a black prefix and a `theme:icons/scales` image inside rich
text, where `currentColor` cannot resolve and a palette change leaves the tag
unreadable. Render the scales glyph through the themed icon engine, register it
as a document image resource, and repaint the log when the palette or theme
changes.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-11-04 18:39:42 +01:00 • committed by GitHub
parent e22a75974a
commit ada0cccfbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 64 additions and 3 deletions

View file

@ -6,6 +6,7 @@
#include "shortcuts_settings.h" #include "shortcuts_settings.h"
#include <QApplication> #include <QApplication>
#include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
@ -47,7 +48,32 @@ Q_GLOBAL_STATIC(SettingsCache, settingsCache)
SettingsCache &SettingsCache::instance() SettingsCache &SettingsCache::instance()
{ {
return *settingsCache; // returns a QT managed singleton reference SettingsCache &cache = *settingsCache; // returns a QT managed singleton reference
// The release channels own QNetworkAccessManagers, which must be destroyed
// while the application object is alive. ~SettingsCache() runs during
// static teardown, after QApplication is gone, so hook the application's
// shutdown instead. Retried here because the first access may predate the
// application object.
cache.installShutdownHook();
return cache;
}
void SettingsCache::installShutdownHook()
{
if (shutdownHookInstalled) {
return;
}
QCoreApplication *application = QCoreApplication::instance();
if (application == nullptr) {
return;
}
connect(application, &QCoreApplication::aboutToQuit, this, [this] {
for (ReleaseChannel *releaseChannel : std::as_const(releaseChannels)) {
delete releaseChannel;
}
releaseChannels.clear();
});
shutdownHookInstalled = true;
} }
QString SettingsCache::getDataPath() QString SettingsCache::getDataPath()

View file

@ -90,8 +90,13 @@ private:
QString themeName; QString themeName;
QList<ReleaseChannel *> releaseChannels; QList<ReleaseChannel *> releaseChannels;
bool shutdownHookInstalled = false;
bool isPortableBuild; bool isPortableBuild;
// Connects the aboutToQuit handler that deletes the release channels while
// the application object is still alive; a no-op before it exists.
void installShutdownHook();
[[nodiscard]] QString getSafeConfigPath(QString configEntry, QString defaultPath) const; [[nodiscard]] QString getSafeConfigPath(QString configEntry, QString defaultPath) const;
[[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const; [[nodiscard]] QString getSafeConfigFilePath(QString configEntry, QString defaultPath) const;
void loadPaths(); void loadPaths();

View file

@ -4,6 +4,8 @@
#include "../../client/settings/card_counter_settings.h" #include "../../client/settings/card_counter_settings.h"
#include "../../client/sound_engine.h" #include "../../client/sound_engine.h"
#include "../../interface/card_localization.h" #include "../../interface/card_localization.h"
#include "../../interface/theme_icon.h"
#include "../../interface/theme_manager.h"
#include "../../interface/widgets/server/chat_view/chat_view.h" #include "../../interface/widgets/server/chat_view/chat_view.h"
#include "../board/card_item.h" #include "../board/card_item.h"
#include "../board/translate_counter_name.h" #include "../board/translate_counter_name.h"
@ -12,8 +14,12 @@
#include <QColor> #include <QColor>
#include <QDebug> #include <QDebug>
#include <QList> #include <QList>
#include <QPalette>
#include <QPixmap>
#include <QSharedPointer> #include <QSharedPointer>
#include <QStringList> #include <QStringList>
#include <QTextDocument>
#include <QUrl>
#include <libcockatrice/game/abstract_game.h> #include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/board/card_list.h> #include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/game_state.h> #include <libcockatrice/game/game_state.h>
@ -120,6 +126,7 @@ MessageLogWidget::getFromStr(CardZoneLogic *zone, QString cardName, int position
void MessageLogWidget::containerProcessingDone() void MessageLogWidget::containerProcessingDone()
{ {
currentContext = MessageContext_None; currentContext = MessageContext_None;
contextJudgeName.clear();
messageSuffix = messagePrefix = QString(); messageSuffix = messagePrefix = QString();
} }
@ -855,8 +862,27 @@ void MessageLogWidget::logUndoDrawFailed(PlayerLogic *player)
void MessageLogWidget::setContextJudgeName(QString name) void MessageLogWidget::setContextJudgeName(QString name)
{ {
messagePrefix = QString("<span style=\"color:black\">"); contextJudgeName = std::move(name);
messageSuffix = QString("</span> [<img height=12 src=\"theme:icons/scales\"> %1]").arg(sanitizeHtml(name)); refreshJudgeSuffix();
}
void MessageLogWidget::refreshJudgeSuffix()
{
const QPixmap pixmap = loadThemedIcon(QStringLiteral("icons/scales")).pixmap(QSize(24, 24));
if (!pixmap.isNull()) {
// Rich text cannot resolve `currentColor`; register a palette-rendered
// image so already-appended judge tags repaint on a theme switch.
document()->addResource(QTextDocument::ImageResource, QUrl(QStringLiteral("cockatrice:scales")), pixmap);
document()->markContentsDirty(0, document()->characterCount());
}
if (contextJudgeName.isEmpty()) {
return;
}
messagePrefix = QStringLiteral("<span style=\"color:%1\">").arg(palette().color(QPalette::WindowText).name());
messageSuffix =
QStringLiteral("</span> [<img height=12 src=\"cockatrice:scales\"> %1]").arg(sanitizeHtml(contextJudgeName));
} }
void MessageLogWidget::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor) void MessageLogWidget::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor)
@ -905,4 +931,6 @@ void MessageLogWidget::connectToPlayerEventHandler(PlayerEventHandler *playerEve
MessageLogWidget::MessageLogWidget(TabSupervisor *_tabSupervisor, AbstractGame *_game, QWidget *parent) MessageLogWidget::MessageLogWidget(TabSupervisor *_tabSupervisor, AbstractGame *_game, QWidget *parent)
: ChatView(_tabSupervisor, _game, true, parent), currentContext(MessageContext_None) : ChatView(_tabSupervisor, _game, true, parent), currentContext(MessageContext_None)
{ {
connect(themeManager, &ThemeManager::paletteChanged, this, &MessageLogWidget::refreshJudgeSuffix);
connect(themeManager, &ThemeManager::themeChanged, this, &MessageLogWidget::refreshJudgeSuffix);
} }

View file

@ -39,8 +39,10 @@ private:
MessageContext currentContext; MessageContext currentContext;
QString messagePrefix, messageSuffix; QString messagePrefix, messageSuffix;
QString contextJudgeName;
static QPair<QString, QString> getFromStr(CardZoneLogic *zone, QString cardName, int position, bool ownerChange); static QPair<QString, QString> getFromStr(CardZoneLogic *zone, QString cardName, int position, bool ownerChange);
void refreshJudgeSuffix();
public: public:
void connectToPlayerEventHandler(PlayerEventHandler *player); void connectToPlayerEventHandler(PlayerEventHandler *player);