[PaletteEditor] Ensure directory is writeable, don't fall back to alternate palette.

Took 17 minutes
This commit is contained in:
Lukas Brübach 2026-07-20 21:26:35 +02:00
parent ae37762ee1
commit 6e41e3502a
3 changed files with 47 additions and 20 deletions

View file

@ -1,5 +1,6 @@
#include "palette_editor_dialog.h" #include "palette_editor_dialog.h"
#include "../../client/settings/cache_settings.h"
#include "../theme_manager.h" #include "../theme_manager.h"
#include "palette_generator.h" #include "palette_generator.h"
#include "palette_grid_widget.h" #include "palette_grid_widget.h"
@ -8,6 +9,8 @@
#include <QApplication> #include <QApplication>
#include <QComboBox> #include <QComboBox>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QDir>
#include <QFileInfo>
#include <QFrame> #include <QFrame>
#include <QGuiApplication> #include <QGuiApplication>
#include <QLabel> #include <QLabel>
@ -22,6 +25,16 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
setMinimumSize(740, 220); setMinimumSize(740, 220);
setupUi(); 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() && QFileInfo(themeDirPath).isWritable()) {
saveDir = themeDirPath;
} else {
saveDir = QDir(SettingsCache::instance().getThemesPath()).absoluteFilePath(themeName);
QDir().mkpath(saveDir);
}
// Load both scheme configs upfront so switching is instant // Load both scheme configs upfront so switching is instant
loadSchemes(); loadSchemes();
@ -185,9 +198,9 @@ void PaletteEditorDialog::retranslateUi()
applyBtn->setToolTip(tr("Preview this palette without saving to disk")); applyBtn->setToolTip(tr("Preview this palette without saving to disk"));
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower())); saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
if (themeDirPath.isEmpty()) { if (saveDir.isEmpty() || !QFileInfo(saveDir).isWritable()) {
saveBtn->setEnabled(false); 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"));
} }
} }
@ -256,20 +269,34 @@ void PaletteEditorDialog::onSave()
return; 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)) { // Persist every scheme that changed, not just the one on screen. Each scheme
QMessageBox::warning(this, tr("Save failed"), // has its own file, so edits to the non-active scheme would otherwise be
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), themeDirPath)); // silently discarded when the dialog closes.
return; 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); // Keep the saved snapshot in sync so Reset behaves correctly afterwards.
globalCfg.colorScheme = loadedScheme; for (auto it = workingConfig.begin(); it != workingConfig.end(); ++it) {
globalCfg.save(themeDirPath); savedConfig[it.key()] = it.value();
}
ThemeConfig globalCfg = ThemeConfig::fromThemeDir(saveDir);
globalCfg.colorScheme = loadedScheme;
globalCfg.save(saveDir);
savedConfig[loadedScheme] = cfg;
workingConfig[loadedScheme] = cfg;
themeManager->reloadCurrentTheme(); themeManager->reloadCurrentTheme();
accept(); accept();
} }

View file

@ -57,6 +57,9 @@ private:
QString themeDirPath; QString themeDirPath;
QString themeName; QString themeName;
QString loadedScheme; 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<QString, PaletteConfig> workingConfig; QMap<QString, PaletteConfig> workingConfig;
QMap<QString, PaletteConfig> savedConfig; QMap<QString, PaletteConfig> savedConfig;

View file

@ -245,14 +245,11 @@ PaletteConfig PaletteConfig::fromDefault(const QString &themeDirPath, const QStr
bool wantDark = colorScheme.compare("Dark", Qt::CaseInsensitive) == 0; bool wantDark = colorScheme.compare("Dark", Qt::CaseInsensitive) == 0;
PaletteConfig cfg = // Only the default file matching the requested scheme is used. Falling back
fromFile(dir.absoluteFilePath(wantDark ? "palette-default-dark.toml" : "palette-default-light.toml")); // 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 /
if (!cfg.hasPalette()) { // application palette when no default palette is available.
cfg = fromFile(dir.absoluteFilePath(wantDark ? "palette-default-light.toml" : "palette-default-dark.toml")); return fromFile(dir.absoluteFilePath(wantDark ? "palette-default-dark.toml" : "palette-default-light.toml"));
}
return cfg;
} }
QPalette PaletteConfig::apply(QPalette base) const QPalette PaletteConfig::apply(QPalette base) const