Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions extra/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@
}
},

// The general vicinae theme as well as the system icon theme (used for applications and file icons) can be customized
// according to the system appearance.
// When editing the current theme through the vicinae GUI, it will modify the value that maps to the current system appearance.
// Light and dark themes are stored separately. "appearance" chooses which slot is active:
// "system" follows the OS color scheme, while "light" / "dark" pin that slot.
"theme": {
"appearance": "system",
"light": {
"name": "vicinae-light",
"icon_theme": "auto"
Expand Down
1 change: 1 addition & 0 deletions src/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ set(SRCS
src/internal/keyboard/keybind-manager.cpp
src/internal/keyboard/keyboard.cpp

src/theme/theme-variant.hpp
src/theme/theme-file.hpp
src/theme/theme-file.cpp
src/theme/theme-parser.cpp
Expand Down
24 changes: 14 additions & 10 deletions src/server/src/config/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,19 @@ template <typename T> T static merge(const auto &v1, const auto &v2) {
return cfg;
}

bool ConfigValue::followsSystemAppearance() const {
return theme.appearance != "light" && theme.appearance != "dark";
}

ThemeVariant ConfigValue::activeAppearance() const {
if (theme.appearance == "light") return ThemeVariant::Light;
if (theme.appearance == "dark") return ThemeVariant::Dark;
return QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Light ? ThemeVariant::Light
: ThemeVariant::Dark;
}

const SystemThemeConfig &ConfigValue::systemTheme() const {
switch (QGuiApplication::styleHints()->colorScheme()) {
case Qt::ColorScheme::Light:
return theme.light;
default:
return theme.dark;
}
return activeAppearance() == ThemeVariant::Light ? theme.light : theme.dark;
}

Manager::Manager(fs::path path) : m_userPath(std::move(path)) {
Expand Down Expand Up @@ -122,12 +128,10 @@ bool Manager::mergeEntrypointWithUser(const EntrypointId &id, const ProviderItem
}

bool Manager::mergeThemeConfig(const config::Partial<config::SystemThemeConfig> &cfg) {
switch (QGuiApplication::styleHints()->colorScheme()) {
case Qt::ColorScheme::Light:
if (m_user.activeAppearance() == ThemeVariant::Light) {
return mergeWithUser({.theme = config::Partial<config::ThemeConfig>{.light = cfg}});
default:
return mergeWithUser({.theme = config::Partial<config::ThemeConfig>{.dark = cfg}});
}
return mergeWithUser({.theme = config::Partial<config::ThemeConfig>{.dark = cfg}});
}

bool Manager::mergeWithUser(const Partial<ConfigValue> &patch) {
Expand Down
5 changes: 5 additions & 0 deletions src/server/src/config/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <expected>
#include <filesystem>
#include "common/entrypoint.hpp"
#include "theme/theme-variant.hpp"
#include "vicinae.hpp"
#include <glaze/core/common.hpp>
#include <glaze/core/reflect.hpp>
Expand Down Expand Up @@ -37,6 +38,7 @@ struct SystemThemeConfig {
};

struct ThemeConfig {
std::string appearance = "system";
SystemThemeConfig light;
SystemThemeConfig dark;
};
Expand All @@ -47,6 +49,7 @@ template <> struct Partial<SystemThemeConfig> {
};

template <> struct Partial<ThemeConfig> {
std::optional<std::string> appearance;
std::optional<Partial<SystemThemeConfig>> light;
std::optional<Partial<SystemThemeConfig>> dark;
};
Expand Down Expand Up @@ -345,6 +348,8 @@ struct ConfigValue {
return std::nullopt;
}

bool followsSystemAppearance() const;
ThemeVariant activeAppearance() const;
const SystemThemeConfig &systemTheme() const;
};

Expand Down
100 changes: 77 additions & 23 deletions src/server/src/qml/general-settings-model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,42 @@
#include <QGuiApplication>
#include <QIcon>
#include <QLocale>
#include <QStyleHints>
#include <optional>

namespace {

QVariantList wrapSection(const QString &title, const QVariantList &items) {
QVariantMap section;
section[QStringLiteral("title")] = title;
section[QStringLiteral("items")] = items;
return {section};
}

QVariant themeDropdownItem(const std::string &id) {
const auto qid = QString::fromStdString(id);
const auto *theme = ThemeService::instance().findTheme(qid);
if (!theme) return qml::makeDropdownItem(qid, qid);
const auto iconUrl = theme->icon() ? ImageURL::local(QString::fromStdString(theme->icon()->string()))
: ImageURL::builtin(BuiltinIcon::Vicinae);
return qml::makeDropdownItem(qid, theme->name(), qml::imageSourceFor(iconUrl));
}

QVariantList themeItemsForVariant(std::optional<ThemeVariant> variant) {
QVariantList items;
for (const auto &theme : ThemeService::instance().themes()) {
if (variant && theme->variant() != *variant) continue;
const auto iconUrl = theme->icon() ? ImageURL::local(QString::fromStdString(theme->icon()->string()))
: ImageURL::builtin(BuiltinIcon::Vicinae);
items.append(qml::makeDropdownItem(theme->id(), theme->name(), qml::imageSourceFor(iconUrl)));
}
const QString title = !variant ? GeneralSettingsModel::tr("Themes")
: *variant == ThemeVariant::Light ? GeneralSettingsModel::tr("Light themes")
: GeneralSettingsModel::tr("Dark themes");
return wrapSection(title, items);
}

} // namespace

GeneralSettingsModel::GeneralSettingsModel(QObject *parent) : QObject(parent) {
m_windowMaterialModel.setSections(windowMaterialItems());
Expand All @@ -34,6 +70,14 @@ void GeneralSettingsModel::refreshDynamicModels() {
m_themeItems = items;
m_themeModel.setSections(items);
}
if (auto items = themeItemsForVariant(ThemeVariant::Light); items != m_lightThemeItems) {
m_lightThemeItems = items;
m_lightThemeModel.setSections(items);
}
if (auto items = themeItemsForVariant(ThemeVariant::Dark); items != m_darkThemeItems) {
m_darkThemeItems = items;
m_darkThemeModel.setSections(items);
}
if (auto items = iconThemeItems(); items != m_iconThemeItems) {
m_iconThemeItems = items;
m_iconThemeModel.setSections(items);
Expand Down Expand Up @@ -175,15 +219,22 @@ void GeneralSettingsModel::setFontSize(const QString &v) {
if (ok) cfgManager().mergeWithUser({.font = config::Partial<config::FontConfig>{.normal{.size = val}}});
}

using qml::makeDropdownItem;
bool GeneralSettingsModel::followSystemAppearance() const { return cfg().followsSystemAppearance(); }

static QVariantList wrapSection(const QString &title, const QVariantList &items) {
QVariantMap section;
section[QStringLiteral("title")] = title;
section[QStringLiteral("items")] = items;
return {section};
void GeneralSettingsModel::setFollowSystemAppearance(bool follow) {
if (follow) {
cfgManager().mergeWithUser(
{.theme = config::Partial<config::ThemeConfig>{.appearance = std::string{"system"}}});
return;
}

const bool light = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Light;
cfgManager().mergeWithUser({.theme = config::Partial<config::ThemeConfig>{
.appearance = light ? std::string{"light"} : std::string{"dark"}}});
}

using qml::makeDropdownItem;

QVariantList GeneralSettingsModel::windowMaterialItems() const {
QVariantList items;
items.append(makeDropdownItem(QStringLiteral("none"), tr("None")));
Expand All @@ -206,24 +257,13 @@ void GeneralSettingsModel::selectWindowMaterial(const QString &id) {
{.launcherWindow = config::Partial<config::WindowConfig>{.material = id.toStdString()}});
}

QVariantList GeneralSettingsModel::themeItems() const {
QVariantList items;
for (const auto &theme : ThemeService::instance().themes()) {
auto iconUrl = theme->icon() ? ImageURL::local(QString::fromStdString(theme->icon()->string()))
: ImageURL::builtin(BuiltinIcon::Vicinae);
items.append(makeDropdownItem(theme->id(), theme->name(), qml::imageSourceFor(iconUrl)));
}
return wrapSection(tr("Themes"), items);
}
QVariantList GeneralSettingsModel::themeItems() const { return themeItemsForVariant(std::nullopt); }

QVariant GeneralSettingsModel::currentTheme() const {
auto id = QString::fromStdString(cfg().systemTheme().name);
auto *theme = ThemeService::instance().findTheme(id);
if (!theme) return makeDropdownItem(id, id);
auto iconUrl = theme->icon() ? ImageURL::local(QString::fromStdString(theme->icon()->string()))
: ImageURL::builtin(BuiltinIcon::Vicinae);
return makeDropdownItem(id, theme->name(), qml::imageSourceFor(iconUrl));
}
QVariant GeneralSettingsModel::currentTheme() const { return themeDropdownItem(cfg().systemTheme().name); }

QVariant GeneralSettingsModel::currentLightTheme() const { return themeDropdownItem(cfg().theme.light.name); }

QVariant GeneralSettingsModel::currentDarkTheme() const { return themeDropdownItem(cfg().theme.dark.name); }

QVariantList GeneralSettingsModel::fontItems() const {
QVariantList items;
Expand Down Expand Up @@ -292,6 +332,20 @@ void GeneralSettingsModel::selectTheme(const QString &id) {
cfgManager().mergeThemeConfig({.name = id.toStdString()});
}

void GeneralSettingsModel::selectLightTheme(const QString &id) {
config::Partial<config::ThemeConfig> theme{
.light = config::Partial<config::SystemThemeConfig>{.name = id.toStdString()}};
if (!followSystemAppearance()) theme.appearance = "light";
cfgManager().mergeWithUser({.theme = theme});
}

void GeneralSettingsModel::selectDarkTheme(const QString &id) {
config::Partial<config::ThemeConfig> theme{
.dark = config::Partial<config::SystemThemeConfig>{.name = id.toStdString()}};
if (!followSystemAppearance()) theme.appearance = "dark";
cfgManager().mergeWithUser({.theme = theme});
}

void GeneralSettingsModel::selectFont(const QString &id) {
cfgManager().mergeWithUser(
{.font = config::Partial<config::FontConfig>{.normal = {.family = id.toStdString()}}});
Expand Down
18 changes: 18 additions & 0 deletions src/server/src/qml/general-settings-model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,22 @@ class GeneralSettingsModel : public QObject {
Q_PROPERTY(
bool nativeTextRendering READ nativeTextRendering WRITE setNativeTextRendering NOTIFY configChanged)
Q_PROPERTY(QString fontSize READ fontSize WRITE setFontSize NOTIFY configChanged)
Q_PROPERTY(bool followSystemAppearance READ followSystemAppearance WRITE setFollowSystemAppearance NOTIFY
configChanged)
Q_PROPERTY(CompletionModel *windowMaterialModel READ windowMaterialModel CONSTANT)
Q_PROPERTY(QVariant currentWindowMaterial READ currentWindowMaterial NOTIFY configChanged)
Q_PROPERTY(CompletionModel *themeModel READ themeModel CONSTANT)
Q_PROPERTY(CompletionModel *lightThemeModel READ lightThemeModel CONSTANT)
Q_PROPERTY(CompletionModel *darkThemeModel READ darkThemeModel CONSTANT)
Q_PROPERTY(CompletionModel *fontModel READ fontModel CONSTANT)
Q_PROPERTY(CompletionModel *iconThemeModel READ iconThemeModel CONSTANT)
Q_PROPERTY(CompletionModel *faviconServiceModel READ faviconServiceModel CONSTANT)
Q_PROPERTY(CompletionModel *keybindingSchemeModel READ keybindingSchemeModel CONSTANT)
Q_PROPERTY(CompletionModel *languageModel READ languageModel CONSTANT)
Q_PROPERTY(QVariant currentLanguage READ currentLanguage NOTIFY configChanged)
Q_PROPERTY(QVariant currentTheme READ currentTheme NOTIFY configChanged)
Q_PROPERTY(QVariant currentLightTheme READ currentLightTheme NOTIFY configChanged)
Q_PROPERTY(QVariant currentDarkTheme READ currentDarkTheme NOTIFY configChanged)
Q_PROPERTY(QVariant currentFont READ currentFont NOTIFY configChanged)
Q_PROPERTY(QVariant currentIconTheme READ currentIconTheme NOTIFY configChanged)
Q_PROPERTY(QVariant currentFaviconService READ currentFaviconService NOTIFY configChanged)
Expand Down Expand Up @@ -95,9 +101,13 @@ class GeneralSettingsModel : public QObject {
void setInputServerEnabled(bool v);
QString fontSize() const;
void setFontSize(const QString &v);
bool followSystemAppearance() const;
void setFollowSystemAppearance(bool v);

CompletionModel *windowMaterialModel() { return &m_windowMaterialModel; }
CompletionModel *themeModel() { return &m_themeModel; }
CompletionModel *lightThemeModel() { return &m_lightThemeModel; }
CompletionModel *darkThemeModel() { return &m_darkThemeModel; }
CompletionModel *fontModel() { return &m_fontModel; }
CompletionModel *iconThemeModel() { return &m_iconThemeModel; }
CompletionModel *faviconServiceModel() { return &m_faviconServiceModel; }
Expand All @@ -109,12 +119,16 @@ class GeneralSettingsModel : public QObject {

QVariant currentLanguage() const;
QVariant currentTheme() const;
QVariant currentLightTheme() const;
QVariant currentDarkTheme() const;
QVariant currentFont() const;
QVariant currentIconTheme() const;
QVariant currentFaviconService() const;
QVariant currentKeybindingScheme() const;

Q_INVOKABLE void selectTheme(const QString &id);
Q_INVOKABLE void selectLightTheme(const QString &id);
Q_INVOKABLE void selectDarkTheme(const QString &id);
Q_INVOKABLE void selectFont(const QString &id);
Q_INVOKABLE void selectIconTheme(const QString &id);
Q_INVOKABLE void selectFaviconService(const QString &id);
Expand All @@ -139,11 +153,15 @@ class GeneralSettingsModel : public QObject {

CompletionModel m_windowMaterialModel{this};
CompletionModel m_themeModel{this};
CompletionModel m_lightThemeModel{this};
CompletionModel m_darkThemeModel{this};
CompletionModel m_fontModel{this};
CompletionModel m_iconThemeModel{this};
CompletionModel m_faviconServiceModel{this};
CompletionModel m_keybindingSchemeModel{this};
CompletionModel m_languageModel{this};
QVariantList m_themeItems;
QVariantList m_lightThemeItems;
QVariantList m_darkThemeItems;
QVariantList m_iconThemeItems;
};
27 changes: 23 additions & 4 deletions src/server/src/qml/qml/AppearanceSettingsPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,31 @@ Flickable {

SettingsGroup {
SettingsRow {
label: qsTr("Theme")
label: qsTr("Follow system appearance")
description: qsTr("Automatically switch between the light and dark themes based on your system appearance.")
SettingsToggle {
checked: root.model.followSystemAppearance
onToggled: checked => root.model.followSystemAppearance = checked
}
}

SettingsRow {
label: qsTr("Light theme")
SearchableDropdown {
width: parent.width
model: root.model.lightThemeModel
currentItem: root.model.currentLightTheme
onActivated: item => root.model.selectLightTheme(item.id)
}
}

SettingsRow {
label: qsTr("Dark theme")
SearchableDropdown {
width: parent.width
model: root.model.themeModel
currentItem: root.model.currentTheme
onActivated: item => root.model.selectTheme(item.id)
model: root.model.darkThemeModel
currentItem: root.model.currentDarkTheme
onActivated: item => root.model.selectDarkTheme(item.id)
}
}

Expand Down
1 change: 1 addition & 0 deletions src/server/src/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ int startServer(const ServerLaunchOptions &launchOpts) {

QObject::connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, [&]() {
auto &value = cfgService->value();
if (!value.followsSystemAppearance()) return;
auto &theme = value.systemTheme();

if (theme.iconTheme != "auto") {
Expand Down
3 changes: 1 addition & 2 deletions src/server/src/theme/theme-file.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
#include <qjsondocument.h>
#include <unordered_map>
#include "theme/colors.hpp"

enum class ThemeVariant { Light, Dark };
#include "theme/theme-variant.hpp"

class ThemeFile {
public:
Expand Down
3 changes: 3 additions & 0 deletions src/server/src/theme/theme-variant.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#pragma once

enum class ThemeVariant { Light, Dark };