[Fonts] Retire max font size setting into theme CardText min/max range (#7544)

* [Fonts] Retire max font size setting into theme CardText min/max range

The per-card text size lives in the theme now, so a theme can pick the size
it was designed for instead of every user overriding it globally.

- CardText stores family + max point size; CardTextMinSize stores the
  overlay shrink-to-fit floor (omitted from fonts.toml when 1).
- ThemeManager::cardTextMaxSize()/cardTextMinSize() resolve the range with
  the retired defaults (12 max, 1 min), and cardTextMaxSize() falls back to
  the CardText role size that #7541 made live.
- The overlay widget starts at the explicit size when one is set (deck
  preview), else the theme max, and shrinks down to the theme floor.
- The appearance page's "Maximum font size" spin is gone; the range is
  edited in the theme's font grid, whose min/max spins now keep the saved
  range consistent with each other.
- Migration: appearance/maxFontSize is seeded into every user theme folder
  and into the active theme's folder (created when the theme is a shipped
  one that only exists in the system directory). The retired
  game/maxfontsize -> appearance/maxFontSize mapping stays, so users still
  on global.ini keep their value through the chain.

* [Fonts] Resolve CardText sizes against the shipped theme

Seeding themes/<name>/fonts.toml made getAvailableThemes() resolve the user dir, which shadowed the shipped theme.cfg and stylesheet: styleName came back empty and QSS was cleared. Fall back to the shipped theme dir for theme.cfg and QSS when the user dir exists, and resolve CardText sizes through FontConfig's 6..72 range so a migrated 80..100 renders at the size the editor shows. Insert CardTextMaxSize inside [Fonts] when another section follows.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-11-09 14:02:59 +01:00 • committed by GitHub
parent 16455c9fd5
commit 3d24597134
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1058 additions and 74 deletions

View file

@ -10,6 +10,7 @@
#include <QTemporaryDir>
#include <QTextStream>
#include <gtest/gtest.h>
#include <string>
namespace
{
@ -186,6 +187,190 @@ TEST(FontConfigTest, InequalityComparesRoles)
EXPECT_NE(left, right);
}
TEST(FontConfigTest, CardTextMinRoundTrip)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 14);
cfg.cardTextMinSize = 3;
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::CardText].pointSize(), 14);
EXPECT_EQ(loaded.cardTextMinSize, 3);
}
TEST(FontConfigTest, CardTextMinDefaultIsOmitted)
{
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
EXPECT_EQ(cfg.cardTextMinSize, 1);
const QString toml = cfg.toToml();
EXPECT_TRUE(toml.contains(QStringLiteral("CardText")));
EXPECT_FALSE(toml.contains(QStringLiteral("CardTextMinSize")));
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml", toml);
EXPECT_EQ(FontConfig::fromFile(filePath).cardTextMinSize, 1);
}
TEST(FontConfigTest, MalformedMinIsSkipped)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml",
"[Fonts]\n"
"CardText = \"Noto Serif, 14\"\n"
"CardTextMinSize = big\n");
// Each malformed value parses independently, so rewrite the file per case.
const QStringList badValues = {"big", "0", "-3", "", "Inter, 10"};
for (const QString &bad : badValues) {
const QString badPath = writeTempFile(
tmp, "bad.toml", QString("[Fonts]\nCardText = \"Noto Serif, 14\"\nCardTextMinSize = %1\n").arg(bad));
const FontConfig loaded = FontConfig::fromFile(badPath);
EXPECT_EQ(loaded.cardTextMinSize, 1) << "bad value: " << bad.toStdString();
ASSERT_TRUE(loaded.fonts.contains(AppFont::CardText));
EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 14);
}
// The valid file still loads.
EXPECT_EQ(FontConfig::fromFile(filePath).cardTextMinSize, 1);
}
TEST(FontConfigTest, CardTextMaxRoundTrip)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 14);
cfg.cardTextMaxSize = 18;
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.cardTextMaxSize, 18);
EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 14);
}
TEST(FontConfigTest, CardTextMaxSizeWithoutFamily)
{
// The retired maxFontSize setting migrates to a size-only key, so the
// family keeps its existing resolution instead of being pinned to Serif.
FontConfig cfg;
cfg.cardTextMaxSize = 18;
ASSERT_TRUE(cfg.hasFonts());
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml());
const FontConfig loaded = FontConfig::fromFile(filePath);
EXPECT_TRUE(loaded.fonts.isEmpty());
EXPECT_EQ(loaded.cardTextMaxSize, 18);
EXPECT_EQ(loaded, cfg);
}
TEST(FontConfigTest, MigratedCardTextMaxResolvesIntoTheEditorsRange)
{
// appearance/maxFontSize came from a control that allowed values above 72
// and the migration seeds it verbatim, so fonts.toml can hold a size the
// font editor cannot show. 72 is what the editor displays and would save,
// so the board has to draw 72 too instead of rendering 100 until the next
// save silently rewrites the theme.
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml", "[Fonts]\nCardTextMaxSize = 100\n");
const FontConfig migrated = FontConfig::fromFile(filePath);
ASSERT_EQ(migrated.cardTextMaxSize, 100);
EXPECT_EQ(migrated.resolvedCardTextMaxSize(), 72);
}
TEST(FontConfigTest, CardTextSizesResolveIntoTheEditorsRange)
{
// The same bound applies when the size comes from the CardText family, a
// stored size below the editor's control floor rises to it, and the max is
// never below the resolved shrink-to-fit floor.
FontConfig fromFamily;
fromFamily.fonts[AppFont::CardText] = makeFont("Noto Serif", 100);
EXPECT_EQ(fromFamily.resolvedCardTextMaxSize(), 72);
FontConfig belowFloor;
belowFloor.cardTextMaxSize = 3;
EXPECT_EQ(belowFloor.resolvedCardTextMaxSize(), 6);
FontConfig narrow;
narrow.cardTextMinSize = 20;
narrow.cardTextMaxSize = 12;
EXPECT_EQ(narrow.resolvedCardTextMinSize(), 20);
EXPECT_EQ(narrow.resolvedCardTextMaxSize(), 20);
}
TEST(FontConfigTest, CardTextMaxDefaultIsOmitted)
{
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
EXPECT_EQ(cfg.cardTextMaxSize, 0);
const QString toml = cfg.toToml();
EXPECT_FALSE(toml.contains(QStringLiteral("CardTextMaxSize")));
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml", toml);
EXPECT_EQ(FontConfig::fromFile(filePath).cardTextMaxSize, 0);
}
TEST(FontConfigTest, MalformedMaxIsSkipped)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QStringList badValues = {"big", "0", "-3", "", "Inter, 18"};
for (const QString &bad : badValues) {
const QString badPath = writeTempFile(
tmp, "bad.toml", QString("[Fonts]\nCardText = \"Noto Serif, 14\"\nCardTextMaxSize = %1\n").arg(bad));
const FontConfig loaded = FontConfig::fromFile(badPath);
EXPECT_EQ(loaded.cardTextMaxSize, 0) << "bad value: " << bad.toStdString();
ASSERT_TRUE(loaded.fonts.contains(AppFont::CardText));
EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 14);
}
}
TEST(FontConfigTest, InequalityComparesMinSize)
{
FontConfig left;
left.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
left.cardTextMinSize = 1;
FontConfig right = left;
right.cardTextMinSize = 2;
EXPECT_NE(left, right);
}
TEST(FontConfigTest, InequalityComparesMaxSize)
{
FontConfig left;
left.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
left.cardTextMaxSize = 12;
FontConfig right = left;
right.cardTextMaxSize = 18;
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)

View file

@ -461,19 +461,6 @@ TEST_F(SettingsDefaultsTest, Appearance_StyleUserList_Default)
ASSERT_EQ(s.getStyleUserList(), true);
}
TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_Default)
{
AppearanceSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getMaxFontSize(), 12);
}
TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_SetAndGet)
{
AppearanceSettings s(settingsPath, nullptr);
s.setMaxFontSize(14);
ASSERT_EQ(s.getMaxFontSize(), 14);
}
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundSource_Default)
{
AppearanceSettings s(settingsPath, nullptr);

View file

@ -984,6 +984,246 @@ TEST_F(SettingsMigrationTest, IsNotMigrationPendingWithoutLegacyData)
ASSERT_FALSE(SettingsMigration::isMigrationPending(settingsPath));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_NonDefaultSeedsThemeAndDeletesKey)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
const QString fontsPath = themesPath + "/MyTheme/fonts.toml";
ASSERT_TRUE(QFile::exists(fontsPath));
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
// Size-only: the migration must not pin the family, or every CardText
// surface switches to Serif for users who never chose it.
EXPECT_FALSE(content.contains("Serif"));
ASSERT_FALSE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_SeedsActiveStockTheme)
{
// A stock theme resolves to the system directory, so there is no user theme
// folder to seed. The migration has to create the active theme's folder,
// otherwise the retired size is deleted without landing anywhere.
const QString themesPath = tempDir.path() + "/themes";
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/themeName", "Fabric");
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
const QString fontsPath = themesPath + "/Fabric/fonts.toml";
ASSERT_TRUE(QFile::exists(fontsPath));
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_KeepsExistingActiveTheme)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/Fabric"));
{
QFile f(themesPath + "/Fabric/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nCardText = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/themeName", "Fabric");
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/Fabric/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("Inter, 10"));
EXPECT_FALSE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_DefaultDeletesKeyWithoutFiles)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 12);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
ASSERT_FALSE(QFile::exists(themesPath + "/MyTheme/fonts.toml"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_DoesNotOverwriteExplicitMax)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nCardText = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("10"));
EXPECT_FALSE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_MergesWithOtherRoles)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nInterfaceDefault = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 20);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("InterfaceDefault"));
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("20"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_InsertsIntoFontsSectionNotAfterIt)
{
// A hand-edited fonts.toml may keep [Fonts] first and another section after
// it. Appending the seeded key at EOF would land it in that trailing
// section, where the font parser never reads it.
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\n"
"# keep this comment\n"
"InterfaceDefault = \"Inter, 10\"\n"
"\n"
"[Other]\n"
"Keep = \"me\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
ASSERT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
// The new key belongs to [Fonts], not to the section that follows it ...
const int keyPos = content.indexOf("CardTextMaxSize");
const int otherSectionPos = content.indexOf("[Other]");
ASSERT_GE(keyPos, 0);
ASSERT_GE(otherSectionPos, 0);
EXPECT_LT(keyPos, otherSectionPos);
// ... and the file keeps its own comment, ordering and trailing section.
EXPECT_TRUE(content.contains("# keep this comment"));
EXPECT_TRUE(content.contains("InterfaceDefault = \"Inter, 10\""));
EXPECT_TRUE(content.endsWith("[Other]\nKeep = \"me\"\n"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_AbsentKeyDoesNothing)
{
ASSERT_FALSE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
}
TEST_F(SettingsMigrationTest, IsMigrationPendingWhenMaxFontSizePresent)
{
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::isMigrationPending(settingsPath));
}
} // namespace
int main(int argc, char **argv)

View file

@ -51,13 +51,19 @@ class ThemeMigrationTest : public ::testing::Test
{
protected:
QTemporaryDir tmp;
// Base of the per-user theme tree (<tmp>/themes).
QString themesPath;
// Base of the shipped/install theme tree (<tmp>/system), deliberately
// separate from themesPath so a fallback cannot pass by reading a user file.
QString systemThemesPath;
void SetUp() override
{
ASSERT_TRUE(tmp.isValid());
themesPath = QDir(tmp.path()).absoluteFilePath("themes");
systemThemesPath = QDir(tmp.path()).absoluteFilePath("system");
ASSERT_TRUE(QDir().mkpath(themesPath));
ASSERT_TRUE(QDir().mkpath(systemThemesPath));
}
};
@ -237,6 +243,133 @@ TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
}
}
TEST_F(ThemeMigrationTest, ThemeConfigFallsBackToTheShippedThemeDir)
{
// A user theme directory only holds what the user (or a migration) wrote.
// Reading theme.cfg from it alone would shadow the shipped theme and leave
// the board without its style and colour scheme.
const QString userDir = QDir(themesPath).absoluteFilePath("Leather");
ASSERT_TRUE(QDir().mkpath(userDir));
writeThemeFile(themesPath, "Leather", "fonts.toml", "[Fonts]\nCardTextMaxSize = 14\n");
writeThemeFile(systemThemesPath, "Leather", "theme.cfg",
"[Appearance]\nColorScheme = Dark\n\n[Style]\nName = Leather\n");
const QString systemDir = QDir(systemThemesPath).absoluteFilePath("Leather");
const ThemeConfig cfg = ThemeManager::loadDefaultThemeConfig(userDir, systemDir);
EXPECT_EQ(cfg.colorScheme.toStdString(), std::string("Dark"));
EXPECT_EQ(cfg.styleName.toStdString(), std::string("Leather"));
}
TEST_F(ThemeMigrationTest, UserThemeConfigWinsOverTheShippedOne)
{
writeThemeFile(themesPath, "Leather", "theme.cfg", "[Appearance]\nColorScheme = Light\n\n[Style]\nName = Custom\n");
writeThemeFile(systemThemesPath, "Leather", "theme.cfg",
"[Appearance]\nColorScheme = Dark\n\n[Style]\nName = Leather\n");
const QString systemDir = QDir(systemThemesPath).absoluteFilePath("Leather");
const ThemeConfig cfg =
ThemeManager::loadDefaultThemeConfig(QDir(themesPath).absoluteFilePath("Leather"), systemDir);
EXPECT_EQ(cfg.colorScheme.toStdString(), std::string("Light"));
EXPECT_EQ(cfg.styleName.toStdString(), std::string("Custom"));
}
TEST_F(ThemeMigrationTest, ColorSchemeFollowsTheOsWhenTheResolvedThemeDirIsMissing)
{
// getAvailableThemes() maps System/Fusion to a user directory that only
// appears on save, so a fresh profile has no resolved directory. A shipped
// theme may set ColorScheme = Dark; inheriting it there would force dark
// mode on every fresh profile regardless of the OS. Only an existing user
// directory is layered on the shipped theme.
const QString userDir = QDir(themesPath).absoluteFilePath("Fusion");
ASSERT_FALSE(QFileInfo::exists(userDir));
writeThemeFile(systemThemesPath, "Fusion", "theme.cfg",
"[Appearance]\nColorScheme = Dark\n\n[Style]\nName = Fusion\n");
const QString systemDir = QDir(systemThemesPath).absoluteFilePath("Fusion");
const ThemeConfig cfg = ThemeManager::loadColorSchemeConfig(userDir, systemDir);
// Empty means isDarkMode() falls through to the OS color scheme.
EXPECT_TRUE(cfg.colorScheme.isEmpty());
}
TEST_F(ThemeMigrationTest, ColorSchemeInheritsTheShippedSchemeForAnExistingUserDir)
{
// The migration seeds an existing user directory with fonts.toml only. Such
// a directory still belongs to the shipped theme and must keep its scheme
// instead of silently flipping to the OS one.
const QString userDir = QDir(themesPath).absoluteFilePath("Fusion");
ASSERT_TRUE(QDir().mkpath(userDir));
writeThemeFile(themesPath, "Fusion", "fonts.toml", "[Fonts]\nCardTextMaxSize = 14\n");
writeThemeFile(systemThemesPath, "Fusion", "theme.cfg",
"[Appearance]\nColorScheme = Dark\n\n[Style]\nName = Fusion\n");
const QString systemDir = QDir(systemThemesPath).absoluteFilePath("Fusion");
const ThemeConfig cfg = ThemeManager::loadColorSchemeConfig(userDir, systemDir);
EXPECT_EQ(cfg.colorScheme.toStdString(), std::string("Dark"));
}
TEST_F(ThemeMigrationTest, DefaultThemeConfigStillCarriesTheShippedStyleWithoutAUserDir)
{
// themeChangedSlot()/previewPalette() use the unconditional fallback: a
// stock theme with no user directory must still get its shipped styleName,
// even though its colour scheme follows the OS.
const QString userDir = QDir(themesPath).absoluteFilePath("Fusion");
ASSERT_FALSE(QFileInfo::exists(userDir));
writeThemeFile(systemThemesPath, "Fusion", "theme.cfg",
"[Appearance]\nColorScheme = Dark\n\n[Style]\nName = Fusion\n");
const ThemeConfig cfg =
ThemeManager::loadDefaultThemeConfig(userDir, QDir(systemThemesPath).absoluteFilePath("Fusion"));
EXPECT_EQ(cfg.styleName.toStdString(), std::string("Fusion"));
}
TEST_F(ThemeMigrationTest, StyleSheetFallsBackToTheShippedThemeDir)
{
const QString userDir = QDir(themesPath).absoluteFilePath("Leather");
ASSERT_TRUE(QDir().mkpath(userDir));
writeThemeFile(themesPath, "Leather", "fonts.toml", "[Fonts]\nCardTextMaxSize = 14\n");
writeThemeFile(systemThemesPath, "Leather", "style.css", "shipped-css");
writeThemeFile(systemThemesPath, "Leather", "style-dark.css", "shipped-dark-css");
const QString systemDir = QDir(systemThemesPath).absoluteFilePath("Leather");
// The scheme-qualified shipped stylesheet wins, else the plain one.
EXPECT_EQ(ThemeManager::themeStyleSheetPath(userDir, systemDir, true).toStdString(),
QDir(systemDir).absoluteFilePath("style-dark.css").toStdString());
EXPECT_EQ(ThemeManager::themeStyleSheetPath(userDir, systemDir, false).toStdString(),
QDir(systemDir).absoluteFilePath("style.css").toStdString());
}
TEST_F(ThemeMigrationTest, StyleSheetPrefersTheUserCopy)
{
const QString userDir = QDir(themesPath).absoluteFilePath("Leather");
writeThemeFile(themesPath, "Leather", "style.css", "user-css");
writeThemeFile(systemThemesPath, "Leather", "style-dark.css", "shipped-dark-css");
const QString systemDir = QDir(systemThemesPath).absoluteFilePath("Leather");
// A user-authored stylesheet is never mixed with the shipped scheme variant.
EXPECT_EQ(ThemeManager::themeStyleSheetPath(userDir, systemDir, true).toStdString(),
QDir(userDir).absoluteFilePath("style.css").toStdString());
}
TEST_F(ThemeMigrationTest, StyleSheetIsEmptyWhenNeitherDirectoryHasOne)
{
const QString userDir = QDir(themesPath).absoluteFilePath("Leather");
ASSERT_TRUE(QDir().mkpath(userDir));
writeThemeFile(themesPath, "Leather", "fonts.toml", "[Fonts]\nCardTextMaxSize = 14\n");
// Only the opposite scheme's stylesheet is shipped, so no fallback matches.
writeThemeFile(systemThemesPath, "Leather", "style-dark.css", "shipped-dark-css");
const QString systemDir = QDir(systemThemesPath).absoluteFilePath("Leather");
EXPECT_TRUE(ThemeManager::themeStyleSheetPath(userDir, systemDir, false).isEmpty());
}
// 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)