From 64c7272f701f9eb291b5118e1a1e902d1b8b0018 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 1 Oct 2026 09:08:15 +0200 Subject: [PATCH] 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 --- ui/CMakeLists.txt | 2 + ui/qml/Main.qml | 75 +++++++-- ui/qml/OutlineView.qml | 43 +++-- ui/src/main.cpp | 5 +- ui/src/thememanager.cpp | 355 ++++++++++++++++++++++++++++++++++++++++ ui/src/thememanager.h | 114 +++++++++++++ 6 files changed, 559 insertions(+), 35 deletions(-) create mode 100644 ui/src/thememanager.cpp create mode 100644 ui/src/thememanager.h diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index b7e9ace..9f1e140 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -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 diff --git a/ui/qml/Main.qml b/ui/qml/Main.qml index 46eaa78..2f4061a 100644 --- a/ui/qml/Main.qml +++ b/ui/qml/Main.qml @@ -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 } } diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 08d7050..62d6d68 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -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 diff --git a/ui/src/main.cpp b/ui/src/main.cpp index 0b81476..5e1cde0 100644 --- a/ui/src/main.cpp +++ b/ui/src/main.cpp @@ -6,6 +6,7 @@ // the GUI walks the server's namespace and renders it. #include "documentmodel.h" #include "orkclient.h" +#include "thememanager.h" #include #include @@ -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, diff --git a/ui/src/thememanager.cpp b/ui/src/thememanager.cpp new file mode 100644 index 0000000..5e987c2 --- /dev/null +++ b/ui/src/thememanager.cpp @@ -0,0 +1,355 @@ +#include "thememanager.h" + +#include +#include +#include +#include +#include + +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); +} diff --git a/ui/src/thememanager.h b/ui/src/thememanager.h new file mode 100644 index 0000000..7497851 --- /dev/null +++ b/ui/src/thememanager.h @@ -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 +#include +#include +#include +#include + +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"; +};