diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 1017f1247..a4e357f60 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -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 diff --git a/cockatrice/src/interface/palette_editor/color_button.cpp b/cockatrice/src/interface/palette_editor/color_button.cpp new file mode 100644 index 000000000..e1a490d20 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/color_button.cpp @@ -0,0 +1,72 @@ +#include "color_button.h" + +#include +#include + +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({}); +} \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/color_button.h b/cockatrice/src/interface/palette_editor/color_button.h new file mode 100644 index 000000000..4b139ef68 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/color_button.h @@ -0,0 +1,30 @@ +#ifndef COCKATRICE_COLOR_BUTTON_H +#define COCKATRICE_COLOR_BUTTON_H + +#include +#include + +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 \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp new file mode 100644 index 000000000..1b9be1dd4 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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("Palette Editor  ·  %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(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(); + } +} diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h new file mode 100644 index 000000000..cec4c1700 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h @@ -0,0 +1,68 @@ +#ifndef COCKATRICE_PALETTE_EDITOR_DIALOG_H +#define COCKATRICE_PALETTE_EDITOR_DIALOG_H + +#include "../theme_config.h" + +#include +#include +#include + +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 workingConfig; + QMap savedConfig; + +protected: + void changeEvent(QEvent *e) override; +}; + +#endif // COCKATRICE_PALETTE_EDITOR_DIALOG_H \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_generator.cpp b/cockatrice/src/interface/palette_editor/palette_generator.cpp new file mode 100644 index 000000000..d30dd14f1 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/palette_generator.cpp @@ -0,0 +1,200 @@ +#include "palette_generator.h" + +#include + +// ════════════════════════════════════════════════════════════════════════════ +// PaletteGenerator::fromAccent +// +// Three intensity bands: +// 0–30 Subtle — Highlight / links / BrightText take on the hue; +// backgrounds stay neutral grey. +// 30–70 Accented — Button, ToolTipBase, AlternateBase, shading tint in; +// backgrounds get a faint hue wash. +// 70–100 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 \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_generator.h b/cockatrice/src/interface/palette_editor/palette_generator.h new file mode 100644 index 000000000..0cec9b5c3 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/palette_generator.h @@ -0,0 +1,16 @@ +#ifndef COCKATRICE_PALETTE_GENERATOR_H +#define COCKATRICE_PALETTE_GENERATOR_H + +#include "../theme_config.h" + +#include +#include + +// 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 \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp new file mode 100644 index 000000000..e4ac5a16f --- /dev/null +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -0,0 +1,179 @@ +#include "palette_grid_widget.h" + +#include +#include +#include +#include +#include +#include +#include + +static QList allRoles() +{ + QList roles; + for (int i = 0; i < QPalette::NColorRoles; ++i) { + auto r = static_cast(i); + if (r != QPalette::NoRole) { + roles << r; + } + } + return roles; +} + +static const QList ALL_GROUPS = {QPalette::Active, QPalette::Disabled, QPalette::Inactive}; + +static const QMap 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(); + + 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; +} \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.h b/cockatrice/src/interface/palette_editor/palette_grid_widget.h new file mode 100644 index 000000000..1d85f1ac8 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.h @@ -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 +#include +#include +#include + +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> colorButtons; + QScrollArea *scroll; + QWidget *gridHost; + QVBoxLayout *layout; + QVector headerLabels; + QVector rowShadeWidgets; +}; + +#endif // COCKATRICE_PALETTE_GRID_WIDGET_H \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp b/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp new file mode 100644 index 000000000..fe3af9b52 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/quick_setup_panel.cpp @@ -0,0 +1,99 @@ +#include "quick_setup_panel.h" + +#include +#include +#include +#include +#include + +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("Quick Setup")); + 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("0–30 Subtle tint — only highlights and links change hue\n" + "30–70 Accented — buttons, tooltips, and borders join in\n" + "70–100 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); +} \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/quick_setup_panel.h b/cockatrice/src/interface/palette_editor/quick_setup_panel.h new file mode 100644 index 000000000..dddd0aaa3 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/quick_setup_panel.h @@ -0,0 +1,94 @@ +#ifndef COCKATRICE_QUICK_SETUP_PANEL_H +#define COCKATRICE_QUICK_SETUP_PANEL_H + +#include "color_button.h" + +#include + +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 \ No newline at end of file diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp new file mode 100644 index 000000000..b79c91265 --- /dev/null +++ b/cockatrice/src/interface/theme_config.cpp @@ -0,0 +1,267 @@ +#include "theme_config.h" + +#include +#include +#include +#include + +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(); + + QString out; + + static const QList> 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(); + + 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(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; +} \ No newline at end of file diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h new file mode 100644 index 000000000..07bf55b7a --- /dev/null +++ b/cockatrice/src/interface/theme_config.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_THEME_CONFIG_H +#define COCKATRICE_THEME_CONFIG_H + +#include +#include +#include +#include + +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> 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 \ No newline at end of file diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 58fb62362..abdc02eef 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -19,9 +19,7 @@ #include #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(); } diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index 416923128..4b1fd2026 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -7,6 +7,8 @@ #ifndef THEMEMANAGER_H #define THEMEMANAGER_H +#include "theme_config.h" + #include #include #include @@ -41,6 +43,7 @@ public: private: QString defaultStyleName; + QString currentThemePath; std::array 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); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 7da0a9207..4323ed724 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -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(&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")); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 42268e997..86266347f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -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; diff --git a/cockatrice/themes/Default/palette-default-dark.toml b/cockatrice/themes/Default/palette-default-dark.toml new file mode 100644 index 000000000..3ee174a2f --- /dev/null +++ b/cockatrice/themes/Default/palette-default-dark.toml @@ -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 + diff --git a/cockatrice/themes/Default/theme.cfg b/cockatrice/themes/Default/theme.cfg new file mode 100644 index 000000000..d2016a238 --- /dev/null +++ b/cockatrice/themes/Default/theme.cfg @@ -0,0 +1,5 @@ +[Appearance] +ColorScheme = Light + +[Style] +Name = Default diff --git a/cockatrice/themes/Fusion/palette-default-dark.toml b/cockatrice/themes/Fusion/palette-default-dark.toml new file mode 100644 index 000000000..c1d83a4cd --- /dev/null +++ b/cockatrice/themes/Fusion/palette-default-dark.toml @@ -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 + diff --git a/cockatrice/themes/Fusion/palette-default-light.toml b/cockatrice/themes/Fusion/palette-default-light.toml new file mode 100644 index 000000000..86c41be78 --- /dev/null +++ b/cockatrice/themes/Fusion/palette-default-light.toml @@ -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 + diff --git a/cockatrice/themes/Fusion/theme.cfg b/cockatrice/themes/Fusion/theme.cfg new file mode 100644 index 000000000..9b38e505e --- /dev/null +++ b/cockatrice/themes/Fusion/theme.cfg @@ -0,0 +1,5 @@ +[Appearance] +ColorScheme = Dark + +[Style] +Name = Fusion diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 3bb4de5df..a51982625 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -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