mirror of
https://github.com/YACReader/yacreader
synced 2026-04-12 15:49:53 -04:00
Implement theme pickers + importing
This commit is contained in:
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -3,5 +3,6 @@
|
||||
<file>../../images/appearance_config/theme-mode-system.svg</file>
|
||||
<file>../../images/appearance_config/theme-mode-light.svg</file>
|
||||
<file>../../images/appearance_config/theme-mode-dark.svg</file>
|
||||
<file>../../images/appearance_config/theme-mode-custom.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -2,39 +2,58 @@
|
||||
|
||||
#include "appearance_configuration.h"
|
||||
#include "theme_editor_dialog.h"
|
||||
#include "theme_repository.h"
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QComboBox>
|
||||
#include <QFileDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
// 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<QJsonObject()> currentThemeJson,
|
||||
std::function<void(const QJsonObject &)> 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<int>(ThemeMode::FollowSystem));
|
||||
modeGroup->addButton(lightBtn, static_cast<int>(ThemeMode::Light));
|
||||
modeGroup->addButton(darkBtn, static_cast<int>(ThemeMode::Dark));
|
||||
modeGroup->addButton(customBtn, static_cast<int>(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<ThemeMode>(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<ThemeVariant> 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);
|
||||
}
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
#ifndef APPEARANCE_TAB_WIDGET_H
|
||||
#define APPEARANCE_TAB_WIDGET_H
|
||||
|
||||
#include "theme_variant.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QPointer>
|
||||
#include <QWidget>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
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<QJsonObject()> currentThemeJson,
|
||||
std::function<void(const QJsonObject &)> applyTheme,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
AppearanceConfiguration *config;
|
||||
ThemeRepository *repository;
|
||||
std::function<QJsonObject()> currentThemeJson;
|
||||
std::function<void(const QJsonObject &)> applyTheme;
|
||||
QPointer<ThemeEditorDialog> 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<ThemeVariant> 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
|
||||
|
||||
@ -25,6 +25,7 @@ public:
|
||||
const Theme &getCurrentTheme() const { return currentTheme; }
|
||||
|
||||
AppearanceConfiguration *getAppearanceConfiguration() const { return config; }
|
||||
ThemeRepository *getRepository() const { return repository; }
|
||||
|
||||
signals:
|
||||
void themeChanged();
|
||||
|
||||
36
images/appearance_config/theme-mode-custom.svg
Normal file
36
images/appearance_config/theme-mode-custom.svg
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 93 58">
|
||||
<!-- Generator: Adobe Illustrator 29.8.5, SVG Export Plug-In . SVG Version: 2.1.1 Build 2) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #d7d7d9;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #bbb;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #474747;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #f7f7f7;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="st3" y="0" width="93" height="58" rx="5" ry="5"/>
|
||||
<rect class="st2" x="4" y="4" width="19" height="50" rx="3.1" ry="3.1"/>
|
||||
<rect class="st1" x="26" y="4" width="63" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="26" y="21" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="39" y="21" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="52" y="21" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="65" y="21" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="78" y="21" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="26" y="38" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="39" y="38" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="52" y="38" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="65" y="38" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
<rect class="st0" x="78" y="38" width="11" height="16" rx="3.1" ry="3.1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user