Read user path, then fall through to system, only write to user.

Took 1 hour 21 minutes

Took 5 seconds


Took 40 seconds
This commit is contained in:
Lukas Brübach 2026-06-19 12:26:43 +02:00
parent e28f31c93e
commit c3ebaefd85
4 changed files with 147 additions and 81 deletions

View file

@ -19,13 +19,15 @@
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent) PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName) : QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
{ {
userThemeDirPath = ThemeManager::userThemeDirFor(themeName);
setMinimumSize(740, 220); setMinimumSize(740, 220);
setupUi(); setupUi();
// Load both scheme configs upfront so switching is instant // Load both scheme configs upfront so switching is instant
loadSchemes(); loadSchemes();
loadedScheme = themeManager->isDarkMode(themeDirPath) ? "Dark" : "Light"; loadedScheme = themeManager->isDarkMode(themeDirPath, userThemeDirPath) ? "Dark" : "Light";
schemeComboBox->blockSignals(true); schemeComboBox->blockSignals(true);
schemeComboBox->setCurrentText(loadedScheme); schemeComboBox->setCurrentText(loadedScheme);
@ -33,7 +35,6 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
paletteGrid->loadPalette(workingConfig[loadedScheme]); paletteGrid->loadPalette(workingConfig[loadedScheme]);
seedAccentFromScheme(loadedScheme); seedAccentFromScheme(loadedScheme);
retranslateUi(); retranslateUi();
} }
@ -162,15 +163,12 @@ void PaletteEditorDialog::retranslateUi()
setWindowTitle(tr("Palette Editor — %1").arg(themeName)); setWindowTitle(tr("Palette Editor — %1").arg(themeName));
titleLabel->setText(tr("<b>Palette Editor</b> &nbsp;·&nbsp; %1").arg(themeName)); titleLabel->setText(tr("<b>Palette Editor</b> &nbsp;·&nbsp; %1").arg(themeName));
// Revert button only makes sense when the theme ships default palette files const bool hasUserOverride =
const bool hasDefault = PaletteConfig::fromDefault(themeDirPath, "Light").hasPalette() || QFile::exists(QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName("Light"))) ||
PaletteConfig::fromDefault(themeDirPath, "Dark").hasPalette(); QFile::exists(QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName("Dark")));
revertButton->setEnabled(hasDefault); revertButton->setEnabled(hasUserOverride);
if (!hasDefault) { revertButton->setToolTip(hasUserOverride ? tr("Delete your custom palette and restore the shipped defaults")
revertButton->setToolTip(tr("This theme ships no default palette files")); : tr("No custom palette overrides exist for this theme"));
} else {
revertButton->setToolTip(tr("Replace current colours with the theme author's defaults"));
}
schemeComboBox->setToolTip(tr("Switch between the light and dark palette files")); schemeComboBox->setToolTip(tr("Switch between the light and dark palette files"));
editingLabel->setText(tr("Editing:")); editingLabel->setText(tr("Editing:"));
@ -195,8 +193,11 @@ void PaletteEditorDialog::loadSchemes()
{ {
const QStringList schemes = {"Light", "Dark"}; const QStringList schemes = {"Light", "Dark"};
for (const QString &scheme : schemes) { 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()) { if (!cfg.hasPalette()) {
cfg = PaletteConfig::fromDefault(themeDirPath, scheme); cfg = PaletteConfig::fromDefault(themeDirPath, scheme);
} }
@ -258,15 +259,17 @@ void PaletteEditorDialog::onSave()
PaletteConfig cfg = paletteGrid->currentPaletteConfig(); PaletteConfig cfg = paletteGrid->currentPaletteConfig();
if (!ThemeManager::savePaletteConfig(themeDirPath, loadedScheme, cfg)) { if (!ThemeManager::savePaletteConfig(userThemeDirPath, loadedScheme, cfg)) {
QMessageBox::warning(this, tr("Save failed"), QMessageBox::warning(
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), themeDirPath)); this, tr("Save failed"),
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), userThemeDirPath));
return; return;
} }
// Record the active scheme in the user dir — never touch the system (read-only) dir
ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath); ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath);
globalCfg.colorScheme = loadedScheme; globalCfg.colorScheme = loadedScheme;
globalCfg.save(themeDirPath); globalCfg.save(userThemeDirPath);
savedConfig[loadedScheme] = cfg; savedConfig[loadedScheme] = cfg;
workingConfig[loadedScheme] = cfg; workingConfig[loadedScheme] = cfg;
@ -282,14 +285,45 @@ void PaletteEditorDialog::onReset()
void PaletteEditorDialog::onRevertToDefault() void PaletteEditorDialog::onRevertToDefault()
{ {
PaletteConfig def = PaletteConfig::fromDefault(themeDirPath, loadedScheme); const QString filePath = QDir(userThemeDirPath).absoluteFilePath(PaletteConfig::fileName(loadedScheme));
if (!def.hasPalette()) {
QMessageBox::information(this, tr("No default found"), if (!QFile::exists(filePath)) {
tr("No default palette file found for the \"%1\" scheme.").arg(loadedScheme)); // Button should already be disabled in this state, but be defensive
return; 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<QPalette::ColorRole>(i);
if (role != QPalette::NoRole) {
def.colors[group][role] = appPal.color(group, role);
}
}
}
}
savedConfig[loadedScheme] = def;
workingConfig[loadedScheme] = def; workingConfig[loadedScheme] = def;
paletteGrid->loadPalette(def); paletteGrid->loadPalette(def);
retranslateUi(); // update button enabled state
} }
void PaletteEditorDialog::changeEvent(QEvent *e) void PaletteEditorDialog::changeEvent(QEvent *e)

View file

@ -54,6 +54,7 @@ private:
QPushButton *revertButton = nullptr; QPushButton *revertButton = nullptr;
// State // State
QString userThemeDirPath;
QString themeDirPath; QString themeDirPath;
QString themeName; QString themeName;
QString loadedScheme; QString loadedScheme;

View file

@ -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); // User override takes precedence over system config
if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) { for (const QString &path : {userDirPath, themeDirPath}) {
return true; if (path.isEmpty()) {
} else if (themeConfig.colorScheme.compare("Light", Qt::CaseInsensitive) == 0) { continue;
return false; }
} else { ThemeConfig cfg = ThemeConfig::fromThemeDir(path);
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) if (cfg.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) {
bool osDark = (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark); return true;
#else }
bool osDark = false; if (cfg.colorScheme.compare("Light", Qt::CaseInsensitive) == 0) {
#endif return false;
return osDark; }
} }
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
return (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark);
#else
return false;
#endif
} }
bool ThemeManager::isBuiltInTheme() bool ThemeManager::isBuiltInTheme()
@ -137,42 +142,49 @@ bool ThemeManager::isBuiltInTheme()
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME; 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() QStringMap &ThemeManager::getAvailableThemes()
{ {
QDir dir;
availableThemes.clear(); availableThemes.clear();
// load themes from user profile dir // ── 1. System themes (read-only, shipped with the application) ──────────
dir.setPath(SettingsCache::instance().getThemesPath()); QDir sysDir(qApp->applicationDirPath() +
// 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() +
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
"/../Resources/themes" "/../Resources/themes"
#elif defined(Q_OS_WIN) #elif defined(Q_OS_WIN)
"/themes" "/themes"
#else // linux #else
"/../share/cockatrice/themes" "/../share/cockatrice/themes"
#endif #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)) { // ── 2. User-only themes (AppData) ────────────────────────────────────────
if (!availableThemes.contains(themeName)) { // We only add themes that don't already exist in the system directory.
availableThemes.insert(themeName, dir.absoluteFilePath(themeName)); // 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; return availableThemes;
} }
@ -244,12 +256,14 @@ bool ThemeManager::savePaletteConfig(const QString &themeDirPath, const QString
void ThemeManager::setColorScheme(const QString &scheme) 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); ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.colorScheme = scheme; cfg.colorScheme = scheme;
cfg.save(userDirPath);
cfg.save(dirPath);
reloadCurrentTheme(); reloadCurrentTheme();
} }
@ -262,7 +276,13 @@ void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &schem
{ {
const QString themeName = SettingsCache::instance().getThemeName(); const QString themeName = SettingsCache::instance().getThemeName();
const QString dirPath = getAvailableThemes().value(themeName); 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); 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(); const QString themeName = SettingsCache::instance().getThemeName();
QString dirPath = getAvailableThemes().value(themeName); const QString dirPath = getAvailableThemes().value(themeName); // system path
const QString userDirPath = userThemeDirFor(themeName); // user override path
currentThemePath = dirPath; currentThemePath = dirPath;
QDir dir(dirPath);
// CSS // CSS — user override first, then system
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) { const auto tryLoadCss = [](const QString &path) -> bool {
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME)); QDir d(path);
} else { 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(""); qApp->setStyleSheet("");
} }
// load theme.cfg for style + scheme preference // ThemeConfig — user override first, then system
ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath); ThemeConfig themeCfg = ThemeConfig::fromThemeDir(userDirPath);
if (themeCfg.colorScheme.isEmpty() && themeCfg.styleName.isEmpty()) {
themeCfg = ThemeConfig::fromThemeDir(dirPath);
}
// Resolve active scheme: const QString activeScheme = isDarkMode(dirPath, userDirPath) ? "Dark" : "Light";
// theme.cfg says Dark/Light → use that
// theme.cfg says System or is absent → follow the OS
QString activeScheme = isDarkMode(dirPath) ? "Dark" : "Light";
// ── Load palette: custom first, then theme default ──────────────────── // Palette — user customisation → system palette → system default
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme); PaletteConfig palette = PaletteConfig::fromScheme(userDirPath, activeScheme);
if (!palette.hasPalette()) {
palette = PaletteConfig::fromScheme(dirPath, activeScheme);
}
if (!palette.hasPalette()) { if (!palette.hasPalette()) {
palette = PaletteConfig::fromDefault(dirPath, activeScheme); palette = PaletteConfig::fromDefault(dirPath, activeScheme);
} }
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
// Search paths — user assets shadow system assets, both fall through to builtins
QStringList resources; QStringList resources;
if (QDir(userDirPath).exists()) {
resources << userDirPath;
}
if (!dirPath.isEmpty()) { if (!dirPath.isEmpty()) {
resources << dir.absolutePath(); resources << dirPath;
} }
resources << DEFAULT_RESOURCE_PATHS; resources << DEFAULT_RESOURCE_PATHS;
QDir::setSearchPaths("theme", resources); QDir::setSearchPaths("theme", resources);
brushes[Role::Hand] = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT); brushes[Role::Hand] = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT);
brushes[Role::Table] = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT); brushes[Role::Table] = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT);
brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT); brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT);
brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT); brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT);
for (auto &brushCache : brushesCache) { for (auto &brushCache : brushesCache) {
brushCache.clear(); brushCache.clear();

View file

@ -63,6 +63,8 @@ protected:
public: public:
bool isBuiltInTheme(); bool isBuiltInTheme();
bool isDarkMode(const QString &themeDirPath); bool isDarkMode(const QString &themeDirPath);
static bool isDarkMode(const QString &themeDirPath, const QString &userDirPath = {});
static QString userThemeDirFor(const QString &themeName);
QStringMap &getAvailableThemes(); QStringMap &getAvailableThemes();
// Returns the path to the currently active theme directory (empty = default) // Returns the path to the currently active theme directory (empty = default)
QString getCurrentThemePath() const QString getCurrentThemePath() const