mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-07 10:33:24 -08:00
[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.
This commit is contained in:
parent
e87cd80c9b
commit
47bb103a87
9 changed files with 415 additions and 79 deletions
|
|
@ -11,10 +11,6 @@
|
|||
#include <QSpinBox>
|
||||
#include <QtGlobal>
|
||||
|
||||
static constexpr int MIN_FONT_SIZE = 6;
|
||||
static constexpr int MAX_FONT_SIZE = 72;
|
||||
static constexpr int CARD_TEXT_MIN_SIZE = 1;
|
||||
static constexpr int CARD_TEXT_MAX_SIZE = 12;
|
||||
static constexpr int FALLBACK_FONT_SIZE = 10;
|
||||
|
||||
static const QMap<AppFont::Role, const char *> APP_FONT_DESCRIPTIONS = {
|
||||
|
|
@ -63,20 +59,23 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
|
|||
|
||||
if (role == AppFont::CardText) {
|
||||
auto *minSpin = new QSpinBox(this);
|
||||
minSpin->setRange(CARD_TEXT_MIN_SIZE, MAX_FONT_SIZE);
|
||||
minSpin->setRange(FontConfig::CARD_TEXT_MIN_SIZE, FontConfig::MAX_FONT_SIZE);
|
||||
grid->addWidget(minSpin, i, 2);
|
||||
connect(minSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
|
||||
|
||||
auto *maxSpin = new QSpinBox(this);
|
||||
maxSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||
maxSpin->setRange(FontConfig::MIN_FONT_SIZE, FontConfig::MAX_FONT_SIZE);
|
||||
grid->addWidget(maxSpin, i, 3);
|
||||
connect(maxSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
|
||||
|
||||
// Keep the range consistent: otherwise min 20 / max 12 saves as-is
|
||||
// and cardTextMaxSize() silently clamps the max up to the min, so
|
||||
// the max spin shows a size that is never used. Moving one spin
|
||||
// pulls the other's bound along.
|
||||
connect(minSpin, &QSpinBox::valueChanged, this, [maxSpin](int value) { maxSpin->setMinimum(value); });
|
||||
// pulls the other's bound along, but never below the max control's
|
||||
// own floor: the renderer resolves the max into the same range, so a
|
||||
// smaller value here would be the same mismatch in reverse.
|
||||
connect(minSpin, &QSpinBox::valueChanged, this,
|
||||
[maxSpin](int value) { maxSpin->setMinimum(qMax(value, FontConfig::MIN_FONT_SIZE)); });
|
||||
connect(maxSpin, &QSpinBox::valueChanged, this, [minSpin](int value) { minSpin->setMaximum(value); });
|
||||
|
||||
label->setBuddy(familyCombo);
|
||||
|
|
@ -86,7 +85,7 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
|
|||
}
|
||||
|
||||
auto *sizeSpin = new QSpinBox(this);
|
||||
sizeSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||
sizeSpin->setRange(FontConfig::MIN_FONT_SIZE, FontConfig::MAX_FONT_SIZE);
|
||||
grid->addWidget(sizeSpin, i, 2);
|
||||
connect(sizeSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
|
||||
|
||||
|
|
@ -121,29 +120,17 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
|
|||
// max up to 20), which would clamp the values written below. Reset
|
||||
// both to the full range first; the valueChanged lambdas re-link
|
||||
// them as the values are set.
|
||||
row.minSpin->setRange(CARD_TEXT_MIN_SIZE, MAX_FONT_SIZE);
|
||||
row.sizeSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||
|
||||
// The max spin is a size-only override. Without CardTextMaxSize it
|
||||
// follows the CardText family size, and without CardText at all it
|
||||
// must match cardTextMaxSize()'s default rather than the system
|
||||
// startup size, which currentFontConfig() would otherwise bake in.
|
||||
const int fallbackMax = cfg.fonts.contains(role) ? font.pointSize() : CARD_TEXT_MAX_SIZE;
|
||||
int maxSize = cfg.cardTextMaxSize > 0 ? cfg.cardTextMaxSize : fallbackMax;
|
||||
if (maxSize <= 0) {
|
||||
maxSize = CARD_TEXT_MAX_SIZE;
|
||||
}
|
||||
int minSize = cfg.cardTextMinSize > 0 ? cfg.cardTextMinSize : themeManager->cardTextMinSize();
|
||||
minSize = qBound(CARD_TEXT_MIN_SIZE, minSize, MAX_FONT_SIZE);
|
||||
maxSize = qBound(MIN_FONT_SIZE, maxSize, MAX_FONT_SIZE);
|
||||
if (maxSize < minSize) {
|
||||
maxSize = minSize;
|
||||
}
|
||||
row.minSpin->setRange(FontConfig::CARD_TEXT_MIN_SIZE, FontConfig::MAX_FONT_SIZE);
|
||||
row.sizeSpin->setRange(FontConfig::MIN_FONT_SIZE, FontConfig::MAX_FONT_SIZE);
|
||||
|
||||
// Resolved by FontConfig, which the renderer reads through as well:
|
||||
// a stored size the editor cannot represent (for example a migrated
|
||||
// appearance/maxFontSize above 72) shows at the bound the board
|
||||
// draws, so a save never rewrites a size that was in use.
|
||||
// Max first, then min: each value re-applies the linked bound, so
|
||||
// the pair ends up consistent instead of clamping the smaller one.
|
||||
row.sizeSpin->setValue(maxSize);
|
||||
row.minSpin->setValue(minSize);
|
||||
row.sizeSpin->setValue(cfg.resolvedCardTextMaxSize());
|
||||
row.minSpin->setValue(cfg.resolvedCardTextMinSize());
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +141,7 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
|
|||
if (size <= 0) {
|
||||
size = FALLBACK_FONT_SIZE;
|
||||
}
|
||||
row.sizeSpin->setValue(qBound(MIN_FONT_SIZE, size, MAX_FONT_SIZE));
|
||||
row.sizeSpin->setValue(qBound(FontConfig::MIN_FONT_SIZE, size, FontConfig::MAX_FONT_SIZE));
|
||||
}
|
||||
blockSignals(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <QMetaEnum>
|
||||
#include <QPair>
|
||||
#include <QTextStream>
|
||||
#include <algorithm>
|
||||
#include <qnamespace.h>
|
||||
#include <utility>
|
||||
|
||||
|
|
@ -308,7 +309,28 @@ QPalette PaletteConfig::apply(QPalette base) const
|
|||
|
||||
bool FontConfig::hasFonts() const
|
||||
{
|
||||
return !fonts.isEmpty() || cardTextMinSize != 1 || cardTextMaxSize > 0;
|
||||
return !fonts.isEmpty() || cardTextMinSize != CARD_TEXT_MIN_SIZE || cardTextMaxSize > 0;
|
||||
}
|
||||
|
||||
int FontConfig::resolvedCardTextMinSize() const
|
||||
{
|
||||
const int size = cardTextMinSize > 0 ? cardTextMinSize : CARD_TEXT_MIN_SIZE;
|
||||
return std::clamp(size, CARD_TEXT_MIN_SIZE, MAX_FONT_SIZE);
|
||||
}
|
||||
|
||||
int FontConfig::resolvedCardTextMaxSize() const
|
||||
{
|
||||
int size = cardTextMaxSize;
|
||||
if (size <= 0) {
|
||||
const auto it = fonts.constFind(AppFont::CardText);
|
||||
size = it != fonts.constEnd() ? it.value().pointSize() : 0;
|
||||
}
|
||||
if (size <= 0) {
|
||||
size = DEFAULT_CARD_TEXT_SIZE;
|
||||
}
|
||||
// Mirror the editor's resolution: clamp the max into its control range, then
|
||||
// raise it to the (already clamped) min so the pair stays consistent.
|
||||
return std::max(resolvedCardTextMinSize(), std::clamp(size, MIN_FONT_SIZE, MAX_FONT_SIZE));
|
||||
}
|
||||
|
||||
QString FontConfig::toToml() const
|
||||
|
|
@ -334,7 +356,7 @@ QString FontConfig::toToml() const
|
|||
entries += QString("%1 = \"%2, %3\"\n").arg(QString(roleName), -20).arg(family).arg(pointSize);
|
||||
}
|
||||
|
||||
if (cardTextMinSize != 1) {
|
||||
if (cardTextMinSize != CARD_TEXT_MIN_SIZE) {
|
||||
entries += QString("%1 = %2\n").arg(QStringLiteral("CardTextMinSize"), -20).arg(cardTextMinSize);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,12 +92,30 @@ struct PaletteConfig
|
|||
*/
|
||||
struct FontConfig
|
||||
{
|
||||
/// Point-size range the theme font editor can represent. CardText is
|
||||
/// resolved through the same bounds where it is rendered, so the board never
|
||||
/// draws a size the editor cannot show and would clamp away on save.
|
||||
static constexpr int MIN_FONT_SIZE = 6;
|
||||
static constexpr int MAX_FONT_SIZE = 72;
|
||||
/// CardText shrink-to-fit floor, editable below the general font floor.
|
||||
static constexpr int CARD_TEXT_MIN_SIZE = 1;
|
||||
/// CardText size when neither CardTextMaxSize nor a CardText family is set.
|
||||
static constexpr int DEFAULT_CARD_TEXT_SIZE = 12;
|
||||
|
||||
QMap<AppFont::Role, QFont> fonts;
|
||||
int cardTextMinSize = 1;
|
||||
int cardTextMinSize = CARD_TEXT_MIN_SIZE;
|
||||
/// Optional size-only maximum for CardText, used by the retired maxFontSize
|
||||
/// migration so seeding it does not have to pin a family.
|
||||
int cardTextMaxSize = 0;
|
||||
|
||||
/// CardText min/max resolved to what the font editor shows and a save would
|
||||
/// write: the stored values (max falling back to the CardText point size,
|
||||
/// then the default) clamped into the representable range. A seeded legacy
|
||||
/// size above the editor's maximum therefore renders at that maximum instead
|
||||
/// of at a size the editor would silently rewrite.
|
||||
int resolvedCardTextMinSize() const;
|
||||
int resolvedCardTextMaxSize() const;
|
||||
|
||||
bool operator==(const FontConfig &rhs) const
|
||||
{
|
||||
return fonts == rhs.fonts && cardTextMinSize == rhs.cardTextMinSize && cardTextMaxSize == rhs.cardTextMaxSize;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ static const QColor PLAYERZONE_BG_DEFAULT = QColor(200, 200, 200);
|
|||
static const QColor STACKZONE_BG_DEFAULT = QColor(113, 43, 43);
|
||||
static const QStringList DEFAULT_RESOURCE_PATHS = {":/resources"};
|
||||
|
||||
// Defined below; forward-declared because isDarkMode() resolves the shipped
|
||||
// theme directory needed for the theme.cfg fallback.
|
||||
static QString systemThemesBasePath();
|
||||
|
||||
struct PaletteColorInfo
|
||||
{
|
||||
QPalette::ColorGroup group;
|
||||
|
|
@ -235,7 +239,15 @@ void ThemeManager::ensureThemeDirectoryExists()
|
|||
|
||||
bool ThemeManager::isDarkMode(const QString &themeDirPath) const
|
||||
{
|
||||
ThemeConfig themeConfig = ThemeConfig::fromThemeDir(themeDirPath);
|
||||
// The theme directory is always named after the theme, so the shipped
|
||||
// fallback for a user directory that carries no theme.cfg is the same-named
|
||||
// directory under the system themes base path — mirroring the font and
|
||||
// palette fallbacks. A resolved directory that does not exist yet is a stock
|
||||
// theme (getAvailableThemes() maps System/Fusion to a user path that
|
||||
// writableThemeDir() only creates on save), so it keeps following the OS
|
||||
// instead of inheriting a shipped ColorScheme such as Fusion's Dark.
|
||||
const QString systemThemeDir = QDir(systemThemesBasePath()).absoluteFilePath(QFileInfo(themeDirPath).fileName());
|
||||
const ThemeConfig themeConfig = loadColorSchemeConfig(themeDirPath, systemThemeDir);
|
||||
if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) {
|
||||
return true;
|
||||
} else if (themeConfig.colorScheme.compare("Light", Qt::CaseInsensitive) == 0) {
|
||||
|
|
@ -401,6 +413,60 @@ bool ThemeManager::saveGlobalConfig(const QString &themeDirPath, const ThemeConf
|
|||
return cfg.save(themeDirPath);
|
||||
}
|
||||
|
||||
ThemeConfig ThemeManager::loadDefaultThemeConfig(const QString &themeDirPath, const QString &systemThemeDirPath)
|
||||
{
|
||||
if (themeDirPath.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// The shipped theme.cfg may live in the system theme directory rather than
|
||||
// the resolved (user) directory. A user directory that only holds a seeded
|
||||
// fonts.toml (or a saved palette) must still inherit the shipped style and
|
||||
// colour scheme instead of shadowing them with an empty config.
|
||||
if (QFileInfo::exists(QDir(themeDirPath).absoluteFilePath("theme.cfg"))) {
|
||||
return ThemeConfig::fromThemeDir(themeDirPath);
|
||||
}
|
||||
return ThemeConfig::fromThemeDir(systemThemeDirPath);
|
||||
}
|
||||
|
||||
ThemeConfig ThemeManager::loadColorSchemeConfig(const QString &themeDirPath, const QString &systemThemeDirPath)
|
||||
{
|
||||
// Only an existing user directory is layered on the shipped theme: it can
|
||||
// hold seeded files (fonts.toml, a saved palette) without a theme.cfg of its
|
||||
// own, so it inherits the shipped style and colour scheme. A directory that
|
||||
// does not exist yet means the user has never customised this theme, and
|
||||
// inheriting the shipped scheme there would override the OS preference on a
|
||||
// fresh profile (Fusion ships Dark).
|
||||
if (themeDirPath.isEmpty() || !QFileInfo(themeDirPath).isDir()) {
|
||||
return {};
|
||||
}
|
||||
return loadDefaultThemeConfig(themeDirPath, systemThemeDirPath);
|
||||
}
|
||||
|
||||
QString ThemeManager::themeStyleSheetPath(const QString &themeDirPath, const QString &systemThemeDirPath, bool dark)
|
||||
{
|
||||
const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
|
||||
(dark ? QStringLiteral("dark") : QStringLiteral("light")) + QStringLiteral(".css");
|
||||
|
||||
// Scheme-qualified stylesheet first, plain style.css as the super fallback,
|
||||
// both inside the resolved (user) directory before the shipped one. A user
|
||||
// directory holding only fonts.toml therefore keeps the shipped styling.
|
||||
const QStringList candidateDirs = {themeDirPath, systemThemeDirPath};
|
||||
const QStringList candidateNames = {schemeCss, QString(STYLE_CSS_NAME)};
|
||||
for (const QString &dirPath : candidateDirs) {
|
||||
if (dirPath.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (const QString &name : candidateNames) {
|
||||
const QString path = QDir(dirPath).absoluteFilePath(name);
|
||||
if (QFileInfo::exists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
PaletteConfig ThemeManager::loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme)
|
||||
{
|
||||
if (themeDirPath.isEmpty()) {
|
||||
|
|
@ -527,7 +593,10 @@ void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &schem
|
|||
{
|
||||
const QString themeName = SettingsCache::instance().getThemeName();
|
||||
const QString dirPath = getAvailableThemes().value(themeName);
|
||||
const ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath);
|
||||
// Same shipped fallback as themeChangedSlot(): a user directory that only
|
||||
// holds seeded files must not preview the theme with its style reset.
|
||||
const QString sysPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName);
|
||||
const ThemeConfig themeCfg = loadDefaultThemeConfig(dirPath, sysPath);
|
||||
applyStyleAndPalette(themeName, themeCfg, cfg, scheme);
|
||||
}
|
||||
|
||||
|
|
@ -705,25 +774,16 @@ QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const
|
|||
|
||||
int ThemeManager::cardTextMaxSize() const
|
||||
{
|
||||
if (currentFonts.cardTextMaxSize > 0) {
|
||||
return currentFonts.cardTextMaxSize;
|
||||
}
|
||||
const auto it = currentFonts.fonts.constFind(AppFont::CardText);
|
||||
if (it != currentFonts.fonts.constEnd()) {
|
||||
const int size = it.value().pointSize();
|
||||
if (size > 0) {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
return 12;
|
||||
// Resolved through FontConfig so the board and the font editor share one
|
||||
// representable range: a stored size the editor cannot show (e.g. a migrated
|
||||
// appearance/maxFontSize of 100) renders at the editor's maximum instead of
|
||||
// at a size the next save would silently rewrite.
|
||||
return currentFonts.resolvedCardTextMaxSize();
|
||||
}
|
||||
|
||||
int ThemeManager::cardTextMinSize() const
|
||||
{
|
||||
if (currentFonts.cardTextMinSize > 0) {
|
||||
return currentFonts.cardTextMinSize;
|
||||
}
|
||||
return 1;
|
||||
return currentFonts.resolvedCardTextMinSize();
|
||||
}
|
||||
|
||||
void ThemeManager::themeChangedSlot()
|
||||
|
|
@ -732,31 +792,24 @@ void ThemeManager::themeChangedSlot()
|
|||
QString dirPath = getAvailableThemes().value(themeName);
|
||||
currentThemePath = dirPath;
|
||||
QDir dir(dirPath);
|
||||
const QString sysPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName);
|
||||
|
||||
// CSS — prefer the scheme-qualified stylesheet (style-dark.css /
|
||||
// style-light.css) when present, else the plain style.css as fallback.
|
||||
if (!dirPath.isEmpty()) {
|
||||
const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light");
|
||||
const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
|
||||
scheme + QStringLiteral(".css");
|
||||
if (dir.exists(schemeCss)) {
|
||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(schemeCss));
|
||||
} else if (dir.exists(STYLE_CSS_NAME)) {
|
||||
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
|
||||
} else {
|
||||
qApp->setStyleSheet("");
|
||||
}
|
||||
} else {
|
||||
qApp->setStyleSheet("");
|
||||
}
|
||||
// A user theme directory that only carries seeded files falls back to the
|
||||
// shipped stylesheet of the same theme.
|
||||
const bool darkMode = isDarkMode(dirPath);
|
||||
const QString styleSheetPath = themeStyleSheetPath(dirPath, sysPath, darkMode);
|
||||
qApp->setStyleSheet(styleSheetPath.isEmpty() ? QString() : "file:///" + styleSheetPath);
|
||||
|
||||
// load theme.cfg for style + scheme preference
|
||||
ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath);
|
||||
// load theme.cfg for style + scheme preference, falling back to the shipped
|
||||
// theme directory when the user copy does not carry one
|
||||
ThemeConfig themeCfg = loadDefaultThemeConfig(dirPath, sysPath);
|
||||
|
||||
// Resolve active scheme:
|
||||
// theme.cfg says Dark/Light → use that
|
||||
// theme.cfg says System or is absent → follow the OS
|
||||
QString activeScheme = isDarkMode(dirPath) ? "Dark" : "Light";
|
||||
QString activeScheme = darkMode ? "Dark" : "Light";
|
||||
|
||||
// ── Load palette: custom first, then theme default ────────────────────
|
||||
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
||||
|
|
@ -799,7 +852,6 @@ void ThemeManager::themeChangedSlot()
|
|||
// include the system theme dir as a fallback so shipped assets like
|
||||
// zones/*.png and style.css still resolve for themes that ship only
|
||||
// those files (e.g. Leather, Plasma, Fabric, VelvetMarble).
|
||||
const QString sysPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName);
|
||||
if (sysPath != dirPath && QDir(sysPath).exists()) {
|
||||
resources << sysPath;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ public:
|
|||
static bool isDirReallyWritable(const QString &dirPath);
|
||||
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
||||
// (Dark/Light), falling back to the OS color scheme when it is "System".
|
||||
// A resolved directory that does not exist yet is an uncustomised stock
|
||||
// theme, so it follows the OS rather than the shipped scheme.
|
||||
bool isDarkMode(const QString &themeDirPath) const;
|
||||
// The resolved scheme of the currently active theme.
|
||||
bool isDarkModeActive() const
|
||||
|
|
@ -107,6 +109,20 @@ public:
|
|||
// Load the global theme settings (style + color scheme preference)
|
||||
static ThemeConfig loadGlobalConfig(const QString &themeDirPath);
|
||||
static bool saveGlobalConfig(const QString &themeDirPath, const ThemeConfig &cfg);
|
||||
// Load a theme's theme.cfg, falling back to the shipped (system) theme
|
||||
// directory when the resolved user directory does not carry one. A user
|
||||
// directory seeded with only fonts.toml would otherwise shadow the shipped
|
||||
// theme and lose its style and colour scheme.
|
||||
static ThemeConfig loadDefaultThemeConfig(const QString &themeDirPath, const QString &systemThemeDirPath);
|
||||
// Load the theme.cfg that decides the active colour scheme. Unlike
|
||||
// loadDefaultThemeConfig(), a resolved directory that does not exist yet is
|
||||
// a stock theme with no user override: it returns an empty config so the
|
||||
// caller follows the OS scheme instead of inheriting the shipped one.
|
||||
static ThemeConfig loadColorSchemeConfig(const QString &themeDirPath, const QString &systemThemeDirPath);
|
||||
// Resolve the theme stylesheet: the scheme-qualified style-<dark|light>.css
|
||||
// when shipped, else style.css, preferring the resolved (user) directory
|
||||
// over the shipped one. Empty when neither directory carries a stylesheet.
|
||||
static QString themeStyleSheetPath(const QString &themeDirPath, const QString &systemThemeDirPath, bool dark);
|
||||
|
||||
// Load/save per-scheme palette colors
|
||||
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
||||
|
|
@ -169,10 +185,11 @@ public:
|
|||
* "what this role resolves to" must use this, because appFont()
|
||||
* reflects a live preview while the editor is open. */
|
||||
QFont defaultAppFont(AppFont::Role role) const;
|
||||
/** @brief CardText max size: stored CardTextMaxSize, else the CardText
|
||||
* point size, else 12. */
|
||||
/** @brief CardText max size resolved the way the font editor shows it:
|
||||
* stored CardTextMaxSize, else the CardText point size, else 12,
|
||||
* clamped to the editor's representable range. */
|
||||
int cardTextMaxSize() const;
|
||||
/** @brief CardText shrink-to-fit floor: stored CardTextMinSize, else 1. */
|
||||
/** @brief CardText shrink-to-fit floor resolved the same way. */
|
||||
int cardTextMinSize() const;
|
||||
|
||||
QBrush &getBgBrush(Role zone);
|
||||
|
|
|
|||
|
|
@ -532,23 +532,46 @@ static bool seedCardTextMaxIntoThemeDir(const QString &themeDirPath, int maxSize
|
|||
// for the whole board (names, player labels, the completer), which is not
|
||||
// what the retired maxFontSize setting meant. Let the family keep its
|
||||
// existing resolution and only constrain the size.
|
||||
const QString entry = QString("%1 = %2\n").arg(QStringLiteral("CardTextMaxSize"), -20).arg(maxSize);
|
||||
const QString entry = QString("%1 = %2").arg(QStringLiteral("CardTextMaxSize"), -20).arg(maxSize);
|
||||
|
||||
QString out;
|
||||
if (existing.isEmpty()) {
|
||||
out = "[Fonts]\n" + entry + "\n";
|
||||
} else if (existing.contains("[Fonts]", Qt::CaseInsensitive)) {
|
||||
out = existing;
|
||||
if (!out.endsWith('\n')) {
|
||||
out += '\n';
|
||||
}
|
||||
out += entry;
|
||||
} else {
|
||||
out = existing;
|
||||
if (!out.endsWith('\n')) {
|
||||
out += '\n';
|
||||
}
|
||||
out += "\n[Fonts]\n" + entry + "\n";
|
||||
|
||||
// Insert the key inside an existing [Fonts] section. Appending at EOF
|
||||
// would land it in whatever section happens to follow, where the font
|
||||
// parser never reads it. Tracking the last non-blank line of the
|
||||
// section keeps the file's own comments, ordering and blank lines.
|
||||
QStringList lines = out.split('\n');
|
||||
int insertAt = -1;
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
const QString trimmed = lines[i].trimmed();
|
||||
if (insertAt < 0) {
|
||||
if (trimmed.startsWith('[') && trimmed.endsWith(']') &&
|
||||
trimmed.mid(1, trimmed.length() - 2).trimmed().compare("Fonts", Qt::CaseInsensitive) == 0) {
|
||||
insertAt = i + 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||
break;
|
||||
}
|
||||
if (!trimmed.isEmpty()) {
|
||||
insertAt = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertAt < 0) {
|
||||
out += "\n[Fonts]\n" + entry + "\n";
|
||||
} else {
|
||||
lines.insert(insertAt, entry);
|
||||
out = lines.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
QFile f(filePath);
|
||||
|
|
|
|||
|
|
@ -282,6 +282,42 @@ TEST(FontConfigTest, CardTextMaxSizeWithoutFamily)
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -1160,6 +1160,54 @@ TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_MergesWithOtherRoles)
|
|||
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));
|
||||
|
|
|
|||
|
|
@ -47,13 +47,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));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -149,6 +155,133 @@ TEST_F(ThemeMigrationTest, IsIdempotent)
|
|||
EXPECT_EQ(readThemeFile(themesPath, CURRENT, "style.css").toStdString(), std::string("css"));
|
||||
}
|
||||
|
||||
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. Fusion
|
||||
// ships 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue