Add KDE color scheme support

ThemeManager reads .colors files from /usr/share/color-schemes and
~/.local/share/color-schemes (KDE standard locations). Supports:
  - 'system' mode: follows kdeglobals ColorScheme setting
  - explicit scheme name: forces specific scheme (BreezeDark, etc.)

QML now uses theme.* colors throughout instead of hardcoded Nord values.
Org-mode semantic colors (todoColor, doneColor, priorityAColor, etc.)
adjust automatically for dark/light themes.

Settings saved to ~/.config/orkmode/theme.conf
This commit is contained in:
Levi Neely 2026-10-01 09:08:15 +02:00
parent bc2130d5b1
commit 64c7272f70
6 changed files with 559 additions and 35 deletions

View File

@ -18,6 +18,8 @@ qt_add_executable(orkmode
src/orkclient.h
src/documentmodel.cpp
src/documentmodel.h
src/thememanager.cpp
src/thememanager.h
)
qt_add_qml_module(orkmode

View File

@ -9,14 +9,55 @@ ApplicationWindow {
visible: true
title: qsTr("orkmode")
// orkmode palette — muted, org-like.
readonly property color bgColor: "#1e2127"
readonly property color panelColor: "#252932"
readonly property color textColor: "#d8dee9"
readonly property color dimColor: "#7a8290"
readonly property color accentColor: "#5e81ac"
// Apply theme palette to Qt Quick Controls
palette.active.window: theme.windowColor
palette.active.windowText: theme.windowTextColor
palette.active.base: theme.baseColor
palette.active.text: theme.textColor
palette.active.button: theme.buttonColor
palette.active.buttonText: theme.buttonTextColor
palette.active.highlight: theme.highlightColor
palette.active.highlightedText: theme.highlightedTextColor
palette.active.mid: theme.midColor
palette.active.dark: theme.darkColor
palette.active.light: theme.lightColor
palette.active.shadow: theme.shadowColor
palette.active.link: theme.linkColor
palette.active.toolTipBase: theme.tooltipBaseColor
palette.active.toolTipText: theme.tooltipTextColor
palette.active.placeholderText: theme.placeholderTextColor
palette.active.alternateBase: theme.alternateBaseColor
palette.inactive.window: theme.windowColor
palette.inactive.windowText: theme.windowTextColor
palette.inactive.base: theme.baseColor
palette.inactive.text: theme.textColor
palette.inactive.button: theme.buttonColor
palette.inactive.buttonText: theme.buttonTextColor
palette.inactive.highlight: theme.highlightColor
palette.inactive.highlightedText: theme.highlightedTextColor
palette.inactive.mid: theme.midColor
palette.inactive.dark: theme.darkColor
palette.inactive.light: theme.lightColor
palette.inactive.shadow: theme.shadowColor
palette.inactive.link: theme.linkColor
palette.inactive.toolTipBase: theme.tooltipBaseColor
palette.inactive.toolTipText: theme.tooltipTextColor
palette.inactive.placeholderText: theme.placeholderTextColor
palette.inactive.alternateBase: theme.alternateBaseColor
palette.disabled.window: theme.windowColor
palette.disabled.windowText: theme.midColor
palette.disabled.base: theme.baseColor
palette.disabled.text: theme.midColor
palette.disabled.button: theme.buttonColor
palette.disabled.buttonText: theme.midColor
palette.disabled.highlight: theme.highlightColor
palette.disabled.highlightedText: theme.highlightedTextColor
palette.disabled.mid: theme.midColor
palette.disabled.dark: theme.darkColor
palette.disabled.light: theme.lightColor
palette.disabled.shadow: theme.shadowColor
color: bgColor
color: theme.windowColor
// Currently selected document name (namespace directory).
property string currentDoc: ""
@ -40,14 +81,14 @@ ApplicationWindow {
}
header: ToolBar {
background: Rectangle { color: root.panelColor }
background: Rectangle { color: theme.buttonColor }
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
Label {
text: "orkmode"
color: root.textColor
color: theme.buttonTextColor
font.pixelSize: 16
font.bold: true
}
@ -56,7 +97,7 @@ ApplicationWindow {
text: orkClient.connected
? "● " + orkClient.address
: "○ disconnected"
color: orkClient.connected ? "#a3be8c" : "#bf616a"
color: orkClient.connected ? theme.doneColor : theme.deadlineColor
font.pixelSize: 12
}
ToolButton {
@ -76,7 +117,7 @@ ApplicationWindow {
Rectangle {
Layout.preferredWidth: 220
Layout.fillHeight: true
color: root.panelColor
color: theme.baseColor
ColumnLayout {
anchors.fill: parent
@ -84,7 +125,7 @@ ApplicationWindow {
Label {
text: qsTr("Documents")
color: root.dimColor
color: theme.placeholderTextColor
font.pixelSize: 11
font.bold: true
Layout.margins: 10
@ -102,21 +143,21 @@ ApplicationWindow {
highlighted: root.currentDoc === modelData.name
onClicked: root.selectDoc(modelData.name)
background: Rectangle {
color: highlighted ? root.accentColor : "transparent"
color: highlighted ? theme.highlightColor : "transparent"
opacity: highlighted ? 0.3 : 1.0
}
contentItem: ColumnLayout {
spacing: 1
Label {
text: modelData.title
color: root.textColor
color: theme.textColor
font.pixelSize: 13
elide: Text.ElideRight
Layout.fillWidth: true
}
Label {
text: modelData.name
color: root.dimColor
color: theme.placeholderTextColor
font.pixelSize: 10
elide: Text.ElideRight
Layout.fillWidth: true
@ -127,7 +168,7 @@ ApplicationWindow {
}
}
Rectangle { Layout.preferredWidth: 1; Layout.fillHeight: true; color: "#11141a" }
Rectangle { Layout.preferredWidth: 1; Layout.fillHeight: true; color: theme.darkColor }
// Outline view.
Item {
@ -146,7 +187,7 @@ ApplicationWindow {
text: orkClient.connected
? qsTr("Select a document")
: qsTr("Not connected: %1").arg(orkClient.error)
color: root.dimColor
color: theme.placeholderTextColor
font.pixelSize: 14
}
}

View File

@ -10,19 +10,27 @@ TreeView {
id: tree
clip: true
readonly property color textColor: "#d8dee9"
readonly property color dimColor: "#7a8290"
readonly property int rowPad: 6
// Palette for TODO keywords by state.
function keywordColor(kw) {
switch (kw) {
case "TODO": return "#bf616a"
case "NEXT": return "#d08770"
case "WAITING": return "#ebcb8b"
case "DONE": return "#a3be8c"
case "CANCELLED": return "#7a8290"
default: return "#88c0d0"
case "TODO": return theme.todoColor
case "NEXT": return theme.todoColor
case "WAITING": return theme.waitingColor
case "DONE": return theme.doneColor
case "CANCELLED": return theme.cancelledColor
default: return theme.linkColor
}
}
// Priority color by level.
function priorityColor(pri) {
switch (pri) {
case "A": return theme.priorityAColor
case "B": return theme.priorityBColor
case "C": return theme.priorityCColor
default: return theme.textColor
}
}
@ -43,7 +51,8 @@ TreeView {
implicitWidth: tree.width
background: Rectangle {
color: del.current ? "#2f3540" : "transparent"
color: del.current ? theme.highlightColor : "transparent"
opacity: del.current ? 0.2 : 1.0
radius: 4
}
@ -79,7 +88,7 @@ TreeView {
Label {
visible: (model.priority ?? "") !== ""
text: "[#" + (model.priority ?? "") + "]"
color: "#b48ead"
color: tree.priorityColor(model.priority ?? "")
font.pixelSize: 12
font.bold: true
}
@ -87,7 +96,7 @@ TreeView {
// Title.
Label {
text: (model.title ?? "")
color: (model.isDone ?? false) ? tree.dimColor : tree.textColor
color: (model.isDone ?? false) ? theme.placeholderTextColor : theme.textColor
font.pixelSize: tree.levelFontSize(model.level ?? 1)
font.strikeout: (model.isDone ?? false)
elide: Text.ElideRight
@ -98,13 +107,13 @@ TreeView {
Label {
visible: (model.scheduled ?? "") !== ""
text: "⏱ " + (model.scheduled ?? "")
color: "#81a1c1"
color: theme.timestampColor
font.pixelSize: 11
}
Label {
visible: (model.deadline ?? "") !== ""
text: "⚑ " + (model.deadline ?? "")
color: "#bf616a"
color: theme.deadlineColor
font.pixelSize: 11
}
@ -114,14 +123,14 @@ TreeView {
delegate: Rectangle {
required property string modelData
radius: 3
color: "#3b4252"
color: theme.alternateBaseColor
implicitWidth: tagLabel.implicitWidth + 8
implicitHeight: tagLabel.implicitHeight + 3
Label {
id: tagLabel
anchors.centerIn: parent
text: parent.modelData
color: "#8fbcbb"
color: theme.tagColor
font.pixelSize: 10
}
}
@ -140,7 +149,7 @@ TreeView {
width: 2
Layout.fillHeight: true
Layout.leftMargin: 2
color: "#3b4252"
color: theme.midColor
}
// Section body text.
@ -149,7 +158,7 @@ TreeView {
Layout.fillWidth: true
Layout.leftMargin: del.bodyIndent - 10
text: del.sectionBody
color: tree.dimColor
color: theme.placeholderTextColor
font.pixelSize: 12
lineHeight: 1.15
wrapMode: Text.WordWrap

View File

@ -6,6 +6,7 @@
// the GUI walks the server's namespace and renders it.
#include "documentmodel.h"
#include "orkclient.h"
#include "thememanager.h"
#include <QGuiApplication>
#include <QImage>
@ -78,9 +79,10 @@ int main(int argc, char *argv[]) {
}
}
// Expose the client and the document model to QML.
// Expose the client, document model, and theme manager to QML.
OrkClient client;
DocumentModel docModel;
ThemeManager theme;
docModel.setClient(&client);
const bool connected = client.connectTo(addr);
@ -119,6 +121,7 @@ int main(int argc, char *argv[]) {
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("orkClient", &client);
engine.rootContext()->setContextProperty("docModel", &docModel);
engine.rootContext()->setContextProperty("theme", &theme);
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,

355
ui/src/thememanager.cpp Normal file
View File

@ -0,0 +1,355 @@
#include "thememanager.h"
#include <QGuiApplication>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include <QDebug>
ThemeManager::ThemeManager(QObject *parent)
: QObject(parent)
, m_mode("system")
{
loadSettings();
applyPalette();
}
QString ThemeManager::mode() const
{
return m_mode;
}
void ThemeManager::setMode(const QString &mode)
{
if (mode == m_mode)
return;
m_mode = mode;
saveSettings();
applyPalette();
emit modeChanged();
emit paletteChanged();
}
QStringList ThemeManager::availableSchemes() const
{
QStringList schemes;
schemes << "system";
QStringList dirs = schemeDirs();
for (const QString &dirPath : dirs) {
QDir dir(dirPath);
const auto entries = dir.entryInfoList({"*.colors"}, QDir::Files, QDir::Name);
for (const auto &entry : entries) {
if (!schemes.contains(entry.baseName()))
schemes << entry.baseName();
}
}
return schemes;
}
bool ThemeManager::isDark() const
{
QColor bg = m_palette.color(QPalette::Window);
qreal luminance = 0.2126 * bg.redF() + 0.7152 * bg.greenF() + 0.0722 * bg.blueF();
return luminance < 0.5;
}
void ThemeManager::applyPalette()
{
QString path;
if (m_mode == "system") {
path = systemSchemePath();
} else {
path = schemePath(m_mode);
}
if (path.isEmpty() || !QFile::exists(path)) {
// Fallback to application default palette
m_palette = QGuiApplication::palette();
return;
}
m_palette = paletteFromScheme(path);
QGuiApplication::setPalette(m_palette);
}
// Palette color accessors
QColor ThemeManager::windowColor() const { return m_palette.color(QPalette::Active, QPalette::Window); }
QColor ThemeManager::windowTextColor() const { return m_palette.color(QPalette::Active, QPalette::WindowText); }
QColor ThemeManager::baseColor() const { return m_palette.color(QPalette::Active, QPalette::Base); }
QColor ThemeManager::alternateBaseColor() const { return m_palette.color(QPalette::Active, QPalette::AlternateBase); }
QColor ThemeManager::textColor() const { return m_palette.color(QPalette::Active, QPalette::Text); }
QColor ThemeManager::buttonColor() const { return m_palette.color(QPalette::Active, QPalette::Button); }
QColor ThemeManager::buttonTextColor() const { return m_palette.color(QPalette::Active, QPalette::ButtonText); }
QColor ThemeManager::highlightColor() const { return m_palette.color(QPalette::Active, QPalette::Highlight); }
QColor ThemeManager::highlightedTextColor() const { return m_palette.color(QPalette::Active, QPalette::HighlightedText); }
QColor ThemeManager::tooltipBaseColor() const { return m_palette.color(QPalette::Active, QPalette::ToolTipBase); }
QColor ThemeManager::tooltipTextColor() const { return m_palette.color(QPalette::Active, QPalette::ToolTipText); }
QColor ThemeManager::linkColor() const { return m_palette.color(QPalette::Active, QPalette::Link); }
QColor ThemeManager::midColor() const { return m_palette.color(QPalette::Active, QPalette::Mid); }
QColor ThemeManager::darkColor() const { return m_palette.color(QPalette::Active, QPalette::Dark); }
QColor ThemeManager::lightColor() const { return m_palette.color(QPalette::Active, QPalette::Light); }
QColor ThemeManager::shadowColor() const { return m_palette.color(QPalette::Active, QPalette::Shadow); }
QColor ThemeManager::placeholderTextColor() const { return m_palette.color(QPalette::Active, QPalette::PlaceholderText); }
// Org-mode semantic colors — adjust brightness based on dark/light theme
QColor ThemeManager::todoColor() const
{
// Orange/amber for active TODO
return isDark() ? QColor("#ebcb8b") : QColor("#bf8b00");
}
QColor ThemeManager::doneColor() const
{
// Green for completed
return isDark() ? QColor("#a3be8c") : QColor("#4c8c4a");
}
QColor ThemeManager::waitingColor() const
{
// Cyan/teal for waiting/blocked
return isDark() ? QColor("#88c0d0") : QColor("#2e7d8a");
}
QColor ThemeManager::cancelledColor() const
{
// Dimmed/grey for cancelled
return midColor();
}
QColor ThemeManager::priorityAColor() const
{
// Red for high priority
return isDark() ? QColor("#bf616a") : QColor("#c0392b");
}
QColor ThemeManager::priorityBColor() const
{
// Yellow/orange for medium priority
return isDark() ? QColor("#ebcb8b") : QColor("#d68910");
}
QColor ThemeManager::priorityCColor() const
{
// Blue for low priority
return isDark() ? QColor("#81a1c1") : QColor("#2874a6");
}
QColor ThemeManager::tagColor() const
{
// Teal/cyan for tags
return isDark() ? QColor("#8fbcbb") : QColor("#117a65");
}
QColor ThemeManager::timestampColor() const
{
// Blue for scheduled timestamps
return isDark() ? QColor("#81a1c1") : QColor("#2874a6");
}
QColor ThemeManager::deadlineColor() const
{
// Red for deadlines
return isDark() ? QColor("#bf616a") : QColor("#c0392b");
}
QPalette ThemeManager::paletteFromScheme(const QString &path) const
{
QSettings scheme(path, QSettings::IniFormat);
QPalette pal;
// Window colors
scheme.beginGroup("Colors:Window");
QColor windowBg = parseColor(scheme.value("BackgroundNormal"));
QColor windowFg = parseColor(scheme.value("ForegroundNormal"));
QColor windowInactive = parseColor(scheme.value("ForegroundInactive"));
scheme.endGroup();
// View colors
scheme.beginGroup("Colors:View");
QColor viewBg = parseColor(scheme.value("BackgroundNormal"));
QColor viewAltBg = parseColor(scheme.value("BackgroundAlternate"));
QColor viewFg = parseColor(scheme.value("ForegroundNormal"));
QColor viewLink = parseColor(scheme.value("ForegroundLink"));
QColor viewVisited = parseColor(scheme.value("ForegroundVisited"));
QColor viewInactive = parseColor(scheme.value("ForegroundInactive"));
scheme.endGroup();
// Button colors
scheme.beginGroup("Colors:Button");
QColor buttonBg = parseColor(scheme.value("BackgroundNormal"));
QColor buttonFg = parseColor(scheme.value("ForegroundNormal"));
scheme.endGroup();
// Selection colors
scheme.beginGroup("Colors:Selection");
QColor selBg = parseColor(scheme.value("BackgroundNormal"));
QColor selFg = parseColor(scheme.value("ForegroundNormal"));
scheme.endGroup();
// Tooltip colors
scheme.beginGroup("Colors:Tooltip");
QColor tipBg = parseColor(scheme.value("BackgroundNormal"));
QColor tipFg = parseColor(scheme.value("ForegroundNormal"));
scheme.endGroup();
// Computed colors
QColor mid = QColor(
(windowBg.red() + windowFg.red()) / 2,
(windowBg.green() + windowFg.green()) / 2,
(windowBg.blue() + windowFg.blue()) / 2
);
QColor dark = windowBg.darker(150);
QColor light = windowBg.lighter(150);
QColor shadow = windowBg.darker(200);
QColor midlight = windowBg.lighter(125);
// Apply Active group
pal.setColor(QPalette::Active, QPalette::Window, windowBg);
pal.setColor(QPalette::Active, QPalette::WindowText, windowFg);
pal.setColor(QPalette::Active, QPalette::Base, viewBg);
pal.setColor(QPalette::Active, QPalette::AlternateBase, viewAltBg);
pal.setColor(QPalette::Active, QPalette::Text, viewFg);
pal.setColor(QPalette::Active, QPalette::Link, viewLink);
pal.setColor(QPalette::Active, QPalette::LinkVisited, viewVisited);
pal.setColor(QPalette::Active, QPalette::Button, buttonBg);
pal.setColor(QPalette::Active, QPalette::ButtonText, buttonFg);
pal.setColor(QPalette::Active, QPalette::Highlight, selBg);
pal.setColor(QPalette::Active, QPalette::HighlightedText, selFg);
pal.setColor(QPalette::Active, QPalette::ToolTipBase, tipBg);
pal.setColor(QPalette::Active, QPalette::ToolTipText, tipFg);
pal.setColor(QPalette::Active, QPalette::Mid, mid);
pal.setColor(QPalette::Active, QPalette::Dark, dark);
pal.setColor(QPalette::Active, QPalette::Light, light);
pal.setColor(QPalette::Active, QPalette::Shadow, shadow);
pal.setColor(QPalette::Active, QPalette::Midlight, midlight);
pal.setColor(QPalette::Active, QPalette::PlaceholderText, viewInactive);
pal.setColor(QPalette::Active, QPalette::BrightText, selFg);
// Inactive group
pal.setColor(QPalette::Inactive, QPalette::Window, windowBg);
pal.setColor(QPalette::Inactive, QPalette::WindowText, windowInactive);
pal.setColor(QPalette::Inactive, QPalette::Base, viewBg);
pal.setColor(QPalette::Inactive, QPalette::AlternateBase, viewAltBg);
pal.setColor(QPalette::Inactive, QPalette::Text, viewFg);
pal.setColor(QPalette::Inactive, QPalette::Link, viewLink);
pal.setColor(QPalette::Inactive, QPalette::LinkVisited, viewVisited);
pal.setColor(QPalette::Inactive, QPalette::Button, buttonBg);
pal.setColor(QPalette::Inactive, QPalette::ButtonText, buttonFg);
pal.setColor(QPalette::Inactive, QPalette::Highlight, selBg);
pal.setColor(QPalette::Inactive, QPalette::HighlightedText, selFg);
pal.setColor(QPalette::Inactive, QPalette::ToolTipBase, tipBg);
pal.setColor(QPalette::Inactive, QPalette::ToolTipText, tipFg);
pal.setColor(QPalette::Inactive, QPalette::Mid, mid);
pal.setColor(QPalette::Inactive, QPalette::Dark, dark);
pal.setColor(QPalette::Inactive, QPalette::Light, light);
pal.setColor(QPalette::Inactive, QPalette::Shadow, shadow);
pal.setColor(QPalette::Inactive, QPalette::Midlight, midlight);
pal.setColor(QPalette::Inactive, QPalette::PlaceholderText, viewInactive);
pal.setColor(QPalette::Inactive, QPalette::BrightText, selFg);
// Disabled group
QColor disabledFg = windowInactive;
pal.setColor(QPalette::Disabled, QPalette::Window, windowBg);
pal.setColor(QPalette::Disabled, QPalette::WindowText, disabledFg);
pal.setColor(QPalette::Disabled, QPalette::Base, viewBg);
pal.setColor(QPalette::Disabled, QPalette::AlternateBase, viewAltBg);
pal.setColor(QPalette::Disabled, QPalette::Text, disabledFg);
pal.setColor(QPalette::Disabled, QPalette::Link, viewLink);
pal.setColor(QPalette::Disabled, QPalette::LinkVisited, viewVisited);
pal.setColor(QPalette::Disabled, QPalette::Button, buttonBg);
pal.setColor(QPalette::Disabled, QPalette::ButtonText, disabledFg);
pal.setColor(QPalette::Disabled, QPalette::Highlight, selBg);
pal.setColor(QPalette::Disabled, QPalette::HighlightedText, selFg);
pal.setColor(QPalette::Disabled, QPalette::ToolTipBase, tipBg);
pal.setColor(QPalette::Disabled, QPalette::ToolTipText, tipFg);
pal.setColor(QPalette::Disabled, QPalette::Mid, mid);
pal.setColor(QPalette::Disabled, QPalette::Dark, dark);
pal.setColor(QPalette::Disabled, QPalette::Light, light);
pal.setColor(QPalette::Disabled, QPalette::Shadow, shadow);
pal.setColor(QPalette::Disabled, QPalette::Midlight, midlight);
pal.setColor(QPalette::Disabled, QPalette::PlaceholderText, disabledFg);
pal.setColor(QPalette::Disabled, QPalette::BrightText, selFg);
return pal;
}
QColor ThemeManager::parseColor(const QVariant &value) const
{
QStringList parts;
if (value.typeId() == QMetaType::QStringList) {
parts = value.toStringList();
} else {
parts = value.toString().split(',');
}
if (parts.size() >= 3) {
return QColor(parts[0].trimmed().toInt(),
parts[1].trimmed().toInt(),
parts[2].trimmed().toInt());
}
return QColor();
}
QString ThemeManager::systemSchemePath() const
{
// Try to read from kdeglobals
QString kdeglobals = QDir::homePath() + "/.config/kdeglobals";
if (QFile::exists(kdeglobals)) {
QSettings settings(kdeglobals, QSettings::IniFormat);
settings.beginGroup("General");
QString schemeName = settings.value("ColorScheme").toString();
settings.endGroup();
if (!schemeName.isEmpty()) {
QString path = schemePath(schemeName);
if (QFile::exists(path))
return path;
}
}
// Fallback to BreezeLight
return schemePath("BreezeLight");
}
QString ThemeManager::schemePath(const QString &name) const
{
for (const QString &dir : schemeDirs()) {
QString path = QString("%1/%2.colors").arg(dir, name);
if (QFile::exists(path))
return path;
}
return QString("%1/%2.colors").arg(SCHEMES_DIR, name);
}
QStringList ThemeManager::schemeDirs() const
{
QStringList dirs;
// User dir first
QString userDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
+ "/color-schemes";
if (QDir(userDir).exists())
dirs << userDir;
dirs << SCHEMES_DIR;
return dirs;
}
void ThemeManager::loadSettings()
{
QString configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
QString configFile = configDir + "/theme.conf";
if (QFile::exists(configFile)) {
QSettings settings(configFile, QSettings::IniFormat);
m_mode = settings.value("General/mode", "system").toString();
}
}
void ThemeManager::saveSettings()
{
QString configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
QDir().mkpath(configDir);
QString configFile = configDir + "/theme.conf";
QSettings settings(configFile, QSettings::IniFormat);
settings.setValue("General/mode", m_mode);
}

114
ui/src/thememanager.h Normal file
View File

@ -0,0 +1,114 @@
// ThemeManager — KDE color scheme integration for QML.
//
// Reads .colors files from /usr/share/color-schemes and ~/.local/share/color-schemes.
// Exposes palette colors as Q_PROPERTYs for QML binding. Supports:
// - "system" mode: follows KDE's kdeglobals ColorScheme setting
// - explicit scheme name: forces a specific scheme (e.g., "BreezeDark")
//
// The QML layer applies theme.* colors to the window palette and custom styling.
#pragma once
#include <QObject>
#include <QPalette>
#include <QColor>
#include <QSettings>
#include <QStringList>
class ThemeManager : public QObject
{
Q_OBJECT
// Mode: "system" or a scheme name (e.g., "BreezeLight", "BreezeDark")
Q_PROPERTY(QString mode READ mode WRITE setMode NOTIFY modeChanged)
Q_PROPERTY(QStringList availableSchemes READ availableSchemes CONSTANT)
Q_PROPERTY(bool isDark READ isDark NOTIFY paletteChanged)
// Palette colors for QML
Q_PROPERTY(QColor windowColor READ windowColor NOTIFY paletteChanged)
Q_PROPERTY(QColor windowTextColor READ windowTextColor NOTIFY paletteChanged)
Q_PROPERTY(QColor baseColor READ baseColor NOTIFY paletteChanged)
Q_PROPERTY(QColor alternateBaseColor READ alternateBaseColor NOTIFY paletteChanged)
Q_PROPERTY(QColor textColor READ textColor NOTIFY paletteChanged)
Q_PROPERTY(QColor buttonColor READ buttonColor NOTIFY paletteChanged)
Q_PROPERTY(QColor buttonTextColor READ buttonTextColor NOTIFY paletteChanged)
Q_PROPERTY(QColor highlightColor READ highlightColor NOTIFY paletteChanged)
Q_PROPERTY(QColor highlightedTextColor READ highlightedTextColor NOTIFY paletteChanged)
Q_PROPERTY(QColor tooltipBaseColor READ tooltipBaseColor NOTIFY paletteChanged)
Q_PROPERTY(QColor tooltipTextColor READ tooltipTextColor NOTIFY paletteChanged)
Q_PROPERTY(QColor linkColor READ linkColor NOTIFY paletteChanged)
Q_PROPERTY(QColor midColor READ midColor NOTIFY paletteChanged)
Q_PROPERTY(QColor darkColor READ darkColor NOTIFY paletteChanged)
Q_PROPERTY(QColor lightColor READ lightColor NOTIFY paletteChanged)
Q_PROPERTY(QColor shadowColor READ shadowColor NOTIFY paletteChanged)
Q_PROPERTY(QColor placeholderTextColor READ placeholderTextColor NOTIFY paletteChanged)
// Org-mode semantic colors (derived from palette or hardcoded for readability)
Q_PROPERTY(QColor todoColor READ todoColor NOTIFY paletteChanged)
Q_PROPERTY(QColor doneColor READ doneColor NOTIFY paletteChanged)
Q_PROPERTY(QColor waitingColor READ waitingColor NOTIFY paletteChanged)
Q_PROPERTY(QColor cancelledColor READ cancelledColor NOTIFY paletteChanged)
Q_PROPERTY(QColor priorityAColor READ priorityAColor NOTIFY paletteChanged)
Q_PROPERTY(QColor priorityBColor READ priorityBColor NOTIFY paletteChanged)
Q_PROPERTY(QColor priorityCColor READ priorityCColor NOTIFY paletteChanged)
Q_PROPERTY(QColor tagColor READ tagColor NOTIFY paletteChanged)
Q_PROPERTY(QColor timestampColor READ timestampColor NOTIFY paletteChanged)
Q_PROPERTY(QColor deadlineColor READ deadlineColor NOTIFY paletteChanged)
public:
explicit ThemeManager(QObject *parent = nullptr);
QString mode() const;
void setMode(const QString &mode);
QStringList availableSchemes() const;
bool isDark() const;
// Palette colors
QColor windowColor() const;
QColor windowTextColor() const;
QColor baseColor() const;
QColor alternateBaseColor() const;
QColor textColor() const;
QColor buttonColor() const;
QColor buttonTextColor() const;
QColor highlightColor() const;
QColor highlightedTextColor() const;
QColor tooltipBaseColor() const;
QColor tooltipTextColor() const;
QColor linkColor() const;
QColor midColor() const;
QColor darkColor() const;
QColor lightColor() const;
QColor shadowColor() const;
QColor placeholderTextColor() const;
// Org-mode semantic colors
QColor todoColor() const;
QColor doneColor() const;
QColor waitingColor() const;
QColor cancelledColor() const;
QColor priorityAColor() const;
QColor priorityBColor() const;
QColor priorityCColor() const;
QColor tagColor() const;
QColor timestampColor() const;
QColor deadlineColor() const;
void applyPalette();
signals:
void modeChanged();
void paletteChanged();
private:
QPalette paletteFromScheme(const QString &path) const;
QColor parseColor(const QVariant &value) const;
QString systemSchemePath() const;
QString schemePath(const QString &name) const;
QStringList schemeDirs() const;
void loadSettings();
void saveSettings();
QString m_mode;
QPalette m_palette;
static constexpr const char *SCHEMES_DIR = "/usr/share/color-schemes";
};