mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-28 20:00:24 -07:00
[PaletteEditor] Ensure directory is writeable, don't fall back to alternate palette (#7048)
* [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 <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
3f9dbdb33b
commit
cf0b453e75
11 changed files with 229 additions and 81 deletions
|
|
@ -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,20 +9,51 @@
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QLoggingCategory>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QStyleHints>
|
#include <QStyleHints>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
|
// 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)
|
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
|
||||||
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
|
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
|
||||||
{
|
{
|
||||||
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() && 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
|
// Load both scheme configs upfront so switching is instant
|
||||||
loadSchemes();
|
loadSchemes();
|
||||||
|
|
||||||
|
|
@ -31,7 +63,9 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
|
||||||
schemeComboBox->setCurrentText(loadedScheme);
|
schemeComboBox->setCurrentText(loadedScheme);
|
||||||
schemeComboBox->blockSignals(false);
|
schemeComboBox->blockSignals(false);
|
||||||
|
|
||||||
|
paletteGrid->blockSignals(true);
|
||||||
paletteGrid->loadPalette(workingConfig[loadedScheme]);
|
paletteGrid->loadPalette(workingConfig[loadedScheme]);
|
||||||
|
paletteGrid->blockSignals(false);
|
||||||
seedAccentFromScheme(loadedScheme);
|
seedAccentFromScheme(loadedScheme);
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
|
@ -124,8 +158,7 @@ void PaletteEditorDialog::setupUi()
|
||||||
|
|
||||||
buttonBox = new QDialogButtonBox;
|
buttonBox = new QDialogButtonBox;
|
||||||
resetBtn = buttonBox->addButton(tr("Reset"), QDialogButtonBox::ResetRole);
|
resetBtn = buttonBox->addButton(tr("Reset"), QDialogButtonBox::ResetRole);
|
||||||
applyBtn = buttonBox->addButton(tr("Apply"), QDialogButtonBox::ApplyRole);
|
saveBtn = buttonBox->addButton(tr("Save"), QDialogButtonBox::AcceptRole);
|
||||||
saveBtn = buttonBox->addButton(tr("Save && Apply"), QDialogButtonBox::AcceptRole);
|
|
||||||
closeBtn = buttonBox->addButton(QDialogButtonBox::Close);
|
closeBtn = buttonBox->addButton(QDialogButtonBox::Close);
|
||||||
|
|
||||||
footerLayout->addWidget(revertButton);
|
footerLayout->addWidget(revertButton);
|
||||||
|
|
@ -135,10 +168,14 @@ void PaletteEditorDialog::setupUi()
|
||||||
|
|
||||||
// Connections
|
// Connections
|
||||||
connect(schemeComboBox, &QComboBox::currentTextChanged, this, &PaletteEditorDialog::onSchemeChanged);
|
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(revertButton, &QPushButton::clicked, this, &PaletteEditorDialog::onRevertToDefault);
|
||||||
connect(resetBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onReset);
|
connect(resetBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onReset);
|
||||||
connect(applyBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onApply);
|
|
||||||
connect(saveBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onSave);
|
connect(saveBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onSave);
|
||||||
connect(closeBtn, &QPushButton::clicked, this, &QDialog::reject);
|
connect(closeBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
|
||||||
|
|
@ -162,15 +199,8 @@ void PaletteEditorDialog::retranslateUi()
|
||||||
setWindowTitle(tr("Palette Editor — %1").arg(themeName));
|
setWindowTitle(tr("Palette Editor — %1").arg(themeName));
|
||||||
titleLabel->setText(tr("<b>Palette Editor</b> · %1").arg(themeName));
|
titleLabel->setText(tr("<b>Palette Editor</b> · %1").arg(themeName));
|
||||||
|
|
||||||
// Revert button only makes sense when the theme ships default palette files
|
revertButton->setToolTip(
|
||||||
const bool hasDefault = PaletteConfig::fromDefault(themeDirPath, "Light").hasPalette() ||
|
tr("Delete this scheme's custom palette and revert to the theme default (or the application palette)"));
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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:"));
|
||||||
|
|
@ -179,15 +209,13 @@ void PaletteEditorDialog::retranslateUi()
|
||||||
revertButton->setText(tr("↺ Revert to theme default"));
|
revertButton->setText(tr("↺ Revert to theme default"));
|
||||||
|
|
||||||
resetBtn->setText(tr("Reset"));
|
resetBtn->setText(tr("Reset"));
|
||||||
applyBtn->setText(tr("Apply"));
|
saveBtn->setText(tr("Save"));
|
||||||
saveBtn->setText(tr("Save && Apply"));
|
|
||||||
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
|
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()));
|
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->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);
|
PaletteConfig cfg = PaletteConfig::fromScheme(themeDirPath, scheme);
|
||||||
|
|
||||||
if (!cfg.hasPalette()) {
|
if (!cfg.hasPalette()) {
|
||||||
cfg = PaletteConfig::fromDefault(themeDirPath, scheme);
|
cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cfg.hasPalette()) {
|
if (!cfg.hasPalette()) {
|
||||||
|
|
@ -235,7 +263,6 @@ void PaletteEditorDialog::onSchemeChanged(const QString &scheme)
|
||||||
loadedScheme = scheme;
|
loadedScheme = scheme;
|
||||||
paletteGrid->loadPalette(workingConfig.value(scheme));
|
paletteGrid->loadPalette(workingConfig.value(scheme));
|
||||||
seedAccentFromScheme(scheme);
|
seedAccentFromScheme(scheme);
|
||||||
onApply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PaletteEditorDialog::onGenerateFromAccent(const QColor &accent, int intensity)
|
void PaletteEditorDialog::onGenerateFromAccent(const QColor &accent, int intensity)
|
||||||
|
|
@ -256,20 +283,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();
|
||||||
}
|
}
|
||||||
|
|
@ -278,18 +319,40 @@ void PaletteEditorDialog::onReset()
|
||||||
{
|
{
|
||||||
workingConfig[loadedScheme] = savedConfig[loadedScheme];
|
workingConfig[loadedScheme] = savedConfig[loadedScheme];
|
||||||
paletteGrid->loadPalette(savedConfig[loadedScheme]);
|
paletteGrid->loadPalette(savedConfig[loadedScheme]);
|
||||||
|
seedAccentFromScheme(loadedScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PaletteEditorDialog::onRevertToDefault()
|
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-<scheme>.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()) {
|
if (!def.hasPalette()) {
|
||||||
QMessageBox::information(this, tr("No default found"),
|
const QPalette appPal = qApp->palette();
|
||||||
tr("No default palette file found for the \"%1\" scheme.").arg(loadedScheme));
|
for (auto group : {QPalette::Active, QPalette::Disabled, QPalette::Inactive}) {
|
||||||
return;
|
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);
|
||||||
|
seedAccentFromScheme(loadedScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PaletteEditorDialog::changeEvent(QEvent *e)
|
void PaletteEditorDialog::changeEvent(QEvent *e)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QDialogButtonBox;
|
class QDialogButtonBox;
|
||||||
|
|
@ -48,7 +50,6 @@ private:
|
||||||
QComboBox *schemeComboBox = nullptr;
|
QComboBox *schemeComboBox = nullptr;
|
||||||
QDialogButtonBox *buttonBox = nullptr;
|
QDialogButtonBox *buttonBox = nullptr;
|
||||||
QPushButton *resetBtn = nullptr;
|
QPushButton *resetBtn = nullptr;
|
||||||
QPushButton *applyBtn = nullptr;
|
|
||||||
QPushButton *saveBtn = nullptr;
|
QPushButton *saveBtn = nullptr;
|
||||||
QPushButton *closeBtn = nullptr;
|
QPushButton *closeBtn = nullptr;
|
||||||
QPushButton *revertButton = nullptr;
|
QPushButton *revertButton = nullptr;
|
||||||
|
|
@ -57,10 +58,15 @@ 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;
|
||||||
|
|
||||||
|
QTimer *autoApplyTimer = nullptr;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void changeEvent(QEvent *e) override;
|
void changeEvent(QEvent *e) override;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ void PaletteGridWidget::buildGrid(QWidget *host)
|
||||||
for (int col = 0; col < 3; ++col) {
|
for (int col = 0; col < 3; ++col) {
|
||||||
auto group = ALL_GROUPS[col];
|
auto group = ALL_GROUPS[col];
|
||||||
auto *btn = new ColorButton(host);
|
auto *btn = new ColorButton(host);
|
||||||
|
connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
|
||||||
colorButtons[group][role] = btn;
|
colorButtons[group][role] = btn;
|
||||||
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ public:
|
||||||
void loadPalette(const PaletteConfig &cfg);
|
void loadPalette(const PaletteConfig &cfg);
|
||||||
PaletteConfig currentPaletteConfig() const;
|
PaletteConfig currentPaletteConfig() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void paletteChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildGrid(QWidget *host);
|
void buildGrid(QWidget *host);
|
||||||
void changeEvent(QEvent *e);
|
void changeEvent(QEvent *e);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
|
|
||||||
QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent)
|
QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent)
|
||||||
|
|
@ -41,8 +40,6 @@ QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent)
|
||||||
intensityPercentageLabel->setFixedWidth(34);
|
intensityPercentageLabel->setFixedWidth(34);
|
||||||
intensityPercentageLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
intensityPercentageLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
|
||||||
generateButton = new QPushButton(this);
|
|
||||||
|
|
||||||
layout->addWidget(heading);
|
layout->addWidget(heading);
|
||||||
layout->addSpacing(6);
|
layout->addSpacing(6);
|
||||||
layout->addWidget(accentLabel);
|
layout->addWidget(accentLabel);
|
||||||
|
|
@ -54,12 +51,13 @@ QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent)
|
||||||
layout->addWidget(labelHigh);
|
layout->addWidget(labelHigh);
|
||||||
layout->addWidget(intensityPercentageLabel);
|
layout->addWidget(intensityPercentageLabel);
|
||||||
layout->addStretch();
|
layout->addStretch();
|
||||||
layout->addWidget(generateButton);
|
|
||||||
|
|
||||||
connect(intensitySlider, &QSlider::valueChanged, this,
|
connect(intensitySlider, &QSlider::valueChanged, this, [this](int v) {
|
||||||
[this](int v) { intensityPercentageLabel->setText(tr("%1%").arg(v)); });
|
intensityPercentageLabel->setText(tr("%1%").arg(v));
|
||||||
connect(generateButton, &QPushButton::clicked, this,
|
emit valueChanged(accentButton->getColor(), v);
|
||||||
[this] { emit generateRequested(accentButton->getColor(), intensitySlider->value()); });
|
});
|
||||||
|
connect(accentButton, &ColorButton::colorChanged, this,
|
||||||
|
[this](const QColor &c) { emit valueChanged(c, intensitySlider->value()); });
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,10 +75,6 @@ void QuickSetupPanel::retranslateUi()
|
||||||
"30–70 Accented — buttons, tooltips, and borders join in\n"
|
"30–70 Accented — buttons, tooltips, and borders join in\n"
|
||||||
"70–100 Full colour — backgrounds, everything"));
|
"70–100 Full colour — backgrounds, everything"));
|
||||||
intensityPercentageLabel->setText(tr("70%"));
|
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
|
QColor QuickSetupPanel::accentColor() const
|
||||||
|
|
@ -95,5 +89,7 @@ int QuickSetupPanel::intensity() const
|
||||||
|
|
||||||
void QuickSetupPanel::setAccentColor(const QColor &c)
|
void QuickSetupPanel::setAccentColor(const QColor &c)
|
||||||
{
|
{
|
||||||
|
accentButton->blockSignals(true);
|
||||||
accentButton->setColor(c);
|
accentButton->setColor(c);
|
||||||
|
accentButton->blockSignals(false);
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QPushButton;
|
|
||||||
class QHBoxLayout;
|
class QHBoxLayout;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QSlider;
|
class QSlider;
|
||||||
|
|
@ -16,12 +15,11 @@ class QSlider;
|
||||||
*
|
*
|
||||||
* The panel contains:
|
* The panel contains:
|
||||||
* - an accent color picker,
|
* - an accent color picker,
|
||||||
* - an intensity slider,
|
* - an intensity slider.
|
||||||
* - and a generate button.
|
|
||||||
*
|
*
|
||||||
* When the user clicks the generate button, the panel emits
|
* Whenever either value changes the panel emits valueChanged() with
|
||||||
* generateRequested() with the currently selected accent color
|
* the current accent colour and intensity, which the parent dialog
|
||||||
* and intensity value.
|
* uses to auto-apply the generated palette.
|
||||||
*
|
*
|
||||||
* Typically used together with PaletteGenerator::fromAccent()
|
* Typically used together with PaletteGenerator::fromAccent()
|
||||||
* to quickly generate color schemes from a chosen accent color.
|
* to quickly generate color schemes from a chosen accent color.
|
||||||
|
|
@ -71,12 +69,12 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/**
|
/**
|
||||||
* @brief Emitted when the user requests palette generation.
|
* @brief Emitted whenever the accent colour or intensity changes.
|
||||||
*
|
*
|
||||||
* @param accent The selected accent color.
|
* The parent dialog consumes this to auto-apply the generated palette
|
||||||
* @param intensity The selected intensity value.
|
* without requiring an explicit Generate click.
|
||||||
*/
|
*/
|
||||||
void generateRequested(QColor accent, int intensity);
|
void valueChanged(QColor accent, int intensity);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHBoxLayout *layout;
|
QHBoxLayout *layout;
|
||||||
|
|
@ -88,7 +86,6 @@ private:
|
||||||
QLabel *labelHigh;
|
QLabel *labelHigh;
|
||||||
QSlider *intensitySlider;
|
QSlider *intensitySlider;
|
||||||
QLabel *intensityPercentageLabel;
|
QLabel *intensityPercentageLabel;
|
||||||
QPushButton *generateButton;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_QUICK_SETUP_PANEL_H
|
#endif // COCKATRICE_QUICK_SETUP_PANEL_H
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,14 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
|
||||||
if (defaultStyleName == "windows11") {
|
if (defaultStyleName == "windows11") {
|
||||||
defaultStyleName = "windowsvista";
|
defaultStyleName = "windowsvista";
|
||||||
}
|
}
|
||||||
|
// Capture the untouched application palette before any theme is applied.
|
||||||
|
defaultPalette = qApp->palette();
|
||||||
ensureThemeDirectoryExists();
|
ensureThemeDirectoryExists();
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
|
#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
|
#endif
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &ThemeManager::themeChangedSlot);
|
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &ThemeManager::themeChangedSlot);
|
||||||
themeChangedSlot();
|
themeChangedSlot();
|
||||||
|
|
@ -137,6 +142,20 @@ bool ThemeManager::isBuiltInTheme()
|
||||||
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
|
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()
|
QStringMap &ThemeManager::getAvailableThemes()
|
||||||
{
|
{
|
||||||
QDir dir;
|
QDir dir;
|
||||||
|
|
@ -157,15 +176,7 @@ QStringMap &ThemeManager::getAvailableThemes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// load themes from cockatrice system dir
|
// load themes from cockatrice system dir
|
||||||
dir.setPath(qApp->applicationDirPath() +
|
dir.setPath(systemThemesBasePath());
|
||||||
#ifdef Q_OS_MAC
|
|
||||||
"/../Resources/themes"
|
|
||||||
#elif defined(Q_OS_WIN)
|
|
||||||
"/themes"
|
|
||||||
#else // linux
|
|
||||||
"/../share/cockatrice/themes"
|
|
||||||
#endif
|
|
||||||
);
|
|
||||||
|
|
||||||
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
|
||||||
if (!availableThemes.contains(themeName)) {
|
if (!availableThemes.contains(themeName)) {
|
||||||
|
|
@ -242,6 +253,20 @@ bool ThemeManager::savePaletteConfig(const QString &themeDirPath, const QString
|
||||||
return true;
|
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)
|
void ThemeManager::setColorScheme(const QString &scheme)
|
||||||
{
|
{
|
||||||
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||||
|
|
@ -253,6 +278,17 @@ void ThemeManager::setColorScheme(const QString &scheme)
|
||||||
reloadCurrentTheme();
|
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()
|
void ThemeManager::reloadCurrentTheme()
|
||||||
{
|
{
|
||||||
themeChangedSlot();
|
themeChangedSlot();
|
||||||
|
|
@ -298,7 +334,11 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} 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
|
// Overlay custom palette colours
|
||||||
|
|
@ -353,7 +393,7 @@ void ThemeManager::themeChangedSlot()
|
||||||
// ── Load palette: custom first, then theme default ────────────────────
|
// ── Load palette: custom first, then theme default ────────────────────
|
||||||
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
|
||||||
if (!palette.hasPalette()) {
|
if (!palette.hasPalette()) {
|
||||||
palette = PaletteConfig::fromDefault(dirPath, activeScheme);
|
palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
|
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
|
||||||
|
|
@ -362,6 +402,16 @@ void ThemeManager::themeChangedSlot()
|
||||||
if (!dirPath.isEmpty()) {
|
if (!dirPath.isEmpty()) {
|
||||||
resources << dir.absolutePath();
|
resources << dir.absolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the resolved dir is a user copy (e.g. user/<theme>), 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;
|
resources << DEFAULT_RESOURCE_PATHS;
|
||||||
|
|
||||||
QDir::setSearchPaths("theme", resources);
|
QDir::setSearchPaths("theme", resources);
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString defaultStyleName;
|
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;
|
QString currentThemePath;
|
||||||
std::array<QBrush, Role::MaxRole + 1> brushes;
|
std::array<QBrush, Role::MaxRole + 1> brushes;
|
||||||
QStringMap availableThemes;
|
QStringMap availableThemes;
|
||||||
|
|
@ -76,7 +80,12 @@ public:
|
||||||
// Load/save per-scheme palette colors
|
// Load/save per-scheme palette colors
|
||||||
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
|
||||||
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
|
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 setColorScheme(const QString &scheme);
|
||||||
|
void setStyleName(const QString &styleName);
|
||||||
|
|
||||||
void reloadCurrentTheme();
|
void reloadCurrentTheme();
|
||||||
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
|
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QStyleFactory>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
AppearanceSettingsPage::AppearanceSettingsPage()
|
AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
|
|
@ -47,6 +48,19 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
connect(&schemeCombo, &QComboBox::currentIndexChanged, this,
|
connect(&schemeCombo, &QComboBox::currentIndexChanged, this,
|
||||||
[this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); });
|
[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] {
|
connect(themeManager, &ThemeManager::themeChanged, this, [this, dirPath] {
|
||||||
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
|
||||||
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
|
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
|
||||||
|
|
@ -56,6 +70,12 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
const int idx = schemeCombo.findData(current);
|
const int idx = schemeCombo.findData(current);
|
||||||
schemeCombo.setCurrentIndex(idx >= 0 ? idx : 0);
|
schemeCombo.setCurrentIndex(idx >= 0 ? idx : 0);
|
||||||
schemeCombo.blockSignals(false);
|
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);
|
connect(&editPaletteButton, &QPushButton::clicked, this, &AppearanceSettingsPage::editPalette);
|
||||||
|
|
@ -66,7 +86,9 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
||||||
themeGrid->addWidget(&openThemeButton, 1, 1);
|
themeGrid->addWidget(&openThemeButton, 1, 1);
|
||||||
themeGrid->addWidget(&schemeComboLabel, 2, 0);
|
themeGrid->addWidget(&schemeComboLabel, 2, 0);
|
||||||
themeGrid->addWidget(&schemeCombo, 2, 1);
|
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 = new QGroupBox;
|
||||||
themeGroupBox->setLayout(themeGrid);
|
themeGroupBox->setLayout(themeGrid);
|
||||||
|
|
@ -400,6 +422,8 @@ void AppearanceSettingsPage::retranslateUi()
|
||||||
themeLabel.setText(tr("Current theme:"));
|
themeLabel.setText(tr("Current theme:"));
|
||||||
openThemeButton.setText(tr("Open themes folder"));
|
openThemeButton.setText(tr("Open themes folder"));
|
||||||
schemeComboLabel.setText(tr("Active theme palette:"));
|
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"));
|
editPaletteButton.setText(tr("Edit theme palette"));
|
||||||
|
|
||||||
homeTabGroupBox->setTitle(tr("Home tab settings"));
|
homeTabGroupBox->setTitle(tr("Home tab settings"));
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ private:
|
||||||
QPushButton openThemeButton;
|
QPushButton openThemeButton;
|
||||||
QLabel schemeComboLabel;
|
QLabel schemeComboLabel;
|
||||||
QComboBox schemeCombo;
|
QComboBox schemeCombo;
|
||||||
|
QLabel styleComboLabel;
|
||||||
|
QComboBox styleCombo;
|
||||||
QPushButton editPaletteButton;
|
QPushButton editPaletteButton;
|
||||||
QLabel homeTabBackgroundSourceLabel;
|
QLabel homeTabBackgroundSourceLabel;
|
||||||
QComboBox homeTabBackgroundSourceBox;
|
QComboBox homeTabBackgroundSourceBox;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue