From 3def6b00ef348987cd5ebcebcef5ad256a498a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 16 Oct 2026 09:00:57 +0200 Subject: [PATCH 1/3] [Fonts] Add AppFont roles and FontConfig fonts.toml round-trip --- cockatrice/src/interface/theme_config.cpp | 148 +++++++++++++++- cockatrice/src/interface/theme_config.h | 49 +++++- tests/CMakeLists.txt | 9 + tests/font_config_test.cpp | 195 ++++++++++++++++++++++ 4 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 tests/font_config_test.cpp diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index 652e322c8..54143d7d3 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -304,4 +304,150 @@ QPalette PaletteConfig::apply(QPalette base) const } return base; -} \ No newline at end of file +} + +bool FontConfig::hasFonts() const +{ + return !fonts.isEmpty(); +} + +QString FontConfig::toToml() const +{ + QMetaEnum roleEnum = QMetaEnum::fromType(); + + QString entries; + + for (auto it = fonts.cbegin(); it != fonts.cend(); ++it) { + const char *roleName = roleEnum.valueToKey(it.key()); + + if (!roleName) { + continue; + } + + QString family = it.value().family().trimmed(); + int pointSize = it.value().pointSize(); + + if (family.isEmpty() || pointSize <= 0) { + continue; + } + + entries += QString("%1 = \"%2, %3\"\n").arg(QString(roleName), -20).arg(family).arg(pointSize); + } + + if (entries.isEmpty()) { + return {}; + } + + return "[Fonts]\n" + entries + "\n"; +} + +QString FontConfig::fileName() +{ + return "fonts.toml"; +} + +FontConfig FontConfig::fromFile(const QString &filePath) +{ + FontConfig cfg; + + QFile f(filePath); + + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return cfg; + } + + QMetaEnum roleEnum = QMetaEnum::fromType(); + + QString currentSection; + + QTextStream in(&f); + + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + + if (line.isEmpty() || line.startsWith('#') || line.startsWith(';')) { + continue; + } + + if (line.startsWith('[') && line.endsWith(']')) { + currentSection = line.mid(1, line.length() - 2).trimmed(); + continue; + } + + if (currentSection.compare("Fonts", Qt::CaseInsensitive) != 0) { + continue; + } + + int eq = line.indexOf('='); + + if (eq < 0) { + continue; + } + + QString key = line.left(eq).trimmed(); + QString value = line.mid(eq + 1).trimmed(); + + // Strip inline comments if preceded by whitespace + for (int i = 1; i < value.size(); ++i) { + if ((value[i] == '#' || value[i] == ';') && value[i - 1].isSpace()) { + value = value.left(i).trimmed(); + break; + } + } + + // Strip surrounding quotes, if any + if (value.size() >= 2 && + ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\'')))) { + value = value.mid(1, value.size() - 2).trimmed(); + } + + int comma = value.lastIndexOf(','); + + if (comma < 0) { + continue; + } + + QString family = value.left(comma).trimmed(); + + // Tolerate a partially-quoted family, e.g. "Inter", 10 + if (family.size() >= 2 && + ((family.startsWith('"') && family.endsWith('"')) || (family.startsWith('\'') && family.endsWith('\'')))) { + family = family.mid(1, family.size() - 2).trimmed(); + } + + bool ok = false; + int pointSize = value.mid(comma + 1).trimmed().toInt(&ok); + + if (!ok || family.isEmpty() || pointSize <= 0) { + continue; + } + + if (key.startsWith("AppFont::")) { + key = key.mid(QStringLiteral("AppFont::").size()); + } + + int roleInt = roleEnum.keyToValue(key.toUtf8().constData()); + + if (roleInt < 0) { + continue; + } + + // Canonical family + point size only: a fresh QFont carries default + // (non-bold, non-italic) weight/style, so nothing caller-side leaks in. + // No validation against installed fonts; Qt substitution applies. + QFont font(family); + font.setPointSize(pointSize); + cfg.fonts[static_cast(roleInt)] = font; + } + + return cfg; +} + +FontConfig FontConfig::fromThemeDir(const QString &themeDirPath) +{ + if (themeDirPath.isEmpty()) { + return {}; + } + + return fromFile(QDir(themeDirPath).absoluteFilePath(fileName())); +} diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 8b91613f5..84601a2bf 100644 --- a/cockatrice/src/interface/theme_config.h +++ b/cockatrice/src/interface/theme_config.h @@ -2,6 +2,7 @@ #define COCKATRICE_THEME_CONFIG_H #include +#include #include #include #include @@ -23,6 +24,23 @@ enum Role Q_ENUM_NS(Role) } // namespace AppColor +// Application-specific font roles, one configurable family + point size per +// purpose. Stored in a shared fonts.toml under a [Fonts] section (one file per +// theme, not split by light/dark). Only family and point size are stored; +// weight/italic are stripped on load and stay caller-side concerns. +namespace AppFont +{ +Q_NAMESPACE +enum Role +{ + InterfaceDefault, + ChatLog, + CardText, + DeckList, +}; +Q_ENUM_NS(Role) +} // namespace AppFont + struct ThemeConfig { QString colorScheme; @@ -60,4 +78,33 @@ struct PaletteConfig QPalette apply(QPalette base) const; }; -#endif // COCKATRICE_THEME_CONFIG_H \ No newline at end of file +/** @brief Per-purpose font configuration, stored as family + point size only. + * + * Serialized to a shared fonts.toml file with a [Fonts] section, e.g. + * @code + * [Fonts] + * InterfaceDefault = "Inter, 10" + * @endcode + */ +struct FontConfig +{ + QMap fonts; + + bool operator==(const FontConfig &rhs) const + { + return fonts == rhs.fonts; + } + bool operator!=(const FontConfig &rhs) const + { + return !(*this == rhs); + } + bool hasFonts() const; + QString toToml() const; + + static QString fileName(); + + static FontConfig fromFile(const QString &filePath); + static FontConfig fromThemeDir(const QString &themeDirPath); +}; + +#endif // COCKATRICE_THEME_CONFIG_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0bb9654a1..5d956c2c3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -412,6 +412,15 @@ cockatrice_add_test( INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src ) +cockatrice_add_test( + NAME + font_config_test + SOURCES + ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp + font_config_test.cpp + INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/cockatrice/src +) cockatrice_add_test( NAME loader_local_matching_test diff --git a/tests/font_config_test.cpp b/tests/font_config_test.cpp new file mode 100644 index 000000000..c8b1fb10d --- /dev/null +++ b/tests/font_config_test.cpp @@ -0,0 +1,195 @@ +#include "interface/theme_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +// Writes content to a file inside tmp, creating parents as needed. +QString writeTempFile(QTemporaryDir &tmp, const QString &relativePath, const QString &content) +{ + const QString filePath = QDir(tmp.path()).absoluteFilePath(relativePath); + EXPECT_TRUE(QDir().mkpath(QFileInfo(filePath).absolutePath())); + QFile file(filePath); + EXPECT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream out(&file); + out << content; + file.close(); + return filePath; +} + +QFont makeFont(const QString &family, int pointSize) +{ + QFont font(family); + font.setPointSize(pointSize); + return font; +} +} // namespace + +TEST(FontConfigTest, FileNameIsFontsToml) +{ + EXPECT_EQ(FontConfig::fileName(), QStringLiteral("fonts.toml")); +} + +TEST(FontConfigTest, EmptyByDefault) +{ + FontConfig cfg; + EXPECT_FALSE(cfg.hasFonts()); + EXPECT_TRUE(cfg.toToml().isEmpty()); + EXPECT_EQ(cfg, FontConfig()); + EXPECT_EQ(cfg, FontConfig::fromFile("/nonexistent/path/fonts.toml")); + EXPECT_EQ(cfg, FontConfig::fromThemeDir(QString())); +} + +TEST(FontConfigTest, RoundTripPreservesFamilyAndSize) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + FontConfig cfg; + cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); + cfg.fonts[AppFont::ChatLog] = makeFont("JetBrains Mono", 9); + cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11); + cfg.fonts[AppFont::DeckList] = makeFont("Roboto", 12); + ASSERT_TRUE(cfg.hasFonts()); + + const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml()); + const FontConfig loaded = FontConfig::fromFile(filePath); + + EXPECT_EQ(loaded, cfg); + EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].family(), QStringLiteral("Inter")); + EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].pointSize(), 10); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("JetBrains Mono")); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 9); + EXPECT_EQ(loaded.fonts[AppFont::CardText].family(), QStringLiteral("Noto Serif")); + EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 11); + EXPECT_EQ(loaded.fonts[AppFont::DeckList].family(), QStringLiteral("Roboto")); + EXPECT_EQ(loaded.fonts[AppFont::DeckList].pointSize(), 12); +} + +TEST(FontConfigTest, RoundTripStripsWeightAndItalic) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + QFont styled("Inter"); + styled.setPointSize(10); + styled.setBold(true); + styled.setItalic(true); + + FontConfig cfg; + cfg.fonts[AppFont::ChatLog] = styled; + + const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml()); + const FontConfig loaded = FontConfig::fromFile(filePath); + + ASSERT_TRUE(loaded.fonts.contains(AppFont::ChatLog)); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("Inter")); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 10); + EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].bold()); + EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].italic()); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("Inter", 10)); +} + +TEST(FontConfigTest, ToTomlFormat) +{ + FontConfig cfg; + cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); + + const QString toml = cfg.toToml(); + EXPECT_TRUE(toml.contains(QStringLiteral("[Fonts]"))); + EXPECT_TRUE(toml.contains(QStringLiteral("InterfaceDefault"))); + EXPECT_TRUE(toml.contains(QStringLiteral("\"Inter, 10\""))); +} + +TEST(FontConfigTest, MalformedLinesAreSkipped) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + const QString filePath = writeTempFile(tmp, "fonts.toml", + "# a comment\n" + "; another comment\n" + "\n" + "StrayBeforeSection = \"Inter, 10\"\n" + "\n" + "[Other]\n" + "ChatLog = \"ShouldBeIgnored, 10\"\n" + "\n" + "[Fonts]\n" + "InterfaceDefault = \"Inter, 10\"\n" + "ChatLog = \"JetBrains Mono\", 9 # inline comment\n" + "CardText = Noto Serif, 11\n" + "AppFont::DeckList = \"Roboto, 12\"\n" + "UnknownRole = \"Inter, 10\"\n" + "QPalette::Window = \"Inter, 10\"\n" + "NoComma = \"Inter\"\n" + "EmptyFamily = \", 10\"\n" + "ZeroSize = \"Inter, 0\"\n" + "NegativeSize = \"Inter, -3\"\n" + "NotANumber = \"Inter, big\"\n" + "NotAnEntry\n"); + + const FontConfig loaded = FontConfig::fromFile(filePath); + + ASSERT_TRUE(loaded.hasFonts()); + EXPECT_EQ(loaded.fonts.size(), 4); + EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault], makeFont("Inter", 10)); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("JetBrains Mono", 9)); + EXPECT_EQ(loaded.fonts[AppFont::CardText], makeFont("Noto Serif", 11)); + EXPECT_EQ(loaded.fonts[AppFont::DeckList], makeFont("Roboto", 12)); +} + +TEST(FontConfigTest, FromThemeDirReadsFontsToml) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + const QString themeDir = QDir(tmp.path()).absoluteFilePath("MyTheme"); + ASSERT_TRUE(QDir().mkpath(themeDir)); + + FontConfig cfg; + cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11); + writeTempFile(tmp, "MyTheme/fonts.toml", cfg.toToml()); + + const FontConfig loaded = FontConfig::fromThemeDir(themeDir); + EXPECT_EQ(loaded, cfg); + + // A theme directory without fonts.toml loads as empty. + const QString bareDir = QDir(tmp.path()).absoluteFilePath("BareTheme"); + ASSERT_TRUE(QDir().mkpath(bareDir)); + EXPECT_FALSE(FontConfig::fromThemeDir(bareDir).hasFonts()); +} + +TEST(FontConfigTest, InequalityComparesRoles) +{ + FontConfig left; + left.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); + + FontConfig right = left; + EXPECT_EQ(left, right); + EXPECT_FALSE(left != right); + + right.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 11); + EXPECT_NE(left, right); + + right = left; + right.fonts[AppFont::ChatLog] = makeFont("Inter", 10); + EXPECT_NE(left, right); +} + +// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and +// the fallback then links the bare gtest library without gtest_main. +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From d854bdfd83737017fc859ee4fbd749dc7b76cb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 16 Oct 2026 09:03:59 +0200 Subject: [PATCH 2/3] [Fonts] Wire FontConfig into ThemeManager with appFont and preview --- cockatrice/src/interface/theme_manager.cpp | 79 ++++++++++++++++++++++ cockatrice/src/interface/theme_manager.h | 10 +++ 2 files changed, 89 insertions(+) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 893cdc795..dc7e815a2 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -440,6 +440,34 @@ PaletteConfig ThemeManager::loadDefaultPaletteConfig(const QString &themeDirPath return cfg; } +FontConfig ThemeManager::loadFontConfig(const QString &themeDirPath) +{ + if (themeDirPath.isEmpty()) { + return {}; + } + return FontConfig::fromThemeDir(themeDirPath); +} + +bool ThemeManager::saveFontConfig(const QString &themeDirPath, const FontConfig &cfg) +{ + if (themeDirPath.isEmpty()) { + return false; + } + + QDir dir(themeDirPath); + if (!dir.exists()) { + dir.mkpath("."); + } + + QFile f(dir.absoluteFilePath(FontConfig::fileName())); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + return false; + } + + QTextStream(&f) << cfg.toToml(); + return true; +} + bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg) { if (!savePaletteConfig(themeDirPath, colorScheme, cfg)) { @@ -488,6 +516,27 @@ void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &schem applyStyleAndPalette(themeName, themeCfg, cfg, scheme); } +void ThemeManager::previewFonts(const FontConfig &cfg) +{ + currentFonts = cfg; + + const auto it = cfg.fonts.constFind(AppFont::InterfaceDefault); + if (it != cfg.fonts.constEnd()) { + qApp->setFont(it.value()); + + // Repolish every widget immediately so the preview applies without + // waiting for natural expose events, mirroring applyStyleAndPalette. + QStyle *style = qApp->style(); + for (QWidget *widget : qApp->allWidgets()) { + style->unpolish(widget); + style->polish(widget); + widget->update(); + } + } + + emit fontsChanged(); +} + void ThemeManager::applyStyleAndPalette(const QString &themeName, const ThemeConfig &themeCfg, const PaletteConfig &palCfg, @@ -591,6 +640,18 @@ QColor ThemeManager::appColor(AppColor::Role role) const return accent; } +QFont ThemeManager::appFont(AppFont::Role role) const +{ + const auto it = currentFonts.fonts.constFind(role); + if (it != currentFonts.fonts.constEnd()) { + return it.value(); + } + + // No per-role scaling: every unset role resolves to the application font, + // so with no fonts.toml present the result is identical to before. + return qApp->font(); +} + void ThemeManager::themeChangedSlot() { QString themeName = SettingsCache::instance().getThemeName(); @@ -642,6 +703,24 @@ void ThemeManager::themeChangedSlot() applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); + // ── Load fonts: shared per theme, no light/dark split ─────────────────── + FontConfig fonts = loadFontConfig(dirPath); + if (!fonts.hasFonts()) { + // The shipped fonts.toml may live in the system theme directory rather + // than the resolved (user) theme directory, so built-in themes still + // get their curated fonts. + const QString sysFontPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName); + if (sysFontPath != dirPath) { + fonts = loadFontConfig(sysFontPath); + } + } + currentFonts = fonts; + const auto fontIt = currentFonts.fonts.constFind(AppFont::InterfaceDefault); + if (fontIt != currentFonts.fonts.constEnd()) { + qApp->setFont(fontIt.value()); + } + emit fontsChanged(); + QStringList resources; if (!dirPath.isEmpty()) { resources << dir.absolutePath(); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index c1ab65685..d59212e92 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -51,6 +52,7 @@ private: std::array brushes; QStringMap availableThemes; QMap currentAppColors; + FontConfig currentFonts; /* Internal cache for multiple backgrounds */ @@ -105,6 +107,9 @@ public: // Load/save per-scheme palette colors static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme); static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); + // Load/save shared per-theme fonts (fonts.toml, not split by scheme) + static FontConfig loadFontConfig(const QString &themeDirPath); + static bool saveFontConfig(const QString &themeDirPath, const FontConfig &cfg); // Resolve prefix to a scheme-qualified "theme:" path. Existence is probed // internally across the formats themes may ship (.png/.jpg/.svg), so // callers load the returned path directly. Prefers "-" @@ -132,10 +137,14 @@ public: void reloadCurrentTheme(); void previewPalette(const PaletteConfig &cfg, const QString &scheme); + void previewFonts(const FontConfig &cfg); // Resolves an application color role: the theme's stored [AppColors] value // when present, otherwise a palette-accent-derived fallback. QColor appColor(AppColor::Role role) const; + /** @brief Resolves an application font role: the theme's stored [Fonts] + * value when present, otherwise the application font. */ + QFont appFont(AppFont::Role role) const; QBrush &getBgBrush(Role zone); QBrush getExtraBgBrush(Role zone, int zoneId = 0); @@ -144,6 +153,7 @@ protected slots: signals: void themeChanged(); void paletteChanged(); + void fontsChanged(); }; extern ThemeManager *themeManager; From 77e9b92c5a238444653492975c03872b52724c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 1 Nov 2026 13:53:21 +0100 Subject: [PATCH 3/3] [Fonts] Restore the startup font when a config has no InterfaceDefault - Capture the untouched application font next to the default palette and use it in previewFonts' else branch, so a config without InterfaceDefault restores the startup font instead of leaving the last preview applied. - Drop the unpolish/polish loop from previewFonts: QApplication::setFont already notifies every widget, and the loop ran on every editor auto-apply. --- cockatrice/src/interface/theme_manager.cpp | 19 +++++++++---------- cockatrice/src/interface/theme_manager.h | 4 ++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index dc7e815a2..b23f15a2b 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -127,8 +127,10 @@ static QString usableDefaultStyle(const QString &style) ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { defaultStyleName = usableDefaultStyle(qApp->style()->objectName()); - // Capture the untouched application palette before any theme is applied. + // Capture the untouched application palette and font before any theme is + // applied. defaultPalette = qApp->palette(); + defaultFont = qApp->font(); ensureThemeDirectoryExists(); #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this] { @@ -520,18 +522,15 @@ void ThemeManager::previewFonts(const FontConfig &cfg) { currentFonts = cfg; + // QApplication::setFont already sends a font change to every widget, so a + // repolish pass is not needed for fonts (unlike the palette path). const auto it = cfg.fonts.constFind(AppFont::InterfaceDefault); if (it != cfg.fonts.constEnd()) { qApp->setFont(it.value()); - - // Repolish every widget immediately so the preview applies without - // waiting for natural expose events, mirroring applyStyleAndPalette. - QStyle *style = qApp->style(); - for (QWidget *widget : qApp->allWidgets()) { - style->unpolish(widget); - style->polish(widget); - widget->update(); - } + } else { + // A config without InterfaceDefault means "no override": restore the + // startup font instead of leaving the last preview applied. + qApp->setFont(defaultFont); } emit fontsChanged(); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index d59212e92..c8fba2eb4 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -48,6 +48,10 @@ private: // palette is applied. Used as the base when a theme supplies no palette, so // switching away from a custom palette restores the original colours. QPalette defaultPalette; + // Pristine application font captured at startup, before any theme font is + // applied. Restored when a theme (or a preview) supplies no InterfaceDefault + // font, so switching away from a themed font cannot leave it behind. + QFont defaultFont; QString currentThemePath; std::array brushes; QStringMap availableThemes;