#include "theme_setup_page.h" #include "../../client/settings/cache_settings.h" #include "../../interface/palette_editor/palette_generator.h" #include "../../interface/palette_editor/quick_setup_panel.h" #include "../../interface/theme_manager.h" #include "../../interface/widgets/general/background_sources.h" #include "libcockatrice/settings/appearance_settings.h" #include #include #include #include #include #include #include #include #include #include #include namespace { /** @brief A theme's shipped identity accent, immune to any user- or auto- * generated palette that may currently be masking appColor(). */ QColor themeIdentityAccent(const QString &themeDirPath, const QString &themeName) { for (const QString &scheme : {QStringLiteral("Light"), QStringLiteral("Dark")}) { const PaletteConfig cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme); if (cfg.appColors.contains(AppColor::AccentStrong)) { return cfg.appColors.value(AppColor::AccentStrong); } const QColor highlight = cfg.colors.value(QPalette::Active).value(QPalette::Highlight); if (highlight.isValid()) { return highlight; } } return {}; } } // namespace ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent) { themeCombo = new QComboBox(this); schemeCombo = new QComboBox(this); schemeCombo->addItem(tr("Light"), QStringLiteral("Light")); schemeCombo->addItem(tr("Dark"), QStringLiteral("Dark")); #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) schemeCombo->addItem(tr("Match system"), QStringLiteral("System")); #endif quickSetupPanel = new QuickSetupPanel(this); // Seed the picker from the current theme's own identity accent rather than // a hardcoded brand green: Plasma seeds violet, Fusion green, etc., and it // is immune to stale generated palettes that may mask appColor(). This was // initially a brand-green workaround from before Fusion became the default. // setAccentColor blocks signals, so this never triggers a generation. lastSeededTheme = SettingsCache::instance().getThemeName(); quickSetupPanel->setAccentColor( themeIdentityAccent(themeManager->getAvailableThemes().value(lastSeededTheme), lastSeededTheme)); connect(themeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged); connect(schemeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged); connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent); homeTabBackgroundCombo = new QComboBox(this); for (const auto &entry : BackgroundSources::all()) { homeTabBackgroundCombo->addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type)); } connect(homeTabBackgroundCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onHomeTabBackgroundChanged); // Keep the scheme combo honest when the *theme* changes underneath it // (switching theme reloads that theme's own stored colorScheme), and // opportunistically seed a palette for themes that ship none at all. // Mirrors AppearanceSettingsPage's identical listener for the combo-sync // half of this. connect(themeManager, &ThemeManager::themeChanged, this, [this] { const QString newTheme = SettingsCache::instance().getThemeName(); const QString newDir = themeManager->getAvailableThemes().value(newTheme); 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); // Keep the picker's accent in step with the theme's own identity; the // swatch seeded at construction would otherwise stay stale (e.g. green // from a previous theme) when the user toggles themes. if (newTheme != lastSeededTheme) { lastSeededTheme = newTheme; quickSetupPanel->setAccentColor(themeIdentityAccent(newDir, newTheme)); } maybeAutoGeneratePalette(); }); auto *form = new QFormLayout; form->addRow(tr("Theme:"), themeCombo); form->addRow(tr("Appearance:"), schemeCombo); form->addRow(tr("Home screen background:"), homeTabBackgroundCombo); accentGroup = new QGroupBox(this); auto *accentLayout = new QVBoxLayout(accentGroup); accentLayout->addWidget(quickSetupPanel); auto *layout = new QVBoxLayout(this); layout->addLayout(form); layout->addWidget(accentGroup); layout->addStretch(); retranslateUi(); } void ThemeSetupPage::initializePage() { themeCombo->blockSignals(true); themeCombo->clear(); const QString currentTheme = SettingsCache::instance().getThemeName(); for (const QString &name : themeManager->getAvailableThemes().keys()) { themeCombo->addItem(name); } const int idx = themeCombo->findText(currentTheme); themeCombo->setCurrentIndex(idx >= 0 ? idx : 0); themeCombo->blockSignals(false); const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); const ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); schemeCombo->blockSignals(true); const int schemeIdx = schemeCombo->findData(cfg.colorScheme); schemeCombo->setCurrentIndex(schemeIdx >= 0 ? schemeIdx : 0); schemeCombo->blockSignals(false); homeTabBackgroundCombo->blockSignals(true); QString homeTabSource = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); int homeTabIdx = homeTabBackgroundCombo->findData(BackgroundSources::fromId(homeTabSource)); homeTabBackgroundCombo->setCurrentIndex(homeTabIdx >= 0 ? homeTabIdx : 0); homeTabBackgroundCombo->blockSignals(false); // Opening the page must not touch the running application's palette: // previews and auto-generation only happen in response to the user // actually changing a control, never on mere page visibility. paletteDirty = false; } QString ThemeSetupPage::currentScheme() const { return schemeCombo->currentData().toString(); } QString ThemeSetupPage::resolvedScheme() const { const QString scheme = currentScheme(); if (scheme.isEmpty() || scheme == QStringLiteral("System")) { return themeManager->isDarkMode(themeManager->getCurrentThemePath()) ? "Dark" : "Light"; } return scheme; } void ThemeSetupPage::onThemeChanged(int index) { if (index < 0) { return; } paletteDirty = false; SettingsCache::instance().setThemeName(themeCombo->itemText(index)); // Scheme-combo sync and auto-generation both happen via the // ThemeManager::themeChanged listener above, triggered by setThemeName. } void ThemeSetupPage::onSchemeChanged() { themeManager->setColorScheme(currentScheme()); } void ThemeSetupPage::onHomeTabBackgroundChanged(int index) { if (index < 0) { return; } auto type = homeTabBackgroundCombo->currentData().value(); SettingsCache::instance().appearance().setHomeTabBackgroundSource(BackgroundSources::toId(type)); } void ThemeSetupPage::onGenerateFromAccent(const QColor &accent, int intensity) { PaletteConfig cfg = PaletteGenerator::fromAccent(accent, intensity, resolvedScheme()); themeManager->previewPalette(cfg, resolvedScheme()); paletteDirty = true; } void ThemeSetupPage::maybeAutoGeneratePalette() { const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); const QString scheme = resolvedScheme(); // The theme dir may resolve to the user profile even for built-in themes // (getAvailableThemes gives the user copy precedence), so consult the // shipped palette too -- both via loadDefaultPaletteConfig's system fallback. // Without it, scheme flips regenerate a fresh palette from the picker accent // and clobber the curated colours the theme explicitly ships. if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() || ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme) .hasPalette()) { return; // theme already has something real to show -- leave it alone } // The theme+scheme combination has nothing saved and nothing shipped, and // the user just switched to it. Rather than leaving a flat, unstyled look, // seed one from whatever accent QuickSetupPanel currently holds and mark // it dirty so it's written to disk if the user moves on. Only ever reached // through user interaction (theme/scheme change, accent drag) -- never on // page open. PaletteConfig generated = PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme); themeManager->previewPalette(generated, scheme); paletteDirty = true; } bool ThemeSetupPage::validatePage() { if (paletteDirty) { const QString scheme = resolvedScheme(); PaletteConfig cfg = PaletteGenerator::fromAccent(quickSetupPanel->accentColor(), quickSetupPanel->intensity(), scheme); if (!ThemeManager::commitPalette(writableThemeDir(), scheme, cfg)) { QMessageBox::warning(this, tr("Save failed"), tr("Could not write the theme palette to:\n%1").arg(writableThemeDir())); return false; } themeManager->reloadCurrentTheme(); } return true; } QString ThemeSetupPage::writableThemeDir() const { // Built-in themes resolve to the read-only system themes directory; // palette edits must go to the user themes directory instead, exactly // as PaletteEditorDialog does. const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); if (!dirPath.isEmpty()) { const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test"); QFile f(probe); if (f.open(QIODevice::WriteOnly)) { f.close(); f.remove(); return dirPath; } } return QDir(SettingsCache::instance().paths().getThemesPath()) .absoluteFilePath(SettingsCache::instance().getThemeName()); } bool ThemeSetupPage::isSkippable() const { return true; } QString ThemeSetupPage::stepTitle() const { return tr("Pick a Look"); } QString ThemeSetupPage::stepSubtitle() const { return tr("You can fine-tune every colour later from Settings → Appearance."); } void ThemeSetupPage::retranslateUi() { accentGroup->setTitle(tr("Accent colour (optional)")); }