diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 642d998a0..d5e383d95 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -154,6 +154,7 @@ set(cockatrice_SOURCES src/interface/palette_editor/palette_generator.cpp src/interface/palette_editor/quick_setup_panel.cpp src/interface/palette_editor/palette_grid_widget.cpp + src/interface/palette_editor/font_grid_widget.cpp src/interface/palette_editor/palette_editor_dialog.cpp src/interface/widgets/cards/additional_info/color_identity_widget.cpp src/interface/widgets/cards/additional_info/deck_color_identity.cpp diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.cpp b/cockatrice/src/interface/palette_editor/font_grid_widget.cpp new file mode 100644 index 000000000..c3841b95f --- /dev/null +++ b/cockatrice/src/interface/palette_editor/font_grid_widget.cpp @@ -0,0 +1,124 @@ +#include "font_grid_widget.h" + +#include "../theme_manager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr int MIN_FONT_SIZE = 6; +static constexpr int MAX_FONT_SIZE = 72; +static constexpr int FALLBACK_FONT_SIZE = 10; + +static const QMap APP_FONT_DESCRIPTIONS = { + {AppFont::InterfaceDefault, QT_TR_NOOP("Default font for menus, dialogs and widgets")}, + {AppFont::ChatLog, QT_TR_NOOP("Font for chat messages and logs")}, + {AppFont::CardText, QT_TR_NOOP("Font for text rendered on cards")}, + {AppFont::DeckList, QT_TR_NOOP("Font for deck lists and the deck editor")}, +}; + +static QString roleDisplayName(AppFont::Role role) +{ + switch (role) { + case AppFont::InterfaceDefault: + return FontGridWidget::tr("Interface default"); + case AppFont::ChatLog: + return FontGridWidget::tr("Chat log"); + case AppFont::CardText: + return FontGridWidget::tr("Card text"); + case AppFont::DeckList: + return FontGridWidget::tr("Deck list"); + default: + return QString(); + } +} + +FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent) +{ + auto *grid = new QGridLayout(this); + grid->setSpacing(3); + grid->setContentsMargins(12, 8, 12, 8); + grid->setColumnStretch(0, 0); + grid->setColumnStretch(1, 1); + grid->setColumnStretch(2, 0); + + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(fontEnum.value(i)); + + auto *label = new QLabel(this); + grid->addWidget(label, i, 0); + + auto *familyCombo = new QFontComboBox(this); + grid->addWidget(familyCombo, i, 1); + connect(familyCombo, &QFontComboBox::currentFontChanged, this, [this] { emit fontsChanged(); }); + + auto *sizeSpin = new QSpinBox(this); + sizeSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE); + grid->addWidget(sizeSpin, i, 2); + connect(sizeSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); }); + + fontRows[role] = {label, familyCombo, sizeSpin}; + } + + retranslateUi(); +} + +void FontGridWidget::loadFonts(const FontConfig &cfg) +{ + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(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)); + row.familyCombo->setCurrentFont(font); + + int size = font.pointSize(); + if (size <= 0) { + size = qApp->font().pointSize(); + } + if (size <= 0) { + size = FALLBACK_FONT_SIZE; + } + row.sizeSpin->setValue(qBound(MIN_FONT_SIZE, size, MAX_FONT_SIZE)); + } +} + +FontConfig FontGridWidget::currentFontConfig() const +{ + FontConfig cfg; + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(fontEnum.value(i)); + const FontRow &row = fontRows.value(role); + + // Canonical family + point size only: a fresh QFont carries default + // (non-bold, non-italic) weight/style, so nothing combo-side leaks in. + QFont font(row.familyCombo->currentFont().family()); + font.setPointSize(row.sizeSpin->value()); + cfg.fonts[role] = font; + } + return cfg; +} + +void FontGridWidget::retranslateUi() +{ + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(fontEnum.value(i)); + const FontRow &row = fontRows.value(role); + + row.label->setText(roleDisplayName(role)); + row.label->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, ""))); + row.familyCombo->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, ""))); + row.sizeSpin->setToolTip(tr("Point size")); + } +} diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.h b/cockatrice/src/interface/palette_editor/font_grid_widget.h new file mode 100644 index 000000000..8c80306d3 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/font_grid_widget.h @@ -0,0 +1,43 @@ +#ifndef COCKATRICE_FONT_GRID_WIDGET_H +#define COCKATRICE_FONT_GRID_WIDGET_H + +#include "../theme_config.h" + +#include +#include +#include + +class QFontComboBox; +class QGridLayout; +class QLabel; +class QSpinBox; + +// Per-role font editors — one (family, point size) row per AppFont::Role. +// Owns the load/read round-trip for FontConfig but has no file I/O, mirroring +// PaletteGridWidget. Only family and point size are edited; weight and italic +// stay caller-side concerns and are stripped when reading back. +class FontGridWidget : public QWidget +{ + Q_OBJECT +public: + explicit FontGridWidget(QWidget *parent = nullptr); + + void loadFonts(const FontConfig &cfg); + FontConfig currentFontConfig() const; + void retranslateUi(); + +signals: + void fontsChanged(); + +private: + struct FontRow + { + QLabel *label = nullptr; + QFontComboBox *familyCombo = nullptr; + QSpinBox *sizeSpin = nullptr; + }; + + QMap fontRows; +}; + +#endif // COCKATRICE_FONT_GRID_WIDGET_H diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index b6271291d..63de3f487 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -2,6 +2,7 @@ #include "../theme_config.h" #include "../theme_manager.h" +#include "font_grid_widget.h" #include "palette_generator.h" #include "palette_grid_widget.h" #include "quick_setup_panel.h" @@ -41,6 +42,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt // Load both scheme configs upfront so switching is instant loadSchemes(); + loadFonts(); loadedScheme = themeManager->isDarkMode(themeDirPath) ? "Dark" : "Light"; @@ -125,6 +127,32 @@ void PaletteEditorDialog::setupUi() paletteGrid->setVisible(false); root->addWidget(paletteGrid, 1); + // Font section — sibling to the palette grid, sharing its collapsible + // advanced area pattern. Fonts are shared per theme, not split by scheme. + fontToggleButton = new QPushButton(this); + fontToggleButton->setCheckable(true); + fontToggleButton->setChecked(false); + fontToggleButton->setFlat(true); + fontToggleButton->setStyleSheet("QPushButton { text-align: left; padding: 5px 12px; font-weight: bold; }" + "QPushButton:checked { }"); + root->addWidget(fontToggleButton); + + fontSeparator = makeSeparator(); + fontSeparator->setVisible(false); + root->addWidget(fontSeparator); + + fontSection = new QWidget; + auto *fontSectionLayout = new QVBoxLayout(fontSection); + fontSectionLayout->setContentsMargins(0, 0, 0, 0); + fontSectionLayout->setSpacing(0); + fontNoteLabel = new QLabel(fontSection); + fontNoteLabel->setContentsMargins(12, 4, 12, 0); + fontSectionLayout->addWidget(fontNoteLabel); + fontGrid = new FontGridWidget(fontSection); + fontSectionLayout->addWidget(fontGrid); + fontSection->setVisible(false); + root->addWidget(fontSection); + // Footer root->addWidget(makeSeparator()); footer = new QWidget; @@ -157,6 +185,7 @@ void PaletteEditorDialog::setupUi() connect(autoApplyTimer, &QTimer::timeout, this, &PaletteEditorDialog::onApply); connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &PaletteEditorDialog::onGenerateFromAccent); connect(paletteGrid, &PaletteGridWidget::paletteChanged, this, [this] { autoApplyTimer->start(); }); + connect(fontGrid, &FontGridWidget::fontsChanged, this, [this] { autoApplyTimer->start(); }); connect(revertButton, &QPushButton::clicked, this, &PaletteEditorDialog::onRevertToDefault); connect(resetBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onReset); connect(saveBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onSave); @@ -166,15 +195,32 @@ void PaletteEditorDialog::setupUi() paletteGridToggleButton->setText(open ? tr("▼ Edit Palette") : tr("▶ Edit Palette")); paletteGridSeparator->setVisible(open); paletteGrid->setVisible(open); - - if (open) { - setMinimumHeight(680); - resize(width(), 680); - } else { - setMinimumHeight(220); - adjustSize(); // shrinks to fit just the visible content - } + updateAdvancedHeight(); }); + connect(fontToggleButton, &QPushButton::toggled, this, [this](bool open) { + fontToggleButton->setText(open ? tr("▼ Edit Fonts") : tr("▶ Edit Fonts")); + fontSeparator->setVisible(open); + fontSection->setVisible(open); + updateAdvancedHeight(); + }); +} + +void PaletteEditorDialog::updateAdvancedHeight() +{ + int height = 220; + if (paletteGridToggleButton->isChecked()) { + height += 460; + } + if (fontToggleButton->isChecked()) { + height += 200; + } + setMinimumHeight(height); + + if (paletteGrid->isVisible() || fontSection->isVisible()) { + resize(width(), height); + } else { + adjustSize(); // shrinks to fit just the visible content + } } void PaletteEditorDialog::retranslateUi() @@ -189,12 +235,16 @@ void PaletteEditorDialog::retranslateUi() editingLabel->setText(tr("Editing:")); paletteGridToggleButton->setText(tr("▶ Edit Palette")); paletteGridToggleButton->setToolTip(tr("Show or hide the per-role colour grid for manual tweaks")); + fontToggleButton->setText(fontSection->isVisible() ? tr("▼ Edit Fonts") : tr("▶ Edit Fonts")); + fontToggleButton->setToolTip(tr("Show or hide the per-purpose font settings for this theme")); + fontNoteLabel->setText(tr("Fonts belong to theme %1").arg(themeName)); + fontGrid->retranslateUi(); revertButton->setText(tr("↺ Revert to theme default")); resetBtn->setText(tr("Reset")); saveBtn->setText(tr("Save")); - resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette")); - saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower())); + resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette and fonts")); + saveBtn->setToolTip(tr("Write palette-%1.toml and fonts.toml, then reload the theme").arg(loadedScheme.toLower())); if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) { saveBtn->setEnabled(false); @@ -228,6 +278,19 @@ void PaletteEditorDialog::loadSchemes() } } +void PaletteEditorDialog::loadFonts() +{ + // Same user-dir > system-dir fallback ThemeManager uses: the shipped + // fonts.toml may live in the system theme directory rather than the + // resolved (user) theme directory. + FontConfig cfg = ThemeManager::loadFontConfig(themeDirPath); + if (!cfg.hasFonts()) { + cfg = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName); + } + savedFontConfig = cfg; + fontGrid->loadFonts(cfg); +} + void PaletteEditorDialog::seedAccentFromScheme(const QString &scheme) { QColor seed = workingConfig.value(scheme).colors.value(QPalette::Active).value(QPalette::Highlight); @@ -258,6 +321,7 @@ void PaletteEditorDialog::onGenerateFromAccent(const QColor &accent, int intensi void PaletteEditorDialog::onApply() { themeManager->previewPalette(paletteGrid->currentPaletteConfig(), loadedScheme); + themeManager->previewFonts(fontGrid->currentFontConfig()); } void PaletteEditorDialog::onSave() @@ -308,6 +372,17 @@ void PaletteEditorDialog::onSave() savedConfig[it.key()] = it.value(); } + // Persist the shared per-theme fonts when they changed. + const FontConfig fontCfg = fontGrid->currentFontConfig(); + if (fontCfg != savedFontConfig) { + if (!ThemeManager::saveFontConfig(saveDir, fontCfg)) { + QMessageBox::warning(this, tr("Save failed"), + tr("Could not write %1 to:\n%2").arg(FontConfig::fileName(), saveDir)); + return; + } + savedFontConfig = fontCfg; + } + themeManager->reloadCurrentTheme(); accept(); } @@ -316,6 +391,7 @@ void PaletteEditorDialog::onReset() { workingConfig[loadedScheme] = savedConfig[loadedScheme]; paletteGrid->loadPalette(savedConfig[loadedScheme]); + fontGrid->loadFonts(savedFontConfig); seedAccentFromScheme(loadedScheme); } @@ -350,6 +426,18 @@ void PaletteEditorDialog::onRevertToDefault() workingConfig[loadedScheme] = def; paletteGrid->loadPalette(def); seedAccentFromScheme(loadedScheme); + + // Revert the shared per-theme fonts the same way: delete the user-written + // fonts.toml so the theme falls back to its shipped default, then reflect + // the resolved fonts in the editor. + QFile::remove(QDir(saveDir).absoluteFilePath(FontConfig::fileName())); + + // Reload the live theme so the font revert takes effect immediately. + themeManager->reloadCurrentTheme(); + + const FontConfig fontDef = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName); + savedFontConfig = fontDef; + fontGrid->loadFonts(fontDef); } void PaletteEditorDialog::changeEvent(QEvent *e) diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h index e29a70cb0..0a9d47c6c 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h @@ -15,6 +15,7 @@ class QComboBox; class QDialogButtonBox; class QPushButton; class PaletteGridWidget; +class FontGridWidget; class QuickSetupPanel; class QFrame; class QWidget; @@ -38,8 +39,10 @@ private: void setupUi(); void retranslateUi(); void refreshChromePalettes(); + void updateAdvancedHeight(); void loadScheme(const QString &scheme); // snapshot current, switch, load void seedAccentFromScheme(const QString &scheme); + void loadFonts(); // shared per theme, not split by light/dark // Sub-widgets QWidget *header; @@ -49,6 +52,11 @@ private: PaletteGridWidget *paletteGrid = nullptr; QPushButton *paletteGridToggleButton = nullptr; QFrame *paletteGridSeparator = nullptr; + FontGridWidget *fontGrid = nullptr; + QPushButton *fontToggleButton = nullptr; + QFrame *fontSeparator = nullptr; + QWidget *fontSection = nullptr; + QLabel *fontNoteLabel = nullptr; QWidget *footer; QComboBox *schemeComboBox = nullptr; QDialogButtonBox *buttonBox = nullptr; @@ -68,6 +76,10 @@ private: QMap workingConfig; QMap savedConfig; + // Fonts are shared per theme (fonts.toml, not split by light/dark), so a + // single saved config suffices; the live edits are read from fontGrid. + FontConfig savedFontConfig; + QTimer *autoApplyTimer = nullptr; protected: diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index b23f15a2b..d1b7a3db8 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -450,6 +450,18 @@ FontConfig ThemeManager::loadFontConfig(const QString &themeDirPath) return FontConfig::fromThemeDir(themeDirPath); } +FontConfig ThemeManager::loadDefaultFontConfig(const QString &themeDirPath, const QString &themeName) +{ + FontConfig cfg = FontConfig::fromThemeDir(themeDirPath); + if (!cfg.hasFonts()) { + // The shipped fonts.toml may live in the system theme directory rather + // than the resolved (user) theme directory, so built-in themes still + // get their curated fonts. + cfg = FontConfig::fromThemeDir(QDir(systemThemesBasePath()).absoluteFilePath(themeName)); + } + return cfg; +} + bool ThemeManager::saveFontConfig(const QString &themeDirPath, const FontConfig &cfg) { if (themeDirPath.isEmpty()) { @@ -717,6 +729,10 @@ void ThemeManager::themeChangedSlot() const auto fontIt = currentFonts.fonts.constFind(AppFont::InterfaceDefault); if (fontIt != currentFonts.fonts.constEnd()) { qApp->setFont(fontIt.value()); + } else { + // No theme font: restore the pristine application font captured at + // startup, so a reverted preview cannot leak into the live theme. + qApp->setFont(defaultFont); } emit fontsChanged(); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index c8fba2eb4..6635b7128 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -132,6 +132,9 @@ public: // theme directory when it is absent from the resolved (user) directory. static PaletteConfig loadDefaultPaletteConfig(const QString &themeDirPath, const QString &themeName, const QString &colorScheme); + // Load the theme's shipped default fonts, falling back to the system theme + // directory when fonts.toml is absent from the resolved (user) directory. + static FontConfig loadDefaultFontConfig(const QString &themeDirPath, const QString &themeName); /** @brief Writes cfg to disk as the theme's palette-.toml and updates the * theme's stored colour scheme to match. Shared by PaletteEditorDialog::onSave * and FirstRunWizard's theme step so the two "generate + keep" paths can't drift. */