Add font customization and UI scaling

ThemeManager now supports:
  - fontFamily, fontSize, fontStyle with full QFontDatabase access
  - uiScale (0.5x to 4.0x) saved to theme.conf
  - resetFont() to return to system defaults

SettingsDialog provides UI for:
  - Color scheme selection (all installed KDE schemes)
  - Font family/style/size with live preview
  - UI scale slider with percentage display

OutlineView uses pointSize (scalable) instead of pixelSize (fixed).
All font sizes derived from theme.fontSize or baseFontSize.
This commit is contained in:
Levi Neely 2026-10-01 09:11:45 +02:00
parent 64c7272f70
commit 5677be6236
6 changed files with 387 additions and 8 deletions

View File

@ -28,6 +28,7 @@ qt_add_qml_module(orkmode
QML_FILES
qml/Main.qml
qml/OutlineView.qml
qml/SettingsDialog.qml
)
target_link_libraries(orkmode PRIVATE

View File

@ -9,6 +9,10 @@ ApplicationWindow {
visible: true
title: qsTr("orkmode")
// Apply theme font globally
font.family: theme.fontFamily || Qt.application.font.family
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
// Apply theme palette to Qt Quick Controls
palette.active.window: theme.windowColor
palette.active.windowText: theme.windowTextColor
@ -80,6 +84,11 @@ ApplicationWindow {
function onConnectedChanged() { root.refreshDocs() }
}
// Settings dialog
SettingsDialog {
id: settingsDialog
}
header: ToolBar {
background: Rectangle { color: theme.buttonColor }
RowLayout {
@ -106,6 +115,12 @@ ApplicationWindow {
ToolTip.text: qsTr("Reload")
ToolTip.visible: hovered
}
ToolButton {
text: "⚙"
onClicked: settingsDialog.open()
ToolTip.text: qsTr("Settings")
ToolTip.visible: hovered
}
}
}

View File

@ -11,6 +11,7 @@ TreeView {
clip: true
readonly property int rowPad: 6
readonly property int baseFontSize: theme.fontSize > 0 ? theme.fontSize : 12
// Palette for TODO keywords by state.
function keywordColor(kw) {
@ -36,7 +37,7 @@ TreeView {
// Headline text scales down slightly with depth.
function levelFontSize(level) {
return Math.max(13, 19 - level)
return Math.max(tree.baseFontSize - 1, tree.baseFontSize + 5 - level)
}
selectionModel: ItemSelectionModel {}
@ -79,7 +80,7 @@ TreeView {
anchors.centerIn: parent
text: model.keyword ?? ""
color: tree.keywordColor(model.keyword ?? "")
font.pixelSize: 11
font.pointSize: tree.baseFontSize - 1
font.bold: true
}
}
@ -89,7 +90,7 @@ TreeView {
visible: (model.priority ?? "") !== ""
text: "[#" + (model.priority ?? "") + "]"
color: tree.priorityColor(model.priority ?? "")
font.pixelSize: 12
font.pointSize: tree.baseFontSize
font.bold: true
}
@ -97,7 +98,7 @@ TreeView {
Label {
text: (model.title ?? "")
color: (model.isDone ?? false) ? theme.placeholderTextColor : theme.textColor
font.pixelSize: tree.levelFontSize(model.level ?? 1)
font.pointSize: tree.levelFontSize(model.level ?? 1)
font.strikeout: (model.isDone ?? false)
elide: Text.ElideRight
Layout.fillWidth: true
@ -108,13 +109,13 @@ TreeView {
visible: (model.scheduled ?? "") !== ""
text: "⏱ " + (model.scheduled ?? "")
color: theme.timestampColor
font.pixelSize: 11
font.pointSize: tree.baseFontSize - 1
}
Label {
visible: (model.deadline ?? "") !== ""
text: "⚑ " + (model.deadline ?? "")
color: theme.deadlineColor
font.pixelSize: 11
font.pointSize: tree.baseFontSize - 1
}
// Tags.
@ -131,7 +132,7 @@ TreeView {
anchors.centerIn: parent
text: parent.modelData
color: theme.tagColor
font.pixelSize: 10
font.pointSize: tree.baseFontSize - 2
}
}
}
@ -159,7 +160,7 @@ TreeView {
Layout.leftMargin: del.bodyIndent - 10
text: del.sectionBody
color: theme.placeholderTextColor
font.pixelSize: 12
font.pointSize: tree.baseFontSize
lineHeight: 1.15
wrapMode: Text.WordWrap
textFormat: Text.PlainText

206
ui/qml/SettingsDialog.qml Normal file
View File

@ -0,0 +1,206 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Dialog {
id: settingsDialog
title: qsTr("Settings")
modal: true
standardButtons: Dialog.Close
width: 450
height: 400
x: (parent.width - width) / 2
y: (parent.height - height) / 2
background: Rectangle {
color: theme.windowColor
border.color: theme.midColor
border.width: 1
radius: 6
}
header: Rectangle {
implicitHeight: 40
color: theme.buttonColor
radius: 6
Label {
anchors.centerIn: parent
text: settingsDialog.title
color: theme.buttonTextColor
font.bold: true
font.pointSize: theme.fontSize > 0 ? theme.fontSize + 1 : 13
}
// Square off bottom corners
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 6
color: theme.buttonColor
}
}
contentItem: ColumnLayout {
spacing: 16
// --- Color Scheme ---
GroupBox {
title: qsTr("Color Scheme")
Layout.fillWidth: true
background: Rectangle {
color: "transparent"
border.color: theme.midColor
border.width: 1
radius: 4
y: 12
}
label: Label {
x: 8
text: parent.title
color: theme.textColor
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
}
RowLayout {
anchors.fill: parent
spacing: 8
ComboBox {
id: schemeCombo
Layout.fillWidth: true
model: theme.availableSchemes
currentIndex: {
let idx = model.indexOf(theme.mode)
return idx >= 0 ? idx : 0
}
onActivated: theme.mode = currentText
}
}
}
// --- Font ---
GroupBox {
title: qsTr("Font")
Layout.fillWidth: true
background: Rectangle {
color: "transparent"
border.color: theme.midColor
border.width: 1
radius: 4
y: 12
}
label: Label {
x: 8
text: parent.title
color: theme.textColor
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
}
GridLayout {
columns: 2
anchors.fill: parent
columnSpacing: 12
rowSpacing: 8
Label { text: qsTr("Family:"); color: theme.textColor }
ComboBox {
id: fontFamilyCombo
Layout.fillWidth: true
model: theme.availableFamilies
editable: true
currentIndex: {
let fam = theme.fontFamily || Qt.application.font.family
let idx = model.indexOf(fam)
return idx >= 0 ? idx : 0
}
onActivated: theme.fontFamily = currentText
onAccepted: theme.fontFamily = editText
}
Label { text: qsTr("Style:"); color: theme.textColor }
ComboBox {
id: fontStyleCombo
Layout.fillWidth: true
model: theme.availableStyles
currentIndex: {
let st = theme.fontStyle
let idx = model.indexOf(st)
return idx >= 0 ? idx : 0
}
onActivated: theme.fontStyle = currentText
}
Label { text: qsTr("Size:"); color: theme.textColor }
SpinBox {
id: fontSizeSpin
from: 6
to: 72
value: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
onValueModified: theme.fontSize = value
}
}
}
// --- UI Scale ---
GroupBox {
title: qsTr("UI Scale")
Layout.fillWidth: true
background: Rectangle {
color: "transparent"
border.color: theme.midColor
border.width: 1
radius: 4
y: 12
}
label: Label {
x: 8
text: parent.title
color: theme.textColor
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
}
RowLayout {
anchors.fill: parent
spacing: 12
Slider {
id: scaleSlider
Layout.fillWidth: true
from: 0.5
to: 3.0
stepSize: 0.1
value: theme.uiScale
onMoved: theme.uiScale = value
}
Label {
text: (theme.uiScale * 100).toFixed(0) + "%"
color: theme.textColor
Layout.preferredWidth: 50
}
}
}
// --- Reset buttons ---
RowLayout {
Layout.fillWidth: true
spacing: 8
Button {
text: qsTr("Reset Font")
onClicked: theme.resetFont()
}
Item { Layout.fillWidth: true }
}
}
}

View File

@ -4,14 +4,19 @@
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include <QFontDatabase>
#include <QScreen>
#include <QDebug>
ThemeManager::ThemeManager(QObject *parent)
: QObject(parent)
, m_mode("system")
, m_fontSize(0)
, m_uiScale(1.0)
{
loadSettings();
applyPalette();
applyFont();
}
QString ThemeManager::mode() const
@ -341,6 +346,10 @@ void ThemeManager::loadSettings()
if (QFile::exists(configFile)) {
QSettings settings(configFile, QSettings::IniFormat);
m_mode = settings.value("General/mode", "system").toString();
m_fontFamily = settings.value("Font/family", "").toString();
m_fontSize = settings.value("Font/size", 0).toInt();
m_fontStyle = settings.value("Font/style", "").toString();
m_uiScale = settings.value("General/uiScale", 1.0).toDouble();
}
}
@ -352,4 +361,119 @@ void ThemeManager::saveSettings()
QSettings settings(configFile, QSettings::IniFormat);
settings.setValue("General/mode", m_mode);
settings.setValue("Font/family", m_fontFamily);
settings.setValue("Font/size", m_fontSize);
settings.setValue("Font/style", m_fontStyle);
settings.setValue("General/uiScale", m_uiScale);
}
// --- Font methods ---
QString ThemeManager::fontFamily() const
{
return m_fontFamily;
}
void ThemeManager::setFontFamily(const QString &family)
{
if (family == m_fontFamily)
return;
m_fontFamily = family;
saveSettings();
applyFont();
emit fontChanged();
}
int ThemeManager::fontSize() const
{
return m_fontSize;
}
void ThemeManager::setFontSize(int size)
{
if (size < 6) size = 6;
if (size > 72) size = 72;
if (size == m_fontSize)
return;
m_fontSize = size;
saveSettings();
applyFont();
emit fontChanged();
}
QString ThemeManager::fontStyle() const
{
return m_fontStyle;
}
void ThemeManager::setFontStyle(const QString &style)
{
if (style == m_fontStyle)
return;
m_fontStyle = style;
saveSettings();
applyFont();
emit fontChanged();
}
QStringList ThemeManager::availableFamilies() const
{
return QFontDatabase::families();
}
QStringList ThemeManager::availableStyles() const
{
QString family = m_fontFamily.isEmpty()
? QGuiApplication::font().family()
: m_fontFamily;
return QFontDatabase::styles(family);
}
void ThemeManager::resetFont()
{
m_fontFamily.clear();
m_fontSize = 0;
m_fontStyle.clear();
saveSettings();
applyFont();
emit fontChanged();
}
void ThemeManager::applyFont()
{
QFont font;
if (!m_fontFamily.isEmpty())
font.setFamily(m_fontFamily);
if (m_fontSize > 0)
font.setPointSize(m_fontSize);
if (!m_fontStyle.isEmpty()) {
QString family = m_fontFamily.isEmpty() ? font.family() : m_fontFamily;
QFont styled = QFontDatabase::font(family, m_fontStyle,
m_fontSize > 0 ? m_fontSize : font.pointSize());
font.setWeight(styled.weight());
font.setItalic(styled.italic());
}
QGuiApplication::setFont(font);
}
// --- HiDPI scaling ---
qreal ThemeManager::uiScale() const
{
return m_uiScale;
}
void ThemeManager::setUiScale(qreal scale)
{
if (scale < 0.5) scale = 0.5;
if (scale > 4.0) scale = 4.0;
if (qFuzzyCompare(scale, m_uiScale))
return;
m_uiScale = scale;
saveSettings();
emit uiScaleChanged();
}

View File

@ -54,6 +54,16 @@ class ThemeManager : public QObject
Q_PROPERTY(QColor timestampColor READ timestampColor NOTIFY paletteChanged)
Q_PROPERTY(QColor deadlineColor READ deadlineColor NOTIFY paletteChanged)
// Font properties
Q_PROPERTY(QString fontFamily READ fontFamily WRITE setFontFamily NOTIFY fontChanged)
Q_PROPERTY(int fontSize READ fontSize WRITE setFontSize NOTIFY fontChanged)
Q_PROPERTY(QString fontStyle READ fontStyle WRITE setFontStyle NOTIFY fontChanged)
Q_PROPERTY(QStringList availableFamilies READ availableFamilies CONSTANT)
Q_PROPERTY(QStringList availableStyles READ availableStyles NOTIFY fontChanged)
// HiDPI scaling
Q_PROPERTY(qreal uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged)
public:
explicit ThemeManager(QObject *parent = nullptr);
@ -93,11 +103,29 @@ public:
QColor timestampColor() const;
QColor deadlineColor() const;
// Font accessors
QString fontFamily() const;
void setFontFamily(const QString &family);
int fontSize() const;
void setFontSize(int size);
QString fontStyle() const;
void setFontStyle(const QString &style);
QStringList availableFamilies() const;
QStringList availableStyles() const;
Q_INVOKABLE void resetFont();
// HiDPI scaling
qreal uiScale() const;
void setUiScale(qreal scale);
void applyPalette();
void applyFont();
signals:
void modeChanged();
void paletteChanged();
void fontChanged();
void uiScaleChanged();
private:
QPalette paletteFromScheme(const QString &path) const;
@ -110,5 +138,9 @@ private:
QString m_mode;
QPalette m_palette;
QString m_fontFamily;
int m_fontSize;
QString m_fontStyle;
qreal m_uiScale;
static constexpr const char *SCHEMES_DIR = "/usr/share/color-schemes";
};