[App/Theme] Palette Editor (#6877)

* [App/Theme] Palette Editor

Took 1 minute

Took 1 hour 47 minutes

Took 6 seconds

Took 3 minutes

Took 5 minutes


Took 3 minutes

* Add oracle, add palette files and configs.

Took 10 minutes

* Fix a stupid include mistake, thanks IDE

Took 3 minutes

Took 20 seconds

* Includes.

Took 4 minutes

* Fix ampersand not displaying correctly.

Took 14 minutes

* Longer variable names.

Took 10 minutes

Took 5 seconds

* Change ampersand everywhere

Took 23 seconds

* Doxygen properly.

Took 1 minute

* Remove namespace, fold I/O into structs.

Took 12 minutes

* Remove namespace, fold I/O into structs.

Took 33 seconds

* Alphabetize.

Took 35 seconds

* Lint.

Took 49 seconds

* Add a combo box to quick switch settings.

Took 19 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-05-17 10:08:00 +02:00 committed by GitHub
parent 989a5be23b
commit 117ea543c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1874 additions and 232 deletions

View file

@ -130,7 +130,13 @@ set(cockatrice_SOURCES
src/interface/layouts/overlap_layout.cpp
src/interface/widgets/utility/line_edit_completer.cpp
src/interface/pixel_map_generator.cpp
src/interface/theme_config.cpp
src/interface/theme_manager.cpp
src/interface/palette_editor/color_button.cpp
src/interface/palette_editor/palette_generator.cpp
src/interface/palette_editor/quick_setup_panel.cpp
src/interface/palette_editor/palette_grid_widget.cpp
src/interface/palette_editor/palette_editor_dialog.cpp
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp

View file

@ -0,0 +1,72 @@
#include "color_button.h"
#include <QColorDialog>
#include <QPainter>
ColorButton::ColorButton(QWidget *parent) : QToolButton(parent)
{
setFixedSize(52, 24);
setCursor(Qt::PointingHandCursor);
setToolTip(tr("Click to pick a color"));
connect(this, &QToolButton::clicked, this, &ColorButton::pickColor);
}
void ColorButton::setColor(const QColor &c)
{
if (color == c) {
return;
}
color = c;
updateSwatch();
emit colorChanged(c);
}
void ColorButton::pickColor()
{
QColor chosen = QColorDialog::getColor(color, this, tr("Pick colour"), QColorDialog::ShowAlphaChannel);
if (chosen.isValid()) {
setColor(chosen);
}
}
void ColorButton::updateSwatch()
{
QPixmap pixmap(size() * devicePixelRatioF());
pixmap.setDevicePixelRatio(devicePixelRatioF());
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
// Checkerboard for alpha
const int cellSize = 4;
for (int y = 0; y < height(); y += cellSize) {
for (int x = 0; x < width(); x += cellSize) {
painter.fillRect(x, y, cellSize, cellSize,
((x / cellSize + y / cellSize) % 2) ? QColor(180, 180, 180) : Qt::white);
}
}
// Color fill
painter.setPen(Qt::NoPen);
painter.setBrush(color);
painter.drawRoundedRect(QRectF(0.5, 0.5, width() - 1, height() - 1), 3, 3);
// Border
QColor border = palette().color(QPalette::Shadow);
border.setAlpha(180);
painter.setPen(QPen(border, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(QRectF(0.5, 0.5, width() - 1, height() - 1), 3, 3);
// Hex label — black or white for contrast
int luma = 0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue();
painter.setPen((luma > 128 && color.alpha() > 80) ? QColor(0, 0, 0, 180) : QColor(255, 255, 255, 200));
QFont f = font();
f.setPixelSize(8);
painter.setFont(f);
painter.drawText(rect(), Qt::AlignCenter, color.name().toUpper());
setIcon(QIcon(pixmap));
setIconSize(size());
setText({});
}

View file

@ -0,0 +1,30 @@
#ifndef COCKATRICE_COLOR_BUTTON_H
#define COCKATRICE_COLOR_BUTTON_H
#include <QColor>
#include <QToolButton>
class ColorButton : public QToolButton
{
Q_OBJECT
public:
explicit ColorButton(QWidget *parent = nullptr);
QColor getColor() const
{
return color;
}
void setColor(const QColor &c);
signals:
void colorChanged(const QColor &color);
private slots:
void pickColor();
private:
void updateSwatch();
QColor color;
};
#endif // COCKATRICE_COLOR_BUTTON_H

View file

@ -0,0 +1,326 @@
#include "palette_editor_dialog.h"
#include "../theme_manager.h"
#include "palette_generator.h"
#include "palette_grid_widget.h"
#include "quick_setup_panel.h"
#include <QApplication>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFrame>
#include <QGuiApplication>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QStyleHints>
#include <QTimer>
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
{
setMinimumSize(740, 220);
setupUi();
// Load both scheme configs upfront so switching is instant
loadSchemes();
loadedScheme = themeManager->isDarkMode(themeDirPath) ? "Dark" : "Light";
schemeComboBox->blockSignals(true);
schemeComboBox->setCurrentText(loadedScheme);
schemeComboBox->blockSignals(false);
paletteGrid->loadPalette(workingConfig[loadedScheme]);
seedAccentFromScheme(loadedScheme);
retranslateUi();
}
void PaletteEditorDialog::setupUi()
{
auto *root = new QVBoxLayout(this);
root->setSpacing(0);
root->setContentsMargins(0, 0, 0, 0);
// Header
header = new QWidget;
header->setAutoFillBackground(true);
{
QPalette hp = header->palette();
hp.setColor(QPalette::Window, qApp->palette().color(QPalette::Window).darker(108));
header->setPalette(hp);
}
auto *headerLayout = new QHBoxLayout(header);
headerLayout->setContentsMargins(12, 8, 12, 8);
titleLabel = new QLabel(this);
titleLabel->setTextFormat(Qt::RichText);
editingLabel = new QLabel(this);
schemeComboBox = new QComboBox;
schemeComboBox->addItems({"Light", "Dark"});
schemeComboBox->setFixedWidth(90);
headerLayout->addWidget(titleLabel);
headerLayout->addStretch();
headerLayout->addWidget(editingLabel);
headerLayout->addWidget(schemeComboBox);
root->addWidget(header);
auto makeSeparator = [&]() {
auto *sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
sep->setFixedHeight(1);
return sep;
};
root->addWidget(makeSeparator());
// Quick Setup panel
quickSetupPanel = new QuickSetupPanel;
quickSetupPanel->setAutoFillBackground(true);
QPalette sp = quickSetupPanel->palette();
sp.setColor(QPalette::Window, qApp->palette().color(QPalette::Window).darker(102));
quickSetupPanel->setPalette(sp);
root->addWidget(quickSetupPanel);
root->addWidget(makeSeparator());
// Toggle button — acts as a section header for the advanced area
paletteGridToggleButton = new QPushButton(this);
paletteGridToggleButton->setCheckable(true);
paletteGridToggleButton->setChecked(false);
paletteGridToggleButton->setFlat(true);
paletteGridToggleButton->setStyleSheet("QPushButton { text-align: left; padding: 5px 12px; font-weight: bold; }"
"QPushButton:checked { }");
root->addWidget(paletteGridToggleButton);
// Separator + grid start hidden; revealed by the toggle
paletteGridSeparator = makeSeparator();
paletteGridSeparator->setVisible(false);
root->addWidget(paletteGridSeparator);
paletteGrid = new PaletteGridWidget;
paletteGrid->setVisible(false);
root->addWidget(paletteGrid, 1);
// Footer
root->addWidget(makeSeparator());
footer = new QWidget;
footer->setAutoFillBackground(true);
{
QPalette fp = footer->palette();
fp.setColor(QPalette::Window, qApp->palette().color(QPalette::Window).darker(104));
footer->setPalette(fp);
}
auto *footerLayout = new QHBoxLayout(footer);
footerLayout->setContentsMargins(12, 8, 12, 8);
revertButton = new QPushButton(this);
buttonBox = new QDialogButtonBox;
resetBtn = buttonBox->addButton(tr("Reset"), QDialogButtonBox::ResetRole);
applyBtn = buttonBox->addButton(tr("Apply"), QDialogButtonBox::ApplyRole);
saveBtn = buttonBox->addButton(tr("Save && Apply"), QDialogButtonBox::AcceptRole);
closeBtn = buttonBox->addButton(QDialogButtonBox::Close);
footerLayout->addWidget(revertButton);
footerLayout->addStretch();
footerLayout->addWidget(buttonBox);
root->addWidget(footer);
// Connections
connect(schemeComboBox, &QComboBox::currentTextChanged, this, &PaletteEditorDialog::onSchemeChanged);
connect(quickSetupPanel, &QuickSetupPanel::generateRequested, this, &PaletteEditorDialog::onGenerateFromAccent);
connect(revertButton, &QPushButton::clicked, this, &PaletteEditorDialog::onRevertToDefault);
connect(resetBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onReset);
connect(applyBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onApply);
connect(saveBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onSave);
connect(closeBtn, &QPushButton::clicked, this, &QDialog::reject);
connect(paletteGridToggleButton, &QPushButton::toggled, this, [this](bool open) {
paletteGridToggleButton->setText(open ? tr("▼ Edit Palette") : tr("▶ Edit Palette"));
paletteGridSeparator->setVisible(open);
paletteGrid->setVisible(open);
if (open) {
setMinimumHeight(680);
resize(width(), 680);
} else {
setMinimumHeight(220);
adjustSize(); // shrinks to fit just the visible content
}
});
}
void PaletteEditorDialog::retranslateUi()
{
setWindowTitle(tr("Palette Editor — %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 hasDefault = PaletteConfig::fromDefault(themeDirPath, "Light").hasPalette() ||
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"));
editingLabel->setText(tr("Editing:"));
paletteGridToggleButton->setText(tr("▶ Edit Palette"));
paletteGridToggleButton->setToolTip(tr("Show or hide the per-role colour grid for manual tweaks"));
revertButton->setText(tr("↺ Revert to theme default"));
resetBtn->setText(tr("Reset"));
applyBtn->setText(tr("Apply"));
saveBtn->setText(tr("Save && Apply"));
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()));
if (themeDirPath.isEmpty()) {
saveBtn->setEnabled(false);
saveBtn->setToolTip(tr("Cannot save: this theme has no directory on disk"));
}
}
void PaletteEditorDialog::loadSchemes()
{
const QStringList schemes = {"Light", "Dark"};
for (const QString &scheme : schemes) {
PaletteConfig cfg = PaletteConfig::fromScheme(themeDirPath, scheme);
if (!cfg.hasPalette()) {
cfg = PaletteConfig::fromDefault(themeDirPath, scheme);
}
if (!cfg.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) {
cfg.colors[group][role] = appPal.color(group, role);
}
}
}
}
savedConfig[scheme] = cfg;
workingConfig[scheme] = cfg;
}
}
void PaletteEditorDialog::seedAccentFromScheme(const QString &scheme)
{
QColor seed = workingConfig.value(scheme).colors.value(QPalette::Active).value(QPalette::Highlight);
if (seed.isValid()) {
quickSetupPanel->setAccentColor(seed);
}
}
void PaletteEditorDialog::onSchemeChanged(const QString &scheme)
{
// Snapshot unsaved edits for the scheme we're leaving
if (!loadedScheme.isEmpty()) {
workingConfig[loadedScheme] = paletteGrid->currentPaletteConfig();
}
loadedScheme = scheme;
paletteGrid->loadPalette(workingConfig.value(scheme));
seedAccentFromScheme(scheme);
onApply();
}
void PaletteEditorDialog::onGenerateFromAccent(const QColor &accent, int intensity)
{
PaletteConfig cfg = PaletteGenerator::fromAccent(accent, intensity, loadedScheme);
workingConfig[loadedScheme] = cfg;
paletteGrid->loadPalette(cfg);
}
void PaletteEditorDialog::onApply()
{
themeManager->previewPalette(paletteGrid->currentPaletteConfig(), loadedScheme);
}
void PaletteEditorDialog::onSave()
{
if (loadedScheme.isEmpty()) {
return;
}
PaletteConfig cfg = paletteGrid->currentPaletteConfig();
if (!ThemeManager::savePaletteConfig(themeDirPath, loadedScheme, cfg)) {
QMessageBox::warning(this, tr("Save failed"),
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), themeDirPath));
return;
}
ThemeConfig globalCfg = ThemeConfig::fromThemeDir(themeDirPath);
globalCfg.colorScheme = loadedScheme;
globalCfg.save(themeDirPath);
savedConfig[loadedScheme] = cfg;
workingConfig[loadedScheme] = cfg;
themeManager->reloadCurrentTheme();
accept();
}
void PaletteEditorDialog::onReset()
{
workingConfig[loadedScheme] = savedConfig[loadedScheme];
paletteGrid->loadPalette(savedConfig[loadedScheme]);
}
void PaletteEditorDialog::onRevertToDefault()
{
PaletteConfig def = PaletteConfig::fromDefault(themeDirPath, loadedScheme);
if (!def.hasPalette()) {
QMessageBox::information(this, tr("No default found"),
tr("No default palette file found for the \"%1\" scheme.").arg(loadedScheme));
return;
}
workingConfig[loadedScheme] = def;
paletteGrid->loadPalette(def);
}
void PaletteEditorDialog::changeEvent(QEvent *e)
{
if (e->type() == QEvent::PaletteChange) {
QTimer::singleShot(0, this, &PaletteEditorDialog::refreshChromePalettes);
}
QDialog::changeEvent(e);
}
void PaletteEditorDialog::refreshChromePalettes()
{
const QPalette base = qApp->palette();
if (header) {
QPalette hp = header->palette();
hp.setColor(QPalette::Window, base.color(QPalette::Window).darker(108));
header->setPalette(hp);
header->update();
}
if (footer) {
QPalette fp = footer->palette();
fp.setColor(QPalette::Window, base.color(QPalette::Window).darker(104));
footer->setPalette(fp);
footer->update();
}
if (quickSetupPanel) {
QPalette sp = quickSetupPanel->palette();
sp.setColor(QPalette::Window, base.color(QPalette::Window).darker(102));
quickSetupPanel->setPalette(sp);
quickSetupPanel->update();
}
}

View file

@ -0,0 +1,68 @@
#ifndef COCKATRICE_PALETTE_EDITOR_DIALOG_H
#define COCKATRICE_PALETTE_EDITOR_DIALOG_H
#include "../theme_config.h"
#include <QDialog>
#include <QFrame>
#include <QMap>
class QLabel;
class QComboBox;
class QDialogButtonBox;
class QPushButton;
class PaletteGridWidget;
class QuickSetupPanel;
class PaletteEditorDialog : public QDialog
{
Q_OBJECT
public:
explicit PaletteEditorDialog(const QString &themeDirPath, const QString &themeName, QWidget *parent = nullptr);
void loadSchemes();
private slots:
void onSave();
void onApply();
void onReset();
void onRevertToDefault();
void onSchemeChanged(const QString &scheme);
void onGenerateFromAccent(const QColor &accent, int intensity);
private:
void setupUi();
void retranslateUi();
void refreshChromePalettes();
void loadScheme(const QString &scheme); // snapshot current, switch, load
void seedAccentFromScheme(const QString &scheme);
// Sub-widgets
QWidget *header;
QLabel *titleLabel;
QLabel *editingLabel;
QuickSetupPanel *quickSetupPanel = nullptr;
PaletteGridWidget *paletteGrid = nullptr;
QPushButton *paletteGridToggleButton = nullptr;
QFrame *paletteGridSeparator = nullptr;
QWidget *footer;
QComboBox *schemeComboBox = nullptr;
QDialogButtonBox *buttonBox = nullptr;
QPushButton *resetBtn = nullptr;
QPushButton *applyBtn = nullptr;
QPushButton *saveBtn = nullptr;
QPushButton *closeBtn = nullptr;
QPushButton *revertButton = nullptr;
// State
QString themeDirPath;
QString themeName;
QString loadedScheme;
QMap<QString, PaletteConfig> workingConfig;
QMap<QString, PaletteConfig> savedConfig;
protected:
void changeEvent(QEvent *e) override;
};
#endif // COCKATRICE_PALETTE_EDITOR_DIALOG_H

View file

@ -0,0 +1,200 @@
#include "palette_generator.h"
#include <QColor>
// ════════════════════════════════════════════════════════════════════════════
// PaletteGenerator::fromAccent
//
// Three intensity bands:
// 030 Subtle — Highlight / links / BrightText take on the hue;
// backgrounds stay neutral grey.
// 3070 Accented — Button, ToolTipBase, AlternateBase, shading tint in;
// backgrounds get a faint hue wash.
// 70100 Chromatic— Window and Base pick up real saturation; the whole
// application reads as that colour, text stays readable.
//
// Key principles:
// • Quadratic saturation curve: low end feels truly subtle, top is vivid.
// • Each role has its own saturation ceiling; buttons always pop above window.
// • Base stays near-white / near-black regardless of intensity for legibility.
// • Button text contrast is computed from the real button color, not assumed.
// • Tooltip blends from classic yellow to hue-tinted above intensity ~25.
// • 3D shading ladder (Light→Shadow) carries the same hue for coherence.
// • Disabled: text → mid-gray, backgrounds → match Window.
// • Inactive Highlight fades to a near-bg tone so it doesn't compete.
// ════════════════════════════════════════════════════════════════════════════
namespace PaletteGenerator
{
PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &scheme)
{
PaletteConfig cfg;
const bool dark = scheme.compare("Dark", Qt::CaseInsensitive) == 0;
const double t = intensity / 100.0; // 0.0 1.0
int h = accent.hslHue();
const bool achromatic = (h < 0);
if (achromatic) {
h = 0;
}
// Saturation budgets
// Quadratic ease-in means the subtle end is genuinely subtle and the
// full end is bold without being garish.
auto sat = [&](double maxSat) -> int {
if (achromatic) {
return 0;
}
return qRound(maxSat * t * t);
};
const int satWindow = sat(dark ? 80.0 : 90.0);
const int satBase = sat(dark ? 25.0 : 20.0); // text areas stay near-white/black
const int satAlt = sat(dark ? 90.0 : 100.0);
const int satButton = sat(dark ? 120.0 : 130.0); // buttons pop above the bg
const int satTooltip = sat(dark ? 90.0 : 80.0);
const int satHighlight = achromatic ? 0 : qRound(accent.hslSaturation() * (0.45 + 0.55 * t));
const int satShadeHi = sat(dark ? 60.0 : 50.0); // Light / Midlight
const int satShadeLo = sat(dark ? 90.0 : 70.0); // Mid / Dark
// Per-role lightness
// Nudge lightness slightly as saturation rises to compensate for the
// Helmholtz-Kohlrausch effect (saturated colors look lighter/heavier).
const int winL = dark ? (28 + qRound(t * 8)) : (242 - qRound(t * 6));
const int baseL = dark ? (43 + qRound(t * 6)) : 252;
const int altL = dark ? (36 + qRound(t * 9)) : (234 - qRound(t * 5));
const int btnL = dark ? (56 + qRound(t * 10)) : (230 - qRound(t * 10));
const int tipL = dark ? (52 + qRound(t * 8)) : (248 - qRound(t * 6));
// Highlight color
QColor hl;
if (achromatic) {
hl = dark ? QColor(105, 105, 105) : QColor(95, 95, 95);
} else if (dark) {
int L = qBound(105, accent.lightness() + qRound(45.0 * (1.0 - t)), 215);
hl = QColor::fromHsl(h, qMin(255, satHighlight), L);
} else {
int L = qBound(50, accent.lightness() - qRound(25.0 * t), 180);
hl = QColor::fromHsl(h, qMin(255, satHighlight), L);
}
const double hlLuma = 0.299 * hl.red() + 0.587 * hl.green() + 0.114 * hl.blue();
const QColor hlText = (hlLuma > 135) ? Qt::black : Qt::white;
// Local helpers
using CR = QPalette::ColorRole;
using CG = QPalette::ColorGroup;
auto hsl = [&](int lightness, int s) -> QColor {
return QColor::fromHsl(h, qBound(0, s, 255), qBound(0, lightness, 255));
};
auto textOn = [](const QColor &bg) -> QColor {
double luma = 0.299 * bg.red() + 0.587 * bg.green() + 0.114 * bg.blue();
return (luma > 135) ? Qt::black : Qt::white;
};
auto set3 = [&](CR role, QColor active, QColor disabled, QColor inactive) {
cfg.colors[CG::Active][role] = active;
cfg.colors[CG::Disabled][role] = disabled;
cfg.colors[CG::Inactive][role] = inactive;
};
auto setAll = [&](CR role, QColor c) { set3(role, c, c, c); };
// Tooltip: blend classic yellow → hue-tinted above t≈0.20
QColor bg_tip;
if (achromatic || t < 0.20) {
bg_tip = QColor(255, 255, 220);
} else {
QColor tinted = hsl(tipL, satTooltip);
double blend = qMin(1.0, (t - 0.20) / 0.55);
QColor yellow(255, 255, 220);
bg_tip = QColor(qRound(yellow.red() * (1.0 - blend) + tinted.red() * blend),
qRound(yellow.green() * (1.0 - blend) + tinted.green() * blend),
qRound(yellow.blue() * (1.0 - blend) + tinted.blue() * blend));
}
// Backgrounds
const QColor bg_win = hsl(winL, satWindow);
const QColor bg_base = hsl(baseL, satBase);
const QColor bg_alt = hsl(altL, satAlt);
const QColor bg_btn = hsl(btnL, satButton);
set3(CR::Window, bg_win, bg_win, bg_win);
set3(CR::Base, bg_base, bg_win, bg_base);
set3(CR::AlternateBase, bg_alt, bg_alt, bg_alt);
set3(CR::Button, bg_btn, bg_win, bg_btn);
set3(CR::ToolTipBase, bg_tip, bg_tip, bg_tip);
// Foreground text
const QColor winText = dark ? Qt::white : Qt::black;
const QColor disText = dark ? QColor(157, 157, 157) : QColor(120, 120, 120);
const QColor disBtnText = dark ? QColor(120, 120, 120) : QColor(150, 150, 150);
set3(CR::WindowText, winText, disText, winText);
set3(CR::Text, winText, disText, winText);
set3(CR::ButtonText, textOn(bg_btn), disBtnText, textOn(bg_btn));
setAll(CR::ToolTipText, textOn(bg_tip));
const QColor phAlpha = dark ? QColor(255, 255, 255, 110) : QColor(0, 0, 0, 110);
const QColor phDis = dark ? QColor(255, 255, 255, 70) : QColor(0, 0, 0, 70);
set3(CR::PlaceholderText, phAlpha, phDis, phAlpha);
// Highlight / selection
const QColor inactiveHl = hsl(winL + (dark ? 14 : -10), satWindow);
cfg.colors[CG::Active][CR::Highlight] = hl;
cfg.colors[CG::Disabled][CR::Highlight] = inactiveHl;
cfg.colors[CG::Inactive][CR::Highlight] = inactiveHl;
cfg.colors[CG::Active][CR::HighlightedText] = hlText;
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
// BrightText
QColor bright;
if (achromatic) {
bright = dark ? Qt::white : Qt::black;
} else if (dark) {
bright = QColor::fromHsl(h, qMin(255, satHighlight + 25), qMin(235, hl.lightness() + 50));
} else {
bright = Qt::white;
}
setAll(CR::BrightText, bright);
// Links
QColor link, linkV;
if (achromatic) {
link = dark ? QColor(100, 200, 255) : QColor(0, 0, 210);
linkV = dark ? QColor(200, 100, 255) : QColor(128, 0, 180);
} else if (dark) {
link = QColor::fromHsl(h, qMin(255, satHighlight), qMin(230, hl.lightness() + 75));
linkV = QColor::fromHsl((h + 30) % 360, qMin(255, satHighlight), qMin(215, hl.lightness() + 55));
} else {
link = QColor::fromHsl(h, qMin(255, satHighlight), qMax(40, hl.lightness() - 75));
linkV = QColor::fromHsl((h + 30) % 360, qMin(255, satHighlight), qMax(30, hl.lightness() - 95));
}
set3(CR::Link, link, dark ? QColor(48, 140, 198) : QColor(0, 0, 255), link);
set3(CR::LinkVisited, linkV, dark ? QColor(180, 80, 255) : QColor(255, 0, 255), linkV);
// 3D / frame shading
if (dark) {
setAll(CR::Light, hsl(115, qMin(255, satShadeHi)));
setAll(CR::Midlight, hsl(82, qMin(255, satShadeHi)));
setAll(CR::Mid, hsl(37, satShadeLo));
setAll(CR::Dark, hsl(22, satShadeLo));
setAll(CR::Shadow, Qt::black);
} else {
setAll(CR::Light, Qt::white);
setAll(CR::Midlight, hsl(226, qMin(255, satShadeHi)));
setAll(CR::Mid, hsl(158, satShadeLo));
setAll(CR::Dark, hsl(148, satShadeLo));
// Shadow stays neutral — tinting it makes the UI look bruised
cfg.colors[CG::Active][CR::Shadow] = QColor(105, 105, 105);
cfg.colors[CG::Disabled][CR::Shadow] = Qt::black;
cfg.colors[CG::Inactive][CR::Shadow] = QColor(105, 105, 105);
}
return cfg;
}
} // namespace PaletteGenerator

View file

@ -0,0 +1,16 @@
#ifndef COCKATRICE_PALETTE_GENERATOR_H
#define COCKATRICE_PALETTE_GENERATOR_H
#include "../theme_config.h"
#include <QColor>
#include <QString>
// All QPalette roles are derived from a single accent color and an
// intensity value (0-100). See the .cpp for the full band breakdown.
namespace PaletteGenerator
{
PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &scheme);
} // namespace PaletteGenerator
#endif // COCKATRICE_PALETTE_GENERATOR_H

View file

@ -0,0 +1,179 @@
#include "palette_grid_widget.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")},
};
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);
colorButtons[group][role] = btn;
grid->addWidget(btn, row + 1, col + 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);
}
}
}
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
{
PaletteConfig cfg;
for (auto group : ALL_GROUPS) {
for (auto role : allRoles()) {
cfg.colors[group][role] = colorButtons[group][role]->getColor();
}
}
return cfg;
}

View file

@ -0,0 +1,38 @@
#ifndef COCKATRICE_PALETTE_GRID_WIDGET_H
#define COCKATRICE_PALETTE_GRID_WIDGET_H
#include "../theme_config.h"
#include "color_button.h"
#include <QMap>
#include <QPalette>
#include <QVBoxLayout>
#include <QWidget>
class QLabel;
class QScrollArea;
// Scrollable grid of ColorButtons — one per (ColorGroup × ColorRole) cell.
// Owns the load/read round-trip for PaletteConfig but has no file I/O itself.
class PaletteGridWidget : public QWidget
{
Q_OBJECT
public:
explicit PaletteGridWidget(QWidget *parent = nullptr);
void loadPalette(const PaletteConfig &cfg);
PaletteConfig currentPaletteConfig() const;
private:
void buildGrid(QWidget *host);
void changeEvent(QEvent *e);
void refreshChromePalettes();
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
QScrollArea *scroll;
QWidget *gridHost;
QVBoxLayout *layout;
QVector<QLabel *> headerLabels;
QVector<QWidget *> rowShadeWidgets;
};
#endif // COCKATRICE_PALETTE_GRID_WIDGET_H

View file

@ -0,0 +1,99 @@
#include "quick_setup_panel.h"
#include <QApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QSlider>
QuickSetupPanel::QuickSetupPanel(QWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
layout->setContentsMargins(12, 8, 12, 8);
layout->setSpacing(10);
heading = new QLabel(this);
heading->setTextFormat(Qt::RichText);
accentLabel = new QLabel(this);
accentButton = new ColorButton(this);
accentButton->setColor(QColor(20, 140, 60));
intensityLabel = new QLabel(this);
labelLow = new QLabel(this);
labelHigh = new QLabel(this);
QFont small = labelLow->font();
small.setPointSizeF(small.pointSizeF() * 0.82);
labelLow->setFont(small);
labelHigh->setFont(small);
QPalette dimmed = labelLow->palette();
dimmed.setColor(QPalette::WindowText, qApp->palette().color(QPalette::Mid));
labelLow->setPalette(dimmed);
labelHigh->setPalette(dimmed);
intensitySlider = new QSlider(Qt::Horizontal, this);
intensitySlider->setRange(0, 100);
intensitySlider->setValue(70);
intensitySlider->setFixedWidth(160);
intensityPercentageLabel = new QLabel(this);
intensityPercentageLabel->setFixedWidth(34);
intensityPercentageLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
generateButton = new QPushButton(this);
layout->addWidget(heading);
layout->addSpacing(6);
layout->addWidget(accentLabel);
layout->addWidget(accentButton);
layout->addSpacing(12);
layout->addWidget(intensityLabel);
layout->addWidget(labelLow);
layout->addWidget(intensitySlider);
layout->addWidget(labelHigh);
layout->addWidget(intensityPercentageLabel);
layout->addStretch();
layout->addWidget(generateButton);
connect(intensitySlider, &QSlider::valueChanged, this,
[this](int v) { intensityPercentageLabel->setText(tr("%1%").arg(v)); });
connect(generateButton, &QPushButton::clicked, this,
[this] { emit generateRequested(accentButton->getColor(), intensitySlider->value()); });
retranslateUi();
}
void QuickSetupPanel::retranslateUi()
{
heading->setText(tr("<b>Quick Setup</b>"));
heading->setToolTip(tr("Generate all palette roles automatically from a single accent colour"));
accentLabel->setText(tr("Accent:"));
accentButton->setToolTip(tr("Primary hue. Used directly for highlights and links.\n"
"At high intensity it also tints buttons and backgrounds."));
intensityLabel->setText(tr("Intensity:"));
labelLow->setText(tr("Subtle"));
labelHigh->setText(tr("Full colour"));
intensitySlider->setToolTip(tr("030 Subtle tint — only highlights and links change hue\n"
"3070 Accented — buttons, tooltips, and borders join in\n"
"70100 Full colour — backgrounds, everything"));
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
{
return accentButton->getColor();
}
int QuickSetupPanel::intensity() const
{
return intensitySlider->value();
}
void QuickSetupPanel::setAccentColor(const QColor &c)
{
accentButton->setColor(c);
}

View file

@ -0,0 +1,94 @@
#ifndef COCKATRICE_QUICK_SETUP_PANEL_H
#define COCKATRICE_QUICK_SETUP_PANEL_H
#include "color_button.h"
#include <QWidget>
class QPushButton;
class QHBoxLayout;
class QLabel;
class QSlider;
/**
* @class QuickSetupPanel
* @brief Provides a compact "Quick Setup" interface for generating theme palettes.
*
* The panel contains:
* - an accent color picker,
* - an intensity slider,
* - and a generate button.
*
* When the user clicks the generate button, the panel emits
* generateRequested() with the currently selected accent color
* and intensity value.
*
* Typically used together with PaletteGenerator::fromAccent()
* to quickly generate color schemes from a chosen accent color.
*/
class QuickSetupPanel : public QWidget
{
Q_OBJECT
public:
/**
* @brief Constructs the quick setup panel.
*
* @param parent Optional parent widget.
*/
explicit QuickSetupPanel(QWidget *parent = nullptr);
/**
* @brief Retranslates all user-visible strings.
*
* Intended to be called when the application language changes.
*/
void retranslateUi();
/**
* @brief Returns the currently selected accent color.
*
* @return The selected accent color.
*/
QColor accentColor() const;
/**
* @brief Returns the current intensity slider value.
*
* @return The selected intensity value.
*/
int intensity() const;
/**
* @brief Updates the displayed accent color.
*
* Used by the parent dialog when switching schemes to keep
* the color swatch synchronized with the active palette.
*
* @param c The new accent color.
*/
void setAccentColor(const QColor &c);
signals:
/**
* @brief Emitted when the user requests palette generation.
*
* @param accent The selected accent color.
* @param intensity The selected intensity value.
*/
void generateRequested(QColor accent, int intensity);
private:
QHBoxLayout *layout;
QLabel *heading;
QLabel *accentLabel;
ColorButton *accentButton;
QLabel *intensityLabel;
QLabel *labelLow;
QLabel *labelHigh;
QSlider *intensitySlider;
QLabel *intensityPercentageLabel;
QPushButton *generateButton;
};
#endif // COCKATRICE_QUICK_SETUP_PANEL_H

View file

@ -0,0 +1,267 @@
#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();
}
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";
}
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>();
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;
}
}
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;
}
QColor color(value);
if (color.isValid()) {
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;
PaletteConfig cfg =
fromFile(dir.absoluteFilePath(wantDark ? "palette-default-dark.toml" : "palette-default-light.toml"));
if (!cfg.hasPalette()) {
cfg = fromFile(dir.absoluteFilePath(wantDark ? "palette-default-light.toml" : "palette-default-dark.toml"));
}
return cfg;
}
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;
}

View file

@ -0,0 +1,37 @@
#ifndef COCKATRICE_THEME_CONFIG_H
#define COCKATRICE_THEME_CONFIG_H
#include <QColor>
#include <QMap>
#include <QPalette>
#include <QString>
struct ThemeConfig
{
QString colorScheme;
QString styleName;
bool isEmpty() const;
QString toIni() const;
static ThemeConfig fromThemeDir(const QString &themeDirPath);
bool save(const QString &themeDirPath) const;
};
struct PaletteConfig
{
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors;
bool hasPalette() const;
QString toToml() const;
static QString fileName(const QString &colorScheme);
static PaletteConfig fromFile(const QString &filePath);
static PaletteConfig fromScheme(const QString &themeDirPath, const QString &colorScheme);
static PaletteConfig fromDefault(const QString &themeDirPath, const QString &colorScheme);
QPalette apply(QPalette base) const;
};
#endif // COCKATRICE_THEME_CONFIG_H

View file

@ -19,9 +19,7 @@
#include <Qt>
#define NONE_THEME_NAME "Default"
#define FUSION_THEME_NAME "Fusion (System Default)"
#define FUSION_THEME_NAME_LIGHT "Fusion (Light)"
#define FUSION_THEME_NAME_DARK "Fusion (Dark)"
#define FUSION_THEME_NAME "Fusion"
#define STYLE_CSS_NAME "style.css"
#define HANDZONE_BG_NAME "handzone"
#define PLAYERZONE_BG_NAME "playerzone"
@ -115,37 +113,28 @@ void ThemeManager::ensureThemeDirectoryExists()
}
}
bool ThemeManager::isDarkMode()
bool ThemeManager::isDarkMode(const QString &themeDirPath)
{
auto themeName = SettingsCache::instance().getThemeName();
// Explicit Dark Mode
if (themeName == FUSION_THEME_NAME_LIGHT || themeName.endsWith("(Light)")) {
ThemeConfig themeConfig = ThemeConfig::fromThemeDir(themeDirPath);
if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) {
return true;
} else if (themeConfig.colorScheme.compare("Light", Qt::CaseInsensitive) == 0) {
return false;
}
// Explicit Light Mode
if (themeName == FUSION_THEME_NAME_DARK || themeName.endsWith("(Dark)")) {
return true;
}
// Auto detection on compatible Qt versions
#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark &&
(themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME || themeName.endsWith("(System Default)"))) {
return true;
} else {
return false;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
bool osDark = (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark);
#else
bool osDark = false;
#endif
// Default to light mode
return false;
return osDark;
}
}
bool ThemeManager::isBuiltInTheme()
{
const auto themeName = SettingsCache::instance().getThemeName();
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME || themeName == FUSION_THEME_NAME_LIGHT ||
themeName == FUSION_THEME_NAME_DARK;
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
}
QStringMap &ThemeManager::getAvailableThemes()
@ -153,15 +142,13 @@ QStringMap &ThemeManager::getAvailableThemes()
QDir dir;
availableThemes.clear();
// add default value
availableThemes.insert(NONE_THEME_NAME, QString());
// load themes from user profile dir
dir.setPath(SettingsCache::instance().getThemesPath());
availableThemes.insert(FUSION_THEME_NAME, dir.filePath("Fusion (System Default)"));
availableThemes.insert(FUSION_THEME_NAME_LIGHT, dir.filePath("Fusion (Light)"));
availableThemes.insert(FUSION_THEME_NAME_DARK, dir.filePath("Fusion (Dark)"));
// 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)) {
@ -217,192 +204,112 @@ QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush)
return brush;
}
static inline QPalette createDarkGreenFusionPalette()
ThemeConfig ThemeManager::loadGlobalConfig(const QString &themeDirPath)
{
QPalette p = QStyleFactory::create("Fusion")->standardPalette();
// ---------- Core backgrounds ----------
p.setColor(QPalette::Window, QColor(30, 30, 30)); // #ff1e1e1e
p.setColor(QPalette::Base, QColor(45, 45, 45)); // #ff2d2d2d
p.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); // #ff353535
p.setColor(QPalette::Button, QColor(60, 60, 60)); // #ff3c3c3c
p.setColor(QPalette::ToolTipBase, QColor(60, 60, 60)); // #ff3c3c3c
// ---------- Core text ----------
p.setColor(QPalette::WindowText, Qt::white); // #ffffffff
p.setColor(QPalette::Text, Qt::white); // #ffffffff
p.setColor(QPalette::ButtonText, Qt::white); // #ffffffff
p.setColor(QPalette::ToolTipText, QColor(212, 212, 212)); // #ffd4d4d4
p.setColor(QPalette::PlaceholderText, QColor(255, 255, 255, 128)); // #80ffffff
// ---------- Selection / focus ----------
const QColor highlight(20, 140, 60); // #ff148c3c
p.setColor(QPalette::Highlight, highlight);
p.setColor(QPalette::HighlightedText, Qt::white); // #ffffffff
// ---------- Links ----------
p.setColor(QPalette::Link, QColor(0, 246, 82)); // #ff00f652
p.setColor(QPalette::LinkVisited, QColor(0, 211, 70)); // #ff00d346
// ---------- Accent (Qt 6) ----------
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
p.setColor(QPalette::Accent, QColor(0, 211, 70)); // #ff00d346
#endif
// ---------- Bright text ----------
p.setColor(QPalette::BrightText, QColor(0, 246, 82)); // #ff00f652
// ---------- 3D / frame shading ----------
p.setColor(QPalette::Light, QColor(120, 120, 120)); // #ff787878
p.setColor(QPalette::Midlight, QColor(90, 90, 90)); // #ff5a5a5a
p.setColor(QPalette::Mid, QColor(40, 40, 40)); // #ff282828
p.setColor(QPalette::Dark, QColor(30, 30, 30)); // #ff1e1e1e
p.setColor(QPalette::Shadow, Qt::black); // #ff000000
// ---------- Disabled state ----------
const QColor disabledText(157, 157, 157); // #ff9d9d9d
p.setColor(QPalette::Disabled, QPalette::WindowText, disabledText);
p.setColor(QPalette::Disabled, QPalette::Text, disabledText);
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabledText);
p.setColor(QPalette::Disabled, QPalette::Base, QColor(30, 30, 30));
p.setColor(QPalette::Disabled, QPalette::Window, QColor(30, 30, 30));
p.setColor(QPalette::Disabled, QPalette::Link, QColor(48, 140, 198)); // #ff308cc6
p.setColor(QPalette::Disabled, QPalette::LinkVisited, QColor(255, 0, 255)); // #ffff00ff
p.setColor(QPalette::Disabled, QPalette::ToolTipBase, QColor(255, 255, 220));
p.setColor(QPalette::Disabled, QPalette::ToolTipText, Qt::black);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
p.setColor(QPalette::Disabled, QPalette::Accent, disabledText);
#endif
// ---------- Inactive state ----------
p.setColor(QPalette::Inactive, QPalette::Highlight, QColor(30, 30, 30));
p.setColor(QPalette::Inactive, QPalette::HighlightedText, Qt::white);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
p.setColor(QPalette::Inactive, QPalette::Accent, QColor(30, 30, 30));
#endif
return p;
return ThemeConfig::fromThemeDir(themeDirPath);
}
static inline QPalette createLightGreenFusionPalette()
bool ThemeManager::saveGlobalConfig(const QString &themeDirPath, const ThemeConfig &cfg)
{
QPalette p = QStyleFactory::create("Fusion")->standardPalette();
// ---------- Core backgrounds ----------
p.setColor(QPalette::Window, QColor(240, 240, 240)); // #fff0f0f0
p.setColor(QPalette::Base, Qt::white); // #ffffffff
p.setColor(QPalette::AlternateBase, QColor(233, 231, 227)); // #ffe9e7e3
p.setColor(QPalette::Button, QColor(240, 240, 240)); // #fff0f0f0
p.setColor(QPalette::ToolTipBase, QColor(255, 255, 220)); // #ffffffdc
// ---------- Core text ----------
p.setColor(QPalette::WindowText, Qt::black); // #ff000000
p.setColor(QPalette::Text, Qt::black); // #ff000000
p.setColor(QPalette::ButtonText, Qt::black); // #ff000000
p.setColor(QPalette::ToolTipText, Qt::black); // #ff000000
p.setColor(QPalette::PlaceholderText, QColor(0, 0, 0, 128)); // #80000000
// ---------- Selection / focus ----------
const QColor highlight(20, 140, 60); // #ff148c3c
p.setColor(QPalette::Highlight, highlight);
p.setColor(QPalette::HighlightedText, Qt::white); // #ffffffff
// ---------- Links ----------
p.setColor(QPalette::Link, QColor(13, 95, 40)); // #ff0d5f28
p.setColor(QPalette::LinkVisited, QColor(8, 64, 27)); // #ff08401b
// ---------- Accent (Qt 6) ----------
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
p.setColor(QPalette::Accent, QColor(16, 117, 50)); // #ff107532
#endif
// ---------- Bright text ----------
p.setColor(QPalette::BrightText, Qt::white); // #ffffffff
// ---------- 3D / frame shading ----------
p.setColor(QPalette::Light, Qt::white); // #ffffffff
p.setColor(QPalette::Midlight, QColor(227, 227, 227)); // #ffe3e3e3
p.setColor(QPalette::Mid, QColor(160, 160, 160)); // #ffa0a0a0
p.setColor(QPalette::Dark, QColor(160, 160, 160)); // #ffa0a0a0
p.setColor(QPalette::Shadow, QColor(105, 105, 105)); // #ff696969
// ---------- Disabled state ----------
const QColor disabledText(120, 120, 120); // #ff787878
p.setColor(QPalette::Disabled, QPalette::WindowText, disabledText);
p.setColor(QPalette::Disabled, QPalette::Text, disabledText);
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabledText);
p.setColor(QPalette::Disabled, QPalette::Base, QColor(240, 240, 240));
p.setColor(QPalette::Disabled, QPalette::Window, QColor(240, 240, 240));
p.setColor(QPalette::Disabled, QPalette::Midlight, QColor(247, 247, 247));
p.setColor(QPalette::Disabled, QPalette::AlternateBase, QColor(247, 247, 247));
p.setColor(QPalette::Disabled, QPalette::Shadow, Qt::black);
p.setColor(QPalette::Disabled, QPalette::Link, QColor(0, 0, 255));
p.setColor(QPalette::Disabled, QPalette::LinkVisited, QColor(255, 0, 255));
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
p.setColor(QPalette::Disabled, QPalette::Accent, disabledText);
#endif
// ---------- Inactive state ----------
p.setColor(QPalette::Inactive, QPalette::Highlight, QColor(240, 240, 240));
p.setColor(QPalette::Inactive, QPalette::HighlightedText, Qt::black);
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
p.setColor(QPalette::Inactive, QPalette::Accent, QColor(240, 240, 240));
#endif
return p;
return cfg.save(themeDirPath);
}
void ThemeManager::themeChangedSlot()
PaletteConfig ThemeManager::loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme)
{
QString themeName = SettingsCache::instance().getThemeName();
qCInfo(ThemeManagerLog) << "Theme changed:" << themeName;
if (themeDirPath.isEmpty()) {
return {};
}
return PaletteConfig::fromScheme(themeDirPath, colorScheme);
}
QString dirPath = getAvailableThemes().value(themeName);
QDir dir = dirPath;
// css
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) {
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
} else {
qApp->setStyleSheet("");
bool ThemeManager::savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg)
{
if (themeDirPath.isEmpty()) {
return false;
}
QStyle *newStyle = nullptr;
QPalette newPalette;
QDir dir(themeDirPath);
if (!dir.exists()) {
dir.mkpath(".");
}
if (themeName == FUSION_THEME_NAME) {
newStyle = QStyleFactory::create("Fusion");
QFile f(dir.absoluteFilePath(PaletteConfig::fileName(colorScheme)));
if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
return false;
}
#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
// Start from Fusion's own palette so dark mode is handled correctly,
// then apply any tweaks on top of it.
newPalette = newStyle->standardPalette();
if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark) {
newPalette.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
QTextStream(&f) << cfg.toToml();
return true;
}
void ThemeManager::setColorScheme(const QString &scheme)
{
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.colorScheme = scheme;
cfg.save(dirPath);
reloadCurrentTheme();
}
void ThemeManager::reloadCurrentTheme()
{
themeChangedSlot();
}
void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &scheme)
{
const QString themeName = SettingsCache::instance().getThemeName();
const QString dirPath = getAvailableThemes().value(themeName);
const ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath);
applyStyleAndPalette(themeName, themeCfg, cfg, scheme);
}
void ThemeManager::applyStyleAndPalette(const QString &themeName,
const ThemeConfig &themeCfg,
const PaletteConfig &palCfg,
const QString &activeScheme)
{
QString styleName = themeCfg.styleName;
if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) {
if (themeName == FUSION_THEME_NAME) {
styleName = "Fusion";
} else {
styleName = defaultStyleName;
}
#else
newPalette = qApp->palette();
#endif
} else if (themeName == FUSION_THEME_NAME_LIGHT) {
newStyle = QStyleFactory::create("Fusion");
newPalette = createLightGreenFusionPalette();
} else if (themeName == FUSION_THEME_NAME_DARK) {
newStyle = QStyleFactory::create("Fusion");
newPalette = createDarkGreenFusionPalette();
} else {
newStyle = QStyleFactory::create(defaultStyleName);
// Use the style's default palette.
newPalette = newStyle->standardPalette();
}
// Apply palette FIRST.
qApp->setPalette(newPalette);
// Then apply style.
qApp->setStyle(newStyle);
QStyle *style = QStyleFactory::create(styleName);
if (!style) {
style = QStyleFactory::create(defaultStyleName);
}
// Base palette
QPalette base;
if (styleName.compare("Fusion", Qt::CaseInsensitive) == 0) {
base = style->standardPalette();
#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
if (activeScheme == "Dark") {
base.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
}
#endif
} else {
base = qApp->palette();
}
// Overlay custom palette colours
if (palCfg.hasPalette()) {
base = palCfg.apply(base);
}
// Palette BEFORE style — setStyle() triggers a synchronous repolish of all
// widgets immediately. If the palette isn't set yet at that point, every
// widget gets polished against the stale colours, requiring a second apply
// to fully resolve. Setting palette first means setStyle's repolish cascade
// already sees the correct colours.
qApp->setPalette(base);
qApp->setStyle(style);
// Force every widget to re-polish and repaint immediately rather than
// waiting for natural expose events, which produces a patchwork of old
@ -412,34 +319,57 @@ void ThemeManager::themeChangedSlot()
// palette (WA_SetPalette not set). Calling it unconditionally would clobber
// intentional per-widget palette customisations across the whole app.
for (QWidget *widget : qApp->allWidgets()) {
if (widget->isVisible()) {
newStyle->unpolish(widget);
newStyle->polish(widget);
widget->update();
}
style->unpolish(widget);
style->polish(widget);
widget->update();
}
}
if (dirPath.isEmpty()) {
// set default values
QDir::setSearchPaths("theme", DEFAULT_RESOURCE_PATHS);
brushes[Role::Hand] = HANDZONE_BG_DEFAULT;
brushes[Role::Table] = TABLEZONE_BG_DEFAULT;
brushes[Role::Player] = PLAYERZONE_BG_DEFAULT;
brushes[Role::Stack] = STACKZONE_BG_DEFAULT;
void ThemeManager::themeChangedSlot()
{
QString themeName = SettingsCache::instance().getThemeName();
QString dirPath = getAvailableThemes().value(themeName);
currentThemePath = dirPath;
QDir dir(dirPath);
// CSS
if (!dirPath.isEmpty() && dir.exists(STYLE_CSS_NAME)) {
qApp->setStyleSheet("file:///" + dir.absoluteFilePath(STYLE_CSS_NAME));
} else {
// resources
QStringList resources;
resources << dir.absolutePath() << DEFAULT_RESOURCE_PATHS;
QDir::setSearchPaths("theme", resources);
// zones bg
dir.cd("zones");
brushes[Role::Hand] = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT);
brushes[Role::Table] = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT);
brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT);
brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT);
qApp->setStyleSheet("");
}
// load theme.cfg for style + scheme preference
ThemeConfig themeCfg = ThemeConfig::fromThemeDir(dirPath);
// Resolve active scheme:
// 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 ────────────────────
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
if (!palette.hasPalette()) {
palette = PaletteConfig::fromDefault(dirPath, activeScheme);
}
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);
QStringList resources;
if (!dirPath.isEmpty()) {
resources << dir.absolutePath();
}
resources << DEFAULT_RESOURCE_PATHS;
QDir::setSearchPaths("theme", resources);
brushes[Role::Hand] = loadBrush(HANDZONE_BG_NAME, HANDZONE_BG_DEFAULT);
brushes[Role::Table] = loadBrush(TABLEZONE_BG_NAME, TABLEZONE_BG_DEFAULT);
brushes[Role::Player] = loadBrush(PLAYERZONE_BG_NAME, PLAYERZONE_BG_DEFAULT);
brushes[Role::Stack] = loadBrush(STACKZONE_BG_NAME, STACKZONE_BG_DEFAULT);
for (auto &brushCache : brushesCache) {
brushCache.clear();
}

View file

@ -7,6 +7,8 @@
#ifndef THEMEMANAGER_H
#define THEMEMANAGER_H
#include "theme_config.h"
#include <QBrush>
#include <QDir>
#include <QLoggingCategory>
@ -41,6 +43,7 @@ public:
private:
QString defaultStyleName;
QString currentThemePath;
std::array<QBrush, Role::MaxRole + 1> brushes;
QStringMap availableThemes;
/*
@ -52,11 +55,31 @@ protected:
void ensureThemeDirectoryExists();
QBrush loadBrush(QString fileName, QColor fallbackColor);
QBrush loadExtraBrush(QString fileName, QBrush &fallbackBrush);
void applyStyleAndPalette(const QString &themeName,
const ThemeConfig &themeCfg,
const PaletteConfig &palCfg,
const QString &activeScheme);
public:
bool isBuiltInTheme();
bool isDarkMode();
bool isDarkMode(const QString &themeDirPath);
QStringMap &getAvailableThemes();
// Returns the path to the currently active theme directory (empty = default)
QString getCurrentThemePath() const
{
return currentThemePath;
}
// Load the global theme settings (style + color scheme preference)
static ThemeConfig loadGlobalConfig(const QString &themeDirPath);
static bool saveGlobalConfig(const QString &themeDirPath, const ThemeConfig &cfg);
// Load/save per-scheme palette colors
static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme);
static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg);
void setColorScheme(const QString &scheme);
void reloadCurrentTheme();
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
QBrush &getBgBrush(Role zone);
QBrush getExtraBgBrush(Role zone, int zoneId = 0);

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcut_treeview.h"
#include "../../palette_editor/palette_editor_dialog.h"
#include "../client/network/update/card_spoiler/spoiler_background_updater.h"
#include "../client/network/update/client/release_channel.h"
#include "../client/sound_engine.h"
@ -433,6 +434,35 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&themeBox, qOverload<int>(&QComboBox::currentIndexChanged), this, &AppearanceSettingsPage::themeBoxChanged);
connect(&openThemeButton, &QPushButton::clicked, this, &AppearanceSettingsPage::openThemeLocation);
schemeCombo.addItem(tr("Light"), QStringLiteral("Light"));
schemeCombo.addItem(tr("Dark"), QStringLiteral("Dark"));
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
schemeCombo.addItem(tr("System"), QStringLiteral("System"));
#endif
// Seed from whatever the current theme already has saved
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
const ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
const QString current = cfg.colorScheme;
const int seedIdx = schemeCombo.findData(current);
schemeCombo.setCurrentIndex(seedIdx >= 0 ? seedIdx : 0);
connect(&schemeCombo, &QComboBox::currentIndexChanged, this,
[this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); });
connect(themeManager, &ThemeManager::themeChanged, this, [this, dirPath] {
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
const QString current = cfg.colorScheme;
schemeCombo.blockSignals(true);
const int idx = schemeCombo.findData(current);
schemeCombo.setCurrentIndex(idx >= 0 ? idx : 0);
schemeCombo.blockSignals(false);
});
connect(&editPaletteButton, &QPushButton::clicked, this, &AppearanceSettingsPage::editPalette);
for (const auto &entry : BackgroundSources::all()) {
homeTabBackgroundSourceBox.addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type));
}
@ -466,12 +496,15 @@ AppearanceSettingsPage::AppearanceSettingsPage()
themeGrid->addWidget(&themeLabel, 0, 0);
themeGrid->addWidget(&themeBox, 0, 1);
themeGrid->addWidget(&openThemeButton, 1, 1);
themeGrid->addWidget(&homeTabBackgroundSourceLabel, 2, 0);
themeGrid->addWidget(&homeTabBackgroundSourceBox, 2, 1);
themeGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 3, 0);
themeGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 3, 1);
themeGrid->addWidget(&homeTabDisplayCardNameLabel, 4, 0);
themeGrid->addWidget(&homeTabDisplayCardNameCheckBox, 4, 1);
themeGrid->addWidget(&schemeComboLabel, 2, 0);
themeGrid->addWidget(&schemeCombo, 2, 1);
themeGrid->addWidget(&editPaletteButton, 3, 1);
themeGrid->addWidget(&homeTabBackgroundSourceLabel, 4, 0);
themeGrid->addWidget(&homeTabBackgroundSourceBox, 4, 1);
themeGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 5, 0);
themeGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 5, 1);
themeGrid->addWidget(&homeTabDisplayCardNameLabel, 6, 0);
themeGrid->addWidget(&homeTabDisplayCardNameCheckBox, 6, 1);
themeGroupBox = new QGroupBox;
themeGroupBox->setLayout(themeGrid);
@ -670,6 +703,12 @@ void AppearanceSettingsPage::openThemeLocation()
}
}
void AppearanceSettingsPage::editPalette()
{
PaletteEditorDialog dlg(themeManager->getCurrentThemePath(), SettingsCache::instance().getThemeName(), this);
dlg.exec();
}
void AppearanceSettingsPage::updateHomeTabSettingsVisibility()
{
bool visible =
@ -734,6 +773,8 @@ void AppearanceSettingsPage::retranslateUi()
themeGroupBox->setTitle(tr("Theme settings"));
themeLabel.setText(tr("Current theme:"));
openThemeButton.setText(tr("Open themes folder"));
schemeComboLabel.setText(tr("Active theme palette"));
editPaletteButton.setText(tr("Edit theme palette"));
homeTabBackgroundSourceLabel.setText(tr("Home tab background source:"));
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));

View file

@ -103,6 +103,7 @@ class AppearanceSettingsPage : public AbstractSettingsPage
private slots:
void themeBoxChanged(int index);
void openThemeLocation();
void editPalette();
void updateHomeTabSettingsVisibility();
void showShortcutsChanged(QT_STATE_CHANGED_T enabled);
void overrideAllCardArtWithPersonalPreferenceToggled(QT_STATE_CHANGED_T enabled);
@ -114,6 +115,9 @@ private:
QLabel themeLabel;
QComboBox themeBox;
QPushButton openThemeButton;
QLabel schemeComboLabel;
QComboBox schemeCombo;
QPushButton editPaletteButton;
QLabel homeTabBackgroundSourceLabel;
QComboBox homeTabBackgroundSourceBox;
QLabel homeTabBackgroundShuffleFrequencyLabel;

View file

@ -0,0 +1,63 @@
[Palette]
WindowText = #ffffffff
Button = #ff383838
Light = #ff737373
Midlight = #ff525252
Dark = #ff161616
Mid = #ff252525
Text = #ffffffff
BrightText = #ffb4dd8b
ButtonText = #ffffffff
Base = #ff2b2b2b
Window = #ff1c1c1c
Shadow = #ff000000
HighlightedText = #ff000000
Link = #ffcde4b6
LinkVisited = #ff99d999
AlternateBase = #ff242424
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #6effffff
[Palette.Disabled]
WindowText = #ff9d9d9d
Button = #ff1c1c1c
Light = #ff737373
Midlight = #ff525252
Dark = #ff161616
Mid = #ff252525
Text = #ff9d9d9d
BrightText = #ffb4dd8b
ButtonText = #ff787878
Base = #ff1c1c1c
Window = #ff1c1c1c
Shadow = #ff000000
HighlightedText = #ff9d9d9d
Link = #ff308cc6
LinkVisited = #ffb450ff
AlternateBase = #ff242424
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #46ffffff
[Palette.Inactive]
WindowText = #ffffffff
Button = #ff383838
Light = #ff737373
Midlight = #ff525252
Dark = #ff161616
Mid = #ff252525
Text = #ffffffff
BrightText = #ffb4dd8b
ButtonText = #ffffffff
Base = #ff2b2b2b
Window = #ff1c1c1c
Shadow = #ff000000
HighlightedText = #ffffffff
Link = #ffcde4b6
LinkVisited = #ff99d999
AlternateBase = #ff242424
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #6effffff

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = Light
[Style]
Name = Default

View file

@ -0,0 +1,69 @@
[Palette]
WindowText = #ffffffff
Button = #ff3c3c3c
Light = #ff787878
Midlight = #ff5a5a5a
Dark = #ff1e1e1e
Mid = #ff282828
Text = #ffffffff
BrightText = #ff00f652
ButtonText = #ffffffff
Base = #ff2d2d2d
Window = #ff1e1e1e
Shadow = #ff000000
Highlight = #ff148c3c
HighlightedText = #ffffffff
Link = #ff00f652
LinkVisited = #ff00d346
AlternateBase = #ff353535
ToolTipBase = #ff3c3c3c
ToolTipText = #ffd4d4d4
PlaceholderText = #80ffffff
Accent = #ff00d346
[Palette.Disabled]
WindowText = #ff9d9d9d
Button = #ff3c3c3c
Light = #ff787878
Midlight = #ff5a5a5a
Dark = #ff1e1e1e
Mid = #ff282828
Text = #ff9d9d9d
BrightText = #ff00f652
ButtonText = #ff9d9d9d
Base = #ff1e1e1e
Window = #ff1e1e1e
Shadow = #ff000000
Highlight = #ff148c3c
HighlightedText = #ffffffff
Link = #ff308cc6
LinkVisited = #ffff00ff
AlternateBase = #ff353535
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80ffffff
Accent = #ff9d9d9d
[Palette.Inactive]
WindowText = #ffffffff
Button = #ff3c3c3c
Light = #ff787878
Midlight = #ff5a5a5a
Dark = #ff1e1e1e
Mid = #ff282828
Text = #ffffffff
BrightText = #ff00f652
ButtonText = #ffffffff
Base = #ff2d2d2d
Window = #ff1e1e1e
Shadow = #ff000000
Highlight = #ff1e1e1e
HighlightedText = #ffffffff
Link = #ff00f652
LinkVisited = #ff00d346
AlternateBase = #ff353535
ToolTipBase = #ff3c3c3c
ToolTipText = #ffd4d4d4
PlaceholderText = #80ffffff
Accent = #ff1e1e1e

View file

@ -0,0 +1,69 @@
[Palette]
WindowText = #ff000000
Button = #fff0f0f0
Light = #ffffffff
Midlight = #ffe3e3e3
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff000000
BrightText = #ffffffff
ButtonText = #ff000000
Base = #ffffffff
Window = #fff0f0f0
Shadow = #ff696969
Highlight = #ff148c3c
HighlightedText = #ffffffff
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
Accent = #ff107532
[Palette.Disabled]
WindowText = #ff787878
Button = #fff0f0f0
Light = #ffffffff
Midlight = #fff7f7f7
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff787878
BrightText = #ffffffff
ButtonText = #ff787878
Base = #fff0f0f0
Window = #fff0f0f0
Shadow = #ff000000
Highlight = #ff148c3c
HighlightedText = #ffffffff
Link = #ff0000ff
LinkVisited = #ffff00ff
AlternateBase = #fff7f7f7
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
Accent = #ff787878
[Palette.Inactive]
WindowText = #ff000000
Button = #fff0f0f0
Light = #ffffffff
Midlight = #ffe3e3e3
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff000000
BrightText = #ffffffff
ButtonText = #ff000000
Base = #ffffffff
Window = #fff0f0f0
Shadow = #ff696969
Highlight = #fff0f0f0
HighlightedText = #ff000000
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
Accent = #fff0f0f0

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = Dark
[Style]
Name = Fusion

View file

@ -28,6 +28,7 @@ set(oracle_SOURCES
../cockatrice/src/client/settings/card_counter_settings.cpp
../cockatrice/src/client/settings/shortcuts_settings.cpp
../cockatrice/src/client/network/update/client/release_channel.cpp
../cockatrice/src/interface/theme_config.cpp
../cockatrice/src/interface/theme_manager.cpp
../cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
../cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp