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:
parent
bc2130d5b1
commit
64c7272f70
|
|
@ -18,6 +18,8 @@ qt_add_executable(orkmode
|
||||||
src/orkclient.h
|
src/orkclient.h
|
||||||
src/documentmodel.cpp
|
src/documentmodel.cpp
|
||||||
src/documentmodel.h
|
src/documentmodel.h
|
||||||
|
src/thememanager.cpp
|
||||||
|
src/thememanager.h
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_qml_module(orkmode
|
qt_add_qml_module(orkmode
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,55 @@ ApplicationWindow {
|
||||||
visible: true
|
visible: true
|
||||||
title: qsTr("orkmode")
|
title: qsTr("orkmode")
|
||||||
|
|
||||||
// orkmode palette — muted, org-like.
|
// Apply theme palette to Qt Quick Controls
|
||||||
readonly property color bgColor: "#1e2127"
|
palette.active.window: theme.windowColor
|
||||||
readonly property color panelColor: "#252932"
|
palette.active.windowText: theme.windowTextColor
|
||||||
readonly property color textColor: "#d8dee9"
|
palette.active.base: theme.baseColor
|
||||||
readonly property color dimColor: "#7a8290"
|
palette.active.text: theme.textColor
|
||||||
readonly property color accentColor: "#5e81ac"
|
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).
|
// Currently selected document name (namespace directory).
|
||||||
property string currentDoc: ""
|
property string currentDoc: ""
|
||||||
|
|
@ -40,14 +81,14 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
header: ToolBar {
|
header: ToolBar {
|
||||||
background: Rectangle { color: root.panelColor }
|
background: Rectangle { color: theme.buttonColor }
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: 12
|
anchors.leftMargin: 12
|
||||||
anchors.rightMargin: 12
|
anchors.rightMargin: 12
|
||||||
Label {
|
Label {
|
||||||
text: "orkmode"
|
text: "orkmode"
|
||||||
color: root.textColor
|
color: theme.buttonTextColor
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +97,7 @@ ApplicationWindow {
|
||||||
text: orkClient.connected
|
text: orkClient.connected
|
||||||
? "● " + orkClient.address
|
? "● " + orkClient.address
|
||||||
: "○ disconnected"
|
: "○ disconnected"
|
||||||
color: orkClient.connected ? "#a3be8c" : "#bf616a"
|
color: orkClient.connected ? theme.doneColor : theme.deadlineColor
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
}
|
}
|
||||||
ToolButton {
|
ToolButton {
|
||||||
|
|
@ -76,7 +117,7 @@ ApplicationWindow {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.preferredWidth: 220
|
Layout.preferredWidth: 220
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
color: root.panelColor
|
color: theme.baseColor
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
@ -84,7 +125,7 @@ ApplicationWindow {
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: qsTr("Documents")
|
text: qsTr("Documents")
|
||||||
color: root.dimColor
|
color: theme.placeholderTextColor
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
font.bold: true
|
font.bold: true
|
||||||
Layout.margins: 10
|
Layout.margins: 10
|
||||||
|
|
@ -102,21 +143,21 @@ ApplicationWindow {
|
||||||
highlighted: root.currentDoc === modelData.name
|
highlighted: root.currentDoc === modelData.name
|
||||||
onClicked: root.selectDoc(modelData.name)
|
onClicked: root.selectDoc(modelData.name)
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: highlighted ? root.accentColor : "transparent"
|
color: highlighted ? theme.highlightColor : "transparent"
|
||||||
opacity: highlighted ? 0.3 : 1.0
|
opacity: highlighted ? 0.3 : 1.0
|
||||||
}
|
}
|
||||||
contentItem: ColumnLayout {
|
contentItem: ColumnLayout {
|
||||||
spacing: 1
|
spacing: 1
|
||||||
Label {
|
Label {
|
||||||
text: modelData.title
|
text: modelData.title
|
||||||
color: root.textColor
|
color: theme.textColor
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: modelData.name
|
text: modelData.name
|
||||||
color: root.dimColor
|
color: theme.placeholderTextColor
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
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.
|
// Outline view.
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -146,7 +187,7 @@ ApplicationWindow {
|
||||||
text: orkClient.connected
|
text: orkClient.connected
|
||||||
? qsTr("Select a document")
|
? qsTr("Select a document")
|
||||||
: qsTr("Not connected: %1").arg(orkClient.error)
|
: qsTr("Not connected: %1").arg(orkClient.error)
|
||||||
color: root.dimColor
|
color: theme.placeholderTextColor
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,27 @@ TreeView {
|
||||||
id: tree
|
id: tree
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
readonly property color textColor: "#d8dee9"
|
|
||||||
readonly property color dimColor: "#7a8290"
|
|
||||||
readonly property int rowPad: 6
|
readonly property int rowPad: 6
|
||||||
|
|
||||||
// Palette for TODO keywords by state.
|
// Palette for TODO keywords by state.
|
||||||
function keywordColor(kw) {
|
function keywordColor(kw) {
|
||||||
switch (kw) {
|
switch (kw) {
|
||||||
case "TODO": return "#bf616a"
|
case "TODO": return theme.todoColor
|
||||||
case "NEXT": return "#d08770"
|
case "NEXT": return theme.todoColor
|
||||||
case "WAITING": return "#ebcb8b"
|
case "WAITING": return theme.waitingColor
|
||||||
case "DONE": return "#a3be8c"
|
case "DONE": return theme.doneColor
|
||||||
case "CANCELLED": return "#7a8290"
|
case "CANCELLED": return theme.cancelledColor
|
||||||
default: return "#88c0d0"
|
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
|
implicitWidth: tree.width
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: del.current ? "#2f3540" : "transparent"
|
color: del.current ? theme.highlightColor : "transparent"
|
||||||
|
opacity: del.current ? 0.2 : 1.0
|
||||||
radius: 4
|
radius: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +88,7 @@ TreeView {
|
||||||
Label {
|
Label {
|
||||||
visible: (model.priority ?? "") !== ""
|
visible: (model.priority ?? "") !== ""
|
||||||
text: "[#" + (model.priority ?? "") + "]"
|
text: "[#" + (model.priority ?? "") + "]"
|
||||||
color: "#b48ead"
|
color: tree.priorityColor(model.priority ?? "")
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +96,7 @@ TreeView {
|
||||||
// Title.
|
// Title.
|
||||||
Label {
|
Label {
|
||||||
text: (model.title ?? "")
|
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.pixelSize: tree.levelFontSize(model.level ?? 1)
|
||||||
font.strikeout: (model.isDone ?? false)
|
font.strikeout: (model.isDone ?? false)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
|
|
@ -98,13 +107,13 @@ TreeView {
|
||||||
Label {
|
Label {
|
||||||
visible: (model.scheduled ?? "") !== ""
|
visible: (model.scheduled ?? "") !== ""
|
||||||
text: "⏱ " + (model.scheduled ?? "")
|
text: "⏱ " + (model.scheduled ?? "")
|
||||||
color: "#81a1c1"
|
color: theme.timestampColor
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
visible: (model.deadline ?? "") !== ""
|
visible: (model.deadline ?? "") !== ""
|
||||||
text: "⚑ " + (model.deadline ?? "")
|
text: "⚑ " + (model.deadline ?? "")
|
||||||
color: "#bf616a"
|
color: theme.deadlineColor
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,14 +123,14 @@ TreeView {
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
required property string modelData
|
required property string modelData
|
||||||
radius: 3
|
radius: 3
|
||||||
color: "#3b4252"
|
color: theme.alternateBaseColor
|
||||||
implicitWidth: tagLabel.implicitWidth + 8
|
implicitWidth: tagLabel.implicitWidth + 8
|
||||||
implicitHeight: tagLabel.implicitHeight + 3
|
implicitHeight: tagLabel.implicitHeight + 3
|
||||||
Label {
|
Label {
|
||||||
id: tagLabel
|
id: tagLabel
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: parent.modelData
|
text: parent.modelData
|
||||||
color: "#8fbcbb"
|
color: theme.tagColor
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +149,7 @@ TreeView {
|
||||||
width: 2
|
width: 2
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.leftMargin: 2
|
Layout.leftMargin: 2
|
||||||
color: "#3b4252"
|
color: theme.midColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section body text.
|
// Section body text.
|
||||||
|
|
@ -149,7 +158,7 @@ TreeView {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: del.bodyIndent - 10
|
Layout.leftMargin: del.bodyIndent - 10
|
||||||
text: del.sectionBody
|
text: del.sectionBody
|
||||||
color: tree.dimColor
|
color: theme.placeholderTextColor
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
lineHeight: 1.15
|
lineHeight: 1.15
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
// the GUI walks the server's namespace and renders it.
|
// the GUI walks the server's namespace and renders it.
|
||||||
#include "documentmodel.h"
|
#include "documentmodel.h"
|
||||||
#include "orkclient.h"
|
#include "orkclient.h"
|
||||||
|
#include "thememanager.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QImage>
|
#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;
|
OrkClient client;
|
||||||
DocumentModel docModel;
|
DocumentModel docModel;
|
||||||
|
ThemeManager theme;
|
||||||
docModel.setClient(&client);
|
docModel.setClient(&client);
|
||||||
|
|
||||||
const bool connected = client.connectTo(addr);
|
const bool connected = client.connectTo(addr);
|
||||||
|
|
@ -119,6 +121,7 @@ int main(int argc, char *argv[]) {
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
engine.rootContext()->setContextProperty("orkClient", &client);
|
engine.rootContext()->setContextProperty("orkClient", &client);
|
||||||
engine.rootContext()->setContextProperty("docModel", &docModel);
|
engine.rootContext()->setContextProperty("docModel", &docModel);
|
||||||
|
engine.rootContext()->setContextProperty("theme", &theme);
|
||||||
|
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
|
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue