Compare commits

...

7 commits

Author SHA1 Message Date
Lukas Brübach
46278f1d13 [Themes] Ship Fusion style + scheme backgrounds for the image themes
Address PR review:
- Add theme.cfg ([Style] Name = Fusion, ColorScheme = System) to Fabric,
  Leather, Plasma and VelvetMarble so their curated palettes actually apply.
  Without it the native style paints button chrome from the OS (windowsvista
  on Windows has no dark mode), making the dark palettes' light ButtonText
  unreadable on light buttons.
- Leverage the scheme-variant asset resolution: register home-dark.png and
  home-light.png in resources and add them to the qrc so the built-in home
  background also flips with the palette instead of staying static.
- Fix Leather [Palette.Inactive] Accent, which copied the Active Highlight
  color instead of the theme accent (dark #4a5f8f -> #c9995a, light
  #34508c -> #a5712f) in both palette files.
2026-09-11 20:34:59 +02:00
Lukas Brübach
0dd8590037 [Themes] Add home backgrounds for the image themes 2026-09-11 20:34:59 +02:00
Lukas Brübach
ae32df6fec [Themes] Add identity default palettes for the image themes
The Fabric, Leather, Plasma and VelvetMarble themes shipped only zone art and no palette, so their chrome fell back to the bare OS palette. Each theme now ships light and dark curated defaults written in the palette editor's own conventions, with [AppColors] so the home-tab buttons keep the theme's identity over static backgrounds.

- Add palette-default-light.toml / palette-default-dark.toml for the four themes; scheme resolution follows the OS since the themes declare no color scheme.
- Tint the window/base/button chrome and bevels towards each theme's identity: navy cloth (Fabric), black-brown with brass accents (Leather), electric violet with cyan sparks (Plasma), charcoal velvet with slate marble (VelvetMarble).
- Fill [AppColors] AccentStrong / AccentSoft per theme so the home-tab button gradient matches instead of falling back to the default greens.
- Derive Disabled and Inactive groups with the same conventions as PaletteGenerator::fromAccent.
2026-09-11 20:34:59 +02:00
Lukas Brübach
43f9173584 [Home] Replace 'Automatic' button color with explicit theme colors default
The Automatic option gated on isBuiltInTheme(): built-in themes used the
theme's accent colors, while non-built-in themes extracted colors from
their own background art. That made the result depend on the theme's
origin rather than what the user actually sees.

Remove Automatic and expose two explicit choices: 'From theme colors'
(always the theme's identity accents, now the default) and 'Extract from
background' (always sample the painted background). Drop the now-unused
isBuiltInTheme() helper.
2026-09-11 20:34:52 +02:00
Lukas Brübach
3590abc174 [Themes] Route theme writes to the user themes directory
setColorScheme()/setStyleName() and the palette editor wrote directly to
the resolved theme directory, which for built-in themes is the read-only
system (install) location. Changes therefore landed in the install dir and
were lost on upgrade.

Add ThemeManager::writableThemeDir(), which always resolves to the user
themes directory, and route all theme writes through it. The palette editor
reuses the same helper, dropping its private writability probe.
2026-09-11 20:34:48 +02:00
Lukas Brübach
b63431ef0d [AppColors] Address review comments
- PaletteEditorDialog::onSave(): compare whole PaletteConfig (colors and
  appColors) so a change to only AccentStrong/AccentSoft writes the file;
  add PaletteConfig::operator==.
- appColor(): derive both roles from QPalette::Highlight unconditionally.
  The Fusion palettes pin Accent to near-Window values, and QPalette::Accent
  only exists on Qt 6.6+, so keying on it made identical themes render very
  differently across Qt versions.
- themeChangedSlot(): merge the theme default's [AppColors] into a custom
  palette that predates the section instead of all-or-nothing per file;
  hasPalette() now counts an appColors-only file as a palette.
- Add Default/palette-default-light.toml so the Default theme's Light scheme
  keeps the classic greens instead of falling back to the OS accent.
- home_widget: restore the isBuiltInTheme() half of the Automatic condition;
  non-built-in themes extract button colors from their own background art.
- palette_grid_widget: use appEnum.value(i) for the role cast (3 sites),
  append appHeader to headerLabels, fix the 'Lighted' typo.
2026-09-11 18:15:33 +02:00
Lukas Brübach
ea09f92029 [Client] Add [AppColors] application palette roles
The home-tab buttons' gradient over the static theme background was hardcoded, and accent-derived fallbacks could not be themed or edited: QPalette's role set is closed, so any application-specific color has to live in Cockatrice's own palette layer.

- Add an AppColor::Role enum (AccentStrong / AccentSoft) stored on PaletteConfig and round-tripped from palette-<scheme>.toml under a new [AppColors] section.
- Cache the applied app colors in ThemeManager and expose appColor(Role) with a palette-accent-derived fallback; emit paletteChanged() from applyStyleAndPalette so previews, scheme switches and OS dark mode repaint palette-driven widgets.
- Fill both app roles in PaletteGenerator::fromAccent and surface them as a dedicated section in the palette editor.
- Drive the home-tab buttons from appColor() whenever the background source is the theme (any theme, not just built-ins).
- Ship AccentStrong / AccentSoft values in the Fusion and Default default palettes so the static home-tab buttons keep their classic greens.

# Conflicts:
#	cockatrice/src/interface/widgets/general/home_widget.cpp
2026-09-11 18:15:30 +02:00
39 changed files with 938 additions and 65 deletions

View file

@ -63,6 +63,8 @@
<file>resources/icons/mana/W.svg</file>
<file>resources/backgrounds/home.png</file>
<file>resources/backgrounds/home-dark.png</file>
<file>resources/backgrounds/home-light.png</file>
<file>resources/backgrounds/card_triplet.svg</file>
<file>resources/backgrounds/placeholder_printing_selector.svg</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 16 MiB

Before After
Before After

View file

@ -1,6 +1,5 @@
#include "palette_editor_dialog.h"
#include "../../client/settings/cache_settings.h"
#include "../theme_manager.h"
#include "palette_generator.h"
#include "palette_grid_widget.h"
@ -11,31 +10,11 @@
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFrame>
#include <QGuiApplication>
#include <QLabel>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QPushButton>
#include <QStyleHints>
#include <QTimer>
#include <libcockatrice/settings/paths_settings.h>
// Probe whether a directory is truly writable by trying to create and remove a
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
// on Windows where UAC VirtualStore can make a system dir appear writable).
static bool isDirReallyWritable(const QString &dirPath)
{
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
QFile f(probe);
if (!f.open(QIODevice::WriteOnly)) {
return false;
}
f.close();
f.remove();
return true;
}
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
@ -46,14 +25,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
// Resolve a writable directory for saving. Built-in (Default / Fusion) and
// other read-only theme directories must be customised in the user-writable
// themes directory; otherwise the write would fail or be lost on upgrade.
if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) {
saveDir = themeDirPath;
} else {
saveDir = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
if (!QDir().mkpath(saveDir)) {
qWarning() << "Failed to create palette save directory:" << saveDir;
}
}
saveDir = ThemeManager::writableThemeDir(themeName);
// Load both scheme configs upfront so switching is instant
loadSchemes();
@ -214,7 +186,7 @@ void PaletteEditorDialog::retranslateUi()
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) {
if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) {
saveBtn->setEnabled(false);
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
}
@ -297,7 +269,7 @@ void PaletteEditorDialog::onSave()
if (it.key() == loadedScheme) {
continue;
}
if (it.value().colors == savedConfig.value(it.key()).colors) {
if (it.value() == savedConfig.value(it.key())) {
continue;
}
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
@ -308,7 +280,7 @@ void PaletteEditorDialog::onSave()
}
// Commit the active scheme last so the global colour scheme matches.
if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) {
if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) {
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
QMessageBox::warning(this, tr("Save failed"),
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));

View file

@ -150,6 +150,17 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
// Accent: same primary hue as Highlight, so palettes derived from a
// QuickSetup accent always carry a matching Accent role.
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
set3(CR::Accent, hl, disText, hl);
#endif
// Application role colors: Strong tracks the primary accent, while Soft is
// the lightened, desaturated companion used for button-gradient highlights.
cfg.appColors[AppColor::AccentStrong] = hl;
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
// BrightText
QColor bright;
if (achromatic) {

View file

@ -1,5 +1,7 @@
#include "palette_grid_widget.h"
#include "../theme_manager.h"
#include <QApplication>
#include <QGridLayout>
#include <QLabel>
@ -45,6 +47,11 @@ static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
};
static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
{AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
{AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
};
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
{
scroll = new QScrollArea(this);
@ -122,6 +129,46 @@ void PaletteGridWidget::buildGrid(QWidget *host)
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
}
}
// Application color section: one ColorButton per role below the role grid.
// These are not tied to a color group, so a single button spans the row.
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
const int appHeaderRow = roles.size() + 1;
auto *appHeader = new QLabel(tr("App colors"), host);
appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
QFont appHeaderFont = appHeader->font();
appHeaderFont.setBold(true);
appHeader->setFont(appHeaderFont);
appHeader->setAutoFillBackground(true);
appHeader->setContentsMargins(4, 4, 4, 4);
grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);
headerLabels.append(appHeader);
for (int i = 0; i < appEnum.keyCount(); ++i) {
auto role = static_cast<AppColor::Role>(appEnum.value(i));
const int row = appHeaderRow + 1 + i;
if (i % 2 == 0) {
for (int col = 0; col < 4; ++col) {
auto *shade = new QWidget(host);
shade->setAutoFillBackground(true);
grid->addWidget(shade, row, col);
rowShadeWidgets.push_back(shade);
}
}
auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {}));
label->setContentsMargins(4, 2, 8, 2);
grid->addWidget(label, row, 0);
auto *btn = new ColorButton(host);
connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
appColorButtons[role] = btn;
grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter);
}
}
void PaletteGridWidget::changeEvent(QEvent *e)
@ -166,6 +213,16 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
colorButtons[group][role]->setColor(color);
}
}
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
for (int i = 0; i < appEnum.keyCount(); ++i) {
auto role = static_cast<AppColor::Role>(appEnum.value(i));
QColor color = cfg.appColors.value(role);
if (!color.isValid()) {
color = themeManager->appColor(role);
}
appColorButtons[role]->setColor(color);
}
}
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
@ -176,5 +233,12 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const
cfg.colors[group][role] = colorButtons[group][role]->getColor();
}
}
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
for (int i = 0; i < appEnum.keyCount(); ++i) {
auto role = static_cast<AppColor::Role>(appEnum.value(i));
cfg.appColors[role] = appColorButtons[role]->getColor();
}
return cfg;
}

View file

@ -31,6 +31,7 @@ private:
void refreshChromePalettes();
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
QMap<AppColor::Role, ColorButton *> appColorButtons;
QScrollArea *scroll;
QWidget *gridHost;
QVBoxLayout *layout;

View file

@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const
bool PaletteConfig::hasPalette() const
{
return !colors.isEmpty();
return !colors.isEmpty() || !appColors.isEmpty();
}
QString PaletteConfig::toToml() const
@ -133,6 +133,24 @@ QString PaletteConfig::toToml() const
out += "\n";
}
if (!appColors.isEmpty()) {
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
out += "[AppColors]\n";
for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) {
const char *roleName = appEnum.valueToKey(it.key());
if (!roleName) {
continue;
}
out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb));
}
out += "\n";
}
return out;
}
@ -152,6 +170,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
}
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
QString currentSection;
QPalette::ColorGroup currentGroup = QPalette::Active;
@ -202,6 +221,26 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
}
}
QColor color(value);
if (!color.isValid()) {
continue;
}
if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) {
if (key.startsWith("AppColor::")) {
key = key.mid(10);
}
int appRoleInt = appEnum.keyToValue(key.toUtf8().constData());
if (appRoleInt >= 0) {
cfg.appColors[static_cast<AppColor::Role>(appRoleInt)] = color;
}
continue;
}
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
continue;
}
@ -216,11 +255,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
continue;
}
QColor color(value);
if (color.isValid()) {
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
}
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
}
return cfg;

View file

@ -3,9 +3,25 @@
#include <QColor>
#include <QMap>
#include <QObject>
#include <QPalette>
#include <QString>
// Application-specific color roles, layered on top of the fixed QPalette role
// set. Stored in the same palette-<scheme>.toml under an [AppColors] section
// and editable from the palette editor, so theme authors can control colors
// beyond what Qt's palette can express.
namespace AppColor
{
Q_NAMESPACE
enum Role
{
AccentStrong,
AccentSoft,
};
Q_ENUM_NS(Role)
} // namespace AppColor
struct ThemeConfig
{
QString colorScheme;
@ -21,7 +37,16 @@ struct ThemeConfig
struct PaletteConfig
{
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors;
QMap<AppColor::Role, QColor> appColors;
bool operator==(const PaletteConfig &rhs) const
{
return colors == rhs.colors && appColors == rhs.appColors;
}
bool operator!=(const PaletteConfig &rhs) const
{
return !(*this == rhs);
}
bool hasPalette() const;
QString toToml() const;

View file

@ -6,6 +6,7 @@
#include <QApplication>
#include <QColor>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QLibraryInfo>
#include <QMap>
@ -184,11 +185,32 @@ QString ThemeManager::assetPath(QStringView prefix) const
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
}
bool ThemeManager::isBuiltInTheme()
// Probe whether a directory is truly writable by trying to create and remove a
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
// on Windows where UAC VirtualStore can make a system dir appear writable).
bool ThemeManager::isDirReallyWritable(const QString &dirPath)
{
const auto themeName = SettingsCache::instance().getThemeName();
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
QFile f(probe);
if (!f.open(QIODevice::WriteOnly)) {
return false;
}
f.close();
f.remove();
return true;
}
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
QString ThemeManager::writableThemeDir(const QString &themeName)
{
// All theme writes go to the user themes directory regardless of whether
// the resolved (system) theme directory happens to be writable. Even when a
// write would succeed in-place, routing it to the user directory keeps the
// install intact and guarantees changes survive upgrades.
const QString dirPath = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
if (!QDir().mkpath(dirPath)) {
qWarning() << "Failed to create theme save directory:" << dirPath;
}
return dirPath;
}
// System (read-only) themes location, relative to the application binary.
@ -331,7 +353,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col
void ThemeManager::setColorScheme(const QString &scheme)
{
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.colorScheme = scheme;
@ -342,7 +364,7 @@ void ThemeManager::setColorScheme(const QString &scheme)
void ThemeManager::setStyleName(const QString &styleName)
{
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.styleName = styleName;
@ -416,6 +438,8 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
qApp->setPalette(base);
qApp->setStyle(style);
currentAppColors = palCfg.appColors;
// Force every widget to re-polish and repaint immediately rather than
// waiting for natural expose events, which produces a patchwork of old
// and new colours during a live preview.
@ -428,6 +452,35 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
style->polish(widget);
widget->update();
}
emit paletteChanged();
}
QColor ThemeManager::appColor(AppColor::Role role) const
{
const auto it = currentAppColors.constFind(role);
if (it != currentAppColors.constEnd()) {
return it.value();
}
// QPalette::Accent was introduced in Qt 6.6 and several shipped palettes
// set it to a value barely distinguishable from Window, so it is not a
// reliable accent source. The selection highlight is the stable accent
// (Accent defaults to Highlight when unset), and deriving from it
// unconditionally keeps every Qt version rendering identically.
const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight);
if (role == AppColor::AccentSoft) {
constexpr int SOFT_SATURATION_PERCENT = 70;
constexpr int SOFT_LIGHTNESS_OFFSET = 60;
// Light end of the gradient: same hue, softened and lightened
return QColor::fromHsl(qMax(0, accent.hslHue()),
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
}
return accent;
}
void ThemeManager::themeChangedSlot()
@ -464,8 +517,19 @@ void ThemeManager::themeChangedSlot()
// ── Load palette: custom first, then theme default ────────────────────
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
if (!palette.hasPalette()) {
palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
if (palette.hasPalette()) {
// A custom palette written before [AppColors] existed carries no app
// colors; merge the theme's shipped defaults so the identity colors
// survive (hasPalette() counts an app-colors-only file as a palette,
// so those are kept wholesale and never reach here empty).
for (auto it = themeDefault.appColors.cbegin(); it != themeDefault.appColors.cend(); ++it) {
if (!palette.appColors.contains(it.key())) {
palette.appColors.insert(it.key(), it.value());
}
}
} else {
palette = themeDefault;
}
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);

View file

@ -50,6 +50,7 @@ private:
QString currentThemePath;
std::array<QBrush, Role::MaxRole + 1> brushes;
QStringMap availableThemes;
QMap<AppColor::Role, QColor> currentAppColors;
/*
Internal cache for multiple backgrounds
*/
@ -65,7 +66,16 @@ protected:
const QString &activeScheme);
public:
bool isBuiltInTheme();
// Resolves the directory to write theme changes to for the given theme
// name. The resolved theme dir (user or system) is used when writable;
// read-only system themes fall back to the user themes directory, creating
// it if needed, so customisations never get lost on upgrade.
static QString writableThemeDir(const QString &themeName);
// Probe whether a directory is truly writable by trying to create and remove
// a temporary file. QFileInfo::isWritable() on a directory is unreliable
// (notably on Windows where UAC VirtualStore can make a system dir appear
// writable).
static bool isDirReallyWritable(const QString &dirPath);
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
// (Dark/Light), falling back to the OS color scheme when it is "System".
bool isDarkMode(const QString &themeDirPath) const;
@ -115,12 +125,17 @@ public:
void reloadCurrentTheme();
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
// Resolves an application color role: the theme's stored [AppColors] value
// when present, otherwise a palette-accent-derived fallback.
QColor appColor(AppColor::Role role) const;
QBrush &getBgBrush(Role zone);
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
protected slots:
void themeChangedSlot();
signals:
void themeChanged();
void paletteChanged();
};
extern ThemeManager *themeManager;

View file

@ -11,8 +11,8 @@ namespace HomeTabButtonColor
*/
enum Source
{
Automatic, ///< Extract color from background, or use theme color if no background
FromBackground, ///< Always extract color from background
FromThemeColors, ///< Use the theme's identity accent colors
FromBackground, ///< Extract colour from the background image
};
struct Entry
@ -23,7 +23,7 @@ struct Entry
inline QList<Entry> all()
{
static QList<Entry> entries = {{Automatic, QT_TR_NOOP("Automatic")},
static QList<Entry> entries = {{FromThemeColors, QT_TR_NOOP("From theme colors")},
{FromBackground, QT_TR_NOOP("Extract from background")}};
return entries;
@ -33,12 +33,12 @@ inline QList<Entry> all()
* Safely converts an int into the corresponding Source.
*
* @param value The int value
* @return The Source. Returns Source::Automatic if the value is not within range
* @return The Source. Returns Source::FromThemeColors if the value is not within range
*/
inline Source intToSource(int value)
{
if (value > FromBackground) {
return Automatic; // default
return FromThemeColors; // default
}
return static_cast<Source>(value);

View file

@ -61,6 +61,7 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// Scheme flips (light/dark/system with an OS switch) fire on themeManager,
// not on SettingsCache::themeChanged, so re-resolve the variant background.
connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource);
connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor);
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
&HomeWidget::updateButtonsToBackgroundColor);
}
@ -105,32 +106,24 @@ void HomeWidget::loadBackgroundSourceDeck()
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
}
static bool isDefaultBackgroundAndTheme()
static QPair<QColor, QColor> paletteDerivedButtonColors()
{
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
}
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
{
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
auto colorSource =
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
switch (colorSource) {
case HomeTabButtonColor::Automatic: {
if (isDefaultBackgroundAndTheme()) {
return defaultColor;
} else {
return extractDominantColors(background);
}
}
case HomeTabButtonColor::FromThemeColors:
return paletteDerivedButtonColors();
case HomeTabButtonColor::FromBackground:
return extractDominantColors(background);
}
return defaultColor;
return paletteDerivedButtonColors();
}
void HomeWidget::setRandomCard(ExactCard &newCard)

View file

@ -510,7 +510,7 @@ void AppearanceSettingsPage::retranslateUi()
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
homeTabButtonColorSourceBox.setToolTip(
tr("Automatic: extract from background if present, otherwise use theme default"));
tr("Use the theme's identity accent colors, or extract colors from the background image"));
playmatGroupBox->setTitle(tr("Playmat settings"));
playmatVisibilityLabel.setText(tr("Playmat visibility:"));

View file

@ -61,3 +61,7 @@ ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #6effffff
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

View file

@ -0,0 +1,67 @@
[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
HighlightedText = #ffffffff
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[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
HighlightedText = #ffffffff
Link = #ff0000ff
LinkVisited = #ffff00ff
AlternateBase = #fff7f7f7
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[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
HighlightedText = #ff000000
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View file

@ -0,0 +1,74 @@
# Fabric — dark identity palette (navy-cloth chrome)
[Palette]
WindowText = #e8ecf2
Button = #2b374a
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #e8ecf2
BrightText = #8ec2ff
ButtonText = #f2f4f8
Base = #222c3d
Window = #18202e
Shadow = #0b0f15
Highlight = #4a82e6
HighlightedText = #ffffff
Link = #7fb0f2
LinkVisited = #a98ad9
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #4a82e6
[Palette.Disabled]
WindowText = #9d9d9d
Button = #18202e
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #9d9d9d
BrightText = #8ec2ff
ButtonText = #787878
Base = #18202e
Window = #18202e
Shadow = #0b0f15
Highlight = #222d40
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #e8ecf2
Button = #2b374a
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #e8ecf2
BrightText = #8ec2ff
ButtonText = #f2f4f8
Base = #222c3d
Window = #18202e
Shadow = #0b0f15
Highlight = #222d40
HighlightedText = #ffffff
Link = #7fb0f2
LinkVisited = #a98ad9
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #4a82e6
[AppColors]
AccentStrong = #3a6fd6
AccentSoft = #8fb8f2

View file

@ -0,0 +1,74 @@
# Fabric — light identity palette (navy-cloth chrome)
[Palette]
WindowText = #1c2330
Button = #c8d6ea
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #1c2330
BrightText = #1d4fa0
ButtonText = #141c29
Base = #f6f8fc
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #2f6fd6
HighlightedText = #ffffff
Link = #2555b0
LinkVisited = #6a4bb8
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #2f6fd6
[Palette.Disabled]
WindowText = #787878
Button = #dbe3ef
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #787878
BrightText = #1d4fa0
ButtonText = #969696
Base = #dbe3ef
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #cdd8e9
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #787878
[Palette.Inactive]
WindowText = #1c2330
Button = #c8d6ea
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #1c2330
BrightText = #1d4fa0
ButtonText = #141c29
Base = #f6f8fc
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #cdd8e9
HighlightedText = #000000
Link = #2555b0
LinkVisited = #6a4bb8
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #2f6fd6
[AppColors]
AccentStrong = #3a6fd6
AccentSoft = #8fb8f2

View file

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

View file

@ -67,3 +67,7 @@ ToolTipText = #ffd4d4d4
PlaceholderText = #80ffffff
Accent = #ff1e1e1e
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

View file

@ -67,3 +67,7 @@ ToolTipText = #ff000000
PlaceholderText = #80000000
Accent = #fff0f0f0
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -0,0 +1,74 @@
# Leather — dark identity palette (black-brown chrome with brass accents)
[Palette]
WindowText = #eee9e2
Button = #332d26
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #eee9e2
BrightText = #e3c27e
ButtonText = #f5f1ea
Base = #26221d
Window = #1d1a16
Shadow = #0d0b07
Highlight = #4a5f8f
HighlightedText = #ffffff
Link = #cfa263
LinkVisited = #9a7db8
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #c9995a
[Palette.Disabled]
WindowText = #9d9d9d
Button = #1d1a16
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #9d9d9d
BrightText = #e3c27e
ButtonText = #787878
Base = #1d1a16
Window = #1d1a16
Shadow = #0d0b07
Highlight = #2d2822
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #eee9e2
Button = #332d26
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #eee9e2
BrightText = #e3c27e
ButtonText = #f5f1ea
Base = #26221d
Window = #1d1a16
Shadow = #0d0b07
Highlight = #2d2822
HighlightedText = #ffffff
Link = #cfa263
LinkVisited = #9a7db8
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #c9995a
[AppColors]
AccentStrong = #b7823b
AccentSoft = #e4c58f

View file

@ -0,0 +1,74 @@
# Leather — light identity palette (black-brown chrome with brass accents)
[Palette]
WindowText = #2a2114
Button = #dfcbb0
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #2a2114
BrightText = #7a531c
ButtonText = #1f1710
Base = #fdf8ee
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #34508c
HighlightedText = #ffffff
Link = #8a5e1e
LinkVisited = #6b5190
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #a5712f
[Palette.Disabled]
WindowText = #787878
Button = #f0e2cb
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #787878
BrightText = #7a531c
ButtonText = #969696
Base = #f0e2cb
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #ecd9bb
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #787878
[Palette.Inactive]
WindowText = #2a2114
Button = #dfcbb0
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #2a2114
BrightText = #7a531c
ButtonText = #1f1710
Base = #fdf8ee
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #ecd9bb
HighlightedText = #000000
Link = #8a5e1e
LinkVisited = #6b5190
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #a5712f
[AppColors]
AccentStrong = #b7823b
AccentSoft = #e4c58f

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,74 @@
# Plasma — dark identity palette (electric violet chrome with cyan sparks)
[Palette]
WindowText = #eeebff
Button = #2a2440
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #eeebff
BrightText = #7fd7ff
ButtonText = #f6f4ff
Base = #1e1a2e
Window = #151220
Shadow = #08070e
Highlight = #6a4df0
HighlightedText = #ffffff
Link = #37c7e8
LinkVisited = #9b7aff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #6a4df0
[Palette.Disabled]
WindowText = #9d9d9d
Button = #151220
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #9d9d9d
BrightText = #7fd7ff
ButtonText = #787878
Base = #151220
Window = #151220
Shadow = #08070e
Highlight = #211c32
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #eeebff
Button = #2a2440
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #eeebff
BrightText = #7fd7ff
ButtonText = #f6f4ff
Base = #1e1a2e
Window = #151220
Shadow = #08070e
Highlight = #211c32
HighlightedText = #ffffff
Link = #37c7e8
LinkVisited = #9b7aff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #6a4df0
[AppColors]
AccentStrong = #5b3ee0
AccentSoft = #a28bf7

View file

@ -0,0 +1,74 @@
# Plasma — light identity palette (electric violet chrome with cyan sparks)
[Palette]
WindowText = #1d1830
Button = #cfc6f0
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #1d1830
BrightText = #10406e
ButtonText = #120d26
Base = #f9f7ff
Window = #e3dcf7
Shadow = #554c85
Highlight = #5a3ee2
HighlightedText = #ffffff
Link = #0f8fb5
LinkVisited = #6a45d6
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #5a3ee2
[Palette.Disabled]
WindowText = #787878
Button = #e3dcf7
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #787878
BrightText = #10406e
ButtonText = #969696
Base = #e3dcf7
Window = #e3dcf7
Shadow = #554c85
Highlight = #d6ccf3
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #787878
[Palette.Inactive]
WindowText = #1d1830
Button = #cfc6f0
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #1d1830
BrightText = #10406e
ButtonText = #120d26
Base = #f9f7ff
Window = #e3dcf7
Shadow = #554c85
Highlight = #d6ccf3
HighlightedText = #000000
Link = #0f8fb5
LinkVisited = #6a45d6
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #5a3ee2
[AppColors]
AccentStrong = #5b3ee0
AccentSoft = #a28bf7

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -0,0 +1,74 @@
# VelvetMarble — dark identity palette (charcoal-velvet chrome with slate marble)
[Palette]
WindowText = #e8eaee
Button = #262a31
Light = #3a3f47
Midlight = #31363d
Dark = #0e1013
Mid = #1f2329
Text = #e8eaee
BrightText = #c3d8f2
ButtonText = #f2f3f6
Base = #1b1e23
Window = #131519
Shadow = #08090b
Highlight = #8fa6c0
HighlightedText = #101318
Link = #a9c6e8
LinkVisited = #b9a8d9
AlternateBase = #171a1f
ToolTipBase = #2d323a
ToolTipText = #f2f3f6
PlaceholderText = #6ee8eaee
Accent = #8fa6c0
[Palette.Disabled]
WindowText = #9d9d9d
Button = #131519
Light = #3a3f47
Midlight = #31363d
Dark = #0e1013
Mid = #1f2329
Text = #9d9d9d
BrightText = #c3d8f2
ButtonText = #787878
Base = #131519
Window = #131519
Shadow = #08090b
Highlight = #1f2229
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #171a1f
ToolTipBase = #2d323a
ToolTipText = #f2f3f6
PlaceholderText = #6ee8eaee
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #e8eaee
Button = #262a31
Light = #3a3f47
Midlight = #31363d
Dark = #0e1013
Mid = #1f2329
Text = #e8eaee
BrightText = #c3d8f2
ButtonText = #f2f3f6
Base = #1b1e23
Window = #131519
Shadow = #08090b
Highlight = #1f2229
HighlightedText = #ffffff
Link = #a9c6e8
LinkVisited = #b9a8d9
AlternateBase = #171a1f
ToolTipBase = #2d323a
ToolTipText = #f2f3f6
PlaceholderText = #6ee8eaee
Accent = #8fa6c0
[AppColors]
AccentStrong = #54687e
AccentSoft = #9db0c4

View file

@ -0,0 +1,74 @@
# VelvetMarble — light identity palette (charcoal-velvet chrome with slate marble)
[Palette]
WindowText = #1b1d21
Button = #c7d1dd
Light = #ffffff
Midlight = #b3becb
Dark = #93a0b1
Mid = #a1adbc
Text = #1b1d21
BrightText = #26465f
ButtonText = #121418
Base = #f7f9fb
Window = #d9e0ea
Shadow = #57626f
Highlight = #54687e
HighlightedText = #ffffff
Link = #2e4d6b
LinkVisited = #5d4a78
AlternateBase = #e6ebf2
ToolTipBase = #f2f4f6
ToolTipText = #121418
PlaceholderText = #7a1b1d21
Accent = #54687e
[Palette.Disabled]
WindowText = #787878
Button = #d9e0ea
Light = #ffffff
Midlight = #b3becb
Dark = #93a0b1
Mid = #a1adbc
Text = #787878
BrightText = #26465f
ButtonText = #969696
Base = #d9e0ea
Window = #d9e0ea
Shadow = #57626f
Highlight = #ccd5e3
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #e6ebf2
ToolTipBase = #f2f4f6
ToolTipText = #121418
PlaceholderText = #7a1b1d21
Accent = #787878
[Palette.Inactive]
WindowText = #1b1d21
Button = #c7d1dd
Light = #ffffff
Midlight = #b3becb
Dark = #93a0b1
Mid = #a1adbc
Text = #1b1d21
BrightText = #26465f
ButtonText = #121418
Base = #f7f9fb
Window = #d9e0ea
Shadow = #57626f
Highlight = #ccd5e3
HighlightedText = #000000
Link = #2e4d6b
LinkVisited = #5d4a78
AlternateBase = #e6ebf2
ToolTipBase = #f2f4f6
ToolTipText = #121418
PlaceholderText = #7a1b1d21
Accent = #54687e
[AppColors]
AccentStrong = #54687e
AccentSoft = #9db0c4

View file

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