Cockatrice/cockatrice/src/interface/theme_config.cpp
Lukas Brübach 7549f9c2cf
[AppColors] Address review comments
- PaletteEditorDialog::onSave(): compare whole PaletteConfig (colors and
  appColors) so a change to only AccentStrong/AccentSoft writes the file;
  add PaletteConfig::operator==.
- appColor(): derive both roles from QPalette::Highlight unconditionally.
  The Fusion palettes pin Accent to near-Window values, and QPalette::Accent
  only exists on Qt 6.6+, so keying on it made identical themes render very
  differently across Qt versions.
- themeChangedSlot(): merge the theme default's [AppColors] into a custom
  palette that predates the section instead of all-or-nothing per file;
  hasPalette() now counts an appColors-only file as a palette.
- Add Default/palette-default-light.toml so the Default theme's Light scheme
  keeps the classic greens instead of falling back to the OS accent.
- home_widget: restore the isBuiltInTheme() half of the Automatic condition;
  non-built-in themes extract button colors from their own background art.
- palette_grid_widget: use appEnum.value(i) for the role cast (3 sites),
  append appHeader to headerLabels, fix the 'Lighted' typo.
2026-09-12 18:00:28 +02:00

299 lines
No EOL
7.8 KiB
C++

#include "theme_config.h"
#include <QDir>
#include <QFile>
#include <QMetaEnum>
#include <QTextStream>
bool ThemeConfig::isEmpty() const
{
return colorScheme.isEmpty() && styleName.isEmpty();
}
QString ThemeConfig::toIni() const
{
QString out;
out += "[Appearance]\n";
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
out += "\n[Style]\n";
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName);
return out;
}
ThemeConfig ThemeConfig::fromThemeDir(const QString &themeDirPath)
{
ThemeConfig cfg;
if (themeDirPath.isEmpty()) {
return cfg;
}
QFile f(QDir(themeDirPath).absoluteFilePath("theme.cfg"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return cfg;
}
QString currentSection;
QTextStream in(&f);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty() || line.startsWith('#') || line.startsWith(';')) {
continue;
}
if (line.startsWith('[') && line.endsWith(']')) {
currentSection = line.mid(1, line.length() - 2).trimmed();
continue;
}
int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
QString key = line.left(eq).trimmed();
QString value = line.mid(eq + 1).trimmed();
if (currentSection.compare("Appearance", Qt::CaseInsensitive) == 0) {
if (key.compare("ColorScheme", Qt::CaseInsensitive) == 0) {
cfg.colorScheme = value;
}
} else if (currentSection.compare("Style", Qt::CaseInsensitive) == 0) {
if (key.compare("Name", Qt::CaseInsensitive) == 0) {
cfg.styleName = value;
}
}
}
return cfg;
}
bool ThemeConfig::save(const QString &themeDirPath) const
{
if (themeDirPath.isEmpty()) {
return false;
}
QDir dir(themeDirPath);
if (!dir.exists()) {
dir.mkpath(".");
}
QFile f(dir.absoluteFilePath("theme.cfg"));
if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
return false;
}
QTextStream out(&f);
out << toIni();
return true;
}
bool PaletteConfig::hasPalette() const
{
return !colors.isEmpty() || !appColors.isEmpty();
}
QString PaletteConfig::toToml() const
{
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
QString out;
static const QList<QPair<QPalette::ColorGroup, QString>> groups = {
{QPalette::Active, "Palette"},
{QPalette::Disabled, "Palette.Disabled"},
{QPalette::Inactive, "Palette.Inactive"},
};
for (const auto &[group, sectionName] : groups) {
if (!colors.contains(group)) {
continue;
}
out += QString("[%1]\n").arg(sectionName);
const auto &roleMap = colors[group];
for (auto it = roleMap.cbegin(); it != roleMap.cend(); ++it) {
const char *roleName = roleEnum.valueToKey(it.key());
if (!roleName) {
continue;
}
out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb));
}
out += "\n";
}
if (!appColors.isEmpty()) {
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
out += "[AppColors]\n";
for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) {
const char *roleName = appEnum.valueToKey(it.key());
if (!roleName) {
continue;
}
out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb));
}
out += "\n";
}
return out;
}
QString PaletteConfig::fileName(const QString &colorScheme)
{
return colorScheme.compare("Dark", Qt::CaseInsensitive) == 0 ? "palette-dark.toml" : "palette-light.toml";
}
PaletteConfig PaletteConfig::fromFile(const QString &filePath)
{
PaletteConfig cfg;
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return cfg;
}
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
QString currentSection;
QPalette::ColorGroup currentGroup = QPalette::Active;
QTextStream in(&f);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty() || line.startsWith('#') || line.startsWith(';')) {
continue;
}
if (line.startsWith('[') && line.endsWith(']')) {
currentSection = line.mid(1, line.length() - 2).trimmed();
if (currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
int dot = currentSection.indexOf('.');
QString groupStr = (dot >= 0) ? currentSection.mid(dot + 1) : "Active";
if (groupStr.compare("Disabled", Qt::CaseInsensitive) == 0) {
currentGroup = QPalette::Disabled;
} else if (groupStr.compare("Inactive", Qt::CaseInsensitive) == 0) {
currentGroup = QPalette::Inactive;
} else {
currentGroup = QPalette::Active;
}
}
continue;
}
int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
QString key = line.left(eq).trimmed();
QString value = line.mid(eq + 1).trimmed();
// Strip inline comments if preceded by whitespace
for (int i = 1; i < value.size(); ++i) {
if (value[i] == '#' && value[i - 1].isSpace()) {
value = value.left(i).trimmed();
break;
}
}
QColor color(value);
if (!color.isValid()) {
continue;
}
if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) {
if (key.startsWith("AppColor::")) {
key = key.mid(10);
}
int appRoleInt = appEnum.keyToValue(key.toUtf8().constData());
if (appRoleInt >= 0) {
cfg.appColors[static_cast<AppColor::Role>(appRoleInt)] = color;
}
continue;
}
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
continue;
}
if (key.startsWith("QPalette::")) {
key = key.mid(10);
}
int roleInt = roleEnum.keyToValue(key.toUtf8().constData());
if (roleInt < 0) {
continue;
}
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
}
return cfg;
}
PaletteConfig PaletteConfig::fromScheme(const QString &themeDirPath, const QString &colorScheme)
{
if (themeDirPath.isEmpty()) {
return {};
}
return fromFile(QDir(themeDirPath).absoluteFilePath(fileName(colorScheme)));
}
PaletteConfig PaletteConfig::fromDefault(const QString &themeDirPath, const QString &colorScheme)
{
if (themeDirPath.isEmpty()) {
return {};
}
QDir dir(themeDirPath);
bool wantDark = colorScheme.compare("Dark", Qt::CaseInsensitive) == 0;
// Only the default file matching the requested scheme is used. Falling back
// 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 /
// application palette when no default palette is available.
return fromFile(dir.absoluteFilePath(wantDark ? "palette-default-dark.toml" : "palette-default-light.toml"));
}
QPalette PaletteConfig::apply(QPalette base) const
{
for (auto git = colors.cbegin(); git != colors.cend(); ++git) {
for (auto rit = git.value().cbegin(); rit != git.value().cend(); ++rit) {
base.setColor(git.key(), rit.key(), rit.value());
}
}
return base;
}