Compare commits

..

13 commits

Author SHA1 Message Date
Lukas Brübach
e50b928f8b [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.
2026-11-02 10:36:54 +01:00
Lukas Brübach
b01fce8037 [Fonts] Polish font editor accessibility and note text 2026-11-02 10:35:11 +01:00
Lukas Brübach
7ba95f6c97 [Fonts] Keep the DeckList fallback on the application font
DeckList is read by the deck editor headers, the filter tree and the deck
view too, not only by the two surfaces that hardcoded Serif. Resolving the
unset role to Serif in appFont() made every user without a fonts.toml see
Serif outside the deck list.

- appFont(DeckList) falls back to the application font again.
- ThemeManager::deckListFont() resolves the role and keeps the Serif
  fallback for the two historical sites: the deck view zone headers and
  the printed deck list.
- DeckView no longer repaints its viewport on fontsChanged; touching the
  scene items already schedules the repaint in every attached view.
2026-11-02 10:33:51 +01:00
Lukas Brübach
a7ab30c071 [Fonts] Restore DeckList Serif default and live-update all role readers 2026-11-02 10:30:26 +01:00
Lukas Brübach
42895e92a3 [Fonts] Fix font preview leaks and reuse default-font loader 2026-11-02 10:29:11 +01:00
Lukas Brübach
e834339c71 [Fonts] Fix pixel sizes and game overlay fonts from review
- Set the sizes the stylesheets carried with setPixelSize() rather than
  setPointSize(): home buttons (34), banner (24), home group box (20),
  the placeholder labels (14), the analytics drag handle (10), the EDHREC
  navigation buttons (24), the user info popup (11/14/11/10/11) and the
  game overlays (14/16/12). Points at 96 DPI were ~33% larger.
- The drag/total counters and tally columns keep the monospace family the
  stylesheets set, so digit widths stop changing as counts tick and
  white-space: pre stays aligned.
- applyDeckListFont() drops the cached tally rows before re-measuring, so
  previewing a larger DeckList font no longer leaves the tally clipped.
- Tally labels and the constructor/fontsChanged pairs in the shared-deck
  preview, quick setup, analytics panel, home box and public decks build
  their font once in a local lambda instead of twice.
2026-11-02 10:28:01 +01:00
Lukas Brübach
70f0f14665 [Fonts] Drop unused lambda captures in font-refresh handlers 2026-11-02 10:28:01 +01:00
Lukas Brübach
e23efebd71 [Fonts] Migrate DeckList and InterfaceDefault surfaces to theme roles 2026-11-02 10:28:01 +01:00
Lukas Brübach
7b3d43c4ea [Fonts] Keep CardText call sites on the app font and fix px sizes
- CardText's unset fallback is qApp->font() again: only the three counter
  painters (paintNumberEllipse, GeneralCounter, PlayerCounter) were Serif
  before the role existed, and they now ask for it through
  ThemeManager::cardTextFont(). Card names, player names, counters, the
  overlay, the Archidekt and EDHREC labels and the completer delegate keep the
  app font they used before.
- Use setPixelSize for the EDHREC card name (20px) and commander name (28px)
  and details (16px) sizes: the stylesheets said px, and setPointSize made
  them about a third larger.
2026-11-02 10:27:43 +01:00
Lukas Brübach
0ff2c4e246 [Fonts] Migrate CardText surfaces to theme font role 2026-11-02 10:27:15 +01:00
Lukas Brübach
bbf055c483 [Fonts] Keep ChatView on the app font and share the report log font
- ChatLog's unset fallback is qApp->font() again: ChatView never set a font
  before the role existed, so an unset role must not switch chat and the game
  log to monospace at the system GeneralFont size.
- Move the monospace fallback the report dialogs used before into
  report_utils::reportLogFont(), which returns the theme's ChatLog role when it
  is set and the old monospace otherwise. tab_report, dlg_my_reports and
  dlg_report_user now share it, so they cannot drift and they honour a themed
  size instead of overriding it.
2026-11-02 10:26:15 +01:00
Lukas Brübach
eeb5cfc333 [Fonts] Migrate ChatLog surfaces to theme font role 2026-11-02 10:26:15 +01:00
Lukas Brübach
7472f69f01 [Fonts] Make the editor's fallback and Reset preview-independent
- Add ThemeManager::defaultAppFont(), which resolves an unset role against the
  startup font instead of currentFonts, and use it in FontGridWidget::loadFonts
  so the grid never shows the live preview as the theme's default.
- Reload the live theme in PaletteEditorDialog::onReset before re-reading the
  grid, so Reset restores the saved fonts before loadFonts resolves fallbacks.
2026-11-02 10:26:05 +01:00
12 changed files with 169 additions and 53 deletions

View file

@ -117,11 +117,10 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
tallyLayout->setSpacing(2);
tallyContainer->hide();
applyDeckListFont();
connect(themeManager, &ThemeManager::fontsChanged, this, &GameView::applyDeckListFont);
applyDeckOverlayFonts();
}
void GameView::applyDeckListFont()
void GameView::applyDeckOverlayFonts()
{
dragCountLabel->setFont(deckOverlayFont(14, true));
totalCountLabel->setFont(deckOverlayFont(16, true));

View file

@ -50,7 +50,7 @@ private slots:
void refreshShortcuts();
void updateTotalSelectionCount(const QSize &viewSize = QSize());
void setFocusDisabled(bool disabled);
void applyDeckListFont();
void applyDeckOverlayFonts();
public slots:
void updateSceneRect(const QRectF &rect);

View file

@ -14,6 +14,7 @@
static constexpr int MIN_FONT_SIZE = 6;
static constexpr int MAX_FONT_SIZE = 72;
static constexpr int CARD_TEXT_MIN_SIZE = 1;
static constexpr int CARD_TEXT_MAX_SIZE = 12;
static constexpr int FALLBACK_FONT_SIZE = 10;
static const QMap<AppFont::Role, const char *> APP_FONT_DESCRIPTIONS = {
@ -107,13 +108,45 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
auto role = static_cast<AppFont::Role>(fontEnum.value(i));
const FontRow &row = fontRows[role];
// Roles absent from the file resolve to the startup font, not to the
// live preview: appFont() would return the previewed config, so Reset
// and a first load would both show the edited font instead of the
// theme's real default.
const QFont font = cfg.fonts.value(role, themeManager->defaultAppFont(role));
// Roles absent from the file resolve the same way the runtime does: an unset
// role falls back to the config's InterfaceDefault, and only then to the
// startup font. appFont() would return the live preview instead, so Reset and
// a first load must not measure against it.
const QFont fallback = cfg.fonts.value(AppFont::InterfaceDefault, themeManager->defaultAppFont(role));
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();
@ -122,11 +155,6 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
size = FALLBACK_FONT_SIZE;
}
row.sizeSpin->setValue(qBound(MIN_FONT_SIZE, size, MAX_FONT_SIZE));
if (role == AppFont::CardText && row.minSpin != nullptr) {
int minSize = cfg.cardTextMinSize > 0 ? cfg.cardTextMinSize : themeManager->cardTextMinSize();
row.minSpin->setValue(qBound(CARD_TEXT_MIN_SIZE, minSize, MAX_FONT_SIZE));
}
}
blockSignals(false);
}
@ -147,6 +175,7 @@ FontConfig FontGridWidget::currentFontConfig() const
if (role == AppFont::CardText && row.minSpin != nullptr) {
cfg.cardTextMinSize = row.minSpin->value();
cfg.cardTextMaxSize = row.sizeSpin->value();
}
}
return cfg;

View file

@ -286,9 +286,7 @@ void PaletteEditorDialog::loadSchemes()
void PaletteEditorDialog::loadFonts()
{
savedFontConfig = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName);
fontGrid->blockSignals(true);
fontGrid->loadFonts(savedFontConfig);
fontGrid->blockSignals(false);
fontsDirty = false;
}
@ -395,7 +393,6 @@ void PaletteEditorDialog::onSave()
void PaletteEditorDialog::onReset()
{
const bool hadFontPreview = fontsDirty;
workingConfig[loadedScheme] = savedConfig[loadedScheme];
paletteGrid->loadPalette(savedConfig[loadedScheme]);
// Restore the live theme before re-reading the grid: loadFonts resolves
@ -404,9 +401,6 @@ void PaletteEditorDialog::onReset()
themeManager->reloadCurrentTheme();
fontGrid->loadFonts(savedFontConfig);
fontsDirty = false;
if (hadFontPreview) {
themeManager->previewFonts(savedFontConfig);
}
seedAccentFromScheme(loadedScheme);
}

View file

@ -19,7 +19,6 @@ class FontGridWidget;
class QuickSetupPanel;
class QFrame;
class QWidget;
class QCloseEvent;
class PaletteEditorDialog : public QDialog
{

View file

@ -308,7 +308,7 @@ QPalette PaletteConfig::apply(QPalette base) const
bool FontConfig::hasFonts() const
{
return !fonts.isEmpty() || cardTextMinSize != 1;
return !fonts.isEmpty() || cardTextMinSize != 1 || cardTextMaxSize > 0;
}
QString FontConfig::toToml() const
@ -338,6 +338,10 @@ QString FontConfig::toToml() const
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 {};
}
@ -405,13 +409,13 @@ FontConfig FontConfig::fromFile(const QString &filePath)
value = value.mid(1, value.size() - 2).trimmed();
}
// CardText floor: bare integer, e.g. CardTextMinSize = 2. Tolerate
// quoting like the family entries; skip invalid lines.
QString minKey = key;
if (minKey.startsWith("AppFont::")) {
minKey = minKey.mid(QStringLiteral("AppFont::").size());
// 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 (minKey.compare("CardTextMinSize", Qt::CaseInsensitive) == 0) {
if (sizeKey.compare("CardTextMinSize", Qt::CaseInsensitive) == 0) {
bool ok = false;
int minSize = value.trimmed().toInt(&ok);
if (!ok || minSize <= 0) {
@ -420,6 +424,15 @@ FontConfig FontConfig::fromFile(const QString &filePath)
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(',');

View file

@ -94,10 +94,13 @@ struct FontConfig
{
QMap<AppFont::Role, QFont> 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 && cardTextMinSize == rhs.cardTextMinSize;
return fonts == rhs.fonts && cardTextMinSize == rhs.cardTextMinSize && cardTextMaxSize == rhs.cardTextMaxSize;
}
bool operator!=(const FontConfig &rhs) const
{

View file

@ -657,23 +657,12 @@ QFont ThemeManager::appFont(AppFont::Role role) const
}
// Role fallbacks preserve the pre-theme defaults when no fonts.toml is
// present: chat logs rendered monospace (matching the report dialogs) and
// every other role resolves to the app font. The surfaces that hardcoded a
// different family ask for it explicitly: counter painters through
// cardTextFont(), the deck view headers and printed deck list through
// deckListFont(). Only the family basis is fixed there; sizes stay
// caller-side.
if (role == AppFont::ChatLog) {
// ChatView never set a font before the theme role existed, so an unset
// ChatLog must keep resolving to the application font. The report
// dialogs that used to hardcode monospace ask for it explicitly
// (report_utils::reportLogFont) instead of through this fallback.
return qApp->font();
}
// No per-role scaling: an unset role resolves to the application font, so
// with no fonts.toml present the result is identical to before. The counter
// painters that used to hardcode Serif ask for it through cardTextFont(),
// and the two deck-list sites that did through deckListFont().
// present: an unset role resolves to the application font. Surfaces that
// hardcoded a different family ask for it explicitly: counter painters
// through cardTextFont(), the deck view headers and printed deck list
// through deckListFont(), the report dialogs through
// report_utils::reportLogFont(). Only the family basis is fixed there;
// sizes stay caller-side.
return qApp->font();
}
@ -713,6 +702,9 @@ QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const
int ThemeManager::cardTextMaxSize() const
{
if (currentFonts.cardTextMaxSize > 0) {
return currentFonts.cardTextMaxSize;
}
const auto it = currentFonts.fonts.constFind(AppFont::CardText);
if (it != currentFonts.fonts.constEnd()) {
const int size = it.value().pointSize();

View file

@ -169,7 +169,8 @@ 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 CardText point size, else 12. */
/** @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;

View file

@ -480,6 +480,9 @@ static bool fontsTomlHasExplicitCardTextMax(const QString &filePath)
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;
}
@ -525,8 +528,11 @@ static bool seedCardTextMaxIntoThemeDir(const QString &themeDirPath, int maxSize
existing = QString::fromUtf8(in.readAll());
}
const QString entry =
QString("%1 = \"%2, %3\"\n").arg(QStringLiteral("CardText"), -20).arg(QStringLiteral("Serif")).arg(maxSize);
// 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()) {

View file

@ -246,6 +246,73 @@ TEST(FontConfigTest, MalformedMinIsSkipped)
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;
@ -257,6 +324,17 @@ TEST(FontConfigTest, InequalityComparesMinSize)
EXPECT_NE(left, right);
}
TEST(FontConfigTest, InequalityComparesMaxSize)
{
FontConfig left;
left.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
left.cardTextMaxSize = 12;
FontConfig right = left;
right.cardTextMaxSize = 18;
EXPECT_NE(left, right);
}
// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and
// the fallback then links the bare gtest library without gtest_main.
int main(int argc, char **argv)

View file

@ -1008,9 +1008,11 @@ TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_NonDefaultSeedsThemeAndDelet
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardText"));
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
EXPECT_TRUE(content.contains("Serif"));
// 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));
}
@ -1042,7 +1044,7 @@ TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_SeedsActiveStockTheme)
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardText"));
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
}
@ -1154,7 +1156,7 @@ TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_MergesWithOtherRoles)
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("InterfaceDefault"));
EXPECT_TRUE(content.contains("CardText"));
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("20"));
}