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] 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