From c3ebaefd85d178ecf3840e1d7bd7350d224758c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 12:26:43 +0200 Subject: [PATCH 01/12] Read user path, then fall through to system, only write to user. Took 1 hour 21 minutes Took 5 seconds Took 40 seconds --- .../palette_editor/palette_editor_dialog.cpp | 76 ++++++--- .../palette_editor/palette_editor_dialog.h | 1 + cockatrice/src/interface/theme_manager.cpp | 149 +++++++++++------- cockatrice/src/interface/theme_manager.h | 2 + 4 files changed, 147 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..c4cb189af 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -19,13 +19,15 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent) : QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName) { + userThemeDirPath = ThemeManager::userThemeDirFor(themeName); + setMinimumSize(740, 220); setupUi(); // Load both scheme configs upfront so switching is instant loadSchemes(); - loadedScheme = themeManager->isDarkMode(themeDirPath) ? "Dark" : "Light"; + loadedScheme = themeManager->isDarkMode(themeDirPath, userThemeDirPath) ? "Dark" : "Light"; schemeComboBox->blockSignals(true); schemeComboBox->setCurrentText(loadedScheme); @@ -33,7 +35,6 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt paletteGrid->loadPalette(workingConfig[loadedScheme]); seedAccentFromScheme(loadedScheme); - retranslateUi(); } @@ -162,15 +163,12 @@ 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")); - } + const bool hasUserOverride = + QFile::exists(QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName("Light"))) || + QFile::exists(QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName("Dark"))); + revertButton->setEnabled(hasUserOverride); + revertButton->setToolTip(hasUserOverride ? tr("Delete your custom palette and restore the shipped defaults") + : tr("No custom palette overrides exist for this theme")); schemeComboBox->setToolTip(tr("Switch between the light and dark palette files")); editingLabel->setText(tr("Editing:")); @@ -195,8 +193,11 @@ void PaletteEditorDialog::loadSchemes() { const QStringList schemes = {"Light", "Dark"}; for (const QString &scheme : schemes) { - PaletteConfig cfg = PaletteConfig::fromScheme(themeDirPath, scheme); - + // user customisation → system palette → system default → app palette + PaletteConfig cfg = PaletteConfig::fromScheme(userThemeDirPath, scheme); + if (!cfg.hasPalette()) { + cfg = PaletteConfig::fromScheme(themeDirPath, scheme); + } if (!cfg.hasPalette()) { cfg = PaletteConfig::fromDefault(themeDirPath, scheme); } @@ -258,15 +259,17 @@ void PaletteEditorDialog::onSave() PaletteConfig cfg = 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)); + if (!ThemeManager::savePaletteConfig(userThemeDirPath, loadedScheme, cfg)) { + QMessageBox::warning( + this, tr("Save failed"), + tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), userThemeDirPath)); return; } + // Record the active scheme in the user dir — never touch the system (read-only) dir ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath); globalCfg.colorScheme = loadedScheme; - globalCfg.save(themeDirPath); + globalCfg.save(userThemeDirPath); savedConfig[loadedScheme] = cfg; workingConfig[loadedScheme] = cfg; @@ -282,14 +285,45 @@ void PaletteEditorDialog::onReset() void PaletteEditorDialog::onRevertToDefault() { - PaletteConfig def = PaletteConfig::fromDefault(themeDirPath, loadedScheme); - if (!def.hasPalette()) { - QMessageBox::information(this, tr("No default found"), - tr("No default palette file found for the \"%1\" scheme.").arg(loadedScheme)); + const QString filePath = QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName(loadedScheme)); + + if (!QFile::exists(filePath)) { + // Button should already be disabled in this state, but be defensive return; } + + const auto reply = + QMessageBox::question(this, tr("Revert to default?"), + tr("This will permanently delete your custom \"%1\" palette for \"%2\".\n\nContinue?") + .arg(loadedScheme, themeName), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (reply != QMessageBox::Yes) { + return; + } + + QFile::remove(filePath); + + // Reload via fallthrough: system palette → system default → app palette + PaletteConfig def = PaletteConfig::fromScheme(themeDirPath, loadedScheme); + if (!def.hasPalette()) { + def = PaletteConfig::fromDefault(themeDirPath, loadedScheme); + } + if (!def.hasPalette()) { + 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); + retranslateUi(); // update button enabled state } 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..8da7c5589 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h @@ -54,6 +54,7 @@ private: QPushButton *revertButton = nullptr; // State + QString userThemeDirPath; QString themeDirPath; QString themeName; QString loadedScheme; diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 086845fe6..556ab10df 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -113,21 +113,26 @@ void ThemeManager::ensureThemeDirectoryExists() } } -bool ThemeManager::isDarkMode(const QString &themeDirPath) +bool ThemeManager::isDarkMode(const QString &themeDirPath, const QString &userDirPath) { - ThemeConfig themeConfig = ThemeConfig::fromThemeDir(themeDirPath); - if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) { - return true; - } else if (themeConfig.colorScheme.compare("Light", Qt::CaseInsensitive) == 0) { - return false; - } else { -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) - bool osDark = (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark); -#else - bool osDark = false; -#endif - return osDark; + // User override takes precedence over system config + for (const QString &path : {userDirPath, themeDirPath}) { + if (path.isEmpty()) { + continue; + } + ThemeConfig cfg = ThemeConfig::fromThemeDir(path); + if (cfg.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) { + return true; + } + if (cfg.colorScheme.compare("Light", Qt::CaseInsensitive) == 0) { + return false; + } } +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + return (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark); +#else + return false; +#endif } bool ThemeManager::isBuiltInTheme() @@ -137,42 +142,49 @@ bool ThemeManager::isBuiltInTheme() return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME; } +QString ThemeManager::userThemeDirFor(const QString &themeName) +{ + return QDir(SettingsCache::instance().getThemesPath()).absoluteFilePath(themeName); +} + QStringMap &ThemeManager::getAvailableThemes() { - QDir dir; availableThemes.clear(); - // load themes from user profile dir - dir.setPath(SettingsCache::instance().getThemesPath()); - - // add default value - availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default")); - - availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion")); - - for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { - if (!availableThemes.contains(themeName)) { - availableThemes.insert(themeName, dir.absoluteFilePath(themeName)); - } - } - - // load themes from cockatrice system dir - dir.setPath(qApp->applicationDirPath() + + // ── 1. System themes (read-only, shipped with the application) ────────── + QDir sysDir(qApp->applicationDirPath() + #ifdef Q_OS_MAC "/../Resources/themes" #elif defined(Q_OS_WIN) "/themes" -#else // linux +#else "/../share/cockatrice/themes" #endif ); + for (const QString &name : sysDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { + availableThemes.insert(name, sysDir.absoluteFilePath(name)); + } - for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { - if (!availableThemes.contains(themeName)) { - availableThemes.insert(themeName, dir.absoluteFilePath(themeName)); + // ── 2. User-only themes (AppData) ──────────────────────────────────────── + // We only add themes that don't already exist in the system directory. + // Customisations to system themes are handled via the fallthrough read + // logic (userThemeDirFor → system path); we intentionally keep the system + // path in the map so shipped assets are always locatable. + QDir userDir(SettingsCache::instance().getThemesPath()); + for (const QString &name : userDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { + if (!availableThemes.contains(name)) { + availableThemes.insert(name, userDir.absoluteFilePath(name)); } } + // ── 3. Ensure built-in sentinels always exist (dev builds without install) + if (!availableThemes.contains(NONE_THEME_NAME)) { + availableThemes.insert(NONE_THEME_NAME, userDir.absoluteFilePath("Default")); + } + if (!availableThemes.contains(FUSION_THEME_NAME)) { + availableThemes.insert(FUSION_THEME_NAME, userDir.absoluteFilePath("Fusion")); + } + return availableThemes; } @@ -244,12 +256,14 @@ bool ThemeManager::savePaletteConfig(const QString &themeDirPath, const QString void ThemeManager::setColorScheme(const QString &scheme) { - const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); + const QString themeName = SettingsCache::instance().getThemeName(); + const QString dirPath = getAvailableThemes().value(themeName); + const QString userDirPath = userThemeDirFor(themeName); + + // Read base config from system; write the scheme override to user dir ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); - cfg.colorScheme = scheme; - - cfg.save(dirPath); + cfg.save(userDirPath); reloadCurrentTheme(); } @@ -262,7 +276,13 @@ void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &schem { const QString themeName = SettingsCache::instance().getThemeName(); const QString dirPath = getAvailableThemes().value(themeName); - const ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath); + const QString userDirPath = userThemeDirFor(themeName); + + ThemeConfig themeCfg = ThemeConfig::fromThemeDir(userDirPath); + if (themeCfg.colorScheme.isEmpty() && themeCfg.styleName.isEmpty()) { + themeCfg = ThemeConfig::fromThemeDir(dirPath); + } + applyStyleAndPalette(themeName, themeCfg, cfg, scheme); } @@ -325,50 +345,59 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, } } -void ThemeManager::themeChangedSlot() +vvoid ThemeManager::themeChangedSlot() { - QString themeName = SettingsCache::instance().getThemeName(); - QString dirPath = getAvailableThemes().value(themeName); + const QString themeName = SettingsCache::instance().getThemeName(); + const QString dirPath = getAvailableThemes().value(themeName); // system path + const QString userDirPath = userThemeDirFor(themeName); // user override path currentThemePath = dirPath; - QDir dir(dirPath); - // CSS - if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) { - qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME)); - } else { + // CSS — user override first, then system + const auto tryLoadCss = [](const QString &path) -> bool { + QDir d(path); + if (!path.isEmpty() && d.exists(STYLE_CSS_NAME)) { + qApp->setStyleSheet("file:///" + d.absoluteFilePath(STYLE_CSS_NAME)); + return true; + } + return false; + }; + if (!tryLoadCss(userDirPath) && !tryLoadCss(dirPath)) { qApp->setStyleSheet(""); } - // load theme.cfg for style + scheme preference - ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath); + // ThemeConfig — user override first, then system + ThemeConfig themeCfg = ThemeConfig::fromThemeDir(userDirPath); + if (themeCfg.colorScheme.isEmpty() && themeCfg.styleName.isEmpty()) { + themeCfg = ThemeConfig::fromThemeDir(dirPath); + } - // Resolve active scheme: - // theme.cfg says Dark/Light → use that - // theme.cfg says System or is absent → follow the OS - QString activeScheme = isDarkMode(dirPath) ? "Dark" : "Light"; + const QString activeScheme = isDarkMode(dirPath, userDirPath) ? "Dark" : "Light"; - // ── Load palette: custom first, then theme default ──────────────────── - PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme); + // Palette — user customisation → system palette → system default + PaletteConfig palette = PaletteConfig::fromScheme(userDirPath, activeScheme); + if (!palette.hasPalette()) { + palette = PaletteConfig::fromScheme(dirPath, activeScheme); + } if (!palette.hasPalette()) { palette = PaletteConfig::fromDefault(dirPath, activeScheme); } applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); + // Search paths — user assets shadow system assets, both fall through to builtins QStringList resources; + if (QDir(userDirPath).exists()) { + resources << userDirPath; + } if (!dirPath.isEmpty()) { - resources << dir.absolutePath(); + resources << dirPath; } resources << DEFAULT_RESOURCE_PATHS; - QDir::setSearchPaths("theme", resources); brushes[Role::Hand] = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT); - brushes[Role::Table] = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT); - brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT); - brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT); for (auto &brushCache : brushesCache) { brushCache.clear(); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index b9e764d08..7091a26e5 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -63,6 +63,8 @@ protected: public: bool isBuiltInTheme(); bool isDarkMode(const QString &themeDirPath); + static bool isDarkMode(const QString &themeDirPath, const QString &userDirPath = {}); + static QString userThemeDirFor(const QString &themeName); QStringMap &getAvailableThemes(); // Returns the path to the currently active theme directory (empty = default) QString getCurrentThemePath() const From 3ea298bfef26c316bd4add8135c000a8084d04ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 12:36:24 +0200 Subject: [PATCH 02/12] Typo. Took 2 minutes --- cockatrice/src/interface/theme_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 556ab10df..95623276b 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -345,7 +345,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, } } -vvoid ThemeManager::themeChangedSlot() +void ThemeManager::themeChangedSlot() { const QString themeName = SettingsCache::instance().getThemeName(); const QString dirPath = getAvailableThemes().value(themeName); // system path From 79bb8a957221a343f99135515bf57ae64338e2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 13:25:57 +0200 Subject: [PATCH 03/12] Add style comboBox Took 12 minutes Took 3 seconds Took 2 minutes --- cockatrice/src/interface/theme_manager.cpp | 35 +++++++++++++ cockatrice/src/interface/theme_manager.h | 2 + .../appearance_settings_page.cpp | 52 +++++++++++++------ .../settings_page/appearance_settings_page.h | 3 ++ 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 95623276b..7d841c3be 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -188,6 +188,41 @@ QStringMap &ThemeManager::getAvailableThemes() return availableThemes; } +ThemeConfig ThemeManager::effectiveThemeConfig(const QString &themeName) +{ + const QString dirPath = ThemeManager().getAvailableThemes().value(themeName); + const QString userDirPath = userThemeDirFor(themeName); + + ThemeConfig userCfg = ThemeConfig::fromThemeDir(userDirPath); + ThemeConfig systemCfg = ThemeConfig::fromThemeDir(dirPath); + + ThemeConfig result = systemCfg; + + // User values override system values on a per-field basis + if (!userCfg.colorScheme.isEmpty()) { + result.colorScheme = userCfg.colorScheme; + } + + if (!userCfg.styleName.isEmpty()) { + result.styleName = userCfg.styleName; + } + + return result; +} + +void ThemeManager::setStyleName(const QString &styleName) +{ + const QString themeName = SettingsCache::instance().getThemeName(); + const QString dirPath = getAvailableThemes().value(themeName); + const QString userDirPath = userThemeDirFor(themeName); + + ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); + cfg.styleName = styleName; + cfg.save(userDirPath); + + reloadCurrentTheme(); +} + QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor) { QBrush brush; diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index 7091a26e5..64a021bfc 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -66,6 +66,8 @@ public: static bool isDarkMode(const QString &themeDirPath, const QString &userDirPath = {}); static QString userThemeDirFor(const QString &themeName); QStringMap &getAvailableThemes(); + ThemeConfig effectiveThemeConfig(const QString &themeName); + void setStyleName(const QString &styleName); // Returns the path to the currently active theme directory (empty = default) QString getCurrentThemePath() const { 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 2d32f3ce1..78a171c0b 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() @@ -31,32 +32,33 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&themeBox, qOverload(&QComboBox::currentIndexChanged), this, &AppearanceSettingsPage::themeBoxChanged); connect(&openThemeButton, &QPushButton::clicked, this, &AppearanceSettingsPage::openThemeLocation); + // Populate Scheme + schemeCombo.addItem(tr("Light"), QStringLiteral("Light")); schemeCombo.addItem(tr("Dark"), QStringLiteral("Dark")); #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) schemeCombo.addItem(tr("System"), QStringLiteral("System")); #endif - // Seed from whatever the current theme already has saved - const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); - const ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); - const QString current = cfg.colorScheme; - const int seedIdx = schemeCombo.findData(current); - schemeCombo.setCurrentIndex(seedIdx >= 0 ? seedIdx : 0); - connect(&schemeCombo, &QComboBox::currentIndexChanged, this, [this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); }); - connect(themeManager, &ThemeManager::themeChanged, this, [this, dirPath] { - const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); - const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir); - const QString current = cfg.colorScheme; + // Populate Style - schemeCombo.blockSignals(true); - const int idx = schemeCombo.findData(current); - schemeCombo.setCurrentIndex(idx >= 0 ? idx : 0); - schemeCombo.blockSignals(false); - }); + styleComboBox.addItem(tr("Default"), QString()); + + for (const QString &style : QStyleFactory::keys()) { + styleComboBox.addItem(style, style); + } + + connect(&styleComboBox, &QComboBox::currentIndexChanged, this, + [this] { themeManager->setStyleName(styleComboBox.currentData().toString()); }); + + // Refresh theme and style + + reloadThemeSettings(); + + connect(themeManager, &ThemeManager::themeChanged, this, &AppearanceSettingsPage::reloadThemeSettings); connect(&editPaletteButton, &QPushButton::clicked, this, &AppearanceSettingsPage::editPalette); @@ -66,7 +68,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(&styleLabel, 3, 0); + themeGrid->addWidget(&styleComboBox, 3, 1); + themeGrid->addWidget(&editPaletteButton, 4, 1); themeGroupBox = new QGroupBox; themeGroupBox->setLayout(themeGrid); @@ -320,6 +324,19 @@ void AppearanceSettingsPage::openThemeLocation() } } +void AppearanceSettingsPage::reloadThemeSettings() +{ + const ThemeConfig cfg = themeManager->effectiveThemeConfig(SettingsCache::instance().getThemeName()); + + schemeCombo.blockSignals(true); + schemeCombo.setCurrentIndex(std::max(0, schemeCombo.findData(cfg.colorScheme))); + schemeCombo.blockSignals(false); + + styleComboBox.blockSignals(true); + styleComboBox.setCurrentIndex(qMax(0, styleComboBox.findData(cfg.styleName))); + styleComboBox.blockSignals(false); +} + void AppearanceSettingsPage::editPalette() { PaletteEditorDialog dlg(themeManager->getCurrentThemePath(), SettingsCache::instance().getThemeName(), this); @@ -390,6 +407,7 @@ void AppearanceSettingsPage::retranslateUi() themeLabel.setText(tr("Current theme:")); openThemeButton.setText(tr("Open themes folder")); schemeComboLabel.setText(tr("Active theme palette:")); + styleLabel.setText(tr("Active theme style:")); 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 9ed27be4d..be2b3da42 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -17,6 +17,7 @@ class AppearanceSettingsPage : public AbstractSettingsPage private slots: void themeBoxChanged(int index); void openThemeLocation(); + void reloadThemeSettings(); void editPalette(); void updateHomeTabSettingsVisibility(); void showShortcutsChanged(QT_STATE_CHANGED_T enabled); @@ -31,6 +32,8 @@ private: QPushButton openThemeButton; QLabel schemeComboLabel; QComboBox schemeCombo; + QLabel styleLabel; + QComboBox styleComboBox; QPushButton editPaletteButton; QLabel homeTabBackgroundSourceLabel; QComboBox homeTabBackgroundSourceBox; From 59afc7047a33480ff6fac191cbabe845f9f8d85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 14:37:06 +0200 Subject: [PATCH 04/12] Start from effective config. Took 8 minutes --- cockatrice/src/interface/theme_manager.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 7d841c3be..6b1cb6bbf 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -210,14 +210,14 @@ ThemeConfig ThemeManager::effectiveThemeConfig(const QString &themeName) return result; } -void ThemeManager::setStyleName(const QString &styleName) +void ThemeManager::setStyleName(const QString &style) { const QString themeName = SettingsCache::instance().getThemeName(); - const QString dirPath = getAvailableThemes().value(themeName); const QString userDirPath = userThemeDirFor(themeName); - ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); - cfg.styleName = styleName; + ThemeConfig cfg = effectiveThemeConfig(themeName); + + cfg.styleName = style; cfg.save(userDirPath); reloadCurrentTheme(); @@ -292,13 +292,13 @@ bool ThemeManager::savePaletteConfig(const QString &themeDirPath, const QString void ThemeManager::setColorScheme(const QString &scheme) { const QString themeName = SettingsCache::instance().getThemeName(); - const QString dirPath = getAvailableThemes().value(themeName); const QString userDirPath = userThemeDirFor(themeName); - // Read base config from system; write the scheme override to user dir - ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); + ThemeConfig cfg = effectiveThemeConfig(themeName); + cfg.colorScheme = scheme; cfg.save(userDirPath); + reloadCurrentTheme(); } From 2e754f6ec0a602247d4a36b3c6d1ff4d47b760ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 15:24:55 +0200 Subject: [PATCH 05/12] Give the other default themes palettes and configs. Took 3 minutes --- .../themes/Fabric/palette-default-dark.toml | 63 +++++++++++++++++++ cockatrice/themes/Fabric/theme.cfg | 5 ++ .../themes/Leather/palette-default-dark.toml | 63 +++++++++++++++++++ cockatrice/themes/Leather/theme.cfg | 5 ++ .../themes/Plasma/palette-default-dark.toml | 63 +++++++++++++++++++ cockatrice/themes/Plasma/theme.cfg | 5 ++ .../VelvetMarble/palette-default-dark.toml | 63 +++++++++++++++++++ cockatrice/themes/VelvetMarble/theme.cfg | 5 ++ 8 files changed, 272 insertions(+) create mode 100644 cockatrice/themes/Fabric/palette-default-dark.toml create mode 100644 cockatrice/themes/Fabric/theme.cfg create mode 100644 cockatrice/themes/Leather/palette-default-dark.toml create mode 100644 cockatrice/themes/Leather/theme.cfg create mode 100644 cockatrice/themes/Plasma/palette-default-dark.toml create mode 100644 cockatrice/themes/Plasma/theme.cfg create mode 100644 cockatrice/themes/VelvetMarble/palette-default-dark.toml create mode 100644 cockatrice/themes/VelvetMarble/theme.cfg diff --git a/cockatrice/themes/Fabric/palette-default-dark.toml b/cockatrice/themes/Fabric/palette-default-dark.toml new file mode 100644 index 000000000..3ee174a2f --- /dev/null +++ b/cockatrice/themes/Fabric/palette-default-dark.toml @@ -0,0 +1,63 @@ +[Palette] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff000000 +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + +[Palette.Disabled] +WindowText = #ff9d9d9d +Button = #ff1c1c1c +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ff9d9d9d +BrightText = #ffb4dd8b +ButtonText = #ff787878 +Base = #ff1c1c1c +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff9d9d9d +Link = #ff308cc6 +LinkVisited = #ffb450ff +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #46ffffff + +[Palette.Inactive] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ffffffff +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + diff --git a/cockatrice/themes/Fabric/theme.cfg b/cockatrice/themes/Fabric/theme.cfg new file mode 100644 index 000000000..d2016a238 --- /dev/null +++ b/cockatrice/themes/Fabric/theme.cfg @@ -0,0 +1,5 @@ +[Appearance] +ColorScheme = Light + +[Style] +Name = Default diff --git a/cockatrice/themes/Leather/palette-default-dark.toml b/cockatrice/themes/Leather/palette-default-dark.toml new file mode 100644 index 000000000..3ee174a2f --- /dev/null +++ b/cockatrice/themes/Leather/palette-default-dark.toml @@ -0,0 +1,63 @@ +[Palette] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff000000 +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + +[Palette.Disabled] +WindowText = #ff9d9d9d +Button = #ff1c1c1c +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ff9d9d9d +BrightText = #ffb4dd8b +ButtonText = #ff787878 +Base = #ff1c1c1c +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff9d9d9d +Link = #ff308cc6 +LinkVisited = #ffb450ff +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #46ffffff + +[Palette.Inactive] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ffffffff +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + diff --git a/cockatrice/themes/Leather/theme.cfg b/cockatrice/themes/Leather/theme.cfg new file mode 100644 index 000000000..d2016a238 --- /dev/null +++ b/cockatrice/themes/Leather/theme.cfg @@ -0,0 +1,5 @@ +[Appearance] +ColorScheme = Light + +[Style] +Name = Default diff --git a/cockatrice/themes/Plasma/palette-default-dark.toml b/cockatrice/themes/Plasma/palette-default-dark.toml new file mode 100644 index 000000000..3ee174a2f --- /dev/null +++ b/cockatrice/themes/Plasma/palette-default-dark.toml @@ -0,0 +1,63 @@ +[Palette] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff000000 +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + +[Palette.Disabled] +WindowText = #ff9d9d9d +Button = #ff1c1c1c +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ff9d9d9d +BrightText = #ffb4dd8b +ButtonText = #ff787878 +Base = #ff1c1c1c +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff9d9d9d +Link = #ff308cc6 +LinkVisited = #ffb450ff +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #46ffffff + +[Palette.Inactive] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ffffffff +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + diff --git a/cockatrice/themes/Plasma/theme.cfg b/cockatrice/themes/Plasma/theme.cfg new file mode 100644 index 000000000..d2016a238 --- /dev/null +++ b/cockatrice/themes/Plasma/theme.cfg @@ -0,0 +1,5 @@ +[Appearance] +ColorScheme = Light + +[Style] +Name = Default diff --git a/cockatrice/themes/VelvetMarble/palette-default-dark.toml b/cockatrice/themes/VelvetMarble/palette-default-dark.toml new file mode 100644 index 000000000..3ee174a2f --- /dev/null +++ b/cockatrice/themes/VelvetMarble/palette-default-dark.toml @@ -0,0 +1,63 @@ +[Palette] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff000000 +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + +[Palette.Disabled] +WindowText = #ff9d9d9d +Button = #ff1c1c1c +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ff9d9d9d +BrightText = #ffb4dd8b +ButtonText = #ff787878 +Base = #ff1c1c1c +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ff9d9d9d +Link = #ff308cc6 +LinkVisited = #ffb450ff +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #46ffffff + +[Palette.Inactive] +WindowText = #ffffffff +Button = #ff383838 +Light = #ff737373 +Midlight = #ff525252 +Dark = #ff161616 +Mid = #ff252525 +Text = #ffffffff +BrightText = #ffb4dd8b +ButtonText = #ffffffff +Base = #ff2b2b2b +Window = #ff1c1c1c +Shadow = #ff000000 +HighlightedText = #ffffffff +Link = #ffcde4b6 +LinkVisited = #ff99d999 +AlternateBase = #ff242424 +ToolTipBase = #ffffffdc +ToolTipText = #ff000000 +PlaceholderText = #6effffff + diff --git a/cockatrice/themes/VelvetMarble/theme.cfg b/cockatrice/themes/VelvetMarble/theme.cfg new file mode 100644 index 000000000..d2016a238 --- /dev/null +++ b/cockatrice/themes/VelvetMarble/theme.cfg @@ -0,0 +1,5 @@ +[Appearance] +ColorScheme = Light + +[Style] +Name = Default From c1098399da5efe88dcf6157044ad6765db0a8583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 15:56:40 +0200 Subject: [PATCH 06/12] Don't make new ThemeManager instance. Took 8 minutes --- .../palette_editor/palette_editor_dialog.cpp | 2 +- cockatrice/src/interface/theme_manager.cpp | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index c4cb189af..dcbb49644 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -267,7 +267,7 @@ void PaletteEditorDialog::onSave() } // Record the active scheme in the user dir — never touch the system (read-only) dir - ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath); + ThemeConfig globalCfg = themeManager->effectiveThemeConfig(themeName); globalCfg.colorScheme = loadedScheme; globalCfg.save(userThemeDirPath); diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 6b1cb6bbf..2c56a03ff 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -190,7 +190,7 @@ QStringMap &ThemeManager::getAvailableThemes() ThemeConfig ThemeManager::effectiveThemeConfig(const QString &themeName) { - const QString dirPath = ThemeManager().getAvailableThemes().value(themeName); + const QString dirPath = getAvailableThemes().value(themeName); const QString userDirPath = userThemeDirFor(themeName); ThemeConfig userCfg = ThemeConfig::fromThemeDir(userDirPath); @@ -310,14 +310,8 @@ void ThemeManager::reloadCurrentTheme() void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &scheme) { const QString themeName = SettingsCache::instance().getThemeName(); - const QString dirPath = getAvailableThemes().value(themeName); - const QString userDirPath = userThemeDirFor(themeName); - - ThemeConfig themeCfg = ThemeConfig::fromThemeDir(userDirPath); - if (themeCfg.colorScheme.isEmpty() && themeCfg.styleName.isEmpty()) { - themeCfg = ThemeConfig::fromThemeDir(dirPath); - } + ThemeConfig themeCfg = effectiveThemeConfig(themeName); applyStyleAndPalette(themeName, themeCfg, cfg, scheme); } @@ -401,10 +395,7 @@ void ThemeManager::themeChangedSlot() } // ThemeConfig — user override first, then system - ThemeConfig themeCfg = ThemeConfig::fromThemeDir(userDirPath); - if (themeCfg.colorScheme.isEmpty() && themeCfg.styleName.isEmpty()) { - themeCfg = ThemeConfig::fromThemeDir(dirPath); - } + ThemeConfig themeCfg = effectiveThemeConfig(themeName); const QString activeScheme = isDarkMode(dirPath, userDirPath) ? "Dark" : "Light"; From d8be3dae9c82728eb05128bc3c02fd6cef7f52a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 18:20:34 +0200 Subject: [PATCH 07/12] Apply wiped theme on revert. Took 4 minutes --- .../interface/palette_editor/palette_editor_dialog.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index dcbb49644..cd247a18d 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -322,8 +322,13 @@ void PaletteEditorDialog::onRevertToDefault() savedConfig[loadedScheme] = def; workingConfig[loadedScheme] = def; - paletteGrid->loadPalette(def); - retranslateUi(); // update button enabled state + + ThemeConfig globalCfg = themeManager->effectiveThemeConfig(themeName); + globalCfg.colorScheme = loadedScheme; + globalCfg.save(userThemeDirPath); + + themeManager->reloadCurrentTheme(); + accept(); } void PaletteEditorDialog::changeEvent(QEvent *e) From e6a5461dad328cba720f3963eb5a4c85c21288a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 20:33:35 +0200 Subject: [PATCH 08/12] Don't bleed through previous themes. Took 3 minutes --- cockatrice/src/interface/theme_manager.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 2c56a03ff..d352d0502 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -344,7 +344,17 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, } #endif } else { - base = qApp->palette(); + // Always start from the style's own standard palette so a previously-applied + // theme can never bleed through. qApp->palette() carries the result of the + // last theme application and must not be used as a base here — if the current + // theme ships no palette file, hasPalette() is false and palCfg.apply() is + // skipped, meaning whatever we put in base IS the effective palette. + base = style->standardPalette(); + if (!base.color(QPalette::Window).isValid()) { + // Exotic platform style that doesn't implement standardPalette() — + // accept the bleed-through risk rather than crash or use garbage. + base = qApp->palette(); + } } // Overlay custom palette colours From 4fbe85616ddbbe88ac6433d958b4acdc32067348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 21:26:00 +0200 Subject: [PATCH 09/12] Don't fall back to alternate palette if desired mode is not available. Took 16 minutes --- cockatrice/src/interface/theme_config.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index b79c91265..a8d068daa 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -248,10 +248,6 @@ PaletteConfig PaletteConfig::fromDefault(const QString &themeDirPath, const QStr 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; } From cfda677afe5335480cd6c3282dee6d3aadf048cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 21:30:32 +0200 Subject: [PATCH 10/12] Reflect per-scheme override reversion availability. Took 5 minutes Took 3 seconds --- .../palette_editor/palette_editor_dialog.cpp | 20 +++++++++++++------ .../palette_editor/palette_editor_dialog.h | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index cd247a18d..79875f0b0 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -158,17 +158,22 @@ void PaletteEditorDialog::setupUi() }); } +void PaletteEditorDialog::updateUserOverrideState() +{ + const bool hasUserOverride = + QFile::exists(QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName(loadedScheme))); + + revertButton->setEnabled(hasUserOverride); + revertButton->setToolTip(hasUserOverride ? tr("Delete your custom palette and restore the shipped defaults") + : tr("No custom palette overrides exist for this scheme")); +} + void PaletteEditorDialog::retranslateUi() { setWindowTitle(tr("Palette Editor — %1").arg(themeName)); titleLabel->setText(tr("Palette Editor  ·  %1").arg(themeName)); - const bool hasUserOverride = - QFile::exists(QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName("Light"))) || - QFile::exists(QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName("Dark"))); - revertButton->setEnabled(hasUserOverride); - revertButton->setToolTip(hasUserOverride ? tr("Delete your custom palette and restore the shipped defaults") - : tr("No custom palette overrides exist for this theme")); + updateUserOverrideState(); schemeComboBox->setToolTip(tr("Switch between the light and dark palette files")); editingLabel->setText(tr("Editing:")); @@ -236,6 +241,9 @@ void PaletteEditorDialog::onSchemeChanged(const QString &scheme) loadedScheme = scheme; paletteGrid->loadPalette(workingConfig.value(scheme)); seedAccentFromScheme(scheme); + + updateUserOverrideState(); + onApply(); } diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h index 8da7c5589..024c2632a 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h @@ -31,6 +31,7 @@ private slots: private: void setupUi(); + void updateUserOverrideState(); void retranslateUi(); void refreshChromePalettes(); void loadScheme(const QString &scheme); // snapshot current, switch, load From 19b3e8b2868eeee34818355103113418c6f7d87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Fri, 19 Jun 2026 23:28:19 +0200 Subject: [PATCH 11/12] Revert style palette. Took 6 minutes Took 14 seconds --- cockatrice/src/interface/theme_manager.cpp | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index d352d0502..2c56a03ff 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -344,17 +344,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, } #endif } else { - // Always start from the style's own standard palette so a previously-applied - // theme can never bleed through. qApp->palette() carries the result of the - // last theme application and must not be used as a base here — if the current - // theme ships no palette file, hasPalette() is false and palCfg.apply() is - // skipped, meaning whatever we put in base IS the effective palette. - base = style->standardPalette(); - if (!base.color(QPalette::Window).isValid()) { - // Exotic platform style that doesn't implement standardPalette() — - // accept the bleed-through risk rather than crash or use garbage. - base = qApp->palette(); - } + base = qApp->palette(); } // Overlay custom palette colours From 3de441a823d0cfe7a47cc1d881e5fba7ca51014a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sat, 20 Jun 2026 01:26:04 +0200 Subject: [PATCH 12/12] Snapshot system palette. Took 8 minutes Took 11 seconds --- cockatrice/src/interface/theme_manager.cpp | 23 +++++++++++++++++++++- cockatrice/src/interface/theme_manager.h | 2 ++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 2c56a03ff..47b921945 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -97,6 +97,13 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent) defaultStyleName = "windowsvista"; } ensureThemeDirectoryExists(); + + // Capture the QPA-initialised palette before we ever call qApp->setPalette(). + // Once setPalette() is called, is_app_palette is locked true and neither + // setStyle() nor colorSchemeChanged will update it automatically. + // This snapshot is our guaranteed-clean base for applyStyleAndPalette. + systemPalette = qApp->palette(); + #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, &ThemeManager::themeChangedSlot); #endif @@ -344,7 +351,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, } #endif } else { - base = qApp->palette(); + base = systemPalette; } // Overlay custom palette colours @@ -434,6 +441,20 @@ void ThemeManager::themeChangedSlot() emit themeChanged(); } +void ThemeManager::onColorSchemeChanged() +{ + // qApp->palette() is locked (is_app_palette = true), so the QPA won't push + // the new OS palette through automatically. style->polish(QPalette&) queries + // GetSysColor on Windows — adequate for light mode, imperfect for Win11 dark + // mode (which uses a different API), but better than a stale light snapshot. + QPalette fresh; + qApp->style()->polish(fresh); + if (fresh.color(QPalette::Window).isValid()) { + systemPalette = fresh; + } + themeChangedSlot(); +} + static QString roleBgName(ThemeManager::Role role) { switch (role) { diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index 64a021bfc..191c5927e 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -42,6 +42,7 @@ public: }; private: + QPalette systemPalette; QString defaultStyleName; QString currentThemePath; std::array brushes; @@ -89,6 +90,7 @@ public: QBrush getExtraBgBrush(Role zone, int zoneId = 0); protected slots: void themeChangedSlot(); + void onColorSchemeChanged(); signals: void themeChanged(); };