From cf0b453e751cf26fd18bf2ec9d78e4a5b97a09a4 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:23:41 +0200 Subject: [PATCH] [PaletteEditor] Ensure directory is writeable, don't fall back to alternate palette (#7048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PaletteEditor] Ensure directory is writeable, don't fall back to alternate palette. Took 17 minutes * [ThemeManager] Extract system theme dir path resolution to helper, use it in default palette fetching Took 8 minutes * [ThemeManager] Expose styling again Took 3 minutes * [ThemeManager] Capture app palette before theme is applied for reverting * [ThemeManager] Simply delete custom palette instead of reverting to default * [ThemeManager] Generate and apply immediately on change. * [PaletteEditor] Block grid signals during initial loading. Took 5 minutes * Address comments Took 8 minutes * Seed accent from scheme on reset and revert Took 2 minutes --------- Co-authored-by: Lukas Brübach --- .../palette_editor/palette_editor_dialog.cpp | 131 +++++++++++++----- .../palette_editor/palette_editor_dialog.h | 8 +- .../palette_editor/palette_grid_widget.cpp | 1 + .../palette_editor/palette_grid_widget.h | 3 + .../palette_editor/quick_setup_panel.cpp | 22 ++- .../palette_editor/quick_setup_panel.h | 21 ++- cockatrice/src/interface/theme_config.cpp | 13 +- cockatrice/src/interface/theme_manager.cpp | 74 ++++++++-- cockatrice/src/interface/theme_manager.h | 9 ++ .../appearance_settings_page.cpp | 26 +++- .../settings_page/appearance_settings_page.h | 2 + 11 files changed, 229 insertions(+), 81 deletions(-) diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index 1b9be1dd4..e01a1caa2 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -1,5 +1,6 @@ #include "palette_editor_dialog.h" +#include "../../client/settings/cache_settings.h" #include "../theme_manager.h" #include "palette_generator.h" #include "palette_grid_widget.h" @@ -8,20 +9,51 @@ #include #include #include +#include +#include +#include #include #include #include +#include #include #include #include #include +// Probe whether a directory is truly writable by trying to create and remove a +// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably +// on Windows where UAC VirtualStore can make a system dir appear writable). +static bool isDirReallyWritable(const QString &dirPath) +{ + const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test"); + QFile f(probe); + if (!f.open(QIODevice::WriteOnly)) { + return false; + } + f.close(); + f.remove(); + return true; +} + PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent) : QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName) { setMinimumSize(740, 220); setupUi(); + // Resolve a writable directory for saving. Built-in (Default / Fusion) and + // other read-only theme directories must be customised in the user-writable + // themes directory; otherwise the write would fail or be lost on upgrade. + if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) { + saveDir = themeDirPath; + } else { + saveDir = QDir(SettingsCache::instance().getThemesPath()).absoluteFilePath(themeName); + if (!QDir().mkpath(saveDir)) { + qWarning() << "Failed to create palette save directory:" << saveDir; + } + } + // Load both scheme configs upfront so switching is instant loadSchemes(); @@ -31,7 +63,9 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt schemeComboBox->setCurrentText(loadedScheme); schemeComboBox->blockSignals(false); + paletteGrid->blockSignals(true); paletteGrid->loadPalette(workingConfig[loadedScheme]); + paletteGrid->blockSignals(false); seedAccentFromScheme(loadedScheme); retranslateUi(); @@ -124,8 +158,7 @@ void PaletteEditorDialog::setupUi() buttonBox = new QDialogButtonBox; resetBtn = buttonBox->addButton(tr("Reset"), QDialogButtonBox::ResetRole); - applyBtn = buttonBox->addButton(tr("Apply"), QDialogButtonBox::ApplyRole); - saveBtn = buttonBox->addButton(tr("Save && Apply"), QDialogButtonBox::AcceptRole); + saveBtn = buttonBox->addButton(tr("Save"), QDialogButtonBox::AcceptRole); closeBtn = buttonBox->addButton(QDialogButtonBox::Close); footerLayout->addWidget(revertButton); @@ -135,10 +168,14 @@ void PaletteEditorDialog::setupUi() // Connections connect(schemeComboBox, &QComboBox::currentTextChanged, this, &PaletteEditorDialog::onSchemeChanged); - connect(quickSetupPanel, &QuickSetupPanel::generateRequested, this, &PaletteEditorDialog::onGenerateFromAccent); + autoApplyTimer = new QTimer(this); + autoApplyTimer->setSingleShot(true); + autoApplyTimer->setInterval(150); + connect(autoApplyTimer, &QTimer::timeout, this, &PaletteEditorDialog::onApply); + connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &PaletteEditorDialog::onGenerateFromAccent); + connect(paletteGrid, &PaletteGridWidget::paletteChanged, this, [this] { autoApplyTimer->start(); }); connect(revertButton, &QPushButton::clicked, this, &PaletteEditorDialog::onRevertToDefault); connect(resetBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onReset); - connect(applyBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onApply); connect(saveBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onSave); connect(closeBtn, &QPushButton::clicked, this, &QDialog::reject); @@ -162,15 +199,8 @@ void PaletteEditorDialog::retranslateUi() setWindowTitle(tr("Palette Editor — %1").arg(themeName)); titleLabel->setText(tr("Palette Editor  ·  %1").arg(themeName)); - // Revert button only makes sense when the theme ships default palette files - const bool hasDefault = PaletteConfig::fromDefault(themeDirPath, "Light").hasPalette() || - PaletteConfig::fromDefault(themeDirPath, "Dark").hasPalette(); - revertButton->setEnabled(hasDefault); - if (!hasDefault) { - revertButton->setToolTip(tr("This theme ships no default palette files")); - } else { - revertButton->setToolTip(tr("Replace current colours with the theme author's defaults")); - } + revertButton->setToolTip( + tr("Delete this scheme's custom palette and revert to the theme default (or the application palette)")); schemeComboBox->setToolTip(tr("Switch between the light and dark palette files")); editingLabel->setText(tr("Editing:")); @@ -179,15 +209,13 @@ void PaletteEditorDialog::retranslateUi() revertButton->setText(tr("↺ Revert to theme default")); resetBtn->setText(tr("Reset")); - applyBtn->setText(tr("Apply")); - saveBtn->setText(tr("Save && Apply")); + saveBtn->setText(tr("Save")); resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette")); - applyBtn->setToolTip(tr("Preview this palette without saving to disk")); saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower())); - if (themeDirPath.isEmpty()) { + if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) { saveBtn->setEnabled(false); - saveBtn->setToolTip(tr("Cannot save: this theme has no directory on disk")); + saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory")); } } @@ -198,7 +226,7 @@ void PaletteEditorDialog::loadSchemes() PaletteConfig cfg = PaletteConfig::fromScheme(themeDirPath, scheme); if (!cfg.hasPalette()) { - cfg = PaletteConfig::fromDefault(themeDirPath, scheme); + cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme); } if (!cfg.hasPalette()) { @@ -235,7 +263,6 @@ void PaletteEditorDialog::onSchemeChanged(const QString &scheme) loadedScheme = scheme; paletteGrid->loadPalette(workingConfig.value(scheme)); seedAccentFromScheme(scheme); - onApply(); } void PaletteEditorDialog::onGenerateFromAccent(const QColor &accent, int intensity) @@ -256,20 +283,34 @@ void PaletteEditorDialog::onSave() return; } - PaletteConfig cfg = paletteGrid->currentPaletteConfig(); + // Snapshot the currently displayed scheme so unsaved edits are not lost. + workingConfig[loadedScheme] = paletteGrid->currentPaletteConfig(); - if (!ThemeManager::savePaletteConfig(themeDirPath, loadedScheme, cfg)) { - QMessageBox::warning(this, tr("Save failed"), - tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), themeDirPath)); - return; + // Persist every scheme that changed, not just the one on screen. Each scheme + // has its own file, so edits to the non-active scheme would otherwise be + // silently discarded when the dialog closes. + for (auto it = workingConfig.begin(); it != workingConfig.end(); ++it) { + const QString &scheme = it.key(); + if (it.value().colors == savedConfig.value(scheme).colors) { + continue; // unchanged — leave the on-disk file alone + } + + if (!ThemeManager::savePaletteConfig(saveDir, scheme, it.value())) { + QMessageBox::warning(this, tr("Save failed"), + tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(scheme), saveDir)); + return; + } } - ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath); - globalCfg.colorScheme = loadedScheme; - globalCfg.save(themeDirPath); + // Keep the saved snapshot in sync so Reset behaves correctly afterwards. + for (auto it = workingConfig.begin(); it != workingConfig.end(); ++it) { + savedConfig[it.key()] = it.value(); + } + + ThemeConfig globalCfg = ThemeConfig::fromThemeDir(saveDir); + globalCfg.colorScheme = loadedScheme; + globalCfg.save(saveDir); - savedConfig[loadedScheme] = cfg; - workingConfig[loadedScheme] = cfg; themeManager->reloadCurrentTheme(); accept(); } @@ -278,18 +319,40 @@ void PaletteEditorDialog::onReset() { workingConfig[loadedScheme] = savedConfig[loadedScheme]; paletteGrid->loadPalette(savedConfig[loadedScheme]); + seedAccentFromScheme(loadedScheme); } void PaletteEditorDialog::onRevertToDefault() { - PaletteConfig def = PaletteConfig::fromDefault(themeDirPath, loadedScheme); + // Delete this scheme's custom palette file so the theme falls back to its + // default (or, when it ships none, the application palette). + // Note: shipped defaults use palette-default-.toml so this only + // removes user-written custom palette files; the theme author's defaults + // are left untouched. + QFile::remove(QDir(saveDir).absoluteFilePath(PaletteConfig::fileName(loadedScheme))); + + // Reload the live theme so the revert takes effect immediately. + themeManager->reloadCurrentTheme(); + + // Reflect the resolved palette (theme default, else current app palette) in + // the editor so it no longer shows the deleted custom colours. + PaletteConfig def = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, loadedScheme); if (!def.hasPalette()) { - QMessageBox::information(this, tr("No default found"), - tr("No default palette file found for the \"%1\" scheme.").arg(loadedScheme)); - return; + const QPalette appPal = qApp->palette(); + for (auto group : {QPalette::Active, QPalette::Disabled, QPalette::Inactive}) { + for (int i = 0; i < QPalette::NColorRoles; ++i) { + auto role = static_cast(i); + if (role != QPalette::NoRole) { + def.colors[group][role] = appPal.color(group, role); + } + } + } } + + savedConfig[loadedScheme] = def; workingConfig[loadedScheme] = def; paletteGrid->loadPalette(def); + seedAccentFromScheme(loadedScheme); } 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 cec4c1700..953902c06 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h @@ -7,6 +7,8 @@ #include #include +class QTimer; + class QLabel; class QComboBox; class QDialogButtonBox; @@ -48,7 +50,6 @@ private: QComboBox *schemeComboBox = nullptr; QDialogButtonBox *buttonBox = nullptr; QPushButton *resetBtn = nullptr; - QPushButton *applyBtn = nullptr; QPushButton *saveBtn = nullptr; QPushButton *closeBtn = nullptr; QPushButton *revertButton = nullptr; @@ -57,10 +58,15 @@ private: QString themeDirPath; QString themeName; QString loadedScheme; + // Directory writes are directed to; may differ from themeDirPath when the + // latter is read-only (e.g. a built-in / system theme directory). + QString saveDir; QMap workingConfig; QMap savedConfig; + QTimer *autoApplyTimer = nullptr; + protected: void changeEvent(QEvent *e) override; }; diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp index e4ac5a16f..67294cd98 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -117,6 +117,7 @@ void PaletteGridWidget::buildGrid(QWidget *host) for (int col = 0; col < 3; ++col) { auto group = ALL_GROUPS[col]; auto *btn = new ColorButton(host); + connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); }); colorButtons[group][role] = btn; grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter); } diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.h b/cockatrice/src/interface/palette_editor/palette_grid_widget.h index 1d85f1ac8..1a665971a 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.h +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.h @@ -22,6 +22,9 @@ public: void loadPalette(const PaletteConfig &cfg); PaletteConfig currentPaletteConfig() const; +signals: + void paletteChanged(); + private: void buildGrid(QWidget *host); void changeEvent(QEvent *e); diff --git a/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp b/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp index fe3af9b52..85098bde5 100644 --- a/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp +++ b/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent) @@ -41,8 +40,6 @@ QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent) intensityPercentageLabel->setFixedWidth(34); intensityPercentageLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); - generateButton = new QPushButton(this); - layout->addWidget(heading); layout->addSpacing(6); layout->addWidget(accentLabel); @@ -54,12 +51,13 @@ QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent) layout->addWidget(labelHigh); layout->addWidget(intensityPercentageLabel); layout->addStretch(); - layout->addWidget(generateButton); - connect(intensitySlider, &QSlider::valueChanged, this, - [this](int v) { intensityPercentageLabel->setText(tr("%1%").arg(v)); }); - connect(generateButton, &QPushButton::clicked, this, - [this] { emit generateRequested(accentButton->getColor(), intensitySlider->value()); }); + connect(intensitySlider, &QSlider::valueChanged, this, [this](int v) { + intensityPercentageLabel->setText(tr("%1%").arg(v)); + emit valueChanged(accentButton->getColor(), v); + }); + connect(accentButton, &ColorButton::colorChanged, this, + [this](const QColor &c) { emit valueChanged(c, intensitySlider->value()); }); retranslateUi(); } @@ -77,10 +75,6 @@ void QuickSetupPanel::retranslateUi() "30–70 Accented — buttons, tooltips, and borders join in\n" "70–100 Full colour — backgrounds, everything")); intensityPercentageLabel->setText(tr("70%")); - - generateButton->setText(tr("Generate ↓")); - generateButton->setToolTip(tr("Derive all palette roles from the accent colour above.\n" - "Fine-tune individual colours in the grid afterwards.")); } QColor QuickSetupPanel::accentColor() const @@ -95,5 +89,7 @@ int QuickSetupPanel::intensity() const void QuickSetupPanel::setAccentColor(const QColor &c) { + accentButton->blockSignals(true); accentButton->setColor(c); -} \ No newline at end of file + accentButton->blockSignals(false); +} diff --git a/cockatrice/src/interface/palette_editor/quick_setup_panel.h b/cockatrice/src/interface/palette_editor/quick_setup_panel.h index dddd0aaa3..c83e27cf2 100644 --- a/cockatrice/src/interface/palette_editor/quick_setup_panel.h +++ b/cockatrice/src/interface/palette_editor/quick_setup_panel.h @@ -5,7 +5,6 @@ #include -class QPushButton; class QHBoxLayout; class QLabel; class QSlider; @@ -16,12 +15,11 @@ class QSlider; * * The panel contains: * - an accent color picker, - * - an intensity slider, - * - and a generate button. + * - an intensity slider. * - * When the user clicks the generate button, the panel emits - * generateRequested() with the currently selected accent color - * and intensity value. + * Whenever either value changes the panel emits valueChanged() with + * the current accent colour and intensity, which the parent dialog + * uses to auto-apply the generated palette. * * Typically used together with PaletteGenerator::fromAccent() * to quickly generate color schemes from a chosen accent color. @@ -71,12 +69,12 @@ public: signals: /** - * @brief Emitted when the user requests palette generation. + * @brief Emitted whenever the accent colour or intensity changes. * - * @param accent The selected accent color. - * @param intensity The selected intensity value. + * The parent dialog consumes this to auto-apply the generated palette + * without requiring an explicit Generate click. */ - void generateRequested(QColor accent, int intensity); + void valueChanged(QColor accent, int intensity); private: QHBoxLayout *layout; @@ -88,7 +86,6 @@ private: QLabel *labelHigh; QSlider *intensitySlider; QLabel *intensityPercentageLabel; - QPushButton *generateButton; }; -#endif // COCKATRICE_QUICK_SETUP_PANEL_H \ No newline at end of file +#endif // COCKATRICE_QUICK_SETUP_PANEL_H diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index b79c91265..3c43c467d 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -245,14 +245,11 @@ PaletteConfig PaletteConfig::fromDefault(const QString &themeDirPath, const QStr bool wantDark = colorScheme.compare("Dark", Qt::CaseInsensitive) == 0; - PaletteConfig cfg = - fromFile(dir.absoluteFilePath(wantDark ? "palette-default-dark.toml" : "palette-default-light.toml")); - - if (!cfg.hasPalette()) { - cfg = fromFile(dir.absoluteFilePath(wantDark ? "palette-default-light.toml" : "palette-default-dark.toml")); - } - - return cfg; + // Only the default file matching the requested scheme is used. Falling back + // to the opposite scheme's default would silently apply dark colours to a + // "Light" scheme (or vice versa). Callers already fall back to the OS / + // application palette when no default palette is available. + return fromFile(dir.absoluteFilePath(wantDark ? "palette-default-dark.toml" : "palette-default-light.toml")); } QPalette PaletteConfig::apply(QPalette base) const diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 7dc757062..c6e2cf902 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -96,9 +96,14 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent) if (defaultStyleName == "windows11") { defaultStyleName = "windowsvista"; } + // Capture the untouched application palette before any theme is applied. + defaultPalette = qApp->palette(); ensureThemeDirectoryExists(); #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) - connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, &ThemeManager::themeChangedSlot); + connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this] { + defaultPalette = qApp->palette(); + themeChangedSlot(); + }); #endif connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &ThemeManager::themeChangedSlot); themeChangedSlot(); @@ -137,6 +142,20 @@ bool ThemeManager::isBuiltInTheme() return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME; } +// System (read-only) themes location, relative to the application binary. +static QString systemThemesBasePath() +{ + QString base = qApp->applicationDirPath(); +#ifdef Q_OS_MAC + base += "/../Resources/themes"; +#elif defined(Q_OS_WIN) + base += "/themes"; +#else // linux + base += "/../share/cockatrice/themes"; +#endif + return base; +} + QStringMap &ThemeManager::getAvailableThemes() { QDir dir; @@ -157,15 +176,7 @@ QStringMap &ThemeManager::getAvailableThemes() } // load themes from cockatrice system dir - dir.setPath(qApp->applicationDirPath() + -#ifdef Q_OS_MAC - "/../Resources/themes" -#elif defined(Q_OS_WIN) - "/themes" -#else // linux - "/../share/cockatrice/themes" -#endif - ); + dir.setPath(systemThemesBasePath()); for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { if (!availableThemes.contains(themeName)) { @@ -242,6 +253,20 @@ bool ThemeManager::savePaletteConfig(const QString &themeDirPath, const QString return true; } +PaletteConfig ThemeManager::loadDefaultPaletteConfig(const QString &themeDirPath, + const QString &themeName, + const QString &colorScheme) +{ + PaletteConfig cfg = PaletteConfig::fromDefault(themeDirPath, colorScheme); + if (!cfg.hasPalette()) { + // The shipped default may live in the system theme directory rather + // than the resolved (user) theme directory, so built-in themes still + // get their curated defaults. + cfg = PaletteConfig::fromDefault(QDir(systemThemesBasePath()).absoluteFilePath(themeName), colorScheme); + } + return cfg; +} + void ThemeManager::setColorScheme(const QString &scheme) { const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); @@ -253,6 +278,17 @@ void ThemeManager::setColorScheme(const QString &scheme) reloadCurrentTheme(); } +void ThemeManager::setStyleName(const QString &styleName) +{ + const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); + ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); + + cfg.styleName = styleName; + + cfg.save(dirPath); + reloadCurrentTheme(); +} + void ThemeManager::reloadCurrentTheme() { themeChangedSlot(); @@ -298,7 +334,11 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, } #endif } else { - base = qApp->palette(); + // Use the pristine startup palette rather than qApp->palette(): the + // latter may already carry a previously-applied custom (e.g. dark) + // palette, which would otherwise persist when switching to a scheme + // that supplies no palette of its own. + base = defaultPalette; } // Overlay custom palette colours @@ -353,7 +393,7 @@ void ThemeManager::themeChangedSlot() // ── Load palette: custom first, then theme default ──────────────────── PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme); if (!palette.hasPalette()) { - palette = PaletteConfig::fromDefault(dirPath, activeScheme); + palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme); } applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); @@ -362,6 +402,16 @@ void ThemeManager::themeChangedSlot() if (!dirPath.isEmpty()) { resources << dir.absolutePath(); } + + // When the resolved dir is a user copy (e.g. user/), also + // include the system theme dir as a fallback so shipped assets like + // zones/*.png and style.css still resolve for themes that ship only + // those files (e.g. Leather, Plasma, Fabric, VelvetMarble). + const QString sysPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName); + if (sysPath != dirPath && QDir(sysPath).exists()) { + resources << sysPath; + } + resources << DEFAULT_RESOURCE_PATHS; QDir::setSearchPaths("theme", resources); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index b9e764d08..861ab838b 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -43,6 +43,10 @@ public: private: QString defaultStyleName; + // Pristine application palette captured at startup, before any custom theme + // palette is applied. Used as the base when a theme supplies no palette, so + // switching away from a custom palette restores the original colours. + QPalette defaultPalette; QString currentThemePath; std::array brushes; QStringMap availableThemes; @@ -76,7 +80,12 @@ public: // Load/save per-scheme palette colors static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme); static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); + // Load the theme's shipped default palette, falling back to the system + // theme directory when it is absent from the resolved (user) directory. + static PaletteConfig + loadDefaultPaletteConfig(const QString &themeDirPath, const QString &themeName, const QString &colorScheme); void setColorScheme(const QString &scheme); + void setStyleName(const QString &styleName); void reloadCurrentTheme(); void previewPalette(const PaletteConfig &cfg, const QString &scheme); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index e00484ebf..17ca9380d 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include AppearanceSettingsPage::AppearanceSettingsPage() @@ -47,6 +48,19 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&schemeCombo, &QComboBox::currentIndexChanged, this, [this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); }); + // Qt widget style; "Default" lets the application decide + styleCombo.addItem(tr("Default"), QStringLiteral("Default")); + for (const QString &key : QStyleFactory::keys()) { + styleCombo.addItem(key, key); + } + + const QString currentStyle = cfg.styleName; + const int styleSeedIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle); + styleCombo.setCurrentIndex(styleSeedIdx >= 0 ? styleSeedIdx : 0); + + connect(&styleCombo, &QComboBox::currentIndexChanged, this, + [this] { themeManager->setStyleName(styleCombo.currentData().toString()); }); + connect(themeManager, &ThemeManager::themeChanged, this, [this, dirPath] { const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir); @@ -56,6 +70,12 @@ AppearanceSettingsPage::AppearanceSettingsPage() const int idx = schemeCombo.findData(current); schemeCombo.setCurrentIndex(idx >= 0 ? idx : 0); schemeCombo.blockSignals(false); + + styleCombo.blockSignals(true); + const QString currentStyle = cfg.styleName; + const int styleIdx = currentStyle.isEmpty() ? 0 : styleCombo.findData(currentStyle); + styleCombo.setCurrentIndex(styleIdx >= 0 ? styleIdx : 0); + styleCombo.blockSignals(false); }); connect(&editPaletteButton, &QPushButton::clicked, this, &AppearanceSettingsPage::editPalette); @@ -66,7 +86,9 @@ AppearanceSettingsPage::AppearanceSettingsPage() themeGrid->addWidget(&openThemeButton, 1, 1); themeGrid->addWidget(&schemeComboLabel, 2, 0); themeGrid->addWidget(&schemeCombo, 2, 1); - themeGrid->addWidget(&editPaletteButton, 3, 1); + themeGrid->addWidget(&styleComboLabel, 3, 0); + themeGrid->addWidget(&styleCombo, 3, 1); + themeGrid->addWidget(&editPaletteButton, 4, 1); themeGroupBox = new QGroupBox; themeGroupBox->setLayout(themeGrid); @@ -400,6 +422,8 @@ void AppearanceSettingsPage::retranslateUi() themeLabel.setText(tr("Current theme:")); openThemeButton.setText(tr("Open themes folder")); schemeComboLabel.setText(tr("Active theme palette:")); + styleComboLabel.setText(tr("Active theme style:")); + styleCombo.setToolTip(tr("Qt widget style saved to this theme (\"Default\" lets the application decide)")); editPaletteButton.setText(tr("Edit theme palette")); homeTabGroupBox->setTitle(tr("Home tab settings")); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index e223d70f8..09b73ad04 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -31,6 +31,8 @@ private: QPushButton openThemeButton; QLabel schemeComboLabel; QComboBox schemeCombo; + QLabel styleComboLabel; + QComboBox styleCombo; QPushButton editPaletteButton; QLabel homeTabBackgroundSourceLabel; QComboBox homeTabBackgroundSourceBox;