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).
|
// before that rename (idempotent, safe to run on every startup, portable builds included).
|
||||||
SettingsMigration::migrateGameFiltersToCamelCase(settingsPath);
|
SettingsMigration::migrateGameFiltersToCamelCase(settingsPath);
|
||||||
|
|
||||||
|
// Retire appearance/maxFontSize into per-theme CardText max (idempotent, safe every startup).
|
||||||
|
SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath);
|
||||||
|
|
||||||
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
|
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
|
||||||
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
|
||||||
serversSettings = new ServersSettings(settingsPath, this);
|
serversSettings = new ServersSettings(settingsPath, this);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <libcockatrice/settings/appearance_settings.h>
|
|
||||||
#include <libcockatrice/settings/cards_display_settings.h>
|
#include <libcockatrice/settings/cards_display_settings.h>
|
||||||
#include <libcockatrice/settings/debug_settings.h>
|
#include <libcockatrice/settings/debug_settings.h>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
|
|
@ -187,8 +186,8 @@ bool AbstractCardItem::shouldPaintNameOverPicture() const
|
||||||
|
|
||||||
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
|
||||||
{
|
{
|
||||||
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
|
const int minSize = std::max(1, themeManager->cardTextMinSize());
|
||||||
const int fontSize = std::max(9, MAX_FONT_SIZE);
|
const int fontSize = std::max(minSize, std::max(1, themeManager->cardTextMaxSize()));
|
||||||
|
|
||||||
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
||||||
static constexpr int MIN_FONT_SIZE = 6;
|
|
||||||
static constexpr int MAX_FONT_SIZE = 72;
|
|
||||||
static constexpr int FALLBACK_FONT_SIZE = 10;
|
static constexpr int FALLBACK_FONT_SIZE = 10;
|
||||||
|
|
||||||
static const QMap<AppFont::Role, const char *> APP_FONT_DESCRIPTIONS = {
|
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(0, 0);
|
||||||
grid->setColumnStretch(1, 1);
|
grid->setColumnStretch(1, 1);
|
||||||
grid->setColumnStretch(2, 0);
|
grid->setColumnStretch(2, 0);
|
||||||
|
grid->setColumnStretch(3, 0);
|
||||||
|
|
||||||
QMetaEnum fontEnum = QMetaEnum::fromType<AppFont::Role>();
|
QMetaEnum fontEnum = QMetaEnum::fromType<AppFont::Role>();
|
||||||
for (int i = 0; i < fontEnum.keyCount(); ++i) {
|
for (int i = 0; i < fontEnum.keyCount(); ++i) {
|
||||||
|
|
@ -58,14 +57,41 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
|
||||||
grid->addWidget(familyCombo, i, 1);
|
grid->addWidget(familyCombo, i, 1);
|
||||||
connect(familyCombo, &QFontComboBox::currentFontChanged, this, [this] { emit fontsChanged(); });
|
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);
|
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);
|
grid->addWidget(sizeSpin, i, 2);
|
||||||
connect(sizeSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
|
connect(sizeSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
|
||||||
|
|
||||||
label->setBuddy(familyCombo);
|
label->setBuddy(familyCombo);
|
||||||
|
|
||||||
fontRows[role] = {label, familyCombo, sizeSpin};
|
fontRows[role] = {label, familyCombo, sizeSpin, nullptr};
|
||||||
}
|
}
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
|
@ -89,6 +115,25 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
|
||||||
const QFont font = cfg.fonts.value(role, fallback);
|
const QFont font = cfg.fonts.value(role, fallback);
|
||||||
row.familyCombo->setCurrentFont(font);
|
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();
|
int size = font.pointSize();
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
size = qApp->font().pointSize();
|
size = qApp->font().pointSize();
|
||||||
|
|
@ -96,7 +141,7 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
size = FALLBACK_FONT_SIZE;
|
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);
|
blockSignals(false);
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +159,11 @@ FontConfig FontGridWidget::currentFontConfig() const
|
||||||
QFont font(row.familyCombo->currentFont().family());
|
QFont font(row.familyCombo->currentFont().family());
|
||||||
font.setPointSize(row.sizeSpin->value());
|
font.setPointSize(row.sizeSpin->value());
|
||||||
cfg.fonts[role] = font;
|
cfg.fonts[role] = font;
|
||||||
|
|
||||||
|
if (role == AppFont::CardText && row.minSpin != nullptr) {
|
||||||
|
cfg.cardTextMinSize = row.minSpin->value();
|
||||||
|
cfg.cardTextMaxSize = row.sizeSpin->value();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +179,15 @@ void FontGridWidget::retranslateUi()
|
||||||
row.label->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
|
row.label->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
|
||||||
row.familyCombo->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
|
row.familyCombo->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
|
||||||
row.familyCombo->setAccessibleName(roleDisplayName(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->setToolTip(tr("Point size for %1").arg(roleDisplayName(role)));
|
||||||
row.sizeSpin->setAccessibleName(tr("Point size for %1").arg(roleDisplayName(role)));
|
row.sizeSpin->setAccessibleName(tr("Point size for %1").arg(roleDisplayName(role)));
|
||||||
row.sizeSpin->setSuffix(tr(" pt"));
|
row.sizeSpin->setSuffix(tr(" pt"));
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ private:
|
||||||
QLabel *label = nullptr;
|
QLabel *label = nullptr;
|
||||||
QFontComboBox *familyCombo = nullptr;
|
QFontComboBox *familyCombo = nullptr;
|
||||||
QSpinBox *sizeSpin = nullptr;
|
QSpinBox *sizeSpin = nullptr;
|
||||||
|
QSpinBox *minSpin = nullptr; // CardText only: shrink-to-fit floor
|
||||||
};
|
};
|
||||||
|
|
||||||
QMap<AppFont::Role, FontRow> fontRows;
|
QMap<AppFont::Role, FontRow> fontRows;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
#include <algorithm>
|
||||||
#include <qnamespace.h>
|
#include <qnamespace.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
|
@ -309,7 +310,28 @@ QPalette PaletteConfig::apply(QPalette base) const
|
||||||
|
|
||||||
bool FontConfig::hasFonts() 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
|
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);
|
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()) {
|
if (entries.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -402,6 +432,31 @@ FontConfig FontConfig::fromFile(const QString &filePath)
|
||||||
value = value.mid(1, value.size() - 2).trimmed();
|
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(',');
|
int comma = value.lastIndexOf(',');
|
||||||
|
|
||||||
if (comma < 0) {
|
if (comma < 0) {
|
||||||
|
|
|
||||||
|
|
@ -128,15 +128,41 @@ struct PaletteConfig
|
||||||
* @code
|
* @code
|
||||||
* [Fonts]
|
* [Fonts]
|
||||||
* InterfaceDefault = "Inter, 10"
|
* InterfaceDefault = "Inter, 10"
|
||||||
|
* CardText = "Noto Serif, 12"
|
||||||
|
* CardTextMinSize = 1
|
||||||
* @endcode
|
* @endcode
|
||||||
|
* CardText stores family + MAX size; cardTextMinSize stores the overlay
|
||||||
|
* shrink-to-fit floor (omitted from TOML when 1).
|
||||||
*/
|
*/
|
||||||
struct FontConfig
|
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;
|
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
|
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
|
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 QColor STACKZONE_BG_DEFAULT = QColor(113, 43, 43);
|
||||||
static const QStringList DEFAULT_RESOURCE_PATHS = {":/resources"};
|
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
|
struct PaletteColorInfo
|
||||||
{
|
{
|
||||||
QPalette::ColorGroup group;
|
QPalette::ColorGroup group;
|
||||||
|
|
@ -236,7 +240,15 @@ void ThemeManager::ensureThemeDirectoryExists()
|
||||||
|
|
||||||
bool ThemeManager::isDarkMode(const QString &themeDirPath) const
|
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) {
|
if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) {
|
||||||
return true;
|
return true;
|
||||||
} else if (themeConfig.colorScheme.compare("Light", Qt::CaseInsensitive) == 0) {
|
} 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);
|
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)
|
PaletteConfig ThemeManager::loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme)
|
||||||
{
|
{
|
||||||
if (themeDirPath.isEmpty()) {
|
if (themeDirPath.isEmpty()) {
|
||||||
|
|
@ -528,7 +594,10 @@ void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &schem
|
||||||
{
|
{
|
||||||
const QString themeName = SettingsCache::instance().getThemeName();
|
const QString themeName = SettingsCache::instance().getThemeName();
|
||||||
const QString dirPath = getAvailableThemes().value(themeName);
|
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);
|
applyStyleAndPalette(themeName, themeCfg, cfg, scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -732,6 +801,7 @@ QFont ThemeManager::cardTextFont() const
|
||||||
}
|
}
|
||||||
QFont serifFont(QStringLiteral("Serif"));
|
QFont serifFont(QStringLiteral("Serif"));
|
||||||
serifFont.setStyleHint(QFont::Serif);
|
serifFont.setStyleHint(QFont::Serif);
|
||||||
|
serifFont.setPointSize(12);
|
||||||
return serifFont;
|
return serifFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -753,37 +823,44 @@ QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const
|
||||||
return defaultFont;
|
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()
|
void ThemeManager::themeChangedSlot()
|
||||||
{
|
{
|
||||||
QString themeName = SettingsCache::instance().getThemeName();
|
QString themeName = SettingsCache::instance().getThemeName();
|
||||||
QString dirPath = getAvailableThemes().value(themeName);
|
QString dirPath = getAvailableThemes().value(themeName);
|
||||||
currentThemePath = dirPath;
|
currentThemePath = dirPath;
|
||||||
QDir dir(dirPath);
|
QDir dir(dirPath);
|
||||||
|
const QString sysPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName);
|
||||||
|
|
||||||
// CSS — prefer the scheme-qualified stylesheet (style-dark.css /
|
// CSS — prefer the scheme-qualified stylesheet (style-dark.css /
|
||||||
// style-light.css) when present, else the plain style.css as fallback.
|
// style-light.css) when present, else the plain style.css as fallback.
|
||||||
if (!dirPath.isEmpty()) {
|
// A user theme directory that only carries seeded files falls back to the
|
||||||
const QString scheme = isDarkMode(dirPath) ? QStringLiteral("dark") : QStringLiteral("light");
|
// shipped stylesheet of the same theme.
|
||||||
const QString schemeCss = QFileInfo(QStringLiteral(STYLE_CSS_NAME)).completeBaseName() + QLatin1Char('-') +
|
const bool darkMode = isDarkMode(dirPath);
|
||||||
scheme + QStringLiteral(".css");
|
const QString styleSheetPath = themeStyleSheetPath(dirPath, sysPath, darkMode);
|
||||||
if (dir.exists(schemeCss)) {
|
qApp->setStyleSheet(styleSheetPath.isEmpty() ? QString() : "file:///" + styleSheetPath);
|
||||||
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("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// load theme.cfg for style + scheme preference
|
// load theme.cfg for style + scheme preference, falling back to the shipped
|
||||||
ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath);
|
// theme directory when the user copy does not carry one
|
||||||
|
ThemeConfig themeCfg = loadDefaultThemeConfig(dirPath, sysPath);
|
||||||
|
|
||||||
// Resolve active scheme:
|
// Resolve active scheme:
|
||||||
// theme.cfg says Dark/Light → use that
|
// theme.cfg says Dark/Light → use that
|
||||||
// theme.cfg says System or is absent → follow the OS
|
// 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 ────────────────────
|
// ── Load palette: custom first, then theme default ────────────────────
|
||||||
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
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
|
// include the system theme dir as a fallback so shipped assets like
|
||||||
// zones/*.png and style.css still resolve for themes that ship only
|
// zones/*.png and style.css still resolve for themes that ship only
|
||||||
// those files (e.g. Leather, Plasma, Fabric, VelvetMarble).
|
// those files (e.g. Leather, Plasma, Fabric, VelvetMarble).
|
||||||
const QString sysPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName);
|
|
||||||
if (sysPath != dirPath && QDir(sysPath).exists()) {
|
if (sysPath != dirPath && QDir(sysPath).exists()) {
|
||||||
resources << sysPath;
|
resources << sysPath;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,8 @@ public:
|
||||||
static bool isDirReallyWritable(const QString &dirPath);
|
static bool isDirReallyWritable(const QString &dirPath);
|
||||||
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
||||||
// (Dark/Light), falling back to the OS color scheme when it is "System".
|
// (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;
|
bool isDarkMode(const QString &themeDirPath) const;
|
||||||
// The resolved scheme of the currently active theme.
|
// The resolved scheme of the currently active theme.
|
||||||
bool isDarkModeActive() const
|
bool isDarkModeActive() const
|
||||||
|
|
@ -107,6 +109,20 @@ public:
|
||||||
// Load the global theme settings (style + color scheme preference)
|
// Load the global theme settings (style + color scheme preference)
|
||||||
static ThemeConfig loadGlobalConfig(const QString &themeDirPath);
|
static ThemeConfig loadGlobalConfig(const QString &themeDirPath);
|
||||||
static bool saveGlobalConfig(const QString &themeDirPath, const ThemeConfig &cfg);
|
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
|
// Load/save per-scheme palette colors
|
||||||
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
||||||
|
|
@ -170,6 +186,12 @@ public:
|
||||||
* "what this role resolves to" must use this, because appFont()
|
* "what this role resolves to" must use this, because appFont()
|
||||||
* reflects a live preview while the editor is open. */
|
* reflects a live preview while the editor is open. */
|
||||||
QFont defaultAppFont(AppFont::Role role) const;
|
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 &getBgBrush(Role zone);
|
||||||
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
|
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ void CardInfoPictureWithTextOverlayWidget::setOutlineColor(const QColor &color)
|
||||||
void CardInfoPictureWithTextOverlayWidget::setFontSize(const int size)
|
void CardInfoPictureWithTextOverlayWidget::setFontSize(const int size)
|
||||||
{
|
{
|
||||||
fontSize = size > 0 ? size : 1;
|
fontSize = size > 0 ? size : 1;
|
||||||
|
hasExplicitFontSize = true;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,10 +180,12 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the optimal font size. The family comes from the CardText
|
// Calculate the optimal font size. The family comes from the CardText
|
||||||
// theme role; the fontSize member stays the starting size and the
|
// theme role; the start size is the explicit size when set (e.g. deck
|
||||||
// shrink-to-fit logic below is unchanged.
|
// preview 24), else the theme max, shrinking down to the theme min floor.
|
||||||
QFont font = themeManager->appFont(AppFont::CardText);
|
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);
|
const QFontMetrics baseMetrics(font);
|
||||||
int textWidth = pixmapRect.width();
|
int textWidth = pixmapRect.width();
|
||||||
|
|
||||||
|
|
@ -200,7 +203,7 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
||||||
}
|
}
|
||||||
|
|
||||||
--optimalFontSize;
|
--optimalFontSize;
|
||||||
} while (optimalFontSize > 1);
|
} while (optimalFontSize > themeMin);
|
||||||
|
|
||||||
// Apply the calculated font size
|
// Apply the calculated font size
|
||||||
painter.setFont(font);
|
painter.setFont(font);
|
||||||
|
|
@ -227,7 +230,7 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
||||||
int totalTextHeight = wrappedText.count('\n') * fontMetrics.height() + fontMetrics.height();
|
int totalTextHeight = wrappedText.count('\n') * fontMetrics.height() + fontMetrics.height();
|
||||||
|
|
||||||
// Adjust font size if the total text height exceeds the pixmap 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;
|
--optimalFontSize;
|
||||||
font.setPointSize(optimalFontSize);
|
font.setPointSize(optimalFontSize);
|
||||||
painter.setFont(font);
|
painter.setFont(font);
|
||||||
|
|
@ -297,7 +300,9 @@ QSize CardInfoPictureWithTextOverlayWidget::minimumSizeHint() const
|
||||||
|
|
||||||
// Get the font metrics for the overlay text
|
// Get the font metrics for the overlay text
|
||||||
QFont font = themeManager->appFont(AppFont::CardText);
|
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);
|
const QFontMetrics fontMetrics(font);
|
||||||
|
|
||||||
// Calculate the height required for the text
|
// Calculate the height required for the text
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ private:
|
||||||
QColor textColor;
|
QColor textColor;
|
||||||
QColor outlineColor;
|
QColor outlineColor;
|
||||||
int fontSize;
|
int fontSize;
|
||||||
|
bool hasExplicitFontSize = false;
|
||||||
Qt::Alignment textAlignment;
|
Qt::Alignment textAlignment;
|
||||||
bool highlighted;
|
bool highlighted;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -284,20 +284,11 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(),
|
connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(),
|
||||||
&CardsDisplaySettings::setRoundCardCorners);
|
&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;
|
auto *cardsGrid = new QGridLayout;
|
||||||
cardsGrid->addWidget(&displayCardNamesCheckBox, 0, 0, 1, 2);
|
cardsGrid->addWidget(&displayCardNamesCheckBox, 0, 0, 1, 2);
|
||||||
cardsGrid->addWidget(&autoRotateSidewaysLayoutCardsCheckBox, 1, 0, 1, 2);
|
cardsGrid->addWidget(&autoRotateSidewaysLayoutCardsCheckBox, 1, 0, 1, 2);
|
||||||
cardsGrid->addWidget(&cardScalingCheckBox, 2, 0, 1, 2);
|
cardsGrid->addWidget(&cardScalingCheckBox, 2, 0, 1, 2);
|
||||||
cardsGrid->addWidget(&roundCardCornersCheckBox, 3, 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 = new QGroupBox;
|
||||||
cardsGroupBox->setLayout(cardsGrid);
|
cardsGroupBox->setLayout(cardsGrid);
|
||||||
|
|
@ -658,7 +649,6 @@ void AppearanceSettingsPage::retranslateUi()
|
||||||
autoRotateSidewaysLayoutCardsCheckBox.setText(tr("Auto-Rotate cards with sideways layout"));
|
autoRotateSidewaysLayoutCardsCheckBox.setText(tr("Auto-Rotate cards with sideways layout"));
|
||||||
cardScalingCheckBox.setText(tr("Scale cards on mouse over"));
|
cardScalingCheckBox.setText(tr("Scale cards on mouse over"));
|
||||||
roundCardCornersCheckBox.setText(tr("Use rounded card corners"));
|
roundCardCornersCheckBox.setText(tr("Use rounded card corners"));
|
||||||
maxFontSizeForCardsLabel.setText(tr("Maximum font size for information displayed on cards:"));
|
|
||||||
|
|
||||||
cardLayoutGroupBox->setTitle(tr("Card layout"));
|
cardLayoutGroupBox->setTitle(tr("Card layout"));
|
||||||
verticalCardOverlapPercentLabel.setText(
|
verticalCardOverlapPercentLabel.setText(
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,6 @@ private:
|
||||||
QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
|
QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
|
||||||
QCheckBox cardScalingCheckBox;
|
QCheckBox cardScalingCheckBox;
|
||||||
QCheckBox roundCardCornersCheckBox;
|
QCheckBox roundCardCornersCheckBox;
|
||||||
QLabel maxFontSizeForCardsLabel;
|
|
||||||
QSpinBox maxFontSizeForCardsEdit;
|
|
||||||
|
|
||||||
QLabel verticalCardOverlapPercentLabel;
|
QLabel verticalCardOverlapPercentLabel;
|
||||||
QSpinBox verticalCardOverlapPercentBox;
|
QSpinBox verticalCardOverlapPercentBox;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[Appearance]
|
[Appearance]
|
||||||
ColorScheme = Dark
|
ColorScheme = System
|
||||||
|
|
||||||
[Style]
|
[Style]
|
||||||
Name = Fusion
|
Name = Fusion
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,6 @@ void AppearanceSettings::setStyleUserList(bool _styleUserList)
|
||||||
emit styleUserListChanged();
|
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
|
QString AppearanceSettings::getHomeTabBackgroundSource() const
|
||||||
{
|
{
|
||||||
return getValue("homeTabBackgroundSource", QString(), QString(), "themed").toString();
|
return getValue("homeTabBackgroundSource", QString(), QString(), "themed").toString();
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ public:
|
||||||
void setThemeName(const QString &_themeName);
|
void setThemeName(const QString &_themeName);
|
||||||
[[nodiscard]] bool getStyleUserList() const;
|
[[nodiscard]] bool getStyleUserList() const;
|
||||||
void setStyleUserList(bool _styleUserList);
|
void setStyleUserList(bool _styleUserList);
|
||||||
[[nodiscard]] int getMaxFontSize() const;
|
|
||||||
void setMaxFontSize(int _max);
|
|
||||||
[[nodiscard]] QString getHomeTabBackgroundSource() const;
|
[[nodiscard]] QString getHomeTabBackgroundSource() const;
|
||||||
void setHomeTabBackgroundSource(const QString &_backgroundSource);
|
void setHomeTabBackgroundSource(const QString &_backgroundSource);
|
||||||
[[nodiscard]] int getHomeTabBackgroundShuffleFrequency() const;
|
[[nodiscard]] int getHomeTabBackgroundShuffleFrequency() const;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
|
#include <QTextStream>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -395,6 +396,10 @@ static void migrateAppearanceSettings(const QString &settingsPath, QSettings &gl
|
||||||
{
|
{
|
||||||
const QMap<QString, QString> appearanceKeyMap = {
|
const QMap<QString, QString> appearanceKeyMap = {
|
||||||
{"theme/name", "appearance/themeName"},
|
{"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"},
|
{"game/maxfontsize", "appearance/maxFontSize"},
|
||||||
{"home/background", "appearance/homeTabBackgroundSource"},
|
{"home/background", "appearance/homeTabBackgroundSource"},
|
||||||
{"home/background/shuffleTimer", "appearance/homeTabBackgroundShuffleFrequency"},
|
{"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)
|
static void migrateNetworkSettings(const QString &settingsPath, QSettings &globalIni)
|
||||||
{
|
{
|
||||||
const QMap<QString, QString> networkKeyMap = {
|
const QMap<QString, QString> networkKeyMap = {
|
||||||
|
|
@ -821,6 +1024,10 @@ bool SettingsMigration::isMigrationPending(const QString &settingsPath)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appearanceMaxFontSizeMigrationPending(settingsPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat);
|
||||||
if (personalIni.value("migration/legacy_complete", false).toBool()) {
|
if (personalIni.value("migration/legacy_complete", false).toBool()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,15 @@ public:
|
||||||
*/
|
*/
|
||||||
static bool migrateGameFiltersToCamelCase(const QString &settingsPath);
|
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
|
* 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.
|
* backup of the settings directory is worth taking before they run.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
@ -186,6 +187,190 @@ TEST(FontConfigTest, InequalityComparesRoles)
|
||||||
EXPECT_NE(left, right);
|
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
|
// 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.
|
// the fallback then links the bare gtest library without gtest_main.
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
|
||||||
|
|
@ -461,19 +461,6 @@ TEST_F(SettingsDefaultsTest, Appearance_StyleUserList_Default)
|
||||||
ASSERT_EQ(s.getStyleUserList(), true);
|
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)
|
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundSource_Default)
|
||||||
{
|
{
|
||||||
AppearanceSettings s(settingsPath, nullptr);
|
AppearanceSettings s(settingsPath, nullptr);
|
||||||
|
|
|
||||||
|
|
@ -984,6 +984,246 @@ TEST_F(SettingsMigrationTest, IsNotMigrationPendingWithoutLegacyData)
|
||||||
ASSERT_FALSE(SettingsMigration::isMigrationPending(settingsPath));
|
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
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,19 @@ class ThemeMigrationTest : public ::testing::Test
|
||||||
{
|
{
|
||||||
protected:
|
protected:
|
||||||
QTemporaryDir tmp;
|
QTemporaryDir tmp;
|
||||||
|
// Base of the per-user theme tree (<tmp>/themes).
|
||||||
QString themesPath;
|
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
|
void SetUp() override
|
||||||
{
|
{
|
||||||
ASSERT_TRUE(tmp.isValid());
|
ASSERT_TRUE(tmp.isValid());
|
||||||
themesPath = QDir(tmp.path()).absoluteFilePath("themes");
|
themesPath = QDir(tmp.path()).absoluteFilePath("themes");
|
||||||
|
systemThemesPath = QDir(tmp.path()).absoluteFilePath("system");
|
||||||
ASSERT_TRUE(QDir().mkpath(themesPath));
|
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
|
// 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.
|
// the fallback then links the bare gtest library without gtest_main.
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue