Compare commits

..

15 commits

Author SHA1 Message Date
Lukas Brübach
f47813dfe4 [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-01 17:34:14 +01:00
Lukas Brübach
5a5671e047 [Fonts] Polish font editor accessibility and note text 2026-11-01 17:25:03 +01:00
Lukas Brübach
86fbf5032e [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-01 17:23:51 +01:00
Lukas Brübach
239642b8a0 [Fonts] Restore DeckList Serif default and live-update all role readers 2026-11-01 17:18:21 +01:00
Lukas Brübach
64d7e28192 [Fonts] Fix font preview leaks and reuse default-font loader 2026-11-01 17:10:05 +01:00
Lukas Brübach
4b616a17e8 [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-01 17:03:17 +01:00
Lukas Brübach
f6b8edc50c [Fonts] Drop unused lambda captures in font-refresh handlers 2026-11-01 14:12:57 +01:00
Lukas Brübach
b5250b440b [Fonts] Migrate DeckList and InterfaceDefault surfaces to theme roles 2026-11-01 14:12:57 +01:00
Lukas Brübach
1407a2e142 [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-01 14:12:35 +01:00
Lukas Brübach
ee2e0a9fa0 [Fonts] Migrate CardText surfaces to theme font role 2026-11-01 14:06:33 +01:00
Lukas Brübach
145f7adf73 [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-01 14:06:05 +01:00
Lukas Brübach
2a3a31c24c [Fonts] Migrate ChatLog surfaces to theme font role 2026-11-01 13:59:37 +01:00
Lukas Brübach
ecc3ecd4ea [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-01 13:59:16 +01:00
Lukas Brübach
c0aeaf72e8 [Fonts] Add per-purpose font section to palette editor 2026-11-01 13:53:56 +01:00
Lukas Brübach
c63a359c3d [Fonts] Restore the startup font when a config has no InterfaceDefault
- Capture the untouched application font next to the default palette and use
  it in previewFonts' else branch, so a config without InterfaceDefault
  restores the startup font instead of leaving the last preview applied.
- Drop the unpolish/polish loop from previewFonts: QApplication::setFont
  already notifies every widget, and the loop ran on every editor auto-apply.
2026-11-01 13:53:21 +01:00
33 changed files with 299 additions and 197 deletions

View file

@ -22,7 +22,7 @@ void AbstractGraphicsItem::paintNumberEllipse(int number,
painter->save();
QString numStr = QString::number(number);
QFont font = themeManager->appFont(AppFont::CardText);
QFont font = themeManager->cardTextFont();
font.setPixelSize(fontSize);
font.setWeight(QFont::Bold);

View file

@ -42,7 +42,7 @@ void GeneralCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /
painter->drawPixmap(QPoint(0, 0), pixmap);
if (value) {
QFont f = themeManager->appFont(AppFont::CardText);
QFont f = themeManager->cardTextFont();
f.setPixelSize(qMax((int)(radius * scaleFactor), 10));
f.setWeight(QFont::Bold);
painter->setPen(Qt::black);

View file

@ -241,8 +241,7 @@ void DeckViewCardContainer::paint(QPainter *painter, const QStyleOptionGraphicsI
painter->setPen(QColor(Qt::white));
// Family comes from the DeckList theme role; pixel sizes/weights unchanged.
QFont f = themeManager->appFont(AppFont::DeckList);
f.setStyleHint(QFont::Serif);
QFont f = themeManager->deckListFont();
f.setPixelSize(24);
f.setWeight(QFont::Bold);
painter->setFont(f);
@ -294,8 +293,7 @@ QList<QPair<int, int>> DeckViewCardContainer::getRowsAndCols() const
int DeckViewCardContainer::getCardTypeTextWidth() const
{
// Family comes from the DeckList theme role; pixel size/weight unchanged.
QFont f = themeManager->appFont(AppFont::DeckList);
f.setStyleHint(QFont::Serif);
QFont f = themeManager->deckListFont();
f.setPixelSize(16);
f.setWeight(QFont::Bold);
QFontMetrics fm(f);
@ -588,9 +586,6 @@ DeckView::DeckView(QWidget *parent) : QGraphicsView(parent)
connect(deckViewScene, &DeckViewScene::sceneRectChanged, this, &DeckView::updateSceneRect);
connect(deckViewScene, &DeckViewScene::newCardAdded, this, &DeckView::newCardAdded);
connect(deckViewScene, &DeckViewScene::sideboardPlanChanged, this, &DeckView::sideboardPlanChanged);
// The scene already touches its items on fontsChanged; repaint the
// viewport here so the new DeckList family shows without interaction.
connect(themeManager, &ThemeManager::fontsChanged, this, [this] { viewport()->update(); });
}
void DeckView::resizeEvent(QResizeEvent *event)

View file

@ -26,6 +26,21 @@
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
namespace
{
QFont deckOverlayFont(int pixelSize, bool bold)
{
// The drag/total counters and the tally columns are in-game overlays, not
// deck list text: keep the monospace family the stylesheets used to set so
// digit widths stay stable as counts tick and `white-space: pre` lines up.
QFont font(QStringLiteral("monospace"));
font.setStyleHint(QFont::Monospace);
font.setPixelSize(pixelSize);
font.setBold(bold);
return font;
}
} // namespace
// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings.
// This subclass disables that behavior so dragCountLabel can appear above it.
class SelectionRubberBand : public QRubberBand
@ -108,34 +123,21 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
void GameView::applyDeckListFont()
{
// Family comes from the DeckList theme role; the pixel sizes and bold
// weights previously carried by the stylesheets stay caller-side.
QFont dragFont = themeManager->appFont(AppFont::DeckList);
dragFont.setPointSize(14);
dragFont.setWeight(QFont::Bold);
dragCountLabel->setFont(dragFont);
QFont totalFont = themeManager->appFont(AppFont::DeckList);
totalFont.setPointSize(16);
totalFont.setWeight(QFont::Bold);
totalCountLabel->setFont(totalFont);
QFont tallyFont = themeManager->appFont(AppFont::DeckList);
tallyFont.setPointSize(12);
tallyContainer->setFont(tallyFont);
dragCountLabel->setFont(deckOverlayFont(14, true));
totalCountLabel->setFont(deckOverlayFont(16, true));
tallyContainer->setFont(deckOverlayFont(12, false));
for (int i = 0; i < tallyLayout->count(); ++i) {
if (auto *label = qobject_cast<QLabel *>(tallyLayout->itemAt(i)->widget())) {
QFont labelFont = themeManager->appFont(AppFont::DeckList);
// Count column (odd positions) keeps the 14px bold emphasis.
if (i % 2 == 1) {
labelFont.setPointSize(14);
labelFont.setWeight(QFont::Bold);
} else {
labelFont.setPointSize(12);
}
label->setFont(labelFont);
// Count column (odd positions) keeps the bold emphasis.
const bool count = i % 2 == 1;
label->setFont(deckOverlayFont(count ? 14 : 12, count));
}
}
// The container keeps the size computed from the previous fonts, so force
// rebuildTallyLabels() to re-measure it instead of reusing cachedTallyRows.
cachedTallyRows.clear();
updateTotalSelectionCount();
}
void GameView::resizeEvent(QResizeEvent *event)
@ -266,18 +268,13 @@ QSize GameView::rebuildTallyLabels(const QList<TallyRow> &entries)
for (const TallyRow &entry : entries) {
auto *nameLabel = new QLabel(entry.name, tallyContainer);
nameLabel->setStyleSheet(nameStyle);
QFont nameFont = themeManager->appFont(AppFont::DeckList);
nameFont.setPointSize(12);
nameLabel->setFont(nameFont);
nameLabel->setFont(deckOverlayFont(12, false));
nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
tallyLayout->addWidget(nameLabel, row, 0);
auto *countLabel = new QLabel(entry.value, tallyContainer);
countLabel->setStyleSheet(countStyle);
QFont countFont = themeManager->appFont(AppFont::DeckList);
countFont.setPointSize(14);
countFont.setWeight(QFont::Bold);
countLabel->setFont(countFont);
countLabel->setFont(deckOverlayFont(14, true));
countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
tallyLayout->addWidget(countLabel, row, 1);

View file

@ -77,7 +77,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*
QRectF translatedRect = path.controlPointRect();
QSize translatedSize = translatedRect.size().toSize();
QFont font = themeManager->appFont(AppFont::CardText);
QFont font = themeManager->cardTextFont();
font.setWeight(QFont::Bold);
font.setPixelSize(qMax(qRound(translatedSize.height() / 1.3), 9));
painter->setFont(font);

View file

@ -581,7 +581,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
QTextBlockFormat blockFormat;
QTextCharFormat charFormat;
// Family comes from the DeckList theme role; sizes/weights unchanged.
charFormat.setFontFamilies({themeManager->appFont(AppFont::DeckList).family()});
charFormat.setFontFamilies({themeManager->deckListFont().family()});
charFormat.setFontPointSize(11);
charFormat.setFontWeight(QFont::Bold);
cursor->insertBlock(blockFormat, charFormat);
@ -595,7 +595,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
const AbstractDecklistCardNode *card = cards[i];
QTextCharFormat cellCharFormat;
cellCharFormat.setFontFamilies({themeManager->appFont(AppFont::DeckList).family()});
cellCharFormat.setFontFamilies({themeManager->deckListFont().family()});
cellCharFormat.setFontPointSize(9);
QTextTableCell cell = table->cellAt(i, 0);
@ -617,7 +617,7 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
QTextBlockFormat blockFormat;
QTextCharFormat charFormat;
charFormat.setFontFamilies({themeManager->appFont(AppFont::DeckList).family()});
charFormat.setFontFamilies({themeManager->deckListFont().family()});
charFormat.setFontPointSize(14);
charFormat.setFontWeight(QFont::Bold);
@ -649,15 +649,14 @@ void DeckLoader::printDeckList(QPrinter *printer, const DeckList &deckList)
// Print output is a one-shot snapshot and intentionally does not subscribe
// to fontsChanged (there is no live view to repaint); reprinting picks up
// the current role font.
QFont font = themeManager->appFont(AppFont::DeckList);
font.setStyleHint(QFont::Serif);
QFont font = themeManager->deckListFont();
doc.setDefaultFont(font);
QTextCursor cursor(&doc);
QTextBlockFormat headerBlockFormat;
QTextCharFormat headerCharFormat;
headerCharFormat.setFontFamilies({themeManager->appFont(AppFont::DeckList).family()});
headerCharFormat.setFontFamilies({themeManager->deckListFont().family()});
headerCharFormat.setFontPointSize(16);
headerCharFormat.setFontWeight(QFont::Bold);

View file

@ -71,6 +71,13 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
grid->addWidget(maxSpin, i, 3);
connect(maxSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
// Keep the range consistent: otherwise min 20 / max 12 saves as-is
// and cardTextMaxSize() silently clamps the max up to the min, so
// the max spin shows a size that is never used. Moving one spin
// pulls the other's bound along.
connect(minSpin, &QSpinBox::valueChanged, this, [maxSpin](int value) { maxSpin->setMinimum(value); });
connect(maxSpin, &QSpinBox::valueChanged, this, [minSpin](int value) { minSpin->setMaximum(value); });
label->setBuddy(familyCombo);
fontRows[role] = {label, familyCombo, maxSpin, minSpin};
@ -100,9 +107,11 @@ 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 theme's effective font, so
// unset roles visibly show the system default instead of blanks.
const QFont font = cfg.fonts.value(role, themeManager->appFont(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));
row.familyCombo->setCurrentFont(font);
int size = font.pointSize();

View file

@ -8,7 +8,6 @@
#include "quick_setup_panel.h"
#include <QApplication>
#include <QCloseEvent>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
@ -399,9 +398,11 @@ void PaletteEditorDialog::onReset()
const bool hadFontPreview = fontsDirty;
workingConfig[loadedScheme] = savedConfig[loadedScheme];
paletteGrid->loadPalette(savedConfig[loadedScheme]);
fontGrid->blockSignals(true);
// Restore the live theme before re-reading the grid: loadFonts resolves
// unset roles, and the preview has currentFonts pointing at the edited
// config until the theme is reloaded.
themeManager->reloadCurrentTheme();
fontGrid->loadFonts(savedFontConfig);
fontGrid->blockSignals(false);
fontsDirty = false;
if (hadFontPreview) {
themeManager->previewFonts(savedFontConfig);
@ -451,9 +452,7 @@ void PaletteEditorDialog::onRevertToDefault()
const FontConfig fontDef = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName);
savedFontConfig = fontDef;
fontGrid->blockSignals(true);
fontGrid->loadFonts(fontDef);
fontGrid->blockSignals(false);
fontsDirty = false;
}
@ -474,14 +473,6 @@ void PaletteEditorDialog::changeEvent(QEvent *e)
QDialog::changeEvent(e);
}
void PaletteEditorDialog::closeEvent(QCloseEvent *e)
{
// Window-system close bypasses the Close button: discard the preview here
// too. The extra reload when closeEvent forwards to reject() is harmless.
themeManager->reloadCurrentTheme();
QDialog::closeEvent(e);
}
void PaletteEditorDialog::refreshChromePalettes()
{
const QPalette base = qApp->palette();

View file

@ -90,7 +90,6 @@ private:
protected:
void changeEvent(QEvent *e) override;
void closeEvent(QCloseEvent *e) override;
};
#endif // COCKATRICE_PALETTE_EDITOR_DIALOG_H

View file

@ -30,16 +30,14 @@ QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent)
labelLow = new QLabel(this);
labelHigh = new QLabel(this);
// Family comes from the InterfaceDefault theme role; the *0.82 size stays caller-side.
QFont small = themeManager->appFont(AppFont::InterfaceDefault);
small.setPointSizeF(small.pointSizeF() * 0.82);
labelLow->setFont(small);
labelHigh->setFont(small);
connect(themeManager, &ThemeManager::fontsChanged, this, [this]() {
QFont refreshedSmall = themeManager->appFont(AppFont::InterfaceDefault);
refreshedSmall.setPointSizeF(refreshedSmall.pointSizeF() * 0.82);
labelLow->setFont(refreshedSmall);
labelHigh->setFont(refreshedSmall);
});
auto applySmallFont = [this]() {
QFont small = themeManager->appFont(AppFont::InterfaceDefault);
small.setPointSizeF(small.pointSizeF() * 0.82);
labelLow->setFont(small);
labelHigh->setFont(small);
};
applySmallFont();
connect(themeManager, &ThemeManager::fontsChanged, this, applySmallFont);
QPalette dimmed = labelLow->palette();
dimmed.setColor(QPalette::WindowText, qApp->palette().color(QPalette::Mid));
labelLow->setPalette(dimmed);

View file

@ -128,9 +128,9 @@ static QString usableDefaultStyle(const QString &style)
ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
{
defaultStyleName = usableDefaultStyle(qApp->style()->objectName());
// Capture the untouched application palette before any theme is applied.
// Capture the untouched application palette and font before any theme is
// applied.
defaultPalette = qApp->palette();
// Capture the untouched application font before any theme font is applied.
defaultFont = qApp->font();
ensureThemeDirectoryExists();
#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
@ -535,18 +535,15 @@ void ThemeManager::previewFonts(const FontConfig &cfg)
{
currentFonts = cfg;
// QApplication::setFont already sends a font change to every widget, so a
// repolish pass is not needed for fonts (unlike the palette path).
const auto it = cfg.fonts.constFind(AppFont::InterfaceDefault);
if (it != cfg.fonts.constEnd()) {
qApp->setFont(it.value());
// Repolish every widget immediately so the preview applies without
// waiting for natural expose events, mirroring applyStyleAndPalette.
QStyle *style = qApp->style();
for (QWidget *widget : qApp->allWidgets()) {
style->unpolish(widget);
style->polish(widget);
widget->update();
}
} else {
// A config without InterfaceDefault means "no override": restore the
// startup font instead of leaving the last preview applied.
qApp->setFont(defaultFont);
}
emit fontsChanged();
@ -660,38 +657,65 @@ 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),
// card text and deck lists from a serif basis (deck_view.cpp and
// deck_loader.cpp previously hardcoded "Serif"), everything else from the
// app font. Only the family basis is fixed here; sizes stay caller-side.
// 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) {
QFont monoFont(QStringLiteral("monospace"));
monoFont.setStyleHint(QFont::Monospace);
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
if (systemPointSize > 0) {
monoFont.setPointSize(systemPointSize);
}
return monoFont;
// 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();
}
if (role == AppFont::CardText) {
QFont serifFont(QStringLiteral("Serif"));
serifFont.setPointSize(12);
return serifFont;
}
if (role == AppFont::DeckList) {
return QFont(QStringLiteral("Serif"));
}
// No per-role scaling: every other unset role resolves to the application
// font, so with no fonts.toml present the result is identical to before.
// 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().
return qApp->font();
}
bool ThemeManager::hasAppFont(AppFont::Role role) const
{
return currentFonts.fonts.contains(role);
}
QFont ThemeManager::cardTextFont() const
{
if (hasAppFont(AppFont::CardText)) {
return appFont(AppFont::CardText);
}
QFont serifFont(QStringLiteral("Serif"));
serifFont.setStyleHint(QFont::Serif);
serifFont.setPointSize(12);
return serifFont;
}
QFont ThemeManager::deckListFont() const
{
if (hasAppFont(AppFont::DeckList)) {
return appFont(AppFont::DeckList);
}
QFont serifFont(QStringLiteral("Serif"));
serifFont.setStyleHint(QFont::Serif);
return serifFont;
}
QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const
{
// No per-role scaling and no preview: an unset role resolves to the font
// captured at startup, which is what the theme falls back to once the
// editor closes.
return defaultFont;
}
int ThemeManager::cardTextMaxSize() const
{
const auto it = currentFonts.fonts.constFind(AppFont::CardText);
if (it != currentFonts.fonts.constEnd()) {
int size = it.value().pointSize();
const int size = it.value().pointSize();
if (size > 0) {
return size;
}

View file

@ -49,8 +49,8 @@ private:
// switching away from a custom palette restores the original colours.
QPalette defaultPalette;
// Pristine application font captured at startup, before any theme font is
// applied. Used to restore the original font when a theme supplies none,
// mirroring defaultPalette above.
// applied. Restored when a theme (or a preview) supplies no InterfaceDefault
// font, so switching away from a themed font cannot leave it behind.
QFont defaultFont;
QString currentThemePath;
std::array<QBrush, Role::MaxRole + 1> brushes;
@ -150,10 +150,25 @@ public:
// when present, otherwise a palette-accent-derived fallback.
QColor appColor(AppColor::Role role) const;
/** @brief Resolves an application font role: the theme's stored [Fonts]
* value when present, otherwise a per-role fallback (monospace for
* ChatLog, serif basis for CardText and DeckList, the application
* font for the remaining roles). */
* value when present, otherwise the application font. Roles that
* had a historical hardcoded family ask for it through
* cardTextFont() and deckListFont() instead. */
QFont appFont(AppFont::Role role) const;
/** @brief Whether the active theme defines this role in fonts.toml. */
bool hasAppFont(AppFont::Role role) const;
/** @brief The CardText role with its historical Serif fallback. Used by the
* counter painters, which were Serif before the role existed. */
QFont cardTextFont() const;
/** @brief The DeckList role with its historical Serif fallback. Used by the
* two surfaces that hardcoded Serif before the role existed (the
* deck view zone headers and the printed deck list); every other
* DeckList reader falls back to the application font. */
QFont deckListFont() const;
/** @brief Resolves an application font role against the startup font only,
* ignoring any previewed config in currentFonts. Editors that show
* "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. */
int cardTextMaxSize() const;
/** @brief CardText shrink-to-fit floor: stored CardTextMinSize, else 1. */

View file

@ -104,14 +104,13 @@ ResizablePanel::ResizablePanel(const QString &_typeId, AbstractAnalyticsPanelWid
"QPushButton:hover { background: %4; }")
.arg(mid.name(), dark.name(), placeholderText.name(), midLight.name()));
// Family comes from the InterfaceDefault theme role; the 10px size stays caller-side.
QFont dragFont = themeManager->appFont(AppFont::InterfaceDefault);
dragFont.setPointSize(10);
dragButton->setFont(dragFont);
connect(themeManager, &ThemeManager::fontsChanged, this, [this]() {
QFont refreshedFont = themeManager->appFont(AppFont::InterfaceDefault);
refreshedFont.setPointSize(10);
dragButton->setFont(refreshedFont);
});
auto applyDragFont = [this]() {
QFont dragFont = themeManager->appFont(AppFont::InterfaceDefault);
dragFont.setPixelSize(10);
dragButton->setFont(dragFont);
};
applyDragFont();
connect(themeManager, &ThemeManager::fontsChanged, this, applyDragFont);
resizeHandle->setStyleSheet(QString("QWidget { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, "
"stop:0 %1, stop:1 %2); }")

View file

@ -8,7 +8,6 @@
#include <QAbstractItemView>
#include <QFont>
#include <QFontDatabase>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
@ -127,14 +126,7 @@ DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent)
void DlgMyReports::applyChatLogFont()
{
// Family comes from the ChatLog theme role; size stays caller-side as before.
QFont monoFont = themeManager->appFont(AppFont::ChatLog);
monoFont.setStyleHint(QFont::Monospace);
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
if (systemPointSize > 0) {
monoFont.setPointSize(systemPointSize);
}
chatLogEdit->setFont(monoFont);
chatLogEdit->setFont(report_utils::reportLogFont());
}
void DlgMyReports::refreshList()

View file

@ -1,13 +1,13 @@
#include "dlg_report_user.h"
#include "../interface/theme_manager.h"
#include "../utility/report_utils.h"
#include "abstract_client.h"
#include "libcockatrice/protocol/pb/response.pb.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFont>
#include <QFontDatabase>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
@ -125,14 +125,7 @@ DlgReportUser::DlgReportUser(AbstractClient *_client,
void DlgReportUser::applyChatLogFont()
{
// Family comes from the ChatLog theme role; size stays caller-side as before.
QFont monoFont = themeManager->appFont(AppFont::ChatLog);
monoFont.setStyleHint(QFont::Monospace);
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
if (systemPointSize > 0) {
monoFont.setPointSize(systemPointSize);
}
chatLogEdit->setFont(monoFont);
chatLogEdit->setFont(report_utils::reportLogFont());
}
void DlgReportUser::actSubmit()

View file

@ -36,16 +36,14 @@ DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent,
auto *titleLabel =
new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this);
// Family comes from the InterfaceDefault theme role; the +2 size and bold stay caller-side.
QFont titleFont = themeManager->appFont(AppFont::InterfaceDefault);
titleFont.setBold(true);
titleFont.setPointSize(titleFont.pointSize() + 2);
titleLabel->setFont(titleFont);
connect(themeManager, &ThemeManager::fontsChanged, this, [titleLabel]() {
QFont refreshedFont = themeManager->appFont(AppFont::InterfaceDefault);
refreshedFont.setBold(true);
refreshedFont.setPointSize(refreshedFont.pointSize() + 2);
titleLabel->setFont(refreshedFont);
});
auto applyTitleFont = [titleLabel]() {
QFont titleFont = themeManager->appFont(AppFont::InterfaceDefault);
titleFont.setBold(true);
titleFont.setPointSize(titleFont.pointSize() + 2);
titleLabel->setFont(titleFont);
};
applyTitleFont();
connect(themeManager, &ThemeManager::fontsChanged, this, applyTitleFont);
mainLayout->addWidget(titleLabel);
if (!serverText.isEmpty()) {

View file

@ -65,7 +65,7 @@ void BannerWidget::applyInterfaceDefaultFont()
// Family comes from the InterfaceDefault theme role; the 24px size and
// bold weight previously carried by the stylesheet stay caller-side.
QFont bannerFont = themeManager->appFont(AppFont::InterfaceDefault);
bannerFont.setPointSize(24);
bannerFont.setPixelSize(24);
bannerFont.setWeight(QFont::Bold);
bannerLabel->setFont(bannerFont);
}

View file

@ -33,7 +33,7 @@ void HomeStyledButton::applyInterfaceDefaultFont()
// Family comes from the InterfaceDefault theme role; the 34px size and
// bold weight previously carried by the stylesheet stay caller-side.
QFont buttonFont = themeManager->appFont(AppFont::InterfaceDefault);
buttonFont.setPointSize(34);
buttonFont.setPixelSize(34);
buttonFont.setWeight(QFont::Bold);
setFont(buttonFont);
}

View file

@ -274,14 +274,13 @@ QGroupBox *HomeWidget::createButtons()
}
)");
// Family comes from the InterfaceDefault theme role; the 20px size stays caller-side.
QFont boxFont = themeManager->appFont(AppFont::InterfaceDefault);
boxFont.setPointSize(20);
box->setFont(boxFont);
connect(themeManager, &ThemeManager::fontsChanged, this, [box]() {
QFont refreshedFont = themeManager->appFont(AppFont::InterfaceDefault);
refreshedFont.setPointSize(20);
box->setFont(refreshedFont);
});
auto applyBoxFont = [box]() {
QFont boxFont = themeManager->appFont(AppFont::InterfaceDefault);
boxFont.setPixelSize(20);
box->setFont(boxFont);
};
applyBoxFont();
connect(themeManager, &ThemeManager::fontsChanged, this, applyBoxFont);
box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
QVBoxLayout *boxLayout = new QVBoxLayout;
boxLayout->setAlignment(Qt::AlignHCenter);

View file

@ -55,6 +55,6 @@ void PrintingSelectorPlaceholderWidget::applyInterfaceDefaultFont()
{
// Family comes from the InterfaceDefault theme role; the 14px size stays caller-side.
QFont placeholderFont = themeManager->appFont(AppFont::InterfaceDefault);
placeholderFont.setPointSize(14);
placeholderFont.setPixelSize(14);
textLabel->setFont(placeholderFont);
}

View file

@ -482,7 +482,7 @@ void UserInfoPopup::applyTheme()
gamesLabel->setStyleSheet(QStringLiteral("color:%1; background:transparent;").arg(colorStr(theme.subText)));
QFont gamesLabelFont = themeManager->appFont(AppFont::InterfaceDefault);
gamesLabelFont.setPointSize(11);
gamesLabelFont.setPixelSize(11);
gamesLabelFont.setWeight(QFont::Bold);
gamesLabel->setFont(gamesLabelFont);
@ -490,13 +490,13 @@ void UserInfoPopup::applyTheme()
"QPushButton:hover{color:%2;}")
.arg(colorStr(theme.subText), colorStr(theme.text)));
QFont refreshFont = themeManager->appFont(AppFont::InterfaceDefault);
refreshFont.setPointSize(14);
refreshFont.setPixelSize(14);
refreshBtn->setFont(refreshFont);
gamesStatus->setStyleSheet(
QStringLiteral("color:%1; padding:10px; background:transparent;").arg(colorStr(theme.statusText)));
QFont statusFont = themeManager->appFont(AppFont::InterfaceDefault);
statusFont.setPointSize(11);
statusFont.setPixelSize(11);
gamesStatus->setFont(statusFont);
gamesView->setStyleSheet(QStringLiteral("QListView{background:%1;border:none;}"
@ -509,7 +509,7 @@ void UserInfoPopup::applyTheme()
"QPushButton:hover{color:%3;background:%4;}")
.arg(colorStr(theme.closeBg), colorStr(theme.subText), colorStr(theme.text), colorStr(theme.closeHover)));
QFont closeFont = themeManager->appFont(AppFont::InterfaceDefault);
closeFont.setPointSize(10);
closeFont.setPixelSize(10);
closeBtn->setFont(closeFont);
header->update();
@ -534,7 +534,7 @@ static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p
colorStr(t.text), colorStr(t.buttonPressed), colorStr(t.buttonDisabled)));
// Family comes from the InterfaceDefault theme role; the 11px size stays caller-side.
QFont btnFont = themeManager->appFont(AppFont::InterfaceDefault);
btnFont.setPointSize(11);
btnFont.setPixelSize(11);
b->setFont(btnFont);
return b;
}

View file

@ -101,9 +101,10 @@ void EdhrecApiResponseCardDetailsDisplayWidget::actRequestPageNavigation()
void EdhrecApiResponseCardDetailsDisplayWidget::applyCardTextFont()
{
// Family comes from the CardText theme role; the 20px size and bold
// weight previously carried by the stylesheet stay caller-side.
// weight previously carried by the stylesheet stay caller-side. Pixel
// size, not point size: the stylesheet said 20px, and 20pt is ~27px.
QFont cardFont = themeManager->appFont(AppFont::CardText);
cardFont.setPointSize(20);
cardFont.setPixelSize(20);
cardFont.setWeight(QFont::Bold);
nameLabel->setFont(cardFont);
}

View file

@ -96,15 +96,16 @@ void EdhrecCommanderResponseCommanderDetailsDisplayWidget::retranslateUi()
void EdhrecCommanderResponseCommanderDetailsDisplayWidget::applyCardTextFont()
{
// Family comes from the CardText theme role; the point sizes and the
// bold weight previously carried by the stylesheets stay caller-side.
// Family comes from the CardText theme role; the sizes and the bold
// weight previously carried by the stylesheets stay caller-side. Pixel
// sizes, not point sizes: the stylesheets said 28px and 16px.
QFont nameFont = themeManager->appFont(AppFont::CardText);
nameFont.setPointSize(28);
nameFont.setPixelSize(28);
nameFont.setWeight(QFont::Bold);
commanderName->setFont(nameFont);
QFont detailsFont = themeManager->appFont(AppFont::CardText);
detailsFont.setPointSize(16);
detailsFont.setPixelSize(16);
label->setFont(detailsFont);
salt->setFont(detailsFont);
}

View file

@ -104,7 +104,7 @@ void EdhrecCommanderApiResponseBracketNavigationWidget::updateOptionButtonSelect
const bool selected = (it.key() == selectedKey);
it.value()->setStyleSheet(selected ? QStringLiteral("background-color: lightblue;") : QString());
QFont buttonFont = themeManager->appFont(AppFont::InterfaceDefault);
buttonFont.setPointSize(24);
buttonFont.setPixelSize(24);
buttonFont.setWeight(selected ? QFont::Bold : QFont::Normal);
it.value()->setFont(buttonFont);
}

View file

@ -106,7 +106,7 @@ void EdhrecCommanderApiResponseBudgetNavigationWidget::updateOptionButtonSelecti
const bool selected = (it.key() == selectedKey);
it.value()->setStyleSheet(selected ? QStringLiteral("background-color: lightblue;") : QString());
QFont buttonFont = themeManager->appFont(AppFont::InterfaceDefault);
buttonFont.setPointSize(24);
buttonFont.setPixelSize(24);
buttonFont.setWeight(selected ? QFont::Bold : QFont::Normal);
it.value()->setFont(buttonFont);
}

View file

@ -78,7 +78,7 @@ void EdhrecCommanderApiResponseNavigationWidget::applyInterfaceDefaultFont()
{
// Family comes from the InterfaceDefault theme role; the 24px size stays caller-side.
QFont navFont = themeManager->appFont(AppFont::InterfaceDefault);
navFont.setPointSize(24);
navFont.setPixelSize(24);
comboPushButton->setFont(navFont);
averageDeckPushButton->setFont(navFont);
}

View file

@ -46,17 +46,16 @@ TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_c
titleLabel = new QLabel(tr("Public decks of %1").arg(userName.toHtmlEscaped()), this);
// Family comes from the InterfaceDefault theme role; bold stays caller-side.
QFont titleFont = themeManager->appFont(AppFont::InterfaceDefault);
titleFont.setBold(true);
titleLabel->setFont(titleFont);
connect(themeManager, &ThemeManager::fontsChanged, this, [this]() {
QFont refreshedFont = themeManager->appFont(AppFont::InterfaceDefault);
refreshedFont.setBold(true);
titleLabel->setFont(refreshedFont);
auto applyThemeFonts = [this]() {
QFont titleFont = themeManager->appFont(AppFont::InterfaceDefault);
titleFont.setBold(true);
titleLabel->setFont(titleFont);
// Preview cells paint with the live role font; repaint the grid so the
// new family shows without waiting for a scroll or hover.
flowWidget->update();
});
};
applyThemeFonts();
connect(themeManager, &ThemeManager::fontsChanged, this, applyThemeFonts);
auto *headerLayout = new QHBoxLayout;
headerLayout->addWidget(titleLabel);

View file

@ -14,7 +14,6 @@
#include <QDateTime>
#include <QDebug>
#include <QFont>
#include <QFontDatabase>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
@ -275,14 +274,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T
void TabReport::applyChatLogFont()
{
// Family comes from the ChatLog theme role; size stays caller-side as before.
QFont monoFont = themeManager->appFont(AppFont::ChatLog);
monoFont.setStyleHint(QFont::Monospace);
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
if (systemPointSize > 0) {
monoFont.setPointSize(systemPointSize);
}
chatLogEdit->setFont(monoFont);
chatLogEdit->setFont(report_utils::reportLogFont());
}
void TabReport::retranslateUi()

View file

@ -1,5 +1,6 @@
#include "report_utils.h"
#include "../../theme_manager.h"
#include "libcockatrice/protocol/pb/serverinfo_report.pb.h"
#include <QApplication>
@ -7,6 +8,7 @@
#include <QChar>
#include <QColor>
#include <QDateTime>
#include <QFontDatabase>
#include <QPalette>
#include <QSharedPointer>
#include <QTableWidget>
@ -19,6 +21,20 @@
namespace report_utils
{
QFont reportLogFont()
{
if (themeManager->hasAppFont(AppFont::ChatLog)) {
return themeManager->appFont(AppFont::ChatLog);
}
QFont monoFont(QStringLiteral("monospace"));
monoFont.setStyleHint(QFont::Monospace);
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
if (systemPointSize > 0) {
monoFont.setPointSize(systemPointSize);
}
return monoFont;
}
namespace
{
QColor reportStatusColor(const QString &status)

View file

@ -1,6 +1,7 @@
#ifndef REPORT_UTILS_H
#define REPORT_UTILS_H
#include <QFont>
#include <QString>
#include <QtGlobal>
@ -11,6 +12,12 @@ class ServerInfo_Report;
namespace report_utils
{
/** @brief Font for the report chat-log views: the theme's ChatLog role when it
* sets one, otherwise the monospace font these views used before the
* role existed. Shared by the three report surfaces so they cannot
* drift apart. */
QFont reportLogFont();
QString formatReportCategory(const QString &category);
QString formatReportTime(qint64 secondsSinceEpoch);

View file

@ -53,6 +53,6 @@ void VisualDeckEditorPlaceholderWidget::applyInterfaceDefaultFont()
{
// Family comes from the InterfaceDefault theme role; the 14px size stays caller-side.
QFont placeholderFont = themeManager->appFont(AppFont::InterfaceDefault);
placeholderFont.setPointSize(14);
placeholderFont.setPixelSize(14);
textLabel->setFont(placeholderFont);
}

View file

@ -396,6 +396,11 @@ static void migrateAppearanceSettings(const QString &settingsPath, QSettings &gl
{
const QMap<QString, QString> appearanceKeyMap = {
{"theme/name", "appearance/themeName"},
// Retired into the theme's CardText size, but still read here so users
// who have not migrated past global.ini keep their card text size:
// SettingsCache runs migrateSettingsFromGlobalIni() before
// migrateAppearanceMaxFontSizeToTheme().
{"game/maxfontsize", "appearance/maxFontSize"},
{"home/background", "appearance/homeTabBackgroundSource"},
{"home/background/shuffleTimer", "appearance/homeTabBackgroundShuffleFrequency"},
{"home/background/displayCardName", "appearance/homeTabDisplayCardName"},
@ -571,11 +576,20 @@ bool SettingsMigration::migrateAppearanceMaxFontSizeToTheme(const QString &setti
if (legacy != 12 && legacy > 0) {
const QString themesPath = themesPathForSettings(settingsPath);
QDir themesDir(themesPath);
if (themesDir.exists()) {
const QStringList themes = themesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
for (const QString &theme : themes) {
seedCardTextMaxIntoThemeDir(themesDir.absoluteFilePath(theme), legacy);
}
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);
}
}

View file

@ -202,6 +202,7 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups)
// theme
g.setValue("theme/name", "custom_theme");
g.setValue("game/maxfontsize", 14);
// appearance
g.setValue("appearance/styleUserList", false);
@ -375,6 +376,7 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups)
ASSERT_TRUE(fileExists("appearance.ini"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/themeName"), QVariant("custom_theme"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/maxFontSize"), QVariant(14));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/styleUserList"), QVariant(false));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundSource"), QVariant("custom_bg"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundShuffleFrequency"), QVariant(30));
@ -1013,6 +1015,68 @@ TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_NonDefaultSeedsThemeAndDelet
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("CardText"));
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";