mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-09 11:33:25 -08:00
[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:
parent
16455c9fd5
commit
3d24597134
21 changed files with 1058 additions and 74 deletions
|
|
@ -163,6 +163,9 @@ SettingsCache::SettingsCache()
|
|||
// before that rename (idempotent, safe to run on every startup, portable builds included).
|
||||
SettingsMigration::migrateGameFiltersToCamelCase(settingsPath);
|
||||
|
||||
// Retire appearance/maxFontSize into per-theme CardText max (idempotent, safe every startup).
|
||||
SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath);
|
||||
|
||||
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
|
||||
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
||||
serversSettings = new ServersSettings(settingsPath, this);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
#include <QVariant>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/debug_settings.h>
|
||||
#include <qnamespace.h>
|
||||
|
|
@ -187,8 +186,8 @@ bool AbstractCardItem::shouldPaintNameOverPicture() const
|
|||
|
||||
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
||||
{
|
||||
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
|
||||
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
||||
const int minSize = std::max(1, themeManager->cardTextMinSize());
|
||||
const int fontSize = std::max(minSize, std::max(1, themeManager->cardTextMaxSize()));
|
||||
|
||||
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@
|
|||
#include <QSpinBox>
|
||||
#include <QtGlobal>
|
||||
|
||||
static constexpr int MIN_FONT_SIZE = 6;
|
||||
static constexpr int MAX_FONT_SIZE = 72;
|
||||
static constexpr int FALLBACK_FONT_SIZE = 10;
|
||||
|
||||
static const QMap<AppFont::Role, const char *> APP_FONT_DESCRIPTIONS = {
|
||||
|
|
@ -46,6 +44,7 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
|
|||
grid->setColumnStretch(0, 0);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->setColumnStretch(2, 0);
|
||||
grid->setColumnStretch(3, 0);
|
||||
|
||||
QMetaEnum fontEnum = QMetaEnum::fromType<AppFont::Role>();
|
||||
for (int i = 0; i < fontEnum.keyCount(); ++i) {
|
||||
|
|
@ -58,14 +57,41 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
|
|||
grid->addWidget(familyCombo, i, 1);
|
||||
connect(familyCombo, &QFontComboBox::currentFontChanged, this, [this] { emit fontsChanged(); });
|
||||
|
||||
if (role == AppFont::CardText) {
|
||||
auto *minSpin = new QSpinBox(this);
|
||||
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(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, 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);
|
||||
|
||||
fontRows[role] = {label, familyCombo, maxSpin, minSpin};
|
||||
continue;
|
||||
}
|
||||
|
||||
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(); });
|
||||
|
||||
label->setBuddy(familyCombo);
|
||||
|
||||
fontRows[role] = {label, familyCombo, sizeSpin};
|
||||
fontRows[role] = {label, familyCombo, sizeSpin, nullptr};
|
||||
}
|
||||
|
||||
retranslateUi();
|
||||
|
|
@ -89,6 +115,25 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
|
|||
const QFont font = cfg.fonts.value(role, fallback);
|
||||
row.familyCombo->setCurrentFont(font);
|
||||
|
||||
if (role == AppFont::CardText && row.minSpin != nullptr) {
|
||||
// A previous load may have narrowed the linked bounds (min 20 pulls
|
||||
// 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(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(cfg.resolvedCardTextMaxSize());
|
||||
row.minSpin->setValue(cfg.resolvedCardTextMinSize());
|
||||
continue;
|
||||
}
|
||||
|
||||
int size = font.pointSize();
|
||||
if (size <= 0) {
|
||||
size = qApp->font().pointSize();
|
||||
|
|
@ -96,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);
|
||||
}
|
||||
|
|
@ -114,6 +159,11 @@ FontConfig FontGridWidget::currentFontConfig() const
|
|||
QFont font(row.familyCombo->currentFont().family());
|
||||
font.setPointSize(row.sizeSpin->value());
|
||||
cfg.fonts[role] = font;
|
||||
|
||||
if (role == AppFont::CardText && row.minSpin != nullptr) {
|
||||
cfg.cardTextMinSize = row.minSpin->value();
|
||||
cfg.cardTextMaxSize = row.sizeSpin->value();
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
|
@ -129,6 +179,15 @@ void FontGridWidget::retranslateUi()
|
|||
row.label->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
|
||||
row.familyCombo->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
|
||||
row.familyCombo->setAccessibleName(roleDisplayName(role));
|
||||
if (role == AppFont::CardText && row.minSpin != nullptr) {
|
||||
row.minSpin->setToolTip(tr("Minimum card text size"));
|
||||
row.minSpin->setAccessibleName(tr("Minimum card text size"));
|
||||
row.minSpin->setSuffix(tr(" pt"));
|
||||
row.sizeSpin->setToolTip(tr("Maximum card text size"));
|
||||
row.sizeSpin->setAccessibleName(tr("Maximum card text size"));
|
||||
row.sizeSpin->setSuffix(tr(" pt"));
|
||||
continue;
|
||||
}
|
||||
row.sizeSpin->setToolTip(tr("Point size for %1").arg(roleDisplayName(role)));
|
||||
row.sizeSpin->setAccessibleName(tr("Point size for %1").arg(roleDisplayName(role)));
|
||||
row.sizeSpin->setSuffix(tr(" pt"));
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ private:
|
|||
QLabel *label = nullptr;
|
||||
QFontComboBox *familyCombo = nullptr;
|
||||
QSpinBox *sizeSpin = nullptr;
|
||||
QSpinBox *minSpin = nullptr; // CardText only: shrink-to-fit floor
|
||||
};
|
||||
|
||||
QMap<AppFont::Role, FontRow> fontRows;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <QPair>
|
||||
#include <QStringLiteral>
|
||||
#include <QTextStream>
|
||||
#include <algorithm>
|
||||
#include <qnamespace.h>
|
||||
#include <utility>
|
||||
|
||||
|
|
@ -309,7 +310,28 @@ QPalette PaletteConfig::apply(QPalette base) const
|
|||
|
||||
bool FontConfig::hasFonts() const
|
||||
{
|
||||
return !fonts.isEmpty();
|
||||
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
|
||||
|
|
@ -335,6 +357,14 @@ QString FontConfig::toToml() const
|
|||
entries += QString("%1 = \"%2, %3\"\n").arg(QString(roleName), -20).arg(family).arg(pointSize);
|
||||
}
|
||||
|
||||
if (cardTextMinSize != CARD_TEXT_MIN_SIZE) {
|
||||
entries += QString("%1 = %2\n").arg(QStringLiteral("CardTextMinSize"), -20).arg(cardTextMinSize);
|
||||
}
|
||||
|
||||
if (cardTextMaxSize > 0) {
|
||||
entries += QString("%1 = %2\n").arg(QStringLiteral("CardTextMaxSize"), -20).arg(cardTextMaxSize);
|
||||
}
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
|
@ -402,6 +432,31 @@ FontConfig FontConfig::fromFile(const QString &filePath)
|
|||
value = value.mid(1, value.size() - 2).trimmed();
|
||||
}
|
||||
|
||||
// CardText size bounds: bare integers, e.g. CardTextMinSize = 2.
|
||||
// Tolerate quoting like the family entries; skip invalid lines.
|
||||
QString sizeKey = key;
|
||||
if (sizeKey.startsWith("AppFont::")) {
|
||||
sizeKey = sizeKey.mid(QStringLiteral("AppFont::").size());
|
||||
}
|
||||
if (sizeKey.compare("CardTextMinSize", Qt::CaseInsensitive) == 0) {
|
||||
bool ok = false;
|
||||
int minSize = value.trimmed().toInt(&ok);
|
||||
if (!ok || minSize <= 0) {
|
||||
continue;
|
||||
}
|
||||
cfg.cardTextMinSize = minSize;
|
||||
continue;
|
||||
}
|
||||
if (sizeKey.compare("CardTextMaxSize", Qt::CaseInsensitive) == 0) {
|
||||
bool ok = false;
|
||||
int maxSize = value.trimmed().toInt(&ok);
|
||||
if (!ok || maxSize <= 0) {
|
||||
continue;
|
||||
}
|
||||
cfg.cardTextMaxSize = maxSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
int comma = value.lastIndexOf(',');
|
||||
|
||||
if (comma < 0) {
|
||||
|
|
|
|||
|
|
@ -128,15 +128,41 @@ struct PaletteConfig
|
|||
* @code
|
||||
* [Fonts]
|
||||
* InterfaceDefault = "Inter, 10"
|
||||
* CardText = "Noto Serif, 12"
|
||||
* CardTextMinSize = 1
|
||||
* @endcode
|
||||
* CardText stores family + MAX size; cardTextMinSize stores the overlay
|
||||
* shrink-to-fit floor (omitted from TOML when 1).
|
||||
*/
|
||||
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 = 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;
|
||||
return fonts == rhs.fonts && cardTextMinSize == rhs.cardTextMinSize && cardTextMaxSize == rhs.cardTextMaxSize;
|
||||
}
|
||||
bool operator!=(const FontConfig &rhs) const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,6 +53,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;
|
||||
|
|
@ -236,7 +240,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 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) {
|
||||
|
|
@ -402,6 +414,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 a shipped Dark scheme there would override the OS preference on
|
||||
// a fresh profile.
|
||||
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()) {
|
||||
|
|
@ -528,7 +594,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);
|
||||
}
|
||||
|
||||
|
|
@ -732,6 +801,7 @@ QFont ThemeManager::cardTextFont() const
|
|||
}
|
||||
QFont serifFont(QStringLiteral("Serif"));
|
||||
serifFont.setStyleHint(QFont::Serif);
|
||||
serifFont.setPointSize(12);
|
||||
return serifFont;
|
||||
}
|
||||
|
||||
|
|
@ -753,37 +823,44 @@ QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const
|
|||
return defaultFont;
|
||||
}
|
||||
|
||||
int ThemeManager::cardTextMaxSize() const
|
||||
{
|
||||
// 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
|
||||
{
|
||||
return currentFonts.resolvedCardTextMinSize();
|
||||
}
|
||||
|
||||
void ThemeManager::themeChangedSlot()
|
||||
{
|
||||
QString themeName = SettingsCache::instance().getThemeName();
|
||||
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);
|
||||
|
|
@ -826,7 +903,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);
|
||||
|
|
@ -170,6 +186,12 @@ 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 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 resolved the same way. */
|
||||
int cardTextMinSize() const;
|
||||
|
||||
QBrush &getBgBrush(Role zone);
|
||||
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ void CardInfoPictureWithTextOverlayWidget::setOutlineColor(const QColor &color)
|
|||
void CardInfoPictureWithTextOverlayWidget::setFontSize(const int size)
|
||||
{
|
||||
fontSize = size > 0 ? size : 1;
|
||||
hasExplicitFontSize = true;
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
@ -179,10 +180,12 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
|||
}
|
||||
|
||||
// Calculate the optimal font size. The family comes from the CardText
|
||||
// theme role; the fontSize member stays the starting size and the
|
||||
// shrink-to-fit logic below is unchanged.
|
||||
// theme role; the start size is the explicit size when set (e.g. deck
|
||||
// preview 24), else the theme max, shrinking down to the theme min floor.
|
||||
QFont font = themeManager->appFont(AppFont::CardText);
|
||||
int optimalFontSize = fontSize; // Start with the user-defined font size
|
||||
const int themeMin = std::max(1, themeManager->cardTextMinSize());
|
||||
const int themeMax = std::max(themeMin, std::max(1, themeManager->cardTextMaxSize()));
|
||||
int optimalFontSize = hasExplicitFontSize ? std::max(1, fontSize) : themeMax;
|
||||
const QFontMetrics baseMetrics(font);
|
||||
int textWidth = pixmapRect.width();
|
||||
|
||||
|
|
@ -200,7 +203,7 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
|||
}
|
||||
|
||||
--optimalFontSize;
|
||||
} while (optimalFontSize > 1);
|
||||
} while (optimalFontSize > themeMin);
|
||||
|
||||
// Apply the calculated font size
|
||||
painter.setFont(font);
|
||||
|
|
@ -227,7 +230,7 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
|||
int totalTextHeight = wrappedText.count('\n') * fontMetrics.height() + fontMetrics.height();
|
||||
|
||||
// Adjust font size if the total text height exceeds the pixmap height
|
||||
while (totalTextHeight > pixmapRect.height() && optimalFontSize > 1) {
|
||||
while (totalTextHeight > pixmapRect.height() && optimalFontSize > themeMin) {
|
||||
--optimalFontSize;
|
||||
font.setPointSize(optimalFontSize);
|
||||
painter.setFont(font);
|
||||
|
|
@ -297,7 +300,9 @@ QSize CardInfoPictureWithTextOverlayWidget::minimumSizeHint() const
|
|||
|
||||
// Get the font metrics for the overlay text
|
||||
QFont font = themeManager->appFont(AppFont::CardText);
|
||||
font.setPointSize(fontSize);
|
||||
const int themeMin = std::max(1, themeManager->cardTextMinSize());
|
||||
const int themeMax = std::max(themeMin, std::max(1, themeManager->cardTextMaxSize()));
|
||||
font.setPointSize(hasExplicitFontSize ? std::max(1, fontSize) : themeMax);
|
||||
const QFontMetrics fontMetrics(font);
|
||||
|
||||
// Calculate the height required for the text
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ private:
|
|||
QColor textColor;
|
||||
QColor outlineColor;
|
||||
int fontSize;
|
||||
bool hasExplicitFontSize = false;
|
||||
Qt::Alignment textAlignment;
|
||||
bool highlighted;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -284,20 +284,11 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(),
|
||||
&CardsDisplaySettings::setRoundCardCorners);
|
||||
|
||||
connect(&maxFontSizeForCardsEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.appearance(),
|
||||
&AppearanceSettings::setMaxFontSize);
|
||||
maxFontSizeForCardsEdit.setValue(settings.appearance().getMaxFontSize());
|
||||
maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit);
|
||||
maxFontSizeForCardsEdit.setMinimum(9);
|
||||
maxFontSizeForCardsEdit.setMaximum(100);
|
||||
|
||||
auto *cardsGrid = new QGridLayout;
|
||||
cardsGrid->addWidget(&displayCardNamesCheckBox, 0, 0, 1, 2);
|
||||
cardsGrid->addWidget(&autoRotateSidewaysLayoutCardsCheckBox, 1, 0, 1, 2);
|
||||
cardsGrid->addWidget(&cardScalingCheckBox, 2, 0, 1, 2);
|
||||
cardsGrid->addWidget(&roundCardCornersCheckBox, 3, 0, 1, 2);
|
||||
cardsGrid->addWidget(&maxFontSizeForCardsLabel, 4, 0, 1, 1);
|
||||
cardsGrid->addWidget(&maxFontSizeForCardsEdit, 4, 1, 1, 1);
|
||||
|
||||
cardsGroupBox = new QGroupBox;
|
||||
cardsGroupBox->setLayout(cardsGrid);
|
||||
|
|
@ -658,7 +649,6 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
autoRotateSidewaysLayoutCardsCheckBox.setText(tr("Auto-Rotate cards with sideways layout"));
|
||||
cardScalingCheckBox.setText(tr("Scale cards on mouse over"));
|
||||
roundCardCornersCheckBox.setText(tr("Use rounded card corners"));
|
||||
maxFontSizeForCardsLabel.setText(tr("Maximum font size for information displayed on cards:"));
|
||||
|
||||
cardLayoutGroupBox->setTitle(tr("Card layout"));
|
||||
verticalCardOverlapPercentLabel.setText(
|
||||
|
|
|
|||
|
|
@ -71,8 +71,6 @@ private:
|
|||
QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
|
||||
QCheckBox cardScalingCheckBox;
|
||||
QCheckBox roundCardCornersCheckBox;
|
||||
QLabel maxFontSizeForCardsLabel;
|
||||
QSpinBox maxFontSizeForCardsEdit;
|
||||
|
||||
QLabel verticalCardOverlapPercentLabel;
|
||||
QSpinBox verticalCardOverlapPercentBox;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[Appearance]
|
||||
ColorScheme = Dark
|
||||
ColorScheme = System
|
||||
|
||||
[Style]
|
||||
Name = Fusion
|
||||
|
|
|
|||
|
|
@ -33,16 +33,6 @@ void AppearanceSettings::setStyleUserList(bool _styleUserList)
|
|||
emit styleUserListChanged();
|
||||
}
|
||||
|
||||
int AppearanceSettings::getMaxFontSize() const
|
||||
{
|
||||
return getValue("maxFontSize", QString(), QString(), 12).toInt();
|
||||
}
|
||||
|
||||
void AppearanceSettings::setMaxFontSize(int _max)
|
||||
{
|
||||
setValue(_max, "maxFontSize");
|
||||
}
|
||||
|
||||
QString AppearanceSettings::getHomeTabBackgroundSource() const
|
||||
{
|
||||
return getValue("homeTabBackgroundSource", QString(), QString(), "themed").toString();
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ public:
|
|||
void setThemeName(const QString &_themeName);
|
||||
[[nodiscard]] bool getStyleUserList() const;
|
||||
void setStyleUserList(bool _styleUserList);
|
||||
[[nodiscard]] int getMaxFontSize() const;
|
||||
void setMaxFontSize(int _max);
|
||||
[[nodiscard]] QString getHomeTabBackgroundSource() const;
|
||||
void setHomeTabBackgroundSource(const QString &_backgroundSource);
|
||||
[[nodiscard]] int getHomeTabBackgroundShuffleFrequency() const;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include <QSharedPointer>
|
||||
#include <QStringList>
|
||||
#include <QStringLiteral>
|
||||
#include <QTextStream>
|
||||
#include <QVariant>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
|
@ -395,6 +396,10 @@ static void migrateAppearanceSettings(const QString &settingsPath, QSettings &gl
|
|||
{
|
||||
const QMap<QString, QString> appearanceKeyMap = {
|
||||
{"theme/name", "appearance/themeName"},
|
||||
// Retired into the theme's CardText size, but still read here so users
|
||||
// who have not migrated past global.ini keep their card text size:
|
||||
// SettingsCache runs migrateSettingsFromGlobalIni() before
|
||||
// migrateAppearanceMaxFontSizeToTheme().
|
||||
{"game/maxfontsize", "appearance/maxFontSize"},
|
||||
{"home/background", "appearance/homeTabBackgroundSource"},
|
||||
{"home/background/shuffleTimer", "appearance/homeTabBackgroundShuffleFrequency"},
|
||||
|
|
@ -424,6 +429,204 @@ static void migrateAppearanceSettings(const QString &settingsPath, QSettings &gl
|
|||
}
|
||||
}
|
||||
|
||||
static QString themesPathForSettings(const QString &settingsPath)
|
||||
{
|
||||
QSettings pathsIni(settingsPath + "paths.ini", QSettings::IniFormat);
|
||||
const QString configured = pathsIni.value("paths/themes").toString().trimmed();
|
||||
if (!configured.isEmpty()) {
|
||||
return configured;
|
||||
}
|
||||
// Default layout: <data>/themes/ sibling of <data>/settings/.
|
||||
return QDir::cleanPath(QDir(settingsPath).absoluteFilePath("../themes"));
|
||||
}
|
||||
|
||||
static bool fontsTomlHasExplicitCardTextMax(const QString &filePath)
|
||||
{
|
||||
QFile f(filePath);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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();
|
||||
for (int i = 1; i < value.size(); ++i) {
|
||||
if ((value[i] == '#' || value[i] == ';') && value[i - 1].isSpace()) {
|
||||
value = value.left(i).trimmed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (value.size() >= 2 &&
|
||||
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\'')))) {
|
||||
value = value.mid(1, value.size() - 2).trimmed();
|
||||
}
|
||||
if (key.startsWith("AppFont::")) {
|
||||
key = key.mid(QStringLiteral("AppFont::").size());
|
||||
}
|
||||
if (key.compare("CardTextMaxSize", Qt::CaseInsensitive) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (key.compare("CardText", Qt::CaseInsensitive) != 0) {
|
||||
continue;
|
||||
}
|
||||
int comma = value.lastIndexOf(',');
|
||||
if (comma < 0) {
|
||||
continue;
|
||||
}
|
||||
QString family = value.left(comma).trimmed();
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool seedCardTextMaxIntoThemeDir(const QString &themeDirPath, int maxSize)
|
||||
{
|
||||
if (themeDirPath.isEmpty() || maxSize <= 0) {
|
||||
return false;
|
||||
}
|
||||
QDir dir(themeDirPath);
|
||||
if (!dir.exists()) {
|
||||
return false;
|
||||
}
|
||||
const QString filePath = dir.absoluteFilePath("fonts.toml");
|
||||
if (fontsTomlHasExplicitCardTextMax(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString existing;
|
||||
if (QFile::exists(filePath)) {
|
||||
QFile in(filePath);
|
||||
if (!in.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
existing = QString::fromUtf8(in.readAll());
|
||||
}
|
||||
|
||||
// Size-only key: seeding the CardText family here would pin it to Serif
|
||||
// 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").arg(QStringLiteral("CardTextMaxSize"), -20).arg(maxSize);
|
||||
|
||||
QString out;
|
||||
if (existing.isEmpty()) {
|
||||
out = "[Fonts]\n" + entry + "\n";
|
||||
} else {
|
||||
out = existing;
|
||||
if (!out.endsWith('\n')) {
|
||||
out += '\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);
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
|
||||
return false;
|
||||
}
|
||||
QTextStream stream(&f);
|
||||
stream << out;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool appearanceMaxFontSizeMigrationPending(const QString &settingsPath)
|
||||
{
|
||||
if (!QFile::exists(settingsPath + "appearance.ini")) {
|
||||
return false;
|
||||
}
|
||||
QSettings appearanceIni(settingsPath + "appearance.ini", QSettings::IniFormat);
|
||||
return appearanceIni.contains("appearance/maxFontSize");
|
||||
}
|
||||
|
||||
bool SettingsMigration::migrateAppearanceMaxFontSizeToTheme(const QString &settingsPath)
|
||||
{
|
||||
if (!appearanceMaxFontSizeMigrationPending(settingsPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSettings appearanceIni(settingsPath + "appearance.ini", QSettings::IniFormat);
|
||||
// Retired default was 12 in AppearanceSettings::getMaxFontSize; only
|
||||
// non-default values are seeded, default users rely on the theme fallback.
|
||||
const int legacy = appearanceIni.value("appearance/maxFontSize").toInt();
|
||||
if (legacy != 12 && legacy > 0) {
|
||||
const QString themesPath = themesPathForSettings(settingsPath);
|
||||
QDir themesDir(themesPath);
|
||||
const QStringList themes = themesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||
for (const QString &theme : themes) {
|
||||
seedCardTextMaxIntoThemeDir(themesDir.absoluteFilePath(theme), legacy);
|
||||
}
|
||||
|
||||
// Shipped themes (Fabric, Leather, ...) resolve to the system theme
|
||||
// directory unless the user has already saved something for them, and
|
||||
// System/Fusion only ever exist in the user directory. Seeding existing
|
||||
// folders alone would therefore drop the retired size for every user on
|
||||
// a stock theme, so create the active theme's user directory the same
|
||||
// way ThemeManager::writableThemeDir() does for palette and font saves.
|
||||
const QString activeTheme = appearanceIni.value("appearance/themeName").toString().trimmed();
|
||||
if (!activeTheme.isEmpty() && !themesDir.exists(activeTheme) && themesDir.mkpath(activeTheme)) {
|
||||
seedCardTextMaxIntoThemeDir(themesDir.absoluteFilePath(activeTheme), legacy);
|
||||
}
|
||||
}
|
||||
|
||||
appearanceIni.remove("appearance/maxFontSize");
|
||||
appearanceIni.sync();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void migrateNetworkSettings(const QString &settingsPath, QSettings &globalIni)
|
||||
{
|
||||
const QMap<QString, QString> networkKeyMap = {
|
||||
|
|
@ -821,6 +1024,10 @@ bool SettingsMigration::isMigrationPending(const QString &settingsPath)
|
|||
return true;
|
||||
}
|
||||
|
||||
if (appearanceMaxFontSizeMigrationPending(settingsPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
||||
if (personalIni.value("migration/legacy_complete", false).toBool()) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ public:
|
|||
*/
|
||||
static bool migrateGameFiltersToCamelCase(const QString &settingsPath);
|
||||
|
||||
/**
|
||||
* Moves a non-default appearance/maxFontSize value into each user-writable theme
|
||||
* directory lacking an explicit CardText max (creating/merging fonts.toml, never
|
||||
* overwriting an explicit max), then deletes the legacy key. Default (12) or absent
|
||||
* values only delete the key and create no files, since the ThemeManager fallback
|
||||
* already covers them. Idempotent, safe to run on every startup.
|
||||
*/
|
||||
static bool migrateAppearanceMaxFontSizeToTheme(const QString &settingsPath);
|
||||
|
||||
/**
|
||||
* Returns true when at least one of the two migrations would still do work, i.e. when a
|
||||
* backup of the settings directory is worth taking before they run.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue