#include "theme_manager.h" #include "../../client/settings/cache_settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define NONE_THEME_NAME "Default" #define FUSION_THEME_NAME "Fusion" #define STYLE_CSS_NAME "style.css" #define HANDZONE_BG_NAME "handzone" #define PLAYERZONE_BG_NAME "playerzone" #define STACKZONE_BG_NAME "stackzone" #define TABLEZONE_BG_NAME "tablezone" static const QColor HANDZONE_BG_DEFAULT = QColor(80, 100, 50); static const QColor TABLEZONE_BG_DEFAULT = QColor(70, 50, 100); static const QColor PLAYERZONE_BG_DEFAULT = QColor(200, 200, 200); static const QColor STACKZONE_BG_DEFAULT = QColor(113, 43, 43); static const QStringList DEFAULT_RESOURCE_PATHS = {":/resources"}; struct PaletteColorInfo { QPalette::ColorGroup group; QPalette::ColorRole role; QColor color; }; [[maybe_unused]] static inline QList queryAllPaletteColors(const QPalette &palette = qApp->palette()) { QList colors; // Iterate through relevant color groups (Active, Disabled, Inactive) const QList groups = {QPalette::Active, QPalette::Disabled, QPalette::Inactive}; for (auto group : groups) { // Iterate through all color roles (excluding NoRole and NColorRoles) for (int r = 0; r < QPalette::NColorRoles; ++r) { auto role = static_cast(r); if (role == QPalette::NoRole) { continue; } PaletteColorInfo info; info.group = group; info.role = role; info.color = palette.color(group, role); colors.append(info); } } return colors; } // Pretty print version [[maybe_unused]] static inline void printPaletteColors(const QPalette &palette = qApp->palette()) { QMetaEnum groupEnum = QMetaEnum::fromType(); QMetaEnum roleEnum = QMetaEnum::fromType(); const QList groups = {QPalette::Active, QPalette::Disabled, QPalette::Inactive}; for (auto group : groups) { qInfo() << "\n===========" << groupEnum.valueToKey(group) << "==========="; for (int r = 0; r < QPalette::NColorRoles; ++r) { auto role = static_cast(r); if (role == QPalette::NoRole) { continue; } QColor color = palette.color(group, role); qInfo().nospace() << qPrintable(QString("%1").arg(roleEnum.valueToKey(role), -20)) << " : " << qPrintable(color.name(QColor::HexArgb)) << " (RGBA: " << color.red() << ", " << color.green() << ", " << color.blue() << ", " << color.alpha() << ")"; } } } ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { defaultStyleName = qApp->style()->objectName(); //! \todo Workaround for windows11 style being broken. if (defaultStyleName == "windows11") { defaultStyleName = "windowsvista"; } ensureThemeDirectoryExists(); #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, &ThemeManager::themeChangedSlot); #endif connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &ThemeManager::themeChangedSlot); themeChangedSlot(); } void ThemeManager::ensureThemeDirectoryExists() { if (SettingsCache::instance().getThemeName().isEmpty() || !getAvailableThemes().contains(SettingsCache::instance().getThemeName())) { qCInfo(ThemeManagerLog) << "Theme name not set, setting default value"; SettingsCache::instance().setThemeName(NONE_THEME_NAME); } } bool ThemeManager::isDarkMode(const QString &themeDirPath) { 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; } else { #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) bool osDark = (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark); #else bool osDark = false; #endif return osDark; } } bool ThemeManager::isBuiltInTheme() { const auto themeName = SettingsCache::instance().getThemeName(); return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME; } QStringMap &ThemeManager::getAvailableThemes() { QDir dir; availableThemes.clear(); // load themes from user profile dir dir.setPath(SettingsCache::instance().getThemesPath()); // 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)) { availableThemes.insert(themeName, dir.absoluteFilePath(themeName)); } } // load themes from cockatrice system dir dir.setPath(qApp->applicationDirPath() + #ifdef Q_OS_MAC "/../Resources/themes" #elif defined(Q_OS_WIN) "/themes" #else // linux "/../share/cockatrice/themes" #endif ); for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { if (!availableThemes.contains(themeName)) { availableThemes.insert(themeName, dir.absoluteFilePath(themeName)); } } return availableThemes; } QBrush ThemeManager::loadBrush(QString fileName, QColor fallbackColor) { QBrush brush; QPixmap tmp = QPixmap("theme:zones/" + fileName); if (tmp.isNull()) { brush.setColor(fallbackColor); brush.setStyle(Qt::SolidPattern); } else { brush.setTexture(tmp); } return brush; } QBrush ThemeManager::loadExtraBrush(QString fileName, QBrush &fallbackBrush) { QBrush brush; QPixmap tmp = QPixmap("theme:zones/" + fileName); if (tmp.isNull()) { brush = fallbackBrush; } else { brush.setTexture(tmp); } return brush; } ThemeConfig ThemeManager::loadGlobalConfig(const QString &themeDirPath) { return ThemeConfig::fromThemeDir(themeDirPath); } bool ThemeManager::saveGlobalConfig(const QString &themeDirPath, const ThemeConfig &cfg) { return cfg.save(themeDirPath); } PaletteConfig ThemeManager::loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme) { if (themeDirPath.isEmpty()) { return {}; } return PaletteConfig::fromScheme(themeDirPath, colorScheme); } bool ThemeManager::savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg) { if (themeDirPath.isEmpty()) { return false; } QDir dir(themeDirPath); if (!dir.exists()) { dir.mkpath("."); } QFile f(dir.absoluteFilePath(PaletteConfig::fileName(colorScheme))); if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { return false; } 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; } } 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 // and new colours during a live preview. // Note: we do NOT call widget->setPalette(base) here — qApp->setPalette() // already propagates to all widgets that haven't explicitly overridden their // palette (WA_SetPalette not set). Calling it unconditionally would clobber // intentional per-widget palette customisations across the whole app. for (QWidget *widget : qApp->allWidgets()) { style->unpolish(widget); style->polish(widget); widget->update(); } } 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 { 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(); } QPixmapCache::clear(); emit themeChanged(); } static QString roleBgName(ThemeManager::Role role) { switch (role) { case ThemeManager::Hand: return HANDZONE_BG_NAME; case ThemeManager::Player: return PLAYERZONE_BG_NAME; case ThemeManager::Stack: return STACKZONE_BG_NAME; case ThemeManager::Table: return TABLEZONE_BG_NAME; default: Q_ASSERT(false); } } QBrush &ThemeManager::getBgBrush(Role role) { return brushes[role]; } QBrush ThemeManager::getExtraBgBrush(Role role, int zoneId) { if (zoneId <= 0) { return getBgBrush(role); } QBrushMap &brushCache = brushesCache[role]; if (!brushCache.contains(zoneId)) { QBrush brush = loadExtraBrush(roleBgName(role) + QString::number(zoneId), getBgBrush(role)); brushCache.insert(zoneId, brush); return brush; } return brushCache.value(zoneId); }