mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
* [Client] Add [AppColors] application palette roles The home-tab buttons' gradient over the static theme background was hardcoded, and accent-derived fallbacks could not be themed or edited: QPalette's role set is closed, so any application-specific color has to live in Cockatrice's own palette layer. - Add an AppColor::Role enum (AccentStrong / AccentSoft) stored on PaletteConfig and round-tripped from palette-<scheme>.toml under a new [AppColors] section. - Cache the applied app colors in ThemeManager and expose appColor(Role) with a palette-accent-derived fallback; emit paletteChanged() from applyStyleAndPalette so previews, scheme switches and OS dark mode repaint palette-driven widgets. - Fill both app roles in PaletteGenerator::fromAccent and surface them as a dedicated section in the palette editor. - Drive the home-tab buttons from appColor() whenever the background source is the theme (any theme, not just built-ins). - Ship AccentStrong / AccentSoft values in the Fusion and Default default palettes so the static home-tab buttons keep their classic greens. # Conflicts: # cockatrice/src/interface/widgets/general/home_widget.cpp * [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. * [Themes] Route theme writes to the user themes directory setColorScheme()/setStyleName() and the palette editor wrote directly to the resolved theme directory, which for built-in themes is the read-only system (install) location. Changes therefore landed in the install dir and were lost on upgrade. Add ThemeManager::writableThemeDir(), which always resolves to the user themes directory, and route all theme writes through it. The palette editor reuses the same helper, dropping its private writability probe. * [Home] Replace 'Automatic' button color with explicit theme colors default The Automatic option gated on isBuiltInTheme(): built-in themes used the theme's accent colors, while non-built-in themes extracted colors from their own background art. That made the result depend on the theme's origin rather than what the user actually sees. Remove Automatic and expose two explicit choices: 'From theme colors' (always the theme's identity accents, now the default) and 'Extract from background' (always sample the painted background). Drop the now-unused isBuiltInTheme() helper. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
244 lines
No EOL
8.5 KiB
C++
244 lines
No EOL
8.5 KiB
C++
#include "palette_grid_widget.h"
|
|
|
|
#include "../theme_manager.h"
|
|
|
|
#include <QApplication>
|
|
#include <QGridLayout>
|
|
#include <QLabel>
|
|
#include <QMetaEnum>
|
|
#include <QScrollArea>
|
|
#include <QTimer>
|
|
#include <QVBoxLayout>
|
|
|
|
static QList<QPalette::ColorRole> allRoles()
|
|
{
|
|
QList<QPalette::ColorRole> roles;
|
|
for (int i = 0; i < QPalette::NColorRoles; ++i) {
|
|
auto r = static_cast<QPalette::ColorRole>(i);
|
|
if (r != QPalette::NoRole) {
|
|
roles << r;
|
|
}
|
|
}
|
|
return roles;
|
|
}
|
|
|
|
static const QList<QPalette::ColorGroup> ALL_GROUPS = {QPalette::Active, QPalette::Disabled, QPalette::Inactive};
|
|
|
|
static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
|
|
{QPalette::Window, QT_TR_NOOP("Main window / dialog background")},
|
|
{QPalette::WindowText, QT_TR_NOOP("Text drawn on Window")},
|
|
{QPalette::Base, QT_TR_NOOP("Background for text input widgets")},
|
|
{QPalette::Text, QT_TR_NOOP("Text in input widgets")},
|
|
{QPalette::Button, QT_TR_NOOP("Button background")},
|
|
{QPalette::ButtonText, QT_TR_NOOP("Button label text")},
|
|
{QPalette::BrightText, QT_TR_NOOP("High-contrast text (e.g. checked items)")},
|
|
{QPalette::Highlight, QT_TR_NOOP("Selection / focus highlight")},
|
|
{QPalette::HighlightedText, QT_TR_NOOP("Text on top of Highlight")},
|
|
{QPalette::Link, QT_TR_NOOP("Unvisited hyperlink")},
|
|
{QPalette::LinkVisited, QT_TR_NOOP("Visited hyperlink")},
|
|
{QPalette::AlternateBase, QT_TR_NOOP("Alternating row background in views")},
|
|
{QPalette::ToolTipBase, QT_TR_NOOP("Tooltip background")},
|
|
{QPalette::ToolTipText, QT_TR_NOOP("Tooltip text")},
|
|
{QPalette::PlaceholderText, QT_TR_NOOP("Placeholder / hint text in inputs")},
|
|
{QPalette::Light, QT_TR_NOOP("Lighter than Button (3-D highlight)")},
|
|
{QPalette::Midlight, QT_TR_NOOP("Between Button and Light")},
|
|
{QPalette::Mid, QT_TR_NOOP("Between Button and Dark")},
|
|
{QPalette::Dark, QT_TR_NOOP("Darker than Button (3-D shadow)")},
|
|
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
|
|
};
|
|
|
|
static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
|
|
{AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
|
|
{AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
|
|
};
|
|
|
|
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
|
{
|
|
scroll = new QScrollArea(this);
|
|
scroll->setWidgetResizable(true);
|
|
scroll->setFrameShape(QFrame::NoFrame);
|
|
|
|
gridHost = new QWidget;
|
|
buildGrid(gridHost);
|
|
refreshChromePalettes();
|
|
scroll->setWidget(gridHost);
|
|
|
|
layout = new QVBoxLayout(this);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
layout->addWidget(scroll);
|
|
}
|
|
|
|
void PaletteGridWidget::buildGrid(QWidget *host)
|
|
{
|
|
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
|
|
|
|
auto *grid = new QGridLayout(host);
|
|
grid->setSpacing(3);
|
|
grid->setContentsMargins(12, 8, 12, 8);
|
|
grid->setColumnStretch(0, 1);
|
|
grid->setColumnStretch(1, 0);
|
|
grid->setColumnStretch(2, 0);
|
|
grid->setColumnStretch(3, 0);
|
|
|
|
// Column headers
|
|
const QStringList groupHeaders = {tr("Active"), tr("Disabled"), tr("Inactive")};
|
|
const QStringList groupTips = {
|
|
tr("Normal interactive state"),
|
|
tr("Widget is disabled / not interactive"),
|
|
tr("Window is in background / unfocused"),
|
|
};
|
|
for (int col = 0; col < 3; ++col) {
|
|
auto *label = new QLabel(groupHeaders[col], host);
|
|
label->setAlignment(Qt::AlignCenter);
|
|
label->setToolTip(groupTips[col]);
|
|
QFont f = label->font();
|
|
f.setBold(true);
|
|
label->setFont(f);
|
|
label->setAutoFillBackground(true);
|
|
label->setContentsMargins(4, 4, 4, 4);
|
|
grid->addWidget(label, 0, col + 1);
|
|
headerLabels.push_back(label);
|
|
}
|
|
|
|
// Role rows
|
|
const auto roles = allRoles();
|
|
for (int row = 0; row < roles.size(); ++row) {
|
|
auto role = roles[row];
|
|
const char *name = roleEnum.valueToKey(role);
|
|
|
|
// Alternating row shade
|
|
if (row % 2 == 0) {
|
|
for (int col = 0; col < 4; ++col) {
|
|
auto *shade = new QWidget(host);
|
|
shade->setAutoFillBackground(true);
|
|
grid->addWidget(shade, row + 1, col);
|
|
rowShadeWidgets.push_back(shade);
|
|
}
|
|
}
|
|
|
|
auto *label = new QLabel(QString(name), host);
|
|
label->setToolTip(ROLE_DESCRIPTIONS.value(role, {}));
|
|
label->setContentsMargins(4, 2, 8, 2);
|
|
grid->addWidget(label, row + 1, 0);
|
|
|
|
for (int col = 0; col < 3; ++col) {
|
|
auto group = ALL_GROUPS[col];
|
|
auto *btn = new ColorButton(host);
|
|
connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
|
|
colorButtons[group][role] = btn;
|
|
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
|
}
|
|
}
|
|
|
|
// Application color section: one ColorButton per role below the role grid.
|
|
// These are not tied to a color group, so a single button spans the row.
|
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
|
|
const int appHeaderRow = roles.size() + 1;
|
|
|
|
auto *appHeader = new QLabel(tr("App colors"), host);
|
|
appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
|
|
QFont appHeaderFont = appHeader->font();
|
|
appHeaderFont.setBold(true);
|
|
appHeader->setFont(appHeaderFont);
|
|
appHeader->setAutoFillBackground(true);
|
|
appHeader->setContentsMargins(4, 4, 4, 4);
|
|
grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);
|
|
headerLabels.append(appHeader);
|
|
|
|
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
|
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
|
const int row = appHeaderRow + 1 + i;
|
|
|
|
if (i % 2 == 0) {
|
|
for (int col = 0; col < 4; ++col) {
|
|
auto *shade = new QWidget(host);
|
|
shade->setAutoFillBackground(true);
|
|
grid->addWidget(shade, row, col);
|
|
rowShadeWidgets.push_back(shade);
|
|
}
|
|
}
|
|
|
|
auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
|
|
label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {}));
|
|
label->setContentsMargins(4, 2, 8, 2);
|
|
grid->addWidget(label, row, 0);
|
|
|
|
auto *btn = new ColorButton(host);
|
|
connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
|
|
appColorButtons[role] = btn;
|
|
grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter);
|
|
}
|
|
}
|
|
|
|
void PaletteGridWidget::changeEvent(QEvent *e)
|
|
{
|
|
if (e->type() == QEvent::PaletteChange) {
|
|
QTimer::singleShot(0, this, &PaletteGridWidget::refreshChromePalettes);
|
|
}
|
|
|
|
QWidget::changeEvent(e);
|
|
}
|
|
|
|
void PaletteGridWidget::refreshChromePalettes()
|
|
{
|
|
const QPalette base = qApp->palette();
|
|
const QColor alt = base.color(QPalette::AlternateBase);
|
|
|
|
// Header labels
|
|
for (auto *label : headerLabels) {
|
|
QPalette lp = label->palette();
|
|
lp.setColor(QPalette::Window, alt);
|
|
label->setPalette(lp);
|
|
label->update();
|
|
}
|
|
|
|
// Alternating row backgrounds
|
|
for (auto *shade : rowShadeWidgets) {
|
|
QPalette sp = shade->palette();
|
|
sp.setColor(QPalette::Window, alt);
|
|
shade->setPalette(sp);
|
|
shade->update();
|
|
}
|
|
}
|
|
|
|
void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
|
|
{
|
|
for (auto group : ALL_GROUPS) {
|
|
for (auto role : allRoles()) {
|
|
QColor color = cfg.colors.value(group).value(role);
|
|
if (!color.isValid()) {
|
|
color = qApp->palette().color(group, role);
|
|
}
|
|
colorButtons[group][role]->setColor(color);
|
|
}
|
|
}
|
|
|
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
|
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
|
QColor color = cfg.appColors.value(role);
|
|
if (!color.isValid()) {
|
|
color = themeManager->appColor(role);
|
|
}
|
|
appColorButtons[role]->setColor(color);
|
|
}
|
|
}
|
|
|
|
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
|
|
{
|
|
PaletteConfig cfg;
|
|
for (auto group : ALL_GROUPS) {
|
|
for (auto role : allRoles()) {
|
|
cfg.colors[group][role] = colorButtons[group][role]->getColor();
|
|
}
|
|
}
|
|
|
|
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
|
|
for (int i = 0; i < appEnum.keyCount(); ++i) {
|
|
auto role = static_cast<AppColor::Role>(appEnum.value(i));
|
|
cfg.appColors[role] = appColorButtons[role]->getColor();
|
|
}
|
|
|
|
return cfg;
|
|
} |