From e87cd80c9b1b6a369074b6606d35916183831e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 16 Oct 2026 11:27:31 +0200 Subject: [PATCH] [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. --- .../src/client/settings/cache_settings.cpp | 3 + .../board/abstract_card_item.cpp | 5 +- .../palette_editor/font_grid_widget.cpp | 74 ++++++- .../palette_editor/font_grid_widget.h | 1 + cockatrice/src/interface/theme_config.cpp | 35 +++- cockatrice/src/interface/theme_config.h | 10 +- cockatrice/src/interface/theme_manager.cpp | 24 +++ cockatrice/src/interface/theme_manager.h | 5 + ..._info_picture_with_text_overlay_widget.cpp | 17 +- ...rd_info_picture_with_text_overlay_widget.h | 1 + .../appearance_settings_page.cpp | 10 - .../settings_page/appearance_settings_page.h | 2 - .../settings/appearance_settings.cpp | 10 - .../settings/appearance_settings.h | 2 - .../settings/settings_migration.cpp | 184 +++++++++++++++++ .../settings/settings_migration.h | 9 + tests/font_config_test.cpp | 149 ++++++++++++++ tests/settings/settings_defaults_test.cpp | 13 -- tests/settings/settings_migration_test.cpp | 192 ++++++++++++++++++ 19 files changed, 697 insertions(+), 49 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 08eccf1e4..e1acb67da 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -137,6 +137,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); diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 84e062cd7..a688cafcf 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -35,7 +35,6 @@ #include #include #include -#include #include #include #include @@ -144,8 +143,8 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) 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()); diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.cpp b/cockatrice/src/interface/palette_editor/font_grid_widget.cpp index 44ad59551..c4b81de86 100644 --- a/cockatrice/src/interface/palette_editor/font_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/font_grid_widget.cpp @@ -13,6 +13,8 @@ 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 APP_FONT_DESCRIPTIONS = { @@ -46,6 +48,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(); for (int i = 0; i < fontEnum.keyCount(); ++i) { @@ -58,6 +61,30 @@ 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(CARD_TEXT_MIN_SIZE, 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); + 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); }); + 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); grid->addWidget(sizeSpin, i, 2); @@ -65,7 +92,7 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent) label->setBuddy(familyCombo); - fontRows[role] = {label, familyCombo, sizeSpin}; + fontRows[role] = {label, familyCombo, sizeSpin, nullptr}; } retranslateUi(); @@ -89,6 +116,37 @@ 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(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; + } + + // 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); + continue; + } + int size = font.pointSize(); if (size <= 0) { size = qApp->font().pointSize(); @@ -114,6 +172,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 +192,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")); diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.h b/cockatrice/src/interface/palette_editor/font_grid_widget.h index 8c80306d3..07ccfa868 100644 --- a/cockatrice/src/interface/palette_editor/font_grid_widget.h +++ b/cockatrice/src/interface/palette_editor/font_grid_widget.h @@ -35,6 +35,7 @@ private: QLabel *label = nullptr; QFontComboBox *familyCombo = nullptr; QSpinBox *sizeSpin = nullptr; + QSpinBox *minSpin = nullptr; // CardText only: shrink-to-fit floor }; QMap fontRows; diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index 54143d7d3..a75330db4 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -308,7 +308,7 @@ QPalette PaletteConfig::apply(QPalette base) const bool FontConfig::hasFonts() const { - return !fonts.isEmpty(); + return !fonts.isEmpty() || cardTextMinSize != 1 || cardTextMaxSize > 0; } QString FontConfig::toToml() const @@ -334,6 +334,14 @@ QString FontConfig::toToml() const entries += QString("%1 = \"%2, %3\"\n").arg(QString(roleName), -20).arg(family).arg(pointSize); } + if (cardTextMinSize != 1) { + 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 {}; } @@ -401,6 +409,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) { diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 84601a2bf..ebad7ef46 100644 --- a/cockatrice/src/interface/theme_config.h +++ b/cockatrice/src/interface/theme_config.h @@ -84,15 +84,23 @@ 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 { QMap fonts; + int cardTextMinSize = 1; + /// 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; 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 { diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index f120af8e4..e23284393 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -681,6 +681,7 @@ QFont ThemeManager::cardTextFont() const } QFont serifFont(QStringLiteral("Serif")); serifFont.setStyleHint(QFont::Serif); + serifFont.setPointSize(12); return serifFont; } @@ -702,6 +703,29 @@ QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const return defaultFont; } +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; +} + +int ThemeManager::cardTextMinSize() const +{ + if (currentFonts.cardTextMinSize > 0) { + return currentFonts.cardTextMinSize; + } + return 1; +} + void ThemeManager::themeChangedSlot() { QString themeName = SettingsCache::instance().getThemeName(); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index ffe1043b5..0cd44125e 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -169,6 +169,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. */ + int cardTextMaxSize() const; + /** @brief CardText shrink-to-fit floor: stored CardTextMinSize, else 1. */ + int cardTextMinSize() const; QBrush &getBgBrush(Role zone); QBrush getExtraBgBrush(Role zone, int zoneId = 0); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 56fe0bda4..4111a9cec 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -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 diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h index 41019f543..c4a967498 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h @@ -60,6 +60,7 @@ private: QColor textColor; QColor outlineColor; int fontSize; + bool hasExplicitFontSize = false; Qt::Alignment textAlignment; bool highlighted; }; diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index de5f67b32..d2214a5af 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -283,20 +283,11 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(), &CardsDisplaySettings::setRoundCardCorners); - connect(&maxFontSizeForCardsEdit, qOverload(&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); @@ -657,7 +648,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( diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index f919a2a2f..8d008dfeb 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -71,8 +71,6 @@ private: QCheckBox autoRotateSidewaysLayoutCardsCheckBox; QCheckBox cardScalingCheckBox; QCheckBox roundCardCornersCheckBox; - QLabel maxFontSizeForCardsLabel; - QSpinBox maxFontSizeForCardsEdit; QLabel verticalCardOverlapPercentLabel; QSpinBox verticalCardOverlapPercentBox; diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp index 14969dae6..4de87991c 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp @@ -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(); diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h index 9d15caddd..1e0823ae1 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h @@ -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; diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp index d9652cde3..3be3c0bf2 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -395,6 +396,10 @@ static void migrateAppearanceSettings(const QString &settingsPath, QSettings &gl { const QMap 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,181 @@ 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: /themes/ sibling of /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\n").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"; + } + + 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 networkKeyMap = { @@ -821,6 +1001,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; diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.h b/libcockatrice_settings/libcockatrice/settings/settings_migration.h index 0a706af98..7616cb83f 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.h +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.h @@ -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. diff --git a/tests/font_config_test.cpp b/tests/font_config_test.cpp index c8b1fb10d..93c9cefb6 100644 --- a/tests/font_config_test.cpp +++ b/tests/font_config_test.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace { @@ -186,6 +187,154 @@ 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, 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) diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index aea7f1d54..156e52695 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -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); diff --git a/tests/settings/settings_migration_test.cpp b/tests/settings/settings_migration_test.cpp index 2c4dcf7a9..4fea3c085 100644 --- a/tests/settings/settings_migration_test.cpp +++ b/tests/settings/settings_migration_test.cpp @@ -984,6 +984,198 @@ 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_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)