From 46661becaf0af3af7cd8c8b12d5cba2a665ce4b9 Mon Sep 17 00:00:00 2001 From: luisangelsm Date: Thu, 5 Mar 2026 08:22:04 +0100 Subject: [PATCH] Implement theme pickers + importing --- YACReader/options_dialog.cpp | 1 + YACReaderLibrary/options_dialog.cpp | 1 + common/themes/appearance_config_images.qrc | 1 + common/themes/appearance_tab_widget.cpp | 225 +++++++++++++++++- common/themes/appearance_tab_widget.h | 29 +++ common/themes/theme_manager.h | 1 + .../appearance_config/theme-mode-custom.svg | 36 +++ 7 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 images/appearance_config/theme-mode-custom.svg diff --git a/YACReader/options_dialog.cpp b/YACReader/options_dialog.cpp index c39e7115..43f50e67 100644 --- a/YACReader/options_dialog.cpp +++ b/YACReader/options_dialog.cpp @@ -210,6 +210,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) auto *pageAppearance = new AppearanceTabWidget( ThemeManager::instance().getAppearanceConfiguration(), + ThemeManager::instance().getRepository(), []() { return ThemeManager::instance().getCurrentTheme().sourceJson; }, [](const QJsonObject &json) { ThemeManager::instance().setTheme(makeTheme(json)); }, this); diff --git a/YACReaderLibrary/options_dialog.cpp b/YACReaderLibrary/options_dialog.cpp index 1087d8ad..f2cdb4bf 100644 --- a/YACReaderLibrary/options_dialog.cpp +++ b/YACReaderLibrary/options_dialog.cpp @@ -419,6 +419,7 @@ QWidget *OptionsDialog::createAppearanceTab() { return new AppearanceTabWidget( ThemeManager::instance().getAppearanceConfiguration(), + ThemeManager::instance().getRepository(), []() { return ThemeManager::instance().getCurrentTheme().sourceJson; }, [](const QJsonObject &json) { ThemeManager::instance().setTheme(makeTheme(json)); }, this); diff --git a/common/themes/appearance_config_images.qrc b/common/themes/appearance_config_images.qrc index 3284bda9..b67edae7 100644 --- a/common/themes/appearance_config_images.qrc +++ b/common/themes/appearance_config_images.qrc @@ -3,5 +3,6 @@ ../../images/appearance_config/theme-mode-system.svg ../../images/appearance_config/theme-mode-light.svg ../../images/appearance_config/theme-mode-dark.svg + ../../images/appearance_config/theme-mode-custom.svg diff --git a/common/themes/appearance_tab_widget.cpp b/common/themes/appearance_tab_widget.cpp index 8563fb05..9dcb49c2 100644 --- a/common/themes/appearance_tab_widget.cpp +++ b/common/themes/appearance_tab_widget.cpp @@ -2,39 +2,58 @@ #include "appearance_configuration.h" #include "theme_editor_dialog.h" +#include "theme_repository.h" #include +#include +#include #include #include +#include #include #include #include #include +// Select the item in combo whose UserRole data matches id (no-op if not found). +static void selectInCombo(QComboBox *combo, const QString &id) +{ + for (int i = 0; i < combo->count(); ++i) { + if (combo->itemData(i).toString() == id) { + combo->setCurrentIndex(i); + return; + } + } +} + AppearanceTabWidget::AppearanceTabWidget( AppearanceConfiguration *config, + ThemeRepository *repository, std::function currentThemeJson, std::function applyTheme, QWidget *parent) - : QWidget(parent), config(config), currentThemeJson(std::move(currentThemeJson)), applyTheme(std::move(applyTheme)) + : QWidget(parent), config(config), repository(repository), currentThemeJson(std::move(currentThemeJson)), applyTheme(std::move(applyTheme)) { - // Color scheme selector + // --- Color scheme selector --- auto *modeBox = new QGroupBox(tr("Color scheme"), this); auto *modeLayout = new QHBoxLayout(); auto *sysBtn = new QToolButton(); auto *lightBtn = new QToolButton(); auto *darkBtn = new QToolButton(); + auto *customBtn = new QToolButton(); sysBtn->setText(tr("System")); lightBtn->setText(tr("Light")); darkBtn->setText(tr("Dark")); + customBtn->setText(tr("Custom")); sysBtn->setIcon(QIcon(":/images/appearance_config/theme-mode-system.svg")); lightBtn->setIcon(QIcon(":/images/appearance_config/theme-mode-light.svg")); darkBtn->setIcon(QIcon(":/images/appearance_config/theme-mode-dark.svg")); + customBtn->setIcon(QIcon(":/images/appearance_config/theme-mode-custom.svg")); - for (auto *btn : { sysBtn, lightBtn, darkBtn }) { + for (auto *btn : { sysBtn, lightBtn, darkBtn, customBtn }) { btn->setCheckable(true); btn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); btn->setIconSize(QSize(93, 58)); @@ -45,6 +64,7 @@ AppearanceTabWidget::AppearanceTabWidget( modeGroup->addButton(sysBtn, static_cast(ThemeMode::FollowSystem)); modeGroup->addButton(lightBtn, static_cast(ThemeMode::Light)); modeGroup->addButton(darkBtn, static_cast(ThemeMode::Dark)); + modeGroup->addButton(customBtn, static_cast(ThemeMode::ForcedTheme)); modeGroup->setExclusive(true); if (this->config) { @@ -54,18 +74,102 @@ AppearanceTabWidget::AppearanceTabWidget( } connect(modeGroup, &QButtonGroup::idClicked, this, [this](int id) { - if (this->config) + if (this->config) { this->config->setMode(static_cast(id)); + updateModeRows(); + } }); modeLayout->addStretch(); modeLayout->addWidget(sysBtn); modeLayout->addWidget(lightBtn); modeLayout->addWidget(darkBtn); + modeLayout->addWidget(customBtn); modeLayout->addStretch(); modeBox->setLayout(modeLayout); - // Theme editor + // --- Theme selection --- + // Each row: [label fixed] [combo expanding] [Remove btn] + // Rows are shown/hidden based on mode — only relevant ones are visible. + auto makeRow = [](const QString &label, QComboBox *&combo, QPushButton *&deleteBtn) { + auto *row = new QWidget(); + auto *hl = new QHBoxLayout(row); + hl->setContentsMargins(0, 0, 0, 0); + + auto *lbl = new QLabel(label); + lbl->setFixedWidth(52); + + combo = new QComboBox(); + combo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + deleteBtn = new QPushButton(tr("Remove")); + deleteBtn->setEnabled(false); + deleteBtn->setToolTip(tr("Remove this user-imported theme")); + + hl->addWidget(lbl); + hl->addWidget(combo); + hl->addWidget(deleteBtn); + return row; + }; + + lightRow = makeRow(tr("Light:"), lightCombo, lightDeleteBtn); + darkRow = makeRow(tr("Dark:"), darkCombo, darkDeleteBtn); + customRow = makeRow(tr("Custom:"), customCombo, customDeleteBtn); + + auto *importBtn = new QPushButton(tr("Import theme...")); + + auto *themeSelBox = new QGroupBox(tr("Theme"), this); + auto *themeSelLayout = new QVBoxLayout(); + themeSelLayout->addWidget(lightRow); + themeSelLayout->addWidget(darkRow); + themeSelLayout->addWidget(customRow); + themeSelLayout->addWidget(importBtn, 0, Qt::AlignRight); + themeSelBox->setLayout(themeSelLayout); + + // Populate combos and set initial row visibility + if (this->config && this->repository) { + const auto &sel = this->config->selection(); + populateCombo(lightCombo, ThemeVariant::Light, sel.lightThemeId); + populateCombo(darkCombo, ThemeVariant::Dark, sel.darkThemeId); + populateCombo(customCombo, std::nullopt, sel.fixedThemeId); + updateDeleteButton(lightCombo, lightDeleteBtn); + updateDeleteButton(darkCombo, darkDeleteBtn); + updateDeleteButton(customCombo, customDeleteBtn); + updateModeRows(); + } + + // Combo selection → update config (live theme preview via selectionChanged chain) + connect(lightCombo, &QComboBox::currentIndexChanged, this, [this](int) { + if (!this->config) + return; + this->config->setLightThemeId(lightCombo->currentData().toString()); + updateDeleteButton(lightCombo, lightDeleteBtn); + }); + connect(darkCombo, &QComboBox::currentIndexChanged, this, [this](int) { + if (!this->config) + return; + this->config->setDarkThemeId(darkCombo->currentData().toString()); + updateDeleteButton(darkCombo, darkDeleteBtn); + }); + connect(customCombo, &QComboBox::currentIndexChanged, this, [this](int) { + if (!this->config) + return; + this->config->setFixedThemeId(customCombo->currentData().toString()); + updateDeleteButton(customCombo, customDeleteBtn); + }); + + // Delete buttons + connect(lightDeleteBtn, &QPushButton::clicked, this, + [this]() { deleteTheme(lightCombo, lightDeleteBtn); }); + connect(darkDeleteBtn, &QPushButton::clicked, this, + [this]() { deleteTheme(darkCombo, darkDeleteBtn); }); + connect(customDeleteBtn, &QPushButton::clicked, this, + [this]() { deleteTheme(customCombo, customDeleteBtn); }); + + // Import + connect(importBtn, &QPushButton::clicked, this, &AppearanceTabWidget::importTheme); + + // --- Theme editor --- auto *themeEditorBox = new QGroupBox(tr("Theme editor"), this); auto *themeEditorLayout = new QVBoxLayout(); auto *openBtn = new QPushButton(tr("Open Theme Editor...")); @@ -93,6 +197,117 @@ AppearanceTabWidget::AppearanceTabWidget( auto *layout = new QVBoxLayout(this); layout->addWidget(modeBox); + layout->addWidget(themeSelBox); layout->addWidget(themeEditorBox); layout->addStretch(); } + +void AppearanceTabWidget::populateCombo(QComboBox *combo, + std::optional variantFilter, + const QString &selectedId) +{ + QSignalBlocker blocker(combo); + combo->clear(); + if (!repository) + return; + + for (const auto &entry : repository->availableThemes()) { + if (variantFilter && entry.variant != *variantFilter) + continue; + combo->addItem(entry.displayName, entry.id); + } + + // Restore selection; fall back to first item if the saved ID is no longer present + for (int i = 0; i < combo->count(); ++i) { + if (combo->itemData(i).toString() == selectedId) { + combo->setCurrentIndex(i); + return; + } + } + if (combo->count() > 0) + combo->setCurrentIndex(0); +} + +void AppearanceTabWidget::repopulateCombos() +{ + if (!config) + return; + const auto &sel = config->selection(); + populateCombo(lightCombo, ThemeVariant::Light, sel.lightThemeId); + populateCombo(darkCombo, ThemeVariant::Dark, sel.darkThemeId); + populateCombo(customCombo, std::nullopt, sel.fixedThemeId); + updateDeleteButton(lightCombo, lightDeleteBtn); + updateDeleteButton(darkCombo, darkDeleteBtn); + updateDeleteButton(customCombo, customDeleteBtn); +} + +void AppearanceTabWidget::updateDeleteButton(QComboBox *combo, QPushButton *btn) +{ + const QString id = combo->currentData().toString(); + btn->setEnabled(!id.isEmpty() && id.startsWith(QLatin1String("user/"))); +} + +void AppearanceTabWidget::updateModeRows() +{ + if (!config) + return; + const auto mode = config->selection().mode; + lightRow->setVisible(mode == ThemeMode::FollowSystem || mode == ThemeMode::Light); + darkRow->setVisible(mode == ThemeMode::FollowSystem || mode == ThemeMode::Dark); + customRow->setVisible(mode == ThemeMode::ForcedTheme); +} + +void AppearanceTabWidget::importTheme() +{ + const QString path = QFileDialog::getOpenFileName( + this, tr("Import theme"), QString(), tr("JSON files (*.json);;All files (*)")); + if (path.isEmpty() || !repository) + return; + + const QString id = repository->importThemeFromFile(path); + if (id.isEmpty()) { + QMessageBox::warning(this, tr("Import failed"), + tr("Could not import theme from:\n%1").arg(path)); + return; + } + + // Detect variant of the imported theme to auto-select it in the right combo + const QJsonObject json = repository->loadThemeJson(id); + const bool isLight = (json["meta"].toObject()["variant"].toString() == "light"); + + repopulateCombos(); + + // Select in the appropriate combo → triggers currentIndexChanged → config update + if (isLight) + selectInCombo(lightCombo, id); + else + selectInCombo(darkCombo, id); + + // If in Custom mode, also select in customCombo + if (config && config->selection().mode == ThemeMode::ForcedTheme) + selectInCombo(customCombo, id); +} + +void AppearanceTabWidget::deleteTheme(QComboBox *combo, QPushButton *deleteBtn) +{ + if (!repository) + return; + const QString id = combo->currentData().toString(); + if (!id.startsWith(QLatin1String("user/"))) + return; + + repository->deleteUserTheme(id); + repopulateCombos(); + + // repopulateCombos() blocked signals; manually push the new selection into config + // so the theme resolves correctly (important when the deleted theme was active). + const QString newId = combo->currentData().toString(); + if (combo == lightCombo && config) + config->setLightThemeId(newId); + else if (combo == darkCombo && config) + config->setDarkThemeId(newId); + else if (combo == customCombo && config) + config->setFixedThemeId(newId); + + updateDeleteButton(combo, deleteBtn); +} diff --git a/common/themes/appearance_tab_widget.h b/common/themes/appearance_tab_widget.h index cbccb88b..98195058 100644 --- a/common/themes/appearance_tab_widget.h +++ b/common/themes/appearance_tab_widget.h @@ -1,13 +1,19 @@ #ifndef APPEARANCE_TAB_WIDGET_H #define APPEARANCE_TAB_WIDGET_H +#include "theme_variant.h" + #include #include #include #include +#include class AppearanceConfiguration; +class QComboBox; +class QPushButton; class ThemeEditorDialog; +class ThemeRepository; class AppearanceTabWidget : public QWidget { @@ -15,15 +21,38 @@ class AppearanceTabWidget : public QWidget public: explicit AppearanceTabWidget( AppearanceConfiguration *config, + ThemeRepository *repository, std::function currentThemeJson, std::function applyTheme, QWidget *parent = nullptr); private: AppearanceConfiguration *config; + ThemeRepository *repository; std::function currentThemeJson; std::function applyTheme; QPointer themeEditor; + + // One row per picker; shown/hidden based on active mode + QWidget *lightRow = nullptr; + QWidget *darkRow = nullptr; + QWidget *customRow = nullptr; + + QComboBox *lightCombo = nullptr; + QComboBox *darkCombo = nullptr; + QComboBox *customCombo = nullptr; + + QPushButton *lightDeleteBtn = nullptr; + QPushButton *darkDeleteBtn = nullptr; + QPushButton *customDeleteBtn = nullptr; + + // Populate a combo with themes, filtered strictly by variant (or all if nullopt). + void populateCombo(QComboBox *combo, std::optional variantFilter, const QString &selectedId); + void repopulateCombos(); + void updateDeleteButton(QComboBox *combo, QPushButton *btn); + void updateModeRows(); + void importTheme(); + void deleteTheme(QComboBox *combo, QPushButton *deleteBtn); }; #endif // APPEARANCE_TAB_WIDGET_H diff --git a/common/themes/theme_manager.h b/common/themes/theme_manager.h index 42b8d0d0..7ffbeea7 100644 --- a/common/themes/theme_manager.h +++ b/common/themes/theme_manager.h @@ -25,6 +25,7 @@ public: const Theme &getCurrentTheme() const { return currentTheme; } AppearanceConfiguration *getAppearanceConfiguration() const { return config; } + ThemeRepository *getRepository() const { return repository; } signals: void themeChanged(); diff --git a/images/appearance_config/theme-mode-custom.svg b/images/appearance_config/theme-mode-custom.svg new file mode 100644 index 00000000..227cd5da --- /dev/null +++ b/images/appearance_config/theme-mode-custom.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file