more unfinished changes, process tab for dashboard
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 59s
Python / verify (pull_request) Successful in 1m48s
Rust / fmt (pull_request) Successful in 47s
Rust / clippy (pull_request) Successful in 1m6s

This commit is contained in:
2026-09-25 16:51:28 +02:00
parent a1d96b60f6
commit 9ea1f846ae
103 changed files with 8677 additions and 3131 deletions
+1
View File
@@ -20,3 +20,4 @@ network-dev/
**/zshell.dist/ **/zshell.dist/
.opencode/ .opencode/
run-agent.sh run-agent.sh
AGENTS.md
+1 -1
View File
@@ -51,7 +51,7 @@ CustomRect {
if (internalChecked) if (internalChecked)
return checkedRadius; return checkedRadius;
if (isRound) if (isRound)
return (height || implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale); return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale);
return defaultRadius; return defaultRadius;
} }
+2 -2
View File
@@ -99,13 +99,13 @@ MouseArea {
id: base id: base
anchors.fill: parent anchors.fill: parent
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
opacity: root.stateOpacity opacity: root.stateOpacity
// Pick up radius from parent if it has one (parent can be anything with radius props) // Pick up radius from parent if it has one (parent can be anything with radius props)
// qmllint disable missing-property // qmllint disable missing-property
radius: root.parent?.radius ?? 0 radius: root.parent?.radius ?? 0
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0 topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0 topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property // qmllint enable missing-property
+11 -4
View File
@@ -2,25 +2,31 @@ import QtQuick
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
CustomClippingRect { Item {
id: track id: track
required property var scrollBar required property var scrollBar
required property var mouseArea required property CustomMouseArea mouseArea
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
implicitWidth: handle.implicitWidth implicitWidth: Tokens.padding.extraSmall * 2
CustomClippingRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
radius: Tokens.rounding.full radius: Tokens.rounding.full
implicitWidth: handle.implicitWidth
CustomRect { CustomRect {
id: handle id: handle
anchors.right: parent.right
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
anchors.right: parent.right
opacity: { opacity: {
if (!track.scrollBar.enabled) if (!track.scrollBar.enabled)
return 0; return 0;
@@ -54,3 +60,4 @@ CustomClippingRect {
} }
} }
} }
}
+16 -1
View File
@@ -143,8 +143,23 @@ CustomWindow {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
active: root.screenState.dock || root.screenState.resources || root.screenState.launcher || root.screenState.sidebar || (!Config.dashboard.showOnHover && root.screenState.dashboard) || root.screenState.settings || root.screenState.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!Config.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1))
windows: [root] windows: [root]
active: {
const s = root.screenState;
const c = Config;
if ((s.launcher && c.launcher.enabled) || (s.sidebar && c.sidebar.enabled) || (s.resources && c.dashboard.performance.enabled) || (s.clipboard && c.clipboard.enabled))
return true;
if (s.dashboard && !c.dashboard.showOnHover && c.dashboard.enabled)
return true;
if (!c.dock.hoverToReveal && s.dock && c.dock.enable)
return true;
if (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1)
return true;
if (s.settings)
return true;
return false;
}
onCleared: { onCleared: {
root.screenState.launcher = false; root.screenState.launcher = false;
+86
View File
@@ -0,0 +1,86 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Internal
import ZShell.Services
Singleton {
id: root
readonly property int historyLength: 30
readonly property CircularBuffer cpuBuffer: _cpuBuffer
readonly property CircularBuffer memoryBuffer: _memoryBuffer
readonly property list<var> cpuInfo: [
{
label: qsTr("Usage"),
value: `${Math.round(Cpu.percentage * 100)}%`
},
{
label: qsTr("Temp"),
value: `${Math.ceil(Cpu.temperature)}°${"C"}`
}
]
readonly property list<var> memoryInfo: [
{
label: qsTr("Used"),
value: memoryUsageFormat(Memory.used, Memory.total)
},
{
label: qsTr("Cached"),
value: memoryTotalFormat(Memory.cached)
},
{
label: qsTr("Writes"),
value: memoryTotalFormat(Memory.writeTotal)
},
{
label: qsTr("Reads"),
value: memoryTotalFormat(Memory.readTotal)
}
]
function memoryTotalFormat(kib: int): string {
const fmt = UsageFmt.formatKib(kib);
return `${fmt.value.toFixed(1)} ${fmt.unit}`;
}
function memoryUsageFormat(used: int, total: int): string {
const fmt = UsageFmt.formatKib(used, total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
}
Connections {
function onUpdateTriggered(): void {
if (Cpu.percentage >= 0 && isFinite(Cpu.percentage))
_cpuBuffer.push(Cpu.percentage);
}
target: Cpu
}
Connections {
function onUpdateTriggered(): void {
if (Memory.percentage >= 0 && isFinite(Memory.percentage))
_memoryBuffer.push(Memory.percentage);
}
target: Memory
}
CircularBuffer {
id: _cpuBuffer
capacity: root.historyLength + 1
smoothingRadius: 2
}
CircularBuffer {
id: _memoryBuffer
capacity: root.historyLength + 1
smoothingRadius: 2
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ Item {
id: root id: root
readonly property int clampedExtent: Math.max(Config.bar.border, extent) readonly property int clampedExtent: Math.max(Config.bar.border, extent)
readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2 readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.sizes.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness
property real extent: fullscreen ? 0 : Config.bar.border property real extent: fullscreen ? 0 : Config.bar.border
required property bool fullscreen required property bool fullscreen
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Services
CustomRect { CustomRect {
id: root id: root
readonly property int baseSize: horizontal ? Math.max(Config.bar.height, Tokens.bar.innerSize) : Math.max(Config.bar.height, Math.floor(Tokens.bar.innerSize * 1.5)) readonly property int baseSize: horizontal ? Math.max(Config.bar.height, Tokens.sizes.bar.innerSize) : Math.max(Config.bar.height, Math.floor(Tokens.sizes.bar.innerSize * 1.5))
required property bool horizontal required property bool horizontal
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
+3 -5
View File
@@ -17,7 +17,7 @@ Item {
required property bool horizontal required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
required property ShellScreen screen required property ShellScreen screen
readonly property real shortSize: Math.max(Config.bar.height, Tokens.bar.innerSize) readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2 readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2 property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
@@ -30,14 +30,12 @@ Item {
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: !root.horizontal enabled: !root.horizontal
Anim { Anim {}
}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.horizontal enabled: root.horizontal
Anim { Anim {}
}
} }
CustomRect { CustomRect {
+30 -22
View File
@@ -11,8 +11,8 @@ import qs.Components
Item { Item {
id: root id: root
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimWidth: view.implicitWidth + clipWrapper.anchors.margins * 2
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2 readonly property real nonAnimHeight: tabs.implicitHeight + clipWrapper.anchors.topMargin + view.implicitHeight + clipWrapper.anchors.margins * 2
required property ScreenState screenState required property ScreenState screenState
@@ -36,12 +36,12 @@ Item {
text: qsTr("Media"), text: qsTr("Media"),
enabled: true enabled: true
}, },
// { {
// component: testingComponent, component: procsComponent,
// iconName: "speed", iconName: "speed",
// text: qsTr("Testing"), text: qsTr("Processes"),
// enabled: true enabled: true
// }, },
]; ];
return allTabs.filter(tab => tab.enabled); return allTabs.filter(tab => tab.enabled);
} }
@@ -50,11 +50,22 @@ Item {
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
Behavior on implicitHeight { Behavior on implicitHeight {
Anim {} Anim {
type: Anim.SlowEffects
}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
Anim {} Anim {
type: Anim.SlowEffects
} }
}
Item {
id: clipWrapper
anchors.fill: parent
anchors.margins: Tokens.padding.small
// radius: Tokens.rounding.extraLarge - anchors.margins
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
Tabs { Tabs {
id: tabs id: tabs
@@ -62,24 +73,20 @@ Item {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
anchors.margins: Tokens.padding.large
nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 nonAnimWidth: view.implicitWidth
screenState: root.screenState screenState: root.screenState
tabs: root.dashboardTabs tabs: root.dashboardTabs
} }
ClippingRectangle { Item {
id: viewWrapper id: viewWrapper
anchors.top: tabs.bottom anchors.top: tabs.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.small anchors.topMargin: Tokens.spacing.small
color: "transparent"
radius: Tokens.rounding.extraLarge - anchors.margins
Flickable { Flickable {
id: view id: view
@@ -165,11 +172,11 @@ Item {
} }
} }
// Component { Component {
// id: testingComponent id: procsComponent
//
// WeatherTest {} ProcessesTab {}
// } }
Behavior on contentX { Behavior on contentX {
Anim { Anim {
@@ -179,3 +186,4 @@ Item {
} }
} }
} }
}
@@ -0,0 +1,209 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Config
Singleton {
id: root
readonly property var columnDefs: [
{
id: "command",
title: qsTr("Command"),
width: 200,
field: 3
},
{
id: "pid",
title: qsTr("PID"),
width: 60,
field: 1
},
{
id: "cpuUsage",
title: qsTr("CPU %"),
width: 70,
field: 7
},
{
id: "memoryUsage",
title: qsTr("Memory"),
width: 90,
field: 8
},
{
id: "elapsed",
title: qsTr("Elapsed"),
width: 80,
field: 9
},
{
id: "ppid",
title: qsTr("PPID"),
width: 60,
field: 2
},
{
id: "user",
title: qsTr("User"),
width: 100,
field: 11
},
{
id: "uid",
title: qsTr("UID"),
width: 60,
field: 10
},
{
id: "state",
title: qsTr("State"),
width: 60,
field: 5
},
{
id: "sleeping",
title: qsTr("Sleeping"),
width: 80,
field: 6
},
{
id: "commandLine",
title: qsTr("Command line"),
width: 240,
field: 4
},
{
id: "ioReadBytes",
title: qsTr("IO read"),
width: 90,
field: 12
},
{
id: "ioWriteBytes",
title: qsTr("IO write"),
width: 90,
field: 13
}
]
readonly property string pinnedColumnId: "command"
readonly property var hiddenColumns: {
const visible = Config.dashboard.processColumns.values.map(e => e.id);
return root.columnDefs.filter(d => !visible.includes(d.id));
}
function pinnedIndex(): int {
const values = Config.dashboard.processColumns.values;
for (let i = 0; i < values.length; i++)
if (values[i].id === root.pinnedColumnId)
return i;
return -1;
}
function ensurePinnedFirst() {
const i = root.pinnedIndex();
if (i > 0)
Qt.callLater(() => {
const idx = root.pinnedIndex();
if (idx > 0)
Config.dashboard.processColumns.move(idx, 0);
});
}
Component.onCompleted: Qt.callLater(ensurePinnedFirst)
function defFor(id: string): var {
for (const def of root.columnDefs)
if (def.id === id)
return def;
return null;
}
function addColumn(id: string) {
const def = root.defFor(id);
Config.dashboard.processColumns.insert({
id: id,
width: def ? def.width : 100
});
}
function removeColumn(index: int) {
const entry = Config.dashboard.processColumns.at(index);
if (entry && entry.id === root.pinnedColumnId)
return;
Config.dashboard.processColumns.remove(index);
}
function moveColumns(from: int, to: int) {
const list = Config.dashboard.processColumns;
const entry = list.at(from);
if (!entry)
return;
if (entry.id === root.pinnedColumnId)
return;
if (to === 0 && list.at(0)?.id === root.pinnedColumnId)
return;
list.move(from, to);
const i = root.pinnedIndex();
if (i > 0)
list.move(i, 0);
}
function resizeColumn(index: int, width: real) {
const entry = Config.dashboard.processColumns.at(index);
if (entry)
entry.width = Math.max(50, width);
}
function dampOvershoot(overshoot: real, maxOvershoot: real): real {
return overshoot / (1 + overshoot / maxOvershoot);
}
function formatCell(id: string, value: var): string {
switch (id) {
case "cpuUsage":
return value.toFixed(1) + "%";
case "memoryUsage":
return root.formatBytes(value * 1024);
case "ioReadBytes":
case "ioWriteBytes":
return root.formatBytes(value);
case "elapsed":
return root.formatElapsed(value);
case "sleeping":
return value ? qsTr("Yes") : qsTr("No");
default:
return String(value);
}
}
function formatBytes(bytes: var): string {
if (bytes === undefined || bytes === null || bytes <= 0)
return "0 B";
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
let v = Number(bytes);
let i = 0;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
++i;
}
return (i === 0 ? v.toFixed(0) : v.toFixed(1)) + " " + units[i];
}
function formatElapsed(secs: var): string {
const s = Math.max(0, Math.floor(Number(secs)));
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const ss = s % 60;
if (h > 0)
return h + "h " + m + "m";
if (m > 0)
return m + "m " + ss + "s";
return ss + "s";
}
}
@@ -0,0 +1,712 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
required property int headerHeight
required property ProcessesList pList
readonly property alias columnList: columnList
readonly property alias columnsModel: columnsData
readonly property bool anyHeld: columnList.contentItem.children.some(c => c.held)
z: 1
implicitHeight: headerHeight
function widthForColumn(id: string, configured: real): real {
for (const item of columnList.contentItem.children) {
if (item.modelData !== undefined && item.modelData.id === id)
return item.liveWidth > 0 ? item.liveWidth : (configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100));
}
return configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100);
}
function fillMissingWidths() {
const n = columnsData.values.length;
if (n === 0 || columnList.width <= 0)
return;
const spacingTotal = columnList.spacing * Math.max(0, n - 1);
let savedTotal = 0;
const unsavedIndices = [];
for (let i = 0; i < n; i++) {
const w = columnsData.values[i].width;
if (w > 0)
savedTotal += w;
else
unsavedIndices.push(i);
}
if (unsavedIndices.length === 0)
return;
const available = columnList.width - spacingTotal - savedTotal;
if (available <= 0)
return;
const each = Math.max(50, available / unsavedIndices.length);
for (const idx of unsavedIndices)
ProcessesColumns.resizeColumn(idx, each);
}
Connections {
target: columnList
function onWidthChanged() {
root.fillMissingWidths();
}
}
Connections {
target: columnsData
function onValuesChanged() {
root.fillMissingWidths();
}
}
Component.onCompleted: Qt.callLater(fillMissingWidths)
Component.onDestruction: Cursor.clearOverride()
RowLayout {
id: rowLayout
anchors.fill: parent
spacing: Tokens.spacing.extraSmall / 2
CustomListView {
id: columnList
Layout.fillWidth: true
orientation: ListView.Horizontal
interactive: false
spacing: parent.spacing
implicitHeight: root.headerHeight
add: Transition {
Anim {
from: 0
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
displaced: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "x"
}
}
move: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "x"
}
}
remove: Transition {
Anim {
to: 0
properties: "opacity"
type: Anim.DefaultEffects
}
}
model: DelegateModel {
id: columnsVisual
delegate: ColumnHeaderCell {
parentModel: columnsVisual
}
model: ScriptModel {
id: columnsData
values: Config.dashboard.processColumns.values
}
}
}
IconButton {
id: addColumnBtn
type: IconButton.Text
icon: "add"
font: Tokens.font.icon.medium
Layout.alignment: Qt.AlignVCenter
visible: ProcessesColumns.hiddenColumns.length > 0
onClicked: addMenu.expanded = true
}
Variants {
id: hiddenColumnList
model: ProcessesColumns.hiddenColumns
HiddenColumnItem {}
}
Menu {
id: addMenu
attachTo: addColumnBtn
items: hiddenColumnList.instances
onItemSelected: item => ProcessesColumns.addColumn(item.modelData.id)
}
}
Item {
id: resizeOverlay
property real gapBoost: pressed ? Tokens.spacing.small : Tokens.spacing.extraSmall
property bool pressed: false
property int geometryTick: 0
property int activeGapIndex: -1
anchors.fill: rowLayout
z: 10
Connections {
target: columnsData
function onValuesChanged() {
resizeOverlay.geometryTick++;
}
}
Connections {
target: columnList
function onCountChanged() {
resizeOverlay.geometryTick++;
}
function onContentWidthChanged() {
resizeOverlay.geometryTick++;
}
}
Component.onCompleted: Qt.callLater(() => geometryTick++)
Repeater {
model: Math.max(0, columnsData.values.length - 1)
MouseArea {
id: handleRoot
required property int index
readonly property bool hovered: containsMouse
property ColumnHeaderCell leftCell: {
const left = leftCell;
if (resizing && left)
return left;
const tick = resizeOverlay.geometryTick;
return resolveCell(true);
}
property ColumnHeaderCell rightCell: {
const right = rightCell;
if (resizing && right)
return right;
const tick = resizeOverlay.geometryTick;
return resolveCell(false);
}
property bool resizing: false
property real originX: 0
property real originWidth: 0
property real originRightWidth: 0
property real originAbsX: 0
function resolveCell(left: bool): ColumnHeaderCell {
if (left) { // qmllint disable incompatible-type
const leftItem = columnList.itemAtIndex(index);
if (leftItem)
return leftItem;
else
return null;
} else {
const rightItem = columnList.itemAtIndex(index + 1);
if (rightItem)
return rightItem;
else
return null;
}
}
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
preventStealing: true
hoverEnabled: true
enabled: !root.anyHeld
x: {
if (!leftCell)
return 0;
const gapCenter = leftCell.x + leftCell.width + columnList.spacing / 2;
return gapCenter - width / 2;
}
y: 0
implicitWidth: 16
implicitHeight: columnList.height
function pairAvailableWidth(rightCell) {
let othersWidth = 0;
for (const item of columnList.contentItem.children) {
if (item.modelData === undefined || item === handleRoot.leftCell || item === rightCell)
continue;
othersWidth += item.width;
}
const spacingTotal = columnList.spacing * Math.max(0, columnsData.values.length - 1);
return columnList.width - othersWidth - spacingTotal;
}
CustomRect {
id: handle
readonly property bool shouldBeVisible: handleRoot.hovered || handleRoot.pressed
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: shouldBeVisible ? resizeOverlay.gapBoost : 0
color: Colors.palette.m3secondary
radius: Tokens.rounding.full
opacity: shouldBeVisible ? 1.0 : 0
Behavior on implicitWidth {
Anim {
type: Anim.FastSpatial
}
}
Behavior on opacity {
Anim {}
}
transform: Scale {
origin {
x: handle.implicitWidth / 2
y: handle.height / 2
}
yScale: handle.shouldBeVisible ? 1 : 0.5
xScale: handle.shouldBeVisible ? 1 : 0.5
Behavior on xScale {
Anim {}
}
Behavior on yScale {
Anim {}
}
}
}
onHoveredChanged: resizeOverlay.activeGapIndex = hovered || pressed ? handleRoot.index : (resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex)
onPressedChanged: {
resizeOverlay.pressed = pressed;
if (!pressed)
resizeOverlay.activeGapIndex = hovered ? handleRoot.index : resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex;
}
onPressed: e => {
if (!handleRoot.leftCell)
return;
Cursor.setOverride(Qt.ClosedHandCursor);
resizing = true;
originAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x;
originWidth = handleRoot.leftCell.implicitWidth;
originRightWidth = handleRoot.rightCell ? handleRoot.rightCell.implicitWidth : 0;
}
onPositionChanged: e => {
if (!resizing || !handleRoot.leftCell)
return;
const nowAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x;
let newLeftWidth = Math.max(50, originWidth + (nowAbsX - originAbsX));
const rightCell = handleRoot.rightCell;
let roundedLeft = Math.round(newLeftWidth);
if (rightCell) {
const available = Math.round(handleRoot.pairAvailableWidth(rightCell));
const maxLeftWidth = Math.max(50, available - 50);
roundedLeft = Math.min(roundedLeft, maxLeftWidth);
rightCell.liveWidth = Math.max(50, available - roundedLeft);
}
handleRoot.leftCell.liveWidth = roundedLeft;
}
onReleased: {
Cursor.clearOverride();
resizing = false;
const cell = handleRoot.leftCell;
const rightCell = handleRoot.rightCell;
if (cell.liveWidth !== 0)
ProcessesColumns.resizeColumn(cell.DelegateModel.itemsIndex, cell.liveWidth);
if (rightCell.liveWidth !== 0)
ProcessesColumns.resizeColumn(rightCell.DelegateModel.itemsIndex, rightCell.liveWidth);
if (cell || rightCell) {
cell.liveWidth = 0;
rightCell.liveWidth = 0;
}
resizeOverlay.geometryTick++;
}
}
}
}
component ColumnHeaderCell: Item {
id: cell
required property var modelData
required property int index
required property DelegateModel parentModel
property int leftRadius: pushSide !== 1 && DelegateModel.itemsIndex === 0 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall
property int rightRadius: pushSide !== -1 && DelegateModel.itemsIndex === parentModel.count - 1 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall
property real radiusLerpProg
property bool held
property bool moved: false
property int pressIndex: -1
property point pressPos
property real lastMoveX: 0
property real liveWidth: 0
readonly property bool isActiveSort: Processes.sortField === (ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0)
readonly property int pushSide: {
const gapIdx = resizeOverlay.activeGapIndex;
if (gapIdx < 0)
return 0;
const myIdx = cell.DelegateModel.itemsIndex;
if (myIdx === gapIdx)
return -1;
if (myIdx === gapIdx + 1)
return 1;
return 0;
}
property real gapBoost: resizeOverlay.gapBoost
readonly property real gapShrink: gapBoost / 2
property real gapProg: pushSide !== 0 ? 1 : 0
property real gapX: pushSide === 1 ? 1 : 0
function lerpRadius(a: real, b: real): real {
return a + (b - a) * radiusLerpProg;
}
Behavior on gapBoost {
Anim {
type: Anim.FastSpatial
}
}
Behavior on gapX {
Anim {
type: Anim.FastSpatial
}
}
Behavior on gapProg {
Anim {
type: Anim.FastSpatial
}
}
implicitHeight: root.headerHeight
implicitWidth: liveWidth > 0 ? liveWidth : (modelData.width > 0 ? modelData.width : (ProcessesColumns.defFor(modelData.id)?.width ?? 100))
z: held || returnAnim.running ? 1 : 0
states: State {
name: "held"
PropertyChanges {
cellBg.color: Colors.palette.m3primaryContainer
elevation.opacity: 1
placeholder.opacity: 0.1
cell.radiusLerpProg: 1
title.color: Colors.palette.m3onPrimaryContainer
removeBtn.inactiveOnColor: Colors.palette.m3onPrimaryContainer
}
}
Behavior on leftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on rightRadius {
Anim {
type: Anim.DefaultEffects
}
}
transitions: Transition {
Anim {
properties: "opacity,radiusLerpProg"
type: Anim.SlowEffects
}
PropertyAction {
properties: "color,inactiveOnColor"
}
}
state: held ? "held" : ""
CustomRect {
id: placeholder
anchors.fill: parent
color: Colors.palette.m3primary
opacity: 0
topLeftRadius: cell.leftRadius
bottomLeftRadius: cell.leftRadius
topRightRadius: cell.rightRadius
bottomRightRadius: cell.rightRadius
}
DragHandler {
id: dragHandler
property real centerOffset: 0
enabled: cell.modelData.id !== ProcessesColumns.pinnedColumnId
target: null
xAxis.enabled: true
dragThreshold: 10
grabPermissions: PointerHandler.CanTakeOverFromItems
onActiveChanged: {
if (active) {
Cursor.setOverride(Qt.ClosedHandCursor);
returnAnim.stop();
cell.pressIndex = cell.DelegateModel.itemsIndex;
cell.lastMoveX = cell.x;
cell.moved = true;
cell.held = true;
const p = centroid.position;
cell.pressPos = Qt.point(p.x, p.y);
dragHandler.centerOffset = p.x - cell.width / 2;
cellContent.x = Qt.binding(() => {
const maxOvershoot = Tokens.padding.extraLarge;
const xDiff = cell.x - cell.lastMoveX;
const x = dragHandler.centroid.position.x - cell.pressPos.x - xDiff;
const absX = cell.mapToItem(columnList.contentItem, x, 0).x;
const maxX = columnList.contentWidth - cell.width;
if (absX < 0)
return x - absX - ProcessesColumns.dampOvershoot(-absX, maxOvershoot);
if (absX > maxX)
return x - absX + maxX + ProcessesColumns.dampOvershoot(absX - maxX, maxOvershoot);
return x;
});
cellContent.y = Qt.binding(() => {
const maxOvershoot = Tokens.padding.extraLargeIncreased;
const y = dragHandler.centroid.position.y - cell.pressPos.y;
return ProcessesColumns.dampOvershoot(Math.abs(y), maxOvershoot) * Math.sign(y);
});
} else {
Cursor.clearOverride();
if (cell.held) {
cellContent.x = cellContent.x;
cellContent.y = cellContent.y;
cell.held = false;
returnAnim.start();
if (cell.DelegateModel.itemsIndex !== cell.pressIndex) {
ProcessesColumns.moveColumns(cell.pressIndex, cell.DelegateModel.itemsIndex);
}
}
}
}
onCentroidChanged: {
if (!active)
return;
const p = centroid.position;
cell.lastMoveX = cell.x;
const cursorAbsX = cell.mapToItem(columnList.contentItem, p.x, 0).x;
const centerAbsX = cursorAbsX - dragHandler.centerOffset;
const movingRight = p.x > cell.pressPos.x;
const grabbedRight = dragHandler.centerOffset > 0;
const useCursor = movingRight === grabbedRight;
const refX = useCursor ? cursorAbsX : centerAbsX;
const myIndex = cell.DelegateModel.itemsIndex;
const rightItem = columnList.itemAtIndex(myIndex + 1);
if (rightItem) {
const rightMid = rightItem.x + rightItem.width / 2;
if (refX > rightMid) {
columnsVisual.items.move(myIndex, myIndex + 1);
resizeOverlay.geometryTick++;
Qt.callLater(() => resizeOverlay.geometryTick++);
return;
}
}
const leftItem = columnList.itemAtIndex(myIndex - 1);
if (leftItem) {
const leftMid = leftItem.x + leftItem.width / 2;
const pinnedAtZero = columnList.itemAtIndex(0)?.modelData?.id === ProcessesColumns.pinnedColumnId;
if (refX < leftMid && !(myIndex === 1 && pinnedAtZero)) {
columnsVisual.items.move(myIndex, myIndex - 1);
resizeOverlay.geometryTick++;
Qt.callLater(() => resizeOverlay.geometryTick++);
}
}
}
}
Anim {
id: returnAnim
properties: "x,y"
target: cellContent
to: 0
}
Item {
id: cellVisual
x: cell.gapShrink * cell.gapX
y: 0
width: cell.width - cell.gapShrink * cell.gapProg
height: cell.height
Item {
id: cellContent
width: cellVisual.width
height: cellVisual.height
readonly property int topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
readonly property int topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
readonly property int bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
readonly property int bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
Elevation {
id: elevation
anchors.fill: parent
level: 3
opacity: 0
topLeftRadius: parent.topLeftRadius
topRightRadius: parent.topRightRadius
bottomLeftRadius: parent.bottomLeftRadius
bottomRightRadius: parent.bottomRightRadius
}
CustomRect {
id: cellBg
anchors.fill: parent
color: cell.isActiveSort ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
StateLayer {
id: layer
color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
manualHoverOverride: cell.held
onClicked: {
Processes.sortBy(ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0);
root.pList.positionViewAtBeginning();
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium
anchors.rightMargin: 12
CustomText {
id: title
anchors.left: parent.left
anchors.right: removeBtn.visible ? removeBtn.left : parent.right
anchors.rightMargin: Tokens.spacing.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
elide: CustomText.ElideRight
text: ProcessesColumns.defFor(cell.modelData.id)?.title ?? cell.modelData.id
}
IconButton {
id: removeBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
type: IconButton.Text
icon: "close"
font: Tokens.font.icon.small
inactiveOnColor: Colors.palette.m3onPrimary
visible: cell.modelData.id !== ProcessesColumns.pinnedColumnId
onClicked: ProcessesColumns.removeColumn(cell.DelegateModel.itemsIndex)
}
MaterialIcon {
anchors.right: removeBtn.visible ? removeBtn.left : parent.right
anchors.verticalCenter: parent.verticalCenter
text: "keyboard_arrow_down"
opacity: cell.isActiveSort ? 1 : 0
visible: opacity > 0
rotation: !Processes.sortAscending && opacity > 0 ? 180 : 0
color: Colors.palette.m3onPrimary
Behavior on rotation {
Anim {}
}
Behavior on opacity {
Anim {}
}
}
}
}
}
}
component HiddenColumnItem: MenuItem {
required property var modelData
text: modelData.title
}
}
@@ -0,0 +1,140 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Services
CustomListView {
id: root
required property ProcessesHeader columnHeader
required property int rowHeight
property real rowSpacing: Tokens.spacing.small
model: Processes.list
spacing: root.rowSpacing
clip: true
CustomScrollBar.vertical: CustomScrollBar {
flickable: root
}
delegate: CustomRect {
id: processRow
required property var modelData
required property int index
implicitWidth: ListView.view.width
implicitHeight: rowLayout.implicitHeight
radius: Tokens.rounding.full
StateLayer {
id: rowHover
}
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: Tokens.spacing.extraSmall / 2
CustomClippingRect {
id: iconClip
Layout.fillHeight: true
Layout.margins: Tokens.padding.medium
implicitWidth: height
radius: Tokens.rounding.full
IconImage {
id: appIcon
anchors.fill: parent
implicitSize: height
source: Quickshell.iconPath(processRow.modelData.iconName, "application-x-executable")
}
}
Repeater {
id: cellRepeater
model: root.columnHeader.columnsModel
Item {
id: rowCell
required property var modelData
required property int index
implicitWidth: root.columnHeader.widthForColumn(modelData.id, modelData.width) - (index === 0 ? iconClip.implicitWidth + iconClip.Layout.margins * 2 + rowLayout.spacing : 0)
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
Layout.preferredWidth: implicitWidth
Layout.fillHeight: true
CustomRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.leftMargin: -(rowLayout.spacing)
implicitWidth: 2
color: Colors.palette.m3outlineVariant
visible: rowCell.index !== 0
}
Item {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.small
anchors.leftMargin: rowCell.index !== 0 ? Tokens.padding.medium : 0
anchors.rightMargin: Tokens.padding.medium
implicitHeight: mainText.implicitHeight + subText.implicitHeight + mainText.anchors.margins * 2 + subText.anchors.topMargin
CustomText {
id: mainText
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
elide: CustomText.ElideRight
text: {
const value = processRow.modelData[rowCell.modelData.id];
return value === undefined ? "" : ProcessesColumns.formatCell(rowCell.modelData.id, value);
}
states: State {
name: "centerAnchored"
when: !subText.visible
AnchorChanges {
target: mainText
anchors.top: undefined
anchors.verticalCenter: layout.verticalCenter
}
}
}
CustomText {
id: subText
anchors.top: mainText.bottom
anchors.left: parent.left
anchors.right: parent.right
visible: rowCell.modelData.id === "command"
elide: CustomText.ElideRight
text: rowCell.modelData.id === "command" ? processRow.modelData["commandLine"] : ""
font: Tokens.font.label.small
color: Colors.palette.m3outline
}
}
}
}
}
}
}
+186
View File
@@ -0,0 +1,186 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell.Services
import qs.Components
import qs.Services
import qs.Helpers
Item {
id: root
implicitHeight: layout.implicitHeight
ServiceRef {
service: Cpu
}
ServiceRef {
service: Memory
}
RowLayout {
id: layout
anchors.fill: parent
spacing: Tokens.spacing.medium
UsageCard {
accent: Colors.palette.m3primary
buffer: ResourceUsage.cpuBuffer
icon: "memory"
label: qsTr("CPU")
values: ResourceUsage.cpuInfo
}
UsageCard {
accent: Colors.palette.m3tertiary
buffer: ResourceUsage.memoryBuffer
icon: "memory_alt"
label: qsTr("Memory")
values: ResourceUsage.memoryInfo
}
}
component UsageCard: CustomRect {
id: card
property color accent
required property var buffer
property string icon
property string label
property list<var> values
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.largeIncreased
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: mainLayout.implicitHeight + mainLayout.anchors.margins * 2
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: 0
RowLayout {
spacing: Tokens.spacing.small
MaterialIcon {
color: card.accent
text: card.icon
}
CustomText {
text: card.label
}
}
Item {
id: graphWrapper
Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: infoColumn.implicitHeight
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: infoColumn.left
SparklineItem {
id: sparkline
anchors.fill: parent
historyLength: ResourceUsage.historyLength
line1: card.buffer
line1Color: card.accent
line1FillAlpha: 0.15
maxValue: 1.0
slideProgress: 1
Connections {
function onValuesChanged(): void {
slideAnim.restart();
}
target: card.buffer
}
NumberAnimation {
id: slideAnim
duration: Config.dashboard.resourceUpdateInterval
easing.type: Easing.Linear
from: 0
property: "slideProgress"
target: sparkline
to: 1
}
}
// "Collecting data" placeholder
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3outline
text: qsTr("Collecting data...")
visible: card.buffer.count < 2
}
}
ColumnLayout {
id: infoColumn
anchors.right: parent.right
anchors.bottom: parent.bottom
width: {
const prefWidth = graphWrapper.width / 3;
const minWidth = graphWrapper.width / 4;
if (prefWidth < implicitWidth)
return implicitWidth;
else if (prefWidth >= implicitWidth * 2)
return minWidth;
else
return prefWidth;
}
Repeater {
model: card.values
Item {
id: moreInfo
required property var modelData
Layout.leftMargin: Tokens.spacing.medium
Layout.fillWidth: true
implicitWidth: label.implicitWidth + value.implicitWidth + label.anchors.margins * 2
implicitHeight: label.implicitHeight
CustomText {
id: label
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant
text: moreInfo.modelData.label
}
CustomText {
id: value
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: card.accent
text: moreInfo.modelData.value
}
}
}
}
}
}
}
}
+64
View File
@@ -0,0 +1,64 @@
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Services
import qs.Modules.Dashboard.Processes
import qs.Components
import qs.Services
Item {
id: root
readonly property var desktopEntries: DesktopEntries.applications.values.map(e => ({
icon: e.icon,
command: e.command
}))
implicitWidth: Tokens.sizes.dashboard.processesWidth
implicitHeight: Tokens.sizes.dashboard.processesHeight
ServiceRef {
service: Processes
}
Connections {
target: DesktopEntries.applications
function onValuesChanged(): void {
Processes.setDesktopEntries(root.desktopEntries);
}
}
Component.onCompleted: Processes.setDesktopEntries(root.desktopEntries)
ProcessesHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
headerHeight: Tokens.sizes.dashboard.processesHeaderHeight
pList: processList
}
ProcessesList {
id: processList
anchors.top: header.bottom
anchors.bottom: sysInfo.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Tokens.spacing.small
anchors.bottomMargin: Tokens.spacing.small
columnHeader: header
rowHeight: Tokens.sizes.dashboard.processesRowHeight
}
SystemInfo {
id: sysInfo
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
}
+2 -3
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Internal import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Helpers import qs.Helpers
import qs.Components import qs.Components
@@ -66,8 +66,7 @@ CustomRect {
Behavior on smoothMax { Behavior on smoothMax {
enabled: sparkline.initialized enabled: sparkline.initialized
Anim { Anim {}
}
} }
Component.onCompleted: { Component.onCompleted: {
+11
View File
@@ -23,6 +23,17 @@ target_compile_definitions(ZShell PRIVATE
GIT_REVISION="${GIT_REVISION}" GIT_REVISION="${GIT_REVISION}"
) )
# Node macros expand to Q_OBJECT, automoc can't see it otherwise
list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
CONFIG_NODE
CONFIG_NODE_NO_CTOR
CONFIG_LIST_TYPE
SETTINGS_NODE
SETTINGS_NODE_NO_CTOR
SETTINGS_LIST_TYPE
)
add_subdirectory(Settings)
add_subdirectory(Models) add_subdirectory(Models)
add_subdirectory(Internal) add_subdirectory(Internal)
add_subdirectory(Services) add_subdirectory(Services)
+4
View File
@@ -7,7 +7,11 @@ qml_module(ZShell-components
buttonrow.hpp buttonrow.cpp buttonrow.hpp buttonrow.cpp
carouselview.hpp carouselview.cpp carouselview.hpp carouselview.cpp
wheelinverter.hpp wheelinverter.hpp
sparklinecontent.hpp sparklinecontent.cpp
sparklineitem.hpp sparklineitem.cpp
sparklinesceneitem.hpp sparklinesceneitem.cpp
LIBRARIES LIBRARIES
Qt::Quick Qt::Quick
Qt::QuickPrivate Qt::QuickPrivate
ZShell-internal
) )
@@ -0,0 +1,250 @@
#include "sparklinecontent.hpp"
#include <qmath.h>
#include <qpainter.h>
#include <qpen.h>
namespace ZShell::components {
SparklineContent::SparklineContent(QQuickItem* parent)
: QQuickPaintedItem(parent) {
setAntialiasing(true);
}
qreal SparklineContent::stepX() const {
if (m_historyLength < 2 || m_viewportW <= 0.0) return 0.0;
if (m_sliding) return m_viewportW / static_cast<qreal>(m_historyLength - 1);
const int len = qMax(
2,
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
: (m_line2 ? m_line2->count() : 2));
return m_viewportW / static_cast<qreal>(len - 1);
}
void SparklineContent::setViewportSize(
qreal viewportWidth, qreal viewportHeight) {
if (qFuzzyCompare(m_viewportW, viewportWidth) &&
qFuzzyCompare(m_viewportH, viewportHeight))
return;
m_viewportW = viewportWidth;
m_viewportH = viewportHeight;
invalidate();
}
void SparklineContent::setLine1(internal::CircularBuffer* buffer) {
if (m_line1 == buffer) return;
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
m_line1 = buffer;
connectBuffer(buffer);
invalidate();
}
void SparklineContent::setLine2(internal::CircularBuffer* buffer) {
if (m_line2 == buffer) return;
if (m_line2) disconnect(m_line2, nullptr, this, nullptr);
m_line2 = buffer;
connectBuffer(buffer);
invalidate();
}
void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) {
if (!buffer) return;
// New data, or the buffer's own smoothingRadius changed (it re-emits
// valuesChanged in that case too) -> geometry is stale.
connect(
buffer,
&internal::CircularBuffer::valuesChanged,
this,
&SparklineContent::invalidate);
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
if (m_line1 == buffer) m_line1 = nullptr;
if (m_line2 == buffer) m_line2 = nullptr;
invalidate();
});
}
void SparklineContent::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
update(); // visual-only, no geometry rebuild needed
}
void SparklineContent::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
update();
}
void SparklineContent::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
update();
}
void SparklineContent::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
update();
}
void SparklineContent::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
invalidate();
}
void SparklineContent::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
invalidate();
}
void SparklineContent::setCurveSmoothing(bool enabled) {
if (m_curveSmoothing == enabled) return;
m_curveSmoothing = enabled;
invalidate();
}
void SparklineContent::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
invalidate();
}
void SparklineContent::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
invalidate();
}
void SparklineContent::invalidate() {
m_dirty = true;
applyGeometry();
update();
}
void SparklineContent::applyGeometry() {
const qreal pad = m_sliding ? stepX() : 0.0;
setSize(QSizeF(m_viewportW + pad, m_viewportH));
}
SparklineContent::LineGeometry SparklineContent::buildGeometry(
internal::CircularBuffer* buffer) const {
LineGeometry geom;
if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 ||
m_viewportW <= 0.0)
return geom;
if (m_historyLength < 2) return geom;
const int len = buffer->count();
const qreal step = stepX();
if (step <= 0.0) return geom;
qreal startX = 0.0;
if (m_sliding)
startX = m_viewportW - (len - 1) * step + step;
else
startX = 0.0;
const qreal h = m_viewportH;
const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0;
const qreal plotTop = strokePad + curvePad;
const qreal plotBottom = h - strokePad;
const qreal fillBottom = h;
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
const qreal value =
qBound<qreal>(0.0, buffer->smoothedAt(i), m_maxValue);
const qreal x = startX + i * step;
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath linePath;
linePath.moveTo(points[0]);
if (m_curveSmoothing && points.size() > 2) {
const qreal tension = 1.0;
const qreal k = tension / 6.0;
const int n = points.size();
for (int i = 0; i < n - 1; ++i) {
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, n - 1)];
QPointF c1 = p1 + (p2 - p0) * k;
QPointF c2 = p2 - (p3 - p1) * k;
const qreal segMinY = qMin(p1.y(), p2.y());
const qreal segMaxY = qMax(p1.y(), p2.y());
c1.setY(qBound(segMinY, c1.y(), segMaxY));
c2.setY(qBound(segMinY, c2.y(), segMaxY));
linePath.cubicTo(c1, c2, p2);
}
} else {
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
linePath.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
}
}
geom.stroke = linePath;
QPainterPath fillPath = linePath;
fillPath.lineTo(startX + (len - 1) * step, fillBottom);
fillPath.lineTo(startX, fillBottom);
fillPath.closeSubpath();
geom.fill = fillPath;
geom.hasData = true;
return geom;
}
void SparklineContent::rebuildIfNeeded() {
if (!m_dirty) return;
m_geom1 = buildGeometry(m_line1);
m_geom2 = buildGeometry(m_line2);
m_dirty = false;
}
void SparklineContent::paint(QPainter* painter) {
rebuildIfNeeded();
if (!m_geom1.hasData && !m_geom2.hasData) return;
painter->setRenderHint(QPainter::Antialiasing, true);
auto drawOne =
[&](const LineGeometry& geom, const QColor& color, qreal fillAlpha) {
if (!geom.hasData) return;
QColor fillColor = color;
fillColor.setAlphaF(static_cast<float>(fillAlpha));
painter->setPen(Qt::NoPen);
painter->setBrush(fillColor);
painter->drawPath(geom.fill);
QPen pen(color, m_lineWidth);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(geom.stroke);
};
drawOne(m_geom1, m_line1Color, m_line1FillAlpha);
drawOne(m_geom2, m_line2Color, m_line2FillAlpha);
}
} // namespace ZShell::components
@@ -0,0 +1,69 @@
#pragma once
#include <qcolor.h>
#include <qpainterpath.h>
#include <qquickpainteditem.h>
#include "internal/circularbuffer.hpp"
namespace ZShell::components {
class SparklineContent : public QQuickPaintedItem {
Q_OBJECT
public:
explicit SparklineContent(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
void setLine1(internal::CircularBuffer* buffer);
void setLine2(internal::CircularBuffer* buffer);
void setLine1Color(const QColor& color);
void setLine2Color(const QColor& color);
void setLine1FillAlpha(qreal alpha);
void setLine2FillAlpha(qreal alpha);
void setMaxValue(qreal value);
void setLineWidth(qreal width);
void setCurveSmoothing(bool enabled);
void setHistoryLength(int length);
void setSliding(bool sliding);
void setViewportSize(qreal viewportWidth, qreal viewportHeight);
[[nodiscard]] qreal stepX() const;
private:
struct LineGeometry {
QPainterPath stroke;
QPainterPath fill;
bool hasData = false;
};
void invalidate();
void rebuildIfNeeded();
[[nodiscard]] LineGeometry buildGeometry(
internal::CircularBuffer* buffer) const;
void connectBuffer(internal::CircularBuffer* buffer);
void applyGeometry();
internal::CircularBuffer* m_line1 = nullptr;
internal::CircularBuffer* m_line2 = nullptr;
QColor m_line1Color;
QColor m_line2Color;
qreal m_line1FillAlpha = 0.15;
qreal m_line2FillAlpha = 0.2;
qreal m_maxValue = 1024.0;
qreal m_lineWidth = 2.0;
bool m_curveSmoothing = false;
int m_historyLength = 30;
bool m_sliding = true;
qreal m_viewportW = 0.0;
qreal m_viewportH = 0.0;
LineGeometry m_geom1;
LineGeometry m_geom2;
bool m_dirty = true;
};
} // namespace ZShell::components
+179
View File
@@ -0,0 +1,179 @@
#include "sparklineitem.hpp"
#include "sparklinecontent.hpp"
namespace ZShell::components {
SparklineItem::SparklineItem(QQuickItem* parent) : QQuickItem(parent) {
setClip(true);
m_content = new SparklineContent(this);
m_content->setHistoryLength(m_historyLength);
m_content->setSliding(m_sliding);
}
void SparklineItem::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
m_content->setViewportSize(newGeometry.width(), newGeometry.height());
updateContentOffset();
}
void SparklineItem::updateContentOffset() {
m_content->setX(-m_content->stepX() * m_slideProgress);
}
void SparklineItem::applySmoothingRadius(
internal::CircularBuffer* buffer) const {
if (buffer) buffer->setSmoothingRadius(qRound(m_smoothing));
}
internal::CircularBuffer* SparklineItem::line1() const {
return m_line1;
}
void SparklineItem::setLine1(internal::CircularBuffer* buffer) {
if (m_line1 == buffer) return;
m_line1 = buffer;
m_content->setLine1(buffer);
applySmoothingRadius(buffer);
emit line1Changed();
}
internal::CircularBuffer* SparklineItem::line2() const {
return m_line2;
}
void SparklineItem::setLine2(internal::CircularBuffer* buffer) {
if (m_line2 == buffer) return;
m_line2 = buffer;
m_content->setLine2(buffer);
applySmoothingRadius(buffer);
emit line2Changed();
}
QColor SparklineItem::line1Color() const {
return m_line1Color;
}
void SparklineItem::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
m_content->setLine1Color(color);
emit line1ColorChanged();
}
QColor SparklineItem::line2Color() const {
return m_line2Color;
}
void SparklineItem::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
m_content->setLine2Color(color);
emit line2ColorChanged();
}
qreal SparklineItem::line1FillAlpha() const {
return m_line1FillAlpha;
}
void SparklineItem::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
m_content->setLine1FillAlpha(alpha);
emit line1FillAlphaChanged();
}
qreal SparklineItem::line2FillAlpha() const {
return m_line2FillAlpha;
}
void SparklineItem::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
m_content->setLine2FillAlpha(alpha);
emit line2FillAlphaChanged();
}
qreal SparklineItem::maxValue() const {
return m_maxValue;
}
void SparklineItem::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
m_content->setMaxValue(value);
emit maxValueChanged();
}
qreal SparklineItem::slideProgress() const {
return m_slideProgress;
}
void SparklineItem::setSlideProgress(qreal progress) {
if (qFuzzyCompare(m_slideProgress, progress)) return;
m_slideProgress = progress;
updateContentOffset();
emit slideProgressChanged();
}
int SparklineItem::historyLength() const {
return m_historyLength;
}
void SparklineItem::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
m_content->setHistoryLength(length);
updateContentOffset();
emit historyLengthChanged();
}
qreal SparklineItem::lineWidth() const {
return m_lineWidth;
}
void SparklineItem::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
m_content->setLineWidth(width);
emit lineWidthChanged();
}
void SparklineItem::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
m_content->setSliding(sliding);
updateContentOffset();
emit slidingChanged();
}
bool SparklineItem::sliding() const {
return m_sliding;
}
void SparklineItem::setSmoothing(qreal smoothing) {
smoothing = qMax<qreal>(0.0, smoothing);
if (qFuzzyCompare(m_smoothing, smoothing)) return;
m_smoothing = smoothing;
applySmoothingRadius(m_line1);
applySmoothingRadius(m_line2);
emit smoothingChanged();
}
qreal SparklineItem::smoothing() const {
return m_smoothing;
}
void SparklineItem::setCurveSmoothing(bool enabled) {
if (m_curveSmoothing == enabled) return;
m_curveSmoothing = enabled;
m_content->setCurveSmoothing(enabled);
emit curveSmoothingChanged();
}
bool SparklineItem::curveSmoothing() const {
return m_curveSmoothing;
}
} // namespace ZShell::components
@@ -3,21 +3,25 @@
#include <qcolor.h> #include <qcolor.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qquickpainteditem.h> #include <qquickitem.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "circularbuffer.hpp" #include "internal/circularbuffer.hpp"
namespace ZShell::internal { namespace ZShell::components {
class SparklineItem : public QQuickPaintedItem { class SparklineContent;
class SparklineItem : public QQuickItem {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY( Q_PROPERTY(
CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed) internal::CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY
line1Changed)
Q_PROPERTY( Q_PROPERTY(
CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed) internal::CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY
line2Changed)
Q_PROPERTY( Q_PROPERTY(
QColor line1Color READ line1Color WRITE setLine1Color NOTIFY QColor line1Color READ line1Color WRITE setLine1Color NOTIFY
line1ColorChanged) line1ColorChanged)
@@ -52,13 +56,11 @@ class SparklineItem : public QQuickPaintedItem {
public: public:
explicit SparklineItem(QQuickItem* parent = nullptr); explicit SparklineItem(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override; [[nodiscard]] internal::CircularBuffer* line1() const;
void setLine1(internal::CircularBuffer* buffer);
[[nodiscard]] CircularBuffer* line1() const; [[nodiscard]] internal::CircularBuffer* line2() const;
void setLine1(CircularBuffer* buffer); void setLine2(internal::CircularBuffer* buffer);
[[nodiscard]] CircularBuffer* line2() const;
void setLine2(CircularBuffer* buffer);
[[nodiscard]] QColor line1Color() const; [[nodiscard]] QColor line1Color() const;
void setLine1Color(const QColor& color); void setLine1Color(const QColor& color);
@@ -108,16 +110,17 @@ class SparklineItem : public QQuickPaintedItem {
void smoothingChanged(); void smoothingChanged();
void curveSmoothingChanged(); void curveSmoothingChanged();
private: protected:
void drawLine( void geometryChange(
QPainter* painter, const QRectF& newGeometry, const QRectF& oldGeometry) override;
CircularBuffer* buffer,
const QColor& color,
qreal fillAlpha);
void connectBuffer(CircularBuffer* buffer);
CircularBuffer* m_line1 = nullptr; private:
CircularBuffer* m_line2 = nullptr; void updateContentOffset();
void applySmoothingRadius(internal::CircularBuffer* buffer) const;
SparklineContent* m_content = nullptr;
internal::CircularBuffer* m_line1 = nullptr;
internal::CircularBuffer* m_line2 = nullptr;
QColor m_line1Color; QColor m_line1Color;
QColor m_line2Color; QColor m_line2Color;
qreal m_line1FillAlpha = 0.15; qreal m_line1FillAlpha = 0.15;
@@ -131,4 +134,4 @@ class SparklineItem : public QQuickPaintedItem {
bool m_curveSmoothing = false; bool m_curveSmoothing = false;
}; };
} // namespace ZShell::internal } // namespace ZShell::components
@@ -0,0 +1,447 @@
#include "sparklinesceneitem.hpp"
#include <qmath.h>
#include <qpainterpath.h>
#include <qsgflatcolormaterial.h>
#include <qsggeometry.h>
namespace ZShell::components {
SparklineSceneItem::SparklineSceneItem(QQuickItem* parent)
: QQuickItem(parent) {
setClip(true);
setFlag(QQuickItem::ItemHasContents, true);
}
void SparklineSceneItem::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
if (newGeometry.size() != oldGeometry.size()) invalidateGeometry();
}
void SparklineSceneItem::invalidateGeometry() {
m_geometryDirty = true;
update();
}
void SparklineSceneItem::applySmoothingRadius(
internal::CircularBuffer* buffer) const {
if (buffer) buffer->setSmoothingRadius(qRound(m_smoothing));
}
void SparklineSceneItem::connectBuffer(internal::CircularBuffer* buffer) {
if (!buffer) return;
connect(
buffer,
&internal::CircularBuffer::valuesChanged,
this,
&SparklineSceneItem::invalidateGeometry);
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
if (m_line1 == buffer) {
m_line1 = nullptr;
emit line1Changed();
}
if (m_line2 == buffer) {
m_line2 = nullptr;
emit line2Changed();
}
invalidateGeometry();
});
}
qreal SparklineSceneItem::stepX() const {
if (m_historyLength < 2 || width() <= 0.0) return 0.0;
if (m_sliding) return width() / static_cast<qreal>(m_historyLength - 1);
const int len = qMax(
2,
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
: (m_line2 ? m_line2->count() : 2));
return width() / static_cast<qreal>(len - 1);
}
SparklineSceneItem::LineSample SparklineSceneItem::buildSample(
internal::CircularBuffer* buffer) const {
LineSample sample;
if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 || width() <= 0.0)
return sample;
if (m_historyLength < 2) return sample;
const int len = buffer->count();
const qreal step = stepX();
if (step <= 0.0) return sample;
qreal startX = m_sliding ? (width() - (len - 1) * step + step) : 0.0;
const qreal h = height();
const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0;
const qreal plotTop = strokePad + curvePad;
const qreal plotBottom = h - strokePad;
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
// Already smoothed once, incrementally, by the buffer itself.
const qreal value =
qBound<qreal>(0.0, buffer->smoothedAt(i), m_maxValue);
const qreal x = startX + i * step;
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath path;
path.moveTo(points[0]);
if (m_curveSmoothing && points.size() > 2) {
const qreal tension = 1.0;
const qreal k = tension / 6.0;
const int n = points.size();
for (int i = 0; i < n - 1; ++i) {
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, n - 1)];
QPointF c1 = p1 + (p2 - p0) * k;
QPointF c2 = p2 - (p3 - p1) * k;
const qreal segMinY = qMin(p1.y(), p2.y());
const qreal segMaxY = qMax(p1.y(), p2.y());
c1.setY(qBound(segMinY, c1.y(), segMaxY));
c2.setY(qBound(segMinY, c2.y(), segMaxY));
path.cubicTo(c1, c2, p2);
}
} else {
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
path.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
}
}
// Flatten the curve to a polyline once, here, on data change only.
// This is CPU math only -- no QPainter draw calls, no image, no
// texture upload happen as a result of this.
const auto subpaths = path.toSubpathPolygons();
if (subpaths.isEmpty()) return sample;
sample.polyline = subpaths.first();
sample.fillBottom = h;
sample.hasData = sample.polyline.size() >= 2;
return sample;
}
void SparklineSceneItem::writeFillGeometry(
QSGGeometryNode* node, const LineSample& sample) {
const int n = sample.polyline.size();
auto* geometry = node->geometry();
geometry->allocate(n * 2);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
auto* vertices = geometry->vertexDataAsPoint2D();
for (int i = 0; i < n; ++i) {
const QPointF& p = sample.polyline[i];
vertices[i * 2].set(p.x(), p.y());
vertices[i * 2 + 1].set(p.x(), sample.fillBottom);
}
node->markDirty(QSGNode::DirtyGeometry);
}
void SparklineSceneItem::writeStrokeGeometry(
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth) {
const int n = sample.polyline.size();
auto* geometry = node->geometry();
geometry->allocate(n * 2);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
auto* vertices = geometry->vertexDataAsPoint2D();
const qreal half = lineWidth * 0.5;
for (int i = 0; i < n; ++i) {
QPointF dir;
if (i == 0)
dir = sample.polyline[1] - sample.polyline[0];
else if (i == n - 1)
dir = sample.polyline[n - 1] - sample.polyline[n - 2];
else
dir = sample.polyline[i + 1] - sample.polyline[i - 1];
const qreal len = std::hypot(dir.x(), dir.y());
QPointF normal = len > 1e-6 ? QPointF(-dir.y() / len, dir.x() / len)
: QPointF(0, 1);
const QPointF& p = sample.polyline[i];
const QPointF offset = normal * half;
vertices[i * 2].set(p.x() + offset.x(), p.y() + offset.y());
vertices[i * 2 + 1].set(p.x() - offset.x(), p.y() - offset.y());
}
node->markDirty(QSGNode::DirtyGeometry);
}
void SparklineSceneItem::setMaterialColor(
QSGGeometryNode* node, const QColor& color, qreal alpha) {
auto* material = static_cast<QSGFlatColorMaterial*>(node->material());
QColor c = color;
c.setAlphaF(static_cast<float>(alpha));
material->setColor(c);
node->markDirty(QSGNode::DirtyMaterial);
}
QSGNode* SparklineSceneItem::updatePaintNode(
QSGNode* node, UpdatePaintNodeData*) {
bool samplesRebuilt = false;
if (m_geometryDirty) {
m_sample1 = buildSample(m_line1);
m_sample2 = buildSample(m_line2);
m_geometryDirty = false;
samplesRebuilt = true;
}
const bool hasAny = m_sample1.hasData || m_sample2.hasData;
if (!hasAny) {
delete node;
m_transformNode = nullptr;
m_line1FillNode = m_line1StrokeNode = nullptr;
m_line2FillNode = m_line2StrokeNode = nullptr;
return nullptr;
}
auto* transformNode = static_cast<QSGTransformNode*>(node);
bool justCreated = false;
if (!transformNode) {
transformNode = new QSGTransformNode();
m_transformNode = transformNode;
auto makeNode = [] {
auto* n = new QSGGeometryNode();
n->setGeometry(
new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0));
n->setFlag(QSGNode::OwnsGeometry);
n->setMaterial(new QSGFlatColorMaterial());
n->setFlag(QSGNode::OwnsMaterial);
return n;
};
m_line1FillNode = makeNode();
m_line1StrokeNode = makeNode();
m_line2FillNode = makeNode();
m_line2StrokeNode = makeNode();
transformNode->appendChildNode(m_line1FillNode);
transformNode->appendChildNode(m_line1StrokeNode);
transformNode->appendChildNode(m_line2FillNode);
transformNode->appendChildNode(m_line2StrokeNode);
justCreated = true;
}
QMatrix4x4 matrix;
matrix.translate(-stepX() * m_slideProgress, 0);
transformNode->setMatrix(matrix);
const bool needsGeometryWrite = samplesRebuilt || justCreated;
const bool needsColorWrite = m_colorsDirty || justCreated;
auto writeLine = [&](QSGGeometryNode* fillNode,
QSGGeometryNode* strokeNode,
const LineSample& sample,
const QColor& color,
qreal fillAlpha) {
if (needsGeometryWrite) {
if (sample.hasData) {
writeFillGeometry(fillNode, sample);
writeStrokeGeometry(strokeNode, sample, m_lineWidth);
} else {
fillNode->geometry()->allocate(0);
strokeNode->geometry()->allocate(0);
fillNode->markDirty(QSGNode::DirtyGeometry);
strokeNode->markDirty(QSGNode::DirtyGeometry);
}
}
if (needsColorWrite) {
setMaterialColor(fillNode, color, fillAlpha);
setMaterialColor(strokeNode, color, 1.0);
}
};
writeLine(
m_line1FillNode,
m_line1StrokeNode,
m_sample1,
m_line1Color,
m_line1FillAlpha);
writeLine(
m_line2FillNode,
m_line2StrokeNode,
m_sample2,
m_line2Color,
m_line2FillAlpha);
m_colorsDirty = false;
return transformNode;
}
internal::CircularBuffer* SparklineSceneItem::line1() const {
return m_line1;
}
void SparklineSceneItem::setLine1(internal::CircularBuffer* buffer) {
if (m_line1 == buffer) return;
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
m_line1 = buffer;
connectBuffer(buffer);
applySmoothingRadius(buffer);
invalidateGeometry();
emit line1Changed();
}
internal::CircularBuffer* SparklineSceneItem::line2() const {
return m_line2;
}
void SparklineSceneItem::setLine2(internal::CircularBuffer* buffer) {
if (m_line2 == buffer) return;
if (m_line2) disconnect(m_line2, nullptr, this, nullptr);
m_line2 = buffer;
connectBuffer(buffer);
applySmoothingRadius(buffer);
invalidateGeometry();
emit line2Changed();
}
QColor SparklineSceneItem::line1Color() const {
return m_line1Color;
}
void SparklineSceneItem::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
m_colorsDirty = true;
update();
emit line1ColorChanged();
}
QColor SparklineSceneItem::line2Color() const {
return m_line2Color;
}
void SparklineSceneItem::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
m_colorsDirty = true;
update();
emit line2ColorChanged();
}
qreal SparklineSceneItem::line1FillAlpha() const {
return m_line1FillAlpha;
}
void SparklineSceneItem::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
m_colorsDirty = true;
update();
emit line1FillAlphaChanged();
}
qreal SparklineSceneItem::line2FillAlpha() const {
return m_line2FillAlpha;
}
void SparklineSceneItem::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
m_colorsDirty = true;
update();
emit line2FillAlphaChanged();
}
qreal SparklineSceneItem::maxValue() const {
return m_maxValue;
}
void SparklineSceneItem::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
invalidateGeometry();
emit maxValueChanged();
}
qreal SparklineSceneItem::slideProgress() const {
return m_slideProgress;
}
void SparklineSceneItem::setSlideProgress(qreal progress) {
if (qFuzzyCompare(m_slideProgress, progress)) return;
m_slideProgress = progress;
update();
emit slideProgressChanged();
}
int SparklineSceneItem::historyLength() const {
return m_historyLength;
}
void SparklineSceneItem::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
invalidateGeometry();
emit historyLengthChanged();
}
qreal SparklineSceneItem::lineWidth() const {
return m_lineWidth;
}
void SparklineSceneItem::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
invalidateGeometry();
emit lineWidthChanged();
}
bool SparklineSceneItem::sliding() const {
return m_sliding;
}
void SparklineSceneItem::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
invalidateGeometry();
emit slidingChanged();
}
qreal SparklineSceneItem::smoothing() const {
return m_smoothing;
}
void SparklineSceneItem::setSmoothing(qreal smoothing) {
smoothing = qMax<qreal>(0.0, smoothing);
if (qFuzzyCompare(m_smoothing, smoothing)) return;
m_smoothing = smoothing;
applySmoothingRadius(m_line1);
applySmoothingRadius(m_line2);
emit smoothingChanged();
}
bool SparklineSceneItem::curveSmoothing() const {
return m_curveSmoothing;
}
void SparklineSceneItem::setCurveSmoothing(bool enabled) {
if (m_curveSmoothing == enabled) return;
m_curveSmoothing = enabled;
invalidateGeometry();
emit curveSmoothingChanged();
}
} // namespace ZShell::components
@@ -0,0 +1,167 @@
#pragma once
#include <qcolor.h>
#include <qmatrix4x4.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include <qvector.h>
#include "internal/circularbuffer.hpp"
class QSGGeometryNode;
class QSGTransformNode;
namespace ZShell::components {
class SparklineSceneItem : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
internal::CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY
line1Changed)
Q_PROPERTY(
internal::CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY
line2Changed)
Q_PROPERTY(
QColor line1Color READ line1Color WRITE setLine1Color NOTIFY
line1ColorChanged)
Q_PROPERTY(
QColor line2Color READ line2Color WRITE setLine2Color NOTIFY
line2ColorChanged)
Q_PROPERTY(
qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY
line1FillAlphaChanged)
Q_PROPERTY(
qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY
line2FillAlphaChanged)
Q_PROPERTY(
qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged)
Q_PROPERTY(
qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY
slideProgressChanged)
Q_PROPERTY(
int historyLength READ historyLength WRITE setHistoryLength NOTIFY
historyLengthChanged)
Q_PROPERTY(
qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY
lineWidthChanged)
Q_PROPERTY(bool sliding READ sliding WRITE setSliding NOTIFY slidingChanged)
Q_PROPERTY(
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
smoothingChanged)
Q_PROPERTY(
bool curveSmoothing READ curveSmoothing WRITE setCurveSmoothing NOTIFY
curveSmoothingChanged)
public:
explicit SparklineSceneItem(QQuickItem* parent = nullptr);
[[nodiscard]] internal::CircularBuffer* line1() const;
void setLine1(internal::CircularBuffer* buffer);
[[nodiscard]] internal::CircularBuffer* line2() const;
void setLine2(internal::CircularBuffer* buffer);
[[nodiscard]] QColor line1Color() const;
void setLine1Color(const QColor& color);
[[nodiscard]] QColor line2Color() const;
void setLine2Color(const QColor& color);
[[nodiscard]] qreal line1FillAlpha() const;
void setLine1FillAlpha(qreal alpha);
[[nodiscard]] qreal line2FillAlpha() const;
void setLine2FillAlpha(qreal alpha);
[[nodiscard]] qreal maxValue() const;
void setMaxValue(qreal value);
[[nodiscard]] qreal slideProgress() const;
void setSlideProgress(qreal progress);
[[nodiscard]] int historyLength() const;
void setHistoryLength(int length);
[[nodiscard]] qreal lineWidth() const;
void setLineWidth(qreal width);
[[nodiscard]] bool sliding() const;
void setSliding(bool sliding);
[[nodiscard]] qreal smoothing() const;
void setSmoothing(qreal smoothing);
[[nodiscard]] bool curveSmoothing() const;
void setCurveSmoothing(bool curveSmoothing);
signals:
void line1Changed();
void line2Changed();
void line1ColorChanged();
void line2ColorChanged();
void line1FillAlphaChanged();
void line2FillAlphaChanged();
void maxValueChanged();
void slideProgressChanged();
void historyLengthChanged();
void lineWidthChanged();
void slidingChanged();
void smoothingChanged();
void curveSmoothingChanged();
protected:
QSGNode* updatePaintNode(QSGNode* node, UpdatePaintNodeData* data) override;
void geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) override;
private:
struct LineSample {
QVector<QPointF> polyline; // local coords, slideProgress baked to 0
qreal fillBottom = 0.0;
bool hasData = false;
};
void connectBuffer(internal::CircularBuffer* buffer);
void invalidateGeometry();
void applySmoothingRadius(internal::CircularBuffer* buffer) const;
[[nodiscard]] qreal stepX() const;
[[nodiscard]] LineSample buildSample(internal::CircularBuffer* buffer) const;
static void writeFillGeometry(
QSGGeometryNode* node, const LineSample& sample);
static void writeStrokeGeometry(
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth);
static void setMaterialColor(
QSGGeometryNode* node, const QColor& color, qreal alpha);
internal::CircularBuffer* m_line1 = nullptr;
internal::CircularBuffer* m_line2 = nullptr;
QColor m_line1Color;
QColor m_line2Color;
qreal m_line1FillAlpha = 0.15;
qreal m_line2FillAlpha = 0.2;
qreal m_maxValue = 1024.0;
qreal m_slideProgress = 0.0;
int m_historyLength = 30;
qreal m_lineWidth = 2.0;
bool m_sliding = true;
qreal m_smoothing = 0.0;
bool m_curveSmoothing = false;
LineSample m_sample1;
LineSample m_sample2;
bool m_geometryDirty = true;
bool m_colorsDirty = true;
QSGTransformNode* m_transformNode = nullptr;
QSGGeometryNode* m_line1FillNode = nullptr;
QSGGeometryNode* m_line1StrokeNode = nullptr;
QSGGeometryNode* m_line2FillNode = nullptr;
QSGGeometryNode* m_line2StrokeNode = nullptr;
};
} // namespace ZShell::components
+12 -7
View File
@@ -1,17 +1,17 @@
qml_module(ZShell-config qml_module(ZShell-config
URI ZShell.Config URI ZShell.Config
INCLUDE_PREFIX config INCLUDE_PREFIX config
DEPENDENCIES
ZShell.Settings
SOURCES SOURCES
confignode.hpp confignode.cpp common.hpp common.cpp
configobject.hpp configobject.cpp rootnodes.hpp rootnodes.cpp
configlist.hpp configlist.cpp
config.hpp config.cpp
migration.hpp migration.cpp migration.hpp migration.cpp
anim.hpp anim.cpp anim.hpp anim.cpp
fontbuilder.hpp fontbuilder.cpp fontbuilder.hpp fontbuilder.cpp
font.hpp font.cpp font.hpp font.cpp
tokens.hpp tokens.hpp
appearance.hpp appearance.hpp appearance.cpp
background.hpp background.hpp
bar.hpp bar.hpp
clipboard.hpp clipboard.hpp
@@ -30,6 +30,11 @@ qml_module(ZShell-config
services.hpp services.hpp
sidebar.hpp sidebar.hpp
utilities.hpp utilities.hpp
LIBRARIES )
Qt::Gui
target_link_libraries(ZShell-config PUBLIC
Qt::Gui
zshell-util
ZShell
ZShell-settings
) )
+87 -16
View File
@@ -1,32 +1,85 @@
#include "anim.hpp" #include "anim.hpp"
#include "tokens.hpp"
#include <qpoint.h> #include <qpoint.h>
#include "appearance.hpp"
#include "tokens.hpp"
namespace ZShell::config { namespace ZShell::config {
QEasingCurve AnimEasing::buildCurve(const QList<qreal>& points) { AnimTokens::AnimTokens(QObject* parent) : QObject(parent) {}
QEasingCurve AnimTokens::emphasized() const {
return m_emphasized;
}
QEasingCurve AnimTokens::emphasizedAccel() const {
return m_emphasizedAccel;
}
QEasingCurve AnimTokens::emphasizedDecel() const {
return m_emphasizedDecel;
}
QEasingCurve AnimTokens::standard() const {
return m_standard;
}
QEasingCurve AnimTokens::standardAccel() const {
return m_standardAccel;
}
QEasingCurve AnimTokens::standardDecel() const {
return m_standardDecel;
}
QEasingCurve AnimTokens::expressiveFastSpatial() const {
return m_expressiveFastSpatial;
}
QEasingCurve AnimTokens::expressiveDefaultSpatial() const {
return m_expressiveDefaultSpatial;
}
QEasingCurve AnimTokens::expressiveSlowSpatial() const {
return m_expressiveSlowSpatial;
}
QEasingCurve AnimTokens::expressiveFastEffects() const {
return m_expressiveFastEffects;
}
QEasingCurve AnimTokens::expressiveDefaultEffects() const {
return m_expressiveDefaultEffects;
}
QEasingCurve AnimTokens::expressiveSlowEffects() const {
return m_expressiveSlowEffects;
}
AnimDurations* AnimTokens::durations() const {
return m_durations;
}
QEasingCurve AnimTokens::buildCurve(const QList<qreal>& points) {
QEasingCurve curve(QEasingCurve::BezierSpline); QEasingCurve curve(QEasingCurve::BezierSpline);
// Points come in pairs of (x, y) forming cubic bezier segments.
// Each segment needs 3 control points: c1, c2, endPoint.
// So 6 values per segment: c1x, c1y, c2x, c2y, endX, endY.
for (int i = 0; i + 5 < points.size(); i += 6) { for (int i = 0; i + 5 < points.size(); i += 6) {
QPointF c1(points[i], points[i + 1]); const QPointF c1(points[i], points[i + 1]);
QPointF c2(points[i + 2], points[i + 3]); const QPointF c2(points[i + 2], points[i + 3]);
QPointF end(points[i + 4], points[i + 5]); const QPointF end(points[i + 4], points[i + 5]);
curve.addCubicBezierSegment(c1, c2, end); curve.addCubicBezierSegment(c1, c2, end);
} }
return curve; return curve;
} }
void AnimEasing::bindCurves(AnimCurves* curves) { void AnimTokens::rebuildCurves() {
m_curves = curves;
connect(
curves,
&AnimCurves::propertiesChanged,
this,
&AnimEasing::rebuildCurves);
rebuildCurves();
}
void AnimEasing::rebuildCurves() {
if (!m_curves) return; if (!m_curves) return;
m_emphasized = buildCurve(m_curves->emphasized()); m_emphasized = buildCurve(m_curves->emphasized());
m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel()); m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel());
m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel()); m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel());
@@ -41,7 +94,25 @@ void AnimEasing::rebuildCurves() {
m_expressiveDefaultEffects = m_expressiveDefaultEffects =
buildCurve(m_curves->expressiveDefaultEffects()); buildCurve(m_curves->expressiveDefaultEffects());
m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects()); m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects());
emit curvesChanged(); emit curvesChanged();
} }
void AnimTokens::bindCurves(AnimCurves* curves) {
m_curves = curves;
// Rebuild when any curve control points change
connect(
curves, &AnimCurves::optionChanged, this, &AnimTokens::rebuildCurves);
rebuildCurves();
}
void AnimTokens::bindDurations(AnimDurations* durations) {
if (m_durations == durations) return;
m_durations = durations;
emit durationsChanged();
}
} // namespace ZShell::config } // namespace ZShell::config
+38 -37
View File
@@ -9,8 +9,10 @@ namespace ZShell::config {
class AnimCurves; class AnimCurves;
class AnimDurations; class AnimDurations;
class AnimEasing : public QObject { class AnimTokens : public QObject {
Q_OBJECT Q_OBJECT
Q_MOC_INCLUDE("tokens.hpp") // AnimCurves
Q_MOC_INCLUDE("appearance.hpp")
QML_ANONYMOUS QML_ANONYMOUS
Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged)
@@ -42,54 +44,53 @@ class AnimEasing : public QObject {
QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY
curvesChanged) curvesChanged)
Q_PROPERTY(
ZShell::config::AnimDurations* durations READ durations NOTIFY
durationsChanged)
public: public:
explicit AnimEasing(QObject* parent = nullptr) : QObject(parent) {} explicit AnimTokens(QObject* parent = nullptr);
void bindCurves(AnimCurves* curves); void bindCurves(AnimCurves* curves);
void bindDurations(AnimDurations* durations);
[[nodiscard]] QEasingCurve emphasized() const { return m_emphasized; } [[nodiscard]] QEasingCurve emphasized() const;
[[nodiscard]] QEasingCurve emphasizedAccel() const { [[nodiscard]] QEasingCurve emphasizedAccel() const;
return m_emphasizedAccel; [[nodiscard]] QEasingCurve emphasizedDecel() const;
} [[nodiscard]] QEasingCurve standard() const;
[[nodiscard]] QEasingCurve emphasizedDecel() const { [[nodiscard]] QEasingCurve standardAccel() const;
return m_emphasizedDecel; [[nodiscard]] QEasingCurve standardDecel() const;
} [[nodiscard]] QEasingCurve expressiveFastSpatial() const;
[[nodiscard]] QEasingCurve standard() const { return m_standard; } [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const;
[[nodiscard]] QEasingCurve standardAccel() const { return m_standardAccel; } [[nodiscard]] QEasingCurve expressiveSlowSpatial() const;
[[nodiscard]] QEasingCurve standardDecel() const { return m_standardDecel; } [[nodiscard]] QEasingCurve expressiveFastEffects() const;
[[nodiscard]] QEasingCurve expressiveFastSpatial() const { [[nodiscard]] QEasingCurve expressiveDefaultEffects() const;
return m_expressiveFastSpatial; [[nodiscard]] QEasingCurve expressiveSlowEffects() const;
} [[nodiscard]] AnimDurations* durations() const;
[[nodiscard]] QEasingCurve expressiveDefaultSpatial() const {
return m_expressiveDefaultSpatial;
}
[[nodiscard]] QEasingCurve expressiveSlowSpatial() const {
return m_expressiveSlowSpatial;
}
[[nodiscard]] QEasingCurve expressiveFastEffects() const {
return m_expressiveFastEffects;
}
[[nodiscard]] QEasingCurve expressiveDefaultEffects() const {
return m_expressiveDefaultEffects;
}
[[nodiscard]] QEasingCurve expressiveSlowEffects() const {
return m_expressiveSlowEffects;
}
signals: signals:
void curvesChanged(); void curvesChanged();
void durationsChanged();
private: private:
static QEasingCurve buildCurve(const QList<qreal>& points);
void rebuildCurves(); void rebuildCurves();
static QEasingCurve buildCurve(const QList<qreal>& points);
AnimCurves* m_curves = nullptr; AnimCurves* m_curves = nullptr;
QEasingCurve m_emphasized, m_emphasizedAccel, m_emphasizedDecel; AnimDurations* m_durations = nullptr;
QEasingCurve m_standard, m_standardAccel, m_standardDecel;
QEasingCurve m_expressiveFastSpatial, m_expressiveDefaultSpatial, QEasingCurve m_emphasized;
m_expressiveSlowSpatial; QEasingCurve m_emphasizedAccel;
QEasingCurve m_expressiveFastEffects, m_expressiveDefaultEffects, QEasingCurve m_emphasizedDecel;
m_expressiveSlowEffects; QEasingCurve m_standard;
QEasingCurve m_standardAccel;
QEasingCurve m_standardDecel;
QEasingCurve m_expressiveFastSpatial;
QEasingCurve m_expressiveDefaultSpatial;
QEasingCurve m_expressiveSlowSpatial;
QEasingCurve m_expressiveFastEffects;
QEasingCurve m_expressiveDefaultEffects;
QEasingCurve m_expressiveSlowEffects;
}; };
} // namespace ZShell::config } // namespace ZShell::config
+229
View File
@@ -0,0 +1,229 @@
#include "appearance.hpp"
#include "tokens.hpp"
namespace ZShell::config {
namespace {
// Helper: connect all changed signals from a token object to a single valuesChanged signal,
// plus connect the local scaleChanged signal.
template <typename Source, typename Target>
void connectTokenSignals(Source* source, Target* target) {
const auto* meta = source->metaObject();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
if (prop.hasNotifySignal())
QObject::connect(
source,
prop.notifySignal(),
target,
target->metaObject()->method(
target->metaObject()->indexOfSignal("valuesChanged()")));
}
QObject::connect(
target, &Target::scaleChanged, target, &Target::valuesChanged);
}
} // namespace
// AppearanceRounding
void AppearanceRounding::bindTokens(RoundingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearanceRounding::extraSmall() const {
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
}
int AppearanceRounding::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearanceRounding::medium() const {
return m_tokens ? static_cast<int>(m_tokens->medium() * m_scale) : 0;
}
int AppearanceRounding::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AppearanceRounding::largeIncreased() const {
return m_tokens ? static_cast<int>(m_tokens->largeIncreased() * m_scale)
: 0;
}
int AppearanceRounding::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AppearanceRounding::extraLargeIncreased() const {
return m_tokens
? static_cast<int>(m_tokens->extraLargeIncreased() * m_scale)
: 0;
}
int AppearanceRounding::extraExtraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraExtraLarge() * m_scale)
: 0;
}
int AppearanceRounding::full() const {
return m_tokens ? m_tokens->full() : 0;
}
// AppearanceSpacing
void AppearanceSpacing::bindTokens(SpacingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearanceSpacing::extraSmall() const {
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
}
int AppearanceSpacing::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearanceSpacing::medium() const {
return m_tokens ? static_cast<int>(m_tokens->medium() * m_scale) : 0;
}
int AppearanceSpacing::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AppearanceSpacing::largeIncreased() const {
return m_tokens ? static_cast<int>(m_tokens->largeIncreased() * m_scale)
: 0;
}
int AppearanceSpacing::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AppearanceSpacing::extraLargeIncreased() const {
return m_tokens
? static_cast<int>(m_tokens->extraLargeIncreased() * m_scale)
: 0;
}
int AppearanceSpacing::extraExtraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraExtraLarge() * m_scale)
: 0;
}
// AppearancePadding
void AppearancePadding::bindTokens(PaddingTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AppearancePadding::extraSmall() const {
return m_tokens ? static_cast<int>(m_tokens->extraSmall() * m_scale) : 0;
}
int AppearancePadding::smaller() const {
return m_tokens ? static_cast<int>(m_tokens->smaller() * m_scale) : 0;
}
int AppearancePadding::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AppearancePadding::medium() const {
return m_tokens ? static_cast<int>(m_tokens->medium() * m_scale) : 0;
}
int AppearancePadding::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AppearancePadding::largeIncreased() const {
return m_tokens ? static_cast<int>(m_tokens->largeIncreased() * m_scale)
: 0;
}
int AppearancePadding::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AppearancePadding::extraLargeIncreased() const {
return m_tokens
? static_cast<int>(m_tokens->extraLargeIncreased() * m_scale)
: 0;
}
int AppearancePadding::extraExtraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraExtraLarge() * m_scale)
: 0;
}
// AnimDurations
void AnimDurations::bindTokens(AnimDurationTokens* tokens) {
m_tokens = tokens;
connectTokenSignals(tokens, this);
}
int AnimDurations::small() const {
return m_tokens ? static_cast<int>(m_tokens->small() * m_scale) : 0;
}
int AnimDurations::normal() const {
return m_tokens ? static_cast<int>(m_tokens->normal() * m_scale) : 0;
}
int AnimDurations::large() const {
return m_tokens ? static_cast<int>(m_tokens->large() * m_scale) : 0;
}
int AnimDurations::extraLarge() const {
return m_tokens ? static_cast<int>(m_tokens->extraLarge() * m_scale) : 0;
}
int AnimDurations::expressiveFastSpatial() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveFastSpatial() * m_scale)
: 0;
}
int AnimDurations::expressiveDefaultSpatial() const {
return m_tokens ? static_cast<int>(
m_tokens->expressiveDefaultSpatial() * m_scale)
: 0;
}
int AnimDurations::expressiveSlowSpatial() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveSlowSpatial() * m_scale)
: 0;
}
int AnimDurations::expressiveFastEffects() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveFastEffects() * m_scale)
: 0;
}
int AnimDurations::expressiveDefaultEffects() const {
return m_tokens ? static_cast<int>(
m_tokens->expressiveDefaultEffects() * m_scale)
: 0;
}
int AnimDurations::expressiveSlowEffects() const {
return m_tokens
? static_cast<int>(m_tokens->expressiveSlowEffects() * m_scale)
: 0;
}
} // namespace ZShell::config
+203 -273
View File
@@ -1,101 +1,126 @@
#pragma once #pragma once
#include "configobject.hpp" #include <qfont.h>
#include <QFont> #include <qstring.h>
#include <QVariantMap> #include <qvariantmap.h>
#include <limits>
#include <qqmlregistration.h> #include "settings/objectnode.hpp"
#include "common.hpp"
namespace ZShell::config { namespace ZShell::config {
class AnimDurations : public ConfigObject { using Qt::StringLiterals::operator""_s;
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(qreal, scale, 1) // Forward declare token types from tokens.hpp
class RoundingTokens;
class SpacingTokens;
class PaddingTokens;
class AnimDurationTokens;
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged) class AppearanceRounding : public settings::ObjectNode {
CONFIG_NODE(AppearanceRounding, settings::ObjectNode)
CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int medium READ medium NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY( Q_PROPERTY(
int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
scaleChanged) Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
Q_PROPERTY( Q_PROPERTY(int full READ full NOTIFY valuesChanged)
int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY
scaleChanged)
Q_PROPERTY(
int expressiveFastEffects READ expressiveFastEffects NOTIFY scaleChanged)
Q_PROPERTY(
int expressiveFastSpatial READ expressiveFastSpatial NOTIFY scaleChanged)
Q_PROPERTY(
int expressiveSlowEffects READ expressiveSlowEffects NOTIFY scaleChanged)
Q_PROPERTY(
int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int normal READ normal NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
public: public:
explicit AnimDurations(QObject* parent = nullptr) : ConfigObject(parent) {} void bindTokens(RoundingTokens* tokens);
[[nodiscard]] int small() const { return static_cast<int>(200 * scale()); } [[nodiscard]] int extraSmall() const;
[[nodiscard]] int normal() const { return static_cast<int>(400 * scale()); } [[nodiscard]] int small() const;
[[nodiscard]] int large() const { return static_cast<int>(600 * scale()); } [[nodiscard]] int medium() const;
[[nodiscard]] int extraLarge() const { [[nodiscard]] int large() const;
return static_cast<int>(1000 * scale()); [[nodiscard]] int largeIncreased() const;
} [[nodiscard]] int extraLarge() const;
[[nodiscard]] int expressiveFastSpatial() const { [[nodiscard]] int extraLargeIncreased() const;
return static_cast<int>(350 * scale()); [[nodiscard]] int extraExtraLarge() const;
} [[nodiscard]] int full() const;
[[nodiscard]] int expressiveDefaultSpatial() const {
return static_cast<int>(500 * scale()); signals:
} void valuesChanged();
[[nodiscard]] int expressiveSlowSpatial() const {
return static_cast<int>(650 * scale()); private:
} RoundingTokens* m_tokens = nullptr;
[[nodiscard]] int expressiveFastEffects() const {
return static_cast<int>(150 * scale());
}
[[nodiscard]] int expressiveDefaultEffects() const {
return static_cast<int>(200 * scale());
}
[[nodiscard]] int expressiveSlowEffects() const {
return static_cast<int>(300 * scale());
}
}; };
class Anim : public ConfigObject { class AppearanceSpacing : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(AppearanceSpacing, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, mediaGifSpeedAdjustment, 300) CONFIG_PROPERTY(qreal, scale, 1)
CFG_PROPERTY(qreal, sessionGifSpeed, 0.7)
CONFIG_SUBOBJECT(AnimDurations, durations) Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int medium READ medium NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
public: public:
explicit Anim(QObject* parent = nullptr) void bindTokens(SpacingTokens* tokens);
: ConfigObject(parent), m_durations(new AnimDurations(this)) {}
[[nodiscard]] int extraSmall() const;
[[nodiscard]] int small() const;
[[nodiscard]] int medium() const;
[[nodiscard]] int large() const;
[[nodiscard]] int largeIncreased() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int extraLargeIncreased() const;
[[nodiscard]] int extraExtraLarge() const;
signals:
void valuesChanged();
private:
SpacingTokens* m_tokens = nullptr;
}; };
class Deform : public ConfigObject { class AppearancePadding : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(AppearancePadding, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(qreal, scale, 1) CONFIG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged)
Q_PROPERTY(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int medium READ medium NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
public: public:
explicit Deform(QObject* parent = nullptr) : ConfigObject(parent) {} void bindTokens(PaddingTokens* tokens);
};
class FontFamily : public ConfigObject { [[nodiscard]] int extraSmall() const;
Q_OBJECT [[nodiscard]] int smaller() const;
QML_ANONYMOUS [[nodiscard]] int small() const;
[[nodiscard]] int medium() const;
[[nodiscard]] int large() const;
[[nodiscard]] int largeIncreased() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int extraLargeIncreased() const;
[[nodiscard]] int extraExtraLarge() const;
CFG_PROPERTY(QString, clock, QStringLiteral("Rubik")) signals:
CFG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) void valuesChanged();
CFG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF"))
CFG_PROPERTY(QString, sans, QStringLiteral("Segoe UI Variable Text"))
public: private:
explicit FontFamily(QObject* parent = nullptr) : ConfigObject(parent) {} PaddingTokens* m_tokens = nullptr;
}; };
namespace detail { namespace detail {
@@ -105,20 +130,19 @@ struct FontConfig {
int size; int size;
int weight = QFont::Normal; int weight = QFont::Normal;
bool italic = false; bool italic = false;
QVariantMap vaxes = {{QStringLiteral("ROND"), 25}}; QVariantMap vaxes = {{u"ROND"_s, 25}};
}; };
} // namespace detail } // namespace detail
class FontSizeNode : public ConfigObject { class FontSizeNode : public settings::ObjectNode {
public: CONFIG_NODE(FontSizeNode, settings::ObjectNode)
explicit FontSizeNode(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class FontStyleNode : public ConfigObject { class FontStyleNode : public settings::ObjectNode {
public: CONFIG_NODE(FontStyleNode, settings::ObjectNode)
explicit FontStyleNode(QObject* parent = nullptr) : ConfigObject(parent) {}
public:
virtual FontSizeNode* largeNode() const = 0; virtual FontSizeNode* largeNode() const = 0;
virtual FontSizeNode* mediumNode() const = 0; virtual FontSizeNode* mediumNode() const = 0;
virtual FontSizeNode* smallNode() const = 0; virtual FontSizeNode* smallNode() const = 0;
@@ -129,17 +153,16 @@ class FontStyleNode : public ConfigObject {
#define FONT(...) ZShell::config::detail::FontConfig __VA_ARGS__ #define FONT(...) ZShell::config::detail::FontConfig __VA_ARGS__
#define FONT_CONFIG(Style, Size, props) \ #define FONT_CONFIG(Style, Size, props) \
class FontConfig##Style##Size : public FontSizeNode { \ class FontConfig##Style##Size : public FontSizeNode { \
Q_OBJECT QML_ANONYMOUS \ CONFIG_NODE(FontConfig##Style##Size, FontSizeNode) \
\ \
CFG_PROPERTY(QString, family, ARG(props).family) \ CONFIG_PROPERTY( \
CFG_PROPERTY(int, size, ARG(props).size) \ QString, \
CFG_PROPERTY(int, weight, ARG(props).weight) \ family, \
CFG_PROPERTY(bool, italic, ARG(props).italic) \ ARG(props).family) /* Empty inherits the style family */ \
CFG_PROPERTY(QVariantMap, vaxes, ARG(props).vaxes) \ CONFIG_PROPERTY(int, size, ARG(props).size) \
\ CONFIG_PROPERTY(int, weight, ARG(props).weight) \
public \ CONFIG_PROPERTY(bool, italic, ARG(props).italic) \
: explicit FontConfig##Style##Size(QObject* parent = nullptr) \ CONFIG_PROPERTY(QVariantMap, vaxes, ARG(props).vaxes) \
: FontSizeNode(parent) {} \
}; };
#define FONT_STYLE(Style, family_, large_, medium_, small_) \ #define FONT_STYLE(Style, family_, large_, medium_, small_) \
FONT_CONFIG(Style, Large, ARG(large_)) \ FONT_CONFIG(Style, Large, ARG(large_)) \
@@ -147,47 +170,41 @@ class FontStyleNode : public ConfigObject {
FONT_CONFIG(Style, Small, ARG(small_)) \ FONT_CONFIG(Style, Small, ARG(small_)) \
\ \
class FontStyle##Style : public FontStyleNode { \ class FontStyle##Style : public FontStyleNode { \
Q_OBJECT QML_ANONYMOUS \ CONFIG_NODE(FontStyle##Style, FontStyleNode) \
\ \
CFG_PROPERTY(QString, family, family_) \ CONFIG_PROPERTY(QString, family, family_) \
CONFIG_SUBOBJECT(FontConfig##Style##Large, large) \ CONFIG_SUBOBJECT(FontConfig##Style##Large, large) \
CONFIG_SUBOBJECT(FontConfig##Style##Medium, medium) \ CONFIG_SUBOBJECT(FontConfig##Style##Medium, medium) \
CONFIG_SUBOBJECT(FontConfig##Style##Small, small) \ CONFIG_SUBOBJECT(FontConfig##Style##Small, small) \
\ \
public \ public: \
: explicit FontStyle##Style(QObject* parent = nullptr) \
: FontStyleNode(parent) \
, m_large(new FontConfig##Style##Large(this)) \
, m_medium(new FontConfig##Style##Medium(this)) \
, m_small(new FontConfig##Style##Small(this)) {} \
\
FontSizeNode* largeNode() const override { return m_large; } \ FontSizeNode* largeNode() const override { return m_large; } \
FontSizeNode* mediumNode() const override { return m_medium; } \ FontSizeNode* mediumNode() const override { return m_medium; } \
FontSizeNode* smallNode() const override { return m_small; } \ FontSizeNode* smallNode() const override { return m_small; } \
}; };
// clang-format off // clang-format off
FONT_STYLE(Headline, QStringLiteral("GoogleSansFlex"), FONT_STYLE(Headline, u"GoogleSansFlex"_s,
FONT({ .size = 32, .weight = QFont::Medium }), FONT({ .size = 32, .weight = QFont::Medium }),
FONT({ .size = 28, .weight = QFont::Medium }), FONT({ .size = 28, .weight = QFont::Medium }),
FONT({ .size = 24, .weight = QFont::Medium }) FONT({ .size = 24, .weight = QFont::Medium })
) )
FONT_STYLE(Title, QStringLiteral("GoogleSansFlex"), FONT_STYLE(Title, u"GoogleSansFlex"_s,
FONT({ .size = 22, .weight = QFont::Medium }), FONT({ .size = 22, .weight = QFont::Medium }),
FONT({ .size = 16, .weight = QFont::Medium }), FONT({ .size = 16, .weight = QFont::Medium }),
FONT({ .size = 14, .weight = QFont::Medium }) FONT({ .size = 14, .weight = QFont::Medium })
) )
FONT_STYLE(Body, QStringLiteral("GoogleSansFlex"), FONT_STYLE(Body, u"GoogleSansFlex"_s,
FONT({ .size = 16 }), FONT({ .size = 16 }),
FONT({ .size = 14 }), FONT({ .size = 14 }),
FONT({ .size = 12 }) FONT({ .size = 12 })
) )
FONT_STYLE(Label, QStringLiteral("GoogleSansFlex"), FONT_STYLE(Label, u"GoogleSansFlex"_s,
FONT({ .size = 14, .weight = QFont::Medium }), FONT({ .size = 14, .weight = QFont::Medium }),
FONT({ .size = 12, .weight = QFont::Medium }), FONT({ .size = 12, .weight = QFont::Medium }),
FONT({ .size = 11 }) FONT({ .size = 11 })
) )
FONT_STYLE(Mono, QStringLiteral("CaskaydiaCove NF"), FONT_STYLE(Mono, u"CaskaydiaCove NF"_s,
FONT({ .size = 16, .vaxes = {} }), FONT({ .size = 16, .vaxes = {} }),
FONT({ .size = 14, .vaxes = {} }), FONT({ .size = 14, .vaxes = {} }),
FONT({ .size = 12, .vaxes = {} }) FONT({ .size = 12, .vaxes = {} })
@@ -200,23 +217,15 @@ FONT_CONFIG(Icon, Medium, FONT({.size = static_cast<int>(24 / 1.33)}))
FONT_CONFIG(Icon, Small, FONT({.size = static_cast<int>(20 / 1.33)})) FONT_CONFIG(Icon, Small, FONT({.size = static_cast<int>(20 / 1.33)}))
class FontStyleIcon : public FontStyleNode { class FontStyleIcon : public FontStyleNode {
Q_OBJECT CONFIG_NODE(FontStyleIcon, FontStyleNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, family, QStringLiteral("Material Symbols Rounded")) CONFIG_PROPERTY(QString, family, u"Material Symbols Rounded"_s)
CONFIG_SUBOBJECT(FontConfigIconExtraLarge, extraLarge) CONFIG_SUBOBJECT(FontConfigIconExtraLarge, extraLarge)
CONFIG_SUBOBJECT(FontConfigIconLarge, large) CONFIG_SUBOBJECT(FontConfigIconLarge, large)
CONFIG_SUBOBJECT(FontConfigIconMedium, medium) CONFIG_SUBOBJECT(FontConfigIconMedium, medium)
CONFIG_SUBOBJECT(FontConfigIconSmall, small) CONFIG_SUBOBJECT(FontConfigIconSmall, small)
public: public:
explicit FontStyleIcon(QObject* parent = nullptr)
: FontStyleNode(parent)
, m_extraLarge(new FontConfigIconExtraLarge(this))
, m_large(new FontConfigIconLarge(this))
, m_medium(new FontConfigIconMedium(this))
, m_small(new FontConfigIconSmall(this)) {}
FontSizeNode* extraLargeNode() const override { return m_extraLarge; } FontSizeNode* extraLargeNode() const override { return m_extraLarge; }
FontSizeNode* largeNode() const override { return m_large; } FontSizeNode* largeNode() const override { return m_large; }
FontSizeNode* mediumNode() const override { return m_medium; } FontSizeNode* mediumNode() const override { return m_medium; }
@@ -228,11 +237,20 @@ class FontStyleIcon : public FontStyleNode {
#undef FONT_CONFIG #undef FONT_CONFIG
#undef FONT_STYLE #undef FONT_STYLE
class AppearanceFont : public ConfigObject { class FontFamily : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(FontFamily, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(qreal, scale, 1) CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CONFIG_PROPERTY(
QString, material, QStringLiteral("Material Symbols Rounded"))
CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF"))
CONFIG_PROPERTY(QString, sans, QStringLiteral("Segoe UI Variable Text"))
};
class AppearanceFont : public settings::ObjectNode {
CONFIG_NODE(AppearanceFont, settings::ObjectNode)
CONFIG_PROPERTY(qreal, scale, 1)
CONFIG_SUBOBJECT(FontFamily, family) CONFIG_SUBOBJECT(FontFamily, family)
CONFIG_SUBOBJECT(FontStyleHeadline, headline) CONFIG_SUBOBJECT(FontStyleHeadline, headline)
CONFIG_SUBOBJECT(FontStyleTitle, title) CONFIG_SUBOBJECT(FontStyleTitle, title)
@@ -240,160 +258,83 @@ class AppearanceFont : public ConfigObject {
CONFIG_SUBOBJECT(FontStyleLabel, label) CONFIG_SUBOBJECT(FontStyleLabel, label)
CONFIG_SUBOBJECT(FontStyleMono, mono) CONFIG_SUBOBJECT(FontStyleMono, mono)
CONFIG_SUBOBJECT(FontStyleIcon, icon) CONFIG_SUBOBJECT(FontStyleIcon, icon)
CFG_PROPERTY(QString, clock, QStringLiteral("Rubik")) CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CFG_PROPERTY(QString, workspaces, QStringLiteral("Rubik")) CONFIG_PROPERTY(QString, workspaces, QStringLiteral("Rubik"))
public:
explicit AppearanceFont(QObject* parent = nullptr)
: ConfigObject(parent)
, m_family(new FontFamily(this))
, m_headline(new FontStyleHeadline(this))
, m_title(new FontStyleTitle(this))
, m_body(new FontStyleBody(this))
, m_label(new FontStyleLabel(this))
, m_mono(new FontStyleMono(this))
, m_icon(new FontStyleIcon(this)) {}
}; };
class Transparency : public ConfigObject { class AnimDurations : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(AnimDurations, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(qreal, base, 0.85) CONFIG_GLOBAL_PROPERTY(qreal, scale, 1)
CFG_PROPERTY(bool, enabled, false)
CFG_PROPERTY(qreal, layers, 0.4)
public: Q_PROPERTY(int small READ small NOTIFY valuesChanged)
explicit Transparency(QObject* parent = nullptr) : ConfigObject(parent) {} Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
}; Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
class AppearanceRounding : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int medium READ medium NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY scaleChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged)
Q_PROPERTY( Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY scaleChanged) int expressiveFastSpatial READ expressiveFastSpatial NOTIFY
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY scaleChanged) valuesChanged)
Q_PROPERTY(int full READ full CONSTANT)
public:
explicit AppearanceRounding(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] int extraSmall() const {
return static_cast<int>(4 * scale());
}
[[nodiscard]] int small() const { return static_cast<int>(8 * scale()); }
[[nodiscard]] int medium() const { return static_cast<int>(12 * scale()); }
[[nodiscard]] int large() const { return static_cast<int>(16 * scale()); }
[[nodiscard]] int largeIncreased() const {
return static_cast<int>(20 * scale());
}
[[nodiscard]] int extraLarge() const {
return static_cast<int>(28 * scale());
}
[[nodiscard]] int extraLargeIncreased() const {
return static_cast<int>(32 * scale());
}
[[nodiscard]] int extraExtraLarge() const {
return static_cast<int>(48 * scale());
}
[[nodiscard]] int full() const { return std::numeric_limits<int>::max(); }
};
class AppearanceSpacing : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int medium READ medium NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY scaleChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged)
Q_PROPERTY( Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY scaleChanged) int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY scaleChanged) valuesChanged)
public:
explicit AppearanceSpacing(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] int extraSmall() const {
return static_cast<int>(4 * scale());
}
[[nodiscard]] int small() const { return static_cast<int>(8 * scale()); }
[[nodiscard]] int medium() const { return static_cast<int>(12 * scale()); }
[[nodiscard]] int large() const { return static_cast<int>(16 * scale()); }
[[nodiscard]] int largeIncreased() const {
return static_cast<int>(20 * scale());
}
[[nodiscard]] int extraLarge() const {
return static_cast<int>(28 * scale());
}
[[nodiscard]] int extraLargeIncreased() const {
return static_cast<int>(32 * scale());
}
[[nodiscard]] int extraExtraLarge() const {
return static_cast<int>(48 * scale());
}
};
class AppearancePadding : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(qreal, scale, 1)
Q_PROPERTY(int extraSmall READ extraSmall NOTIFY scaleChanged)
Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged)
Q_PROPERTY(int small READ small NOTIFY scaleChanged)
Q_PROPERTY(int medium READ medium NOTIFY scaleChanged)
Q_PROPERTY(int large READ large NOTIFY scaleChanged)
Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY scaleChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged)
Q_PROPERTY( Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY scaleChanged) int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY scaleChanged) valuesChanged)
Q_PROPERTY(
int expressiveFastEffects READ expressiveFastEffects NOTIFY
valuesChanged)
Q_PROPERTY(
int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY
valuesChanged)
Q_PROPERTY(
int expressiveSlowEffects READ expressiveSlowEffects NOTIFY
valuesChanged)
public: public:
explicit AppearancePadding(QObject* parent = nullptr) void bindTokens(AnimDurationTokens* tokens);
: ConfigObject(parent) {}
[[nodiscard]] int extraSmall() const { [[nodiscard]] int small() const;
return static_cast<int>(4 * scale()); [[nodiscard]] int normal() const;
} [[nodiscard]] int large() const;
[[nodiscard]] int smaller() const { return static_cast<int>(5 * scale()); } [[nodiscard]] int extraLarge() const;
[[nodiscard]] int small() const { return static_cast<int>(8 * scale()); } [[nodiscard]] int expressiveFastSpatial() const;
[[nodiscard]] int medium() const { return static_cast<int>(12 * scale()); } [[nodiscard]] int expressiveDefaultSpatial() const;
[[nodiscard]] int large() const { return static_cast<int>(16 * scale()); } [[nodiscard]] int expressiveSlowSpatial() const;
[[nodiscard]] int largeIncreased() const { [[nodiscard]] int expressiveFastEffects() const;
return static_cast<int>(20 * scale()); [[nodiscard]] int expressiveDefaultEffects() const;
} [[nodiscard]] int expressiveSlowEffects() const;
[[nodiscard]] int extraLarge() const {
return static_cast<int>(28 * scale()); signals:
} void valuesChanged();
[[nodiscard]] int extraLargeIncreased() const {
return static_cast<int>(32 * scale()); private:
} AnimDurationTokens* m_tokens = nullptr;
[[nodiscard]] int extraExtraLarge() const {
return static_cast<int>(48 * scale());
}
}; };
class Appearance : public ConfigObject { class Anim : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Anim, settings::ObjectNode)
QML_ANONYMOUS
CONFIG_PROPERTY(int, mediaGifSpeedAdjustment, 300)
CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7)
CONFIG_SUBOBJECT(AnimDurations, durations)
};
class Deform : public settings::ObjectNode {
CONFIG_NODE(Deform, settings::ObjectNode)
CONFIG_PROPERTY(qreal, scale, 1)
};
class AppearanceTransparency : public settings::ObjectNode {
CONFIG_NODE(AppearanceTransparency, settings::ObjectNode)
CONFIG_GLOBAL_PROPERTY(bool, enabled, false)
CONFIG_GLOBAL_PROPERTY(qreal, base, 0.85)
CONFIG_GLOBAL_PROPERTY(qreal, layers, 0.4)
};
class Appearance : public settings::ObjectNode {
CONFIG_NODE(Appearance, settings::ObjectNode)
CONFIG_SUBOBJECT(Anim, anim) CONFIG_SUBOBJECT(Anim, anim)
CONFIG_SUBOBJECT(Deform, deform) CONFIG_SUBOBJECT(Deform, deform)
@@ -401,18 +342,7 @@ class Appearance : public ConfigObject {
CONFIG_SUBOBJECT(AppearanceRounding, rounding) CONFIG_SUBOBJECT(AppearanceRounding, rounding)
CONFIG_SUBOBJECT(AppearanceSpacing, spacing) CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
CONFIG_SUBOBJECT(AppearancePadding, padding) CONFIG_SUBOBJECT(AppearancePadding, padding)
CONFIG_SUBOBJECT(Transparency, transparency) CONFIG_SUBOBJECT(AppearanceTransparency, transparency)
public:
explicit Appearance(QObject* parent = nullptr)
: ConfigObject(parent)
, m_anim(new Anim(this))
, m_deform(new Deform(this))
, m_font(new AppearanceFont(this))
, m_rounding(new AppearanceRounding(this))
, m_spacing(new AppearanceSpacing(this))
, m_padding(new AppearancePadding(this))
, m_transparency(new Transparency(this)) {};
}; };
} // namespace ZShell::config } // namespace ZShell::config
+5 -9
View File
@@ -1,18 +1,14 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class Background : public ConfigObject { class Background : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Background, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, wallFadeDuration, 300) CONFIG_PROPERTY(int, wallFadeDuration, 300)
public:
explicit Background(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+26 -46
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "configlist.hpp" #include "common.hpp"
#include "configobject.hpp"
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qhashfunctions.h> #include <qhashfunctions.h>
#include <qqmlregistration.h> #include <qqmlregistration.h>
@@ -9,66 +8,53 @@
namespace ZShell::config { namespace ZShell::config {
class BarEntry : public ListEntry { class BarEntry : public ListEntry {
Q_OBJECT CONFIG_NODE(BarEntry, ListEntry)
QML_ANONYMOUS
public:
explicit BarEntry(QObject* parent = nullptr) : ListEntry(parent) {}
}; };
class Popouts : public ConfigObject { class Popouts : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Popouts, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, statusIcons, true) CONFIG_PROPERTY(bool, statusIcons, true)
CFG_PROPERTY(bool, tray, true) CONFIG_PROPERTY(bool, tray, true)
public:
explicit Popouts(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Tray : public ConfigObject { class Tray : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Tray, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, recolorIcons, false) CONFIG_PROPERTY(bool, recolorIcons, false)
CFG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(bool, showOnHover, true)
CFG_PROPERTY(int, trayIconSize, 24) CONFIG_PROPERTY(int, trayIconSize, 24)
CONFIG_LIST( CONFIG_LIST(
EntryList, EntryList,
statusIcons, statusIcons,
{ DEFAULT_ARG({
LIST_ENTRY(audio, true), LIST_ENTRY(audio, true),
LIST_ENTRY(microphone, true), LIST_ENTRY(microphone, true),
LIST_ENTRY(bluetooth, false), LIST_ENTRY(bluetooth, false),
LIST_ENTRY(network, false), LIST_ENTRY(network, false),
LIST_ENTRY(wifi, false), LIST_ENTRY(wifi, false),
LIST_ENTRY(power, true), LIST_ENTRY(power, true),
}) }))
public:
explicit Tray(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Bar : public ConfigObject { class Bar : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Bar, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, autoHide, false) CONFIG_PROPERTY(bool, autoHide, false)
CFG_PROPERTY(int, border, 4) CONFIG_PROPERTY(int, border, 4)
CFG_PROPERTY(bool, fullscreenReveal, true) CONFIG_PROPERTY(bool, fullscreenReveal, true)
CFG_PROPERTY(int, height, 24) CONFIG_PROPERTY(int, height, 24)
CFG_PROPERTY(bool, hideWhenNotif, true) CONFIG_PROPERTY(bool, hideWhenNotif, true)
CONFIG_SUBOBJECT(Popouts, popouts) CONFIG_SUBOBJECT(Popouts, popouts)
CFG_PROPERTY(int, revealDelay, 100) CONFIG_PROPERTY(int, revealDelay, 100)
CFG_PROPERTY(int, rounding, 14) CONFIG_PROPERTY(int, rounding, 14)
CFG_PROPERTY(int, smoothing, 32) CONFIG_PROPERTY(int, smoothing, 32)
CFG_PROPERTY(QString, position, QStringLiteral("top")) CONFIG_PROPERTY(QString, position, QStringLiteral("top"))
CONFIG_SUBOBJECT(Tray, tray) CONFIG_SUBOBJECT(Tray, tray)
CONFIG_LIST( CONFIG_LIST(
EntryList, EntryList,
entries, entries,
{ DEFAULT_ARG({
LIST_ENTRY(workspaces, true), LIST_ENTRY(workspaces, true),
LIST_ENTRY(audio, true), LIST_ENTRY(audio, true),
LIST_ENTRY(media, true), LIST_ENTRY(media, true),
@@ -85,13 +71,7 @@ class Bar : public ConfigObject {
LIST_ENTRY(upower, true), LIST_ENTRY(upower, true),
LIST_ENTRY(clock, true), LIST_ENTRY(clock, true),
LIST_ENTRY(notifBell, true), LIST_ENTRY(notifBell, true),
}) }))
public:
explicit Bar(QObject* parent = nullptr)
: ConfigObject(parent)
, m_popouts(new Popouts(this))
, m_tray(new Tray(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+11 -20
View File
@@ -1,33 +1,24 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class ClipboardSizes : public ConfigObject { class ClipboardSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(ClipboardSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, itemHeight, 60) CONFIG_PROPERTY(int, itemHeight, 60)
CFG_PROPERTY(int, minPreviewWidth, 200) CONFIG_PROPERTY(int, minPreviewWidth, 200)
CFG_PROPERTY(int, previewWidth, 800) CONFIG_PROPERTY(int, previewWidth, 800)
CFG_PROPERTY(int, width, 500) CONFIG_PROPERTY(int, width, 500)
public:
explicit ClipboardSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Clipboard : public ConfigObject { class Clipboard : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Clipboard, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, maxEntriesShown, 10) CONFIG_PROPERTY(int, maxEntriesShown, 10)
CONFIG_SUBOBJECT(ClipboardSizes, sizes) CONFIG_SUBOBJECT(ClipboardSizes, sizes)
public:
explicit Clipboard(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new ClipboardSizes(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+16 -25
View File
@@ -1,38 +1,29 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class Presets : public ConfigObject { class Presets : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Presets, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, accent, QString()) CONFIG_PROPERTY(QString, accent, QString())
CFG_PROPERTY(QString, name, QStringLiteral("Catppuccin")) CONFIG_PROPERTY(QString, name, QStringLiteral("Catppuccin"))
CFG_PROPERTY(QString, variant, QStringLiteral("latte")) CONFIG_PROPERTY(QString, variant, QStringLiteral("latte"))
public:
explicit Presets(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Colors : public ConfigObject { class Colors : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Colors, settings::ObjectNode)
QML_ANONYMOUS
CONFIG_SUBOBJECT(Presets, presets) CONFIG_SUBOBJECT(Presets, presets)
CFG_PROPERTY(QString, schemeType, QStringLiteral("fidelity")) CONFIG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
CFG_PROPERTY(bool, scheduleDark, false) CONFIG_PROPERTY(bool, scheduleDark, false)
CFG_PROPERTY(int, scheduleDarkEnd, 600) CONFIG_PROPERTY(int, scheduleDarkEnd, 600)
CFG_PROPERTY(int, scheduleDarkStart, 1140) CONFIG_PROPERTY(int, scheduleDarkStart, 1140)
CFG_PROPERTY(bool, schemeGen, true) CONFIG_PROPERTY(bool, schemeGen, true)
CFG_PROPERTY(bool, neovimColors, false) CONFIG_PROPERTY(bool, neovimColors, false)
CFG_PROPERTY(QString, mode, QStringLiteral("dark")) CONFIG_PROPERTY(QString, mode, QStringLiteral("dark"))
CFG_PROPERTY(bool, smart, false) CONFIG_PROPERTY(bool, smart, false)
public:
explicit Colors(QObject* parent = nullptr)
: ConfigObject(parent), m_presets(new Presets(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+22
View File
@@ -0,0 +1,22 @@
#include "common.hpp"
#include <QDir>
#include <QStandardPaths>
namespace ZShell::config {
Q_LOGGING_CATEGORY(lcConfig, "zshell.config", QtInfoMsg)
QString configDir() {
const QString dir =
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
QStringLiteral("/zshell");
QDir().mkpath(dir);
return dir;
}
QString monitorConfigDir() {
return configDir() + QStringLiteral("/monitors");
}
} // namespace ZShell::config
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <qloggingcategory.h>
#include <qstring.h>
#include "settings/macros.hpp"
#include "settings/objectnode.hpp"
#define CONFIG_NODE_NO_CTOR SETTINGS_NODE_NO_CTOR
#define CONFIG_NODE SETTINGS_NODE
#define CONFIG_PROPERTY SETTINGS_PROPERTY
#define CONFIG_GLOBAL_PROPERTY SETTINGS_GLOBAL_PROPERTY
#define CONFIG_ENUM_PROPERTY(Type, name, defaultVal) \
CONFIG_PROPERTY(ZShell::config::Type::Enum, name, defaultVal)
#define CONFIG_GLOBAL_ENUM_PROPERTY(Type, name, defaultVal) \
CONFIG_GLOBAL_PROPERTY(ZShell::config::Type::Enum, name, defaultVal)
#define CONFIG_SUBOBJECT(Type, name) \
SETTINGS_SUBOBJECT(ZShell::config::Type, name)
#define CONFIG_GLOBAL_SUBOBJECT(Type, name) \
SETTINGS_GLOBAL_SUBOBJECT(ZShell::config::Type, name)
#define CONFIG_LIST_TYPE SETTINGS_LIST_TYPE
#define CONFIG_LIST(Type, name, defaultVal, ...) \
SETTINGS_LIST( \
ZShell::config::Type, name, DEFAULT_ARG(defaultVal), __VA_ARGS__)
#define CONFIG_GLOBAL_LIST(Type, name, defaultVal, ...) \
SETTINGS_GLOBAL_LIST( \
ZShell::config::Type, name, DEFAULT_ARG(defaultVal), __VA_ARGS__)
namespace ZShell::config {
Q_DECLARE_LOGGING_CATEGORY(lcConfig)
QString configDir();
QString monitorConfigDir();
class ListEntry : public settings::ObjectNode {
CONFIG_NODE(ListEntry, settings::ObjectNode)
CONFIG_PROPERTY(QString, id, {})
CONFIG_PROPERTY(bool, enabled, true)
};
CONFIG_LIST_TYPE(ListEntry, EntryList)
} // namespace ZShell::config
// Shorthand for declaring an ID'd entry (bar entries/status icons, quick toggles, etc)
#define LIST_ENTRY(id, enabled) \
settings::vmap({ \
{QStringLiteral("id"), QStringLiteral(#id)}, \
{QStringLiteral("enabled"), enabled}, \
})
-296
View File
@@ -1,296 +0,0 @@
#include "config.hpp"
#include "appearance.hpp"
#include "background.hpp"
#include "bar.hpp"
#include "clipboard.hpp"
#include "colors.hpp"
#include "dashboard.hpp"
#include "display.hpp"
#include "dock.hpp"
#include "general.hpp"
#include "launcher.hpp"
#include "llm.hpp"
#include "lock.hpp"
#include "migration.hpp"
#include "notifs.hpp"
#include "osd.hpp"
#include "screenshot.hpp"
#include "services.hpp"
#include "sidebar.hpp"
#include "utilities.hpp"
#include <QFile>
#include <QSaveFile>
#include <QJsonDocument>
#include <QStandardPaths>
#include <QDir>
#include <QDebug>
#include <QFutureWatcher>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <qcontainerfwd.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qthreadpool.h>
namespace ZShell::config {
Config* Config::s_instance = nullptr;
Config::Config(QObject* parent)
: ConfigObject(parent)
, m_appearance(new Appearance(this))
, m_background(new Background(this))
, m_bar(new Bar(this))
, m_clipboard(new Clipboard(this))
, m_colors(new Colors(this))
, m_dashboard(new Dashboard(this))
, m_display(new Display(this))
, m_dock(new Dock(this))
, m_general(new General(this))
, m_launcher(new Launcher(this))
, m_llm(new Llm(this))
, m_lock(new Lock(this))
, m_notifs(new Notifs(this))
, m_osd(new Osd(this))
, m_screenshot(new Screenshot(this))
, m_services(new Services(this))
, m_sidebar(new Sidebar(this))
, m_utilities(new Utilities(this)) {
s_instance = this;
connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave);
m_saveTimer.setSingleShot(true);
m_saveTimer.setInterval(300);
connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync);
m_reloadTimer.setSingleShot(true);
m_reloadTimer.setInterval(50);
connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync);
connectAutoSave(this);
connect(
&m_watcher,
&QFileSystemWatcher::directoryChanged,
this,
&Config::onWatcherEvent);
connect(
&m_watcher,
&QFileSystemWatcher::fileChanged,
this,
&Config::onWatcherEvent);
loadSync();
updateWatch();
m_firstLoadDone = true;
}
Config* Config::instance() {
return s_instance;
}
Config* Config::create(QQmlEngine*, QJSEngine*) {
if (!s_instance) s_instance = new Config();
return s_instance;
}
QString Config::filePath() const {
const QString dir =
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
QStringLiteral("/zshell");
QDir().mkpath(dir);
return dir + QStringLiteral("/config.json");
}
void Config::loadSync() {
m_loading = true;
QFile f(filePath());
QJsonObject before;
bool existed = false;
bool migrated = false;
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
if (doc.isObject()) {
const QJsonObject raw = doc.object();
before = ConfigMigrations::apply(raw);
migrated = before != raw;
existed = true;
} else {
qInfo() << "Config: existing config at" << filePath()
<< "is empty or not a valid JSON object - using defaults";
}
} else {
qInfo() << "Config: no existing config at" << filePath()
<< "- using defaults";
}
loadFromJson(QJsonValue(before));
m_loading = false;
const auto after = toJson().toObject();
if (!existed || migrated || after != before) saveNow();
}
void Config::updateWatch() {
const QFileInfo info(filePath());
const QString dir = info.absolutePath();
const QString file = info.absoluteFilePath();
if (!m_watcher.directories().contains(dir)) m_watcher.addPath(dir);
if (info.exists() && !m_watcher.files().contains(file))
m_watcher.addPath(file);
}
void Config::onWatcherEvent(const QString&) {
updateWatch();
const QFileInfo info(filePath());
if (!info.exists()) return;
QFile file(info.absoluteFilePath());
if (!file.open(QIODevice::ReadOnly)) return;
const QByteArray hash =
QCryptographicHash::hash(file.readAll(), QCryptographicHash::Sha256);
{
QMutexLocker lock(&m_writeMutex);
if (hash == m_lastWriteHash) return;
}
m_reloadPending = true;
if (!m_loading) m_reloadTimer.start();
}
void Config::loadAsync() {
m_loading = true;
auto future = QtConcurrent::run([this]() {
QFile f(filePath());
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
const bool valid = doc.isObject();
const QJsonObject raw = valid ? doc.object() : QJsonObject();
const QJsonObject before = ConfigMigrations::apply(raw);
const bool migrated = valid && before != raw;
QMetaObject::invokeMethod(
this,
[this, valid, migrated, before]() {
loadFromJson(QJsonValue(before));
m_loading = false;
const auto after = toJson().toObject();
if (!valid || migrated || after != before) saveNow();
if (m_reloadPending) m_reloadTimer.start();
},
Qt::QueuedConnection);
} else {
qInfo() << "Config: failed to reload from" << filePath()
<< "- using in-memory values";
QMetaObject::invokeMethod(
this,
[this]() {
m_loading = false;
if (m_reloadPending) m_reloadTimer.start();
},
Qt::QueuedConnection);
}
});
m_loadFuture = future;
}
void Config::load() {
if (!m_firstLoadDone) {
loadSync();
} else {
loadAsync();
}
}
void Config::reloadAsync() {
if (m_loading) return;
m_reloadPending = false;
loadAsync();
}
void Config::scheduleSave() {
if (m_loading) return;
m_saveTimer.start();
}
void Config::flushAsync() {
const QByteArray data =
QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented);
const QByteArray hash =
QCryptographicHash::hash(data, QCryptographicHash::Sha256);
QThreadPool::globalInstance()->start([this, data, hash]() {
const bool success = writeAtomically(data);
QMetaObject::invokeMethod(
this,
[this, success, hash]() {
if (!success) return;
QMutexLocker lock(&m_writeMutex);
m_lastWriteHash = hash;
},
Qt::QueuedConnection);
});
}
void Config::saveNow() {
m_saveTimer.stop();
flushAsync();
}
void Config::connectAutoSave(ConfigNode* node) {
connect(node, &ConfigNode::propertiesChanged, this, [this] {
scheduleSave();
});
for (auto* child : node->childNodes())
connectAutoSave(child);
}
bool Config::writeAtomically(const QByteArray& data) {
QSaveFile f(filePath());
if (!f.open(QIODevice::WriteOnly)) {
qWarning() << "Config: failed to open for atomic write:"
<< f.errorString();
return false;
}
if (f.write(data) != data.size()) {
qWarning() << "Config: failed to write:" << f.errorString();
return false;
}
if (!f.commit()) {
qWarning() << "Config: commit failed:" << f.errorString();
return false;
}
return true;
}
}; // namespace ZShell::config
-117
View File
@@ -1,117 +0,0 @@
#pragma once
#include "configobject.hpp"
#include <QByteArray>
#include <QFileSystemWatcher>
#include <QFuture>
#include <QMutex>
#include <QTimer>
#include <qqmlregistration.h>
#include <QtConcurrent/QtConcurrent>
class QQmlEngine;
class QJSEngine;
namespace ZShell::config {
class Appearance;
class Background;
class Bar;
class Clipboard;
class Colors;
class Dashboard;
class Display;
class Dock;
class General;
class Launcher;
class Llm;
class Lock;
class Notifs;
class Osd;
class Screenshot;
class Services;
class Sidebar;
class Utilities;
class Config : public ConfigObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_MOC_INCLUDE("appearance.hpp")
Q_MOC_INCLUDE("background.hpp")
Q_MOC_INCLUDE("bar.hpp")
Q_MOC_INCLUDE("clipboard.hpp")
Q_MOC_INCLUDE("colors.hpp")
Q_MOC_INCLUDE("dashboard.hpp")
Q_MOC_INCLUDE("display.hpp")
Q_MOC_INCLUDE("dock.hpp")
Q_MOC_INCLUDE("general.hpp")
Q_MOC_INCLUDE("launcher.hpp")
Q_MOC_INCLUDE("llm.hpp")
Q_MOC_INCLUDE("lock.hpp")
Q_MOC_INCLUDE("notifs.hpp")
Q_MOC_INCLUDE("osd.hpp")
Q_MOC_INCLUDE("screenshot.hpp")
Q_MOC_INCLUDE("services.hpp")
Q_MOC_INCLUDE("sidebar.hpp")
Q_MOC_INCLUDE("utilities.hpp")
CONFIG_SUBOBJECT(Appearance, appearance)
CONFIG_SUBOBJECT(Background, background)
CONFIG_SUBOBJECT(Bar, bar)
CONFIG_SUBOBJECT(Clipboard, clipboard)
CONFIG_SUBOBJECT(Colors, colors)
CONFIG_SUBOBJECT(Dashboard, dashboard)
CONFIG_SUBOBJECT(Display, display)
CONFIG_SUBOBJECT(Dock, dock)
CONFIG_SUBOBJECT(General, general)
CONFIG_SUBOBJECT(Launcher, launcher)
CONFIG_SUBOBJECT(Llm, llm)
CONFIG_SUBOBJECT(Lock, lock)
CONFIG_SUBOBJECT(Notifs, notifs)
CONFIG_SUBOBJECT(Osd, osd)
CONFIG_SUBOBJECT(Screenshot, screenshot)
CONFIG_SUBOBJECT(Services, services)
CONFIG_SUBOBJECT(Sidebar, sidebar)
CONFIG_SUBOBJECT(Utilities, utilities)
public:
explicit Config(QObject* parent = nullptr);
static Config* create(QQmlEngine*, QJSEngine*);
[[nodiscard]] static Config* instance();
Q_INVOKABLE void load();
Q_INVOKABLE void saveNow();
Q_INVOKABLE void reloadAsync();
private Q_SLOTS:
void scheduleSave();
void flushAsync();
void onWatcherEvent(const QString& path);
private:
QString filePath() const;
bool writeAtomically(const QByteArray& data);
void updateWatch();
void loadSync();
void loadAsync();
void connectAutoSave(ConfigNode* node);
QTimer m_saveTimer;
QTimer m_reloadTimer;
QFileSystemWatcher m_watcher;
QMutex m_writeMutex;
QByteArray m_lastWriteHash;
bool m_reloadPending = false;
bool m_loading = false;
bool m_firstLoadDone = false;
QFuture<void> m_loadFuture;
static Config* s_instance;
};
} // namespace ZShell::config
-314
View File
@@ -1,314 +0,0 @@
#include "configlist.hpp"
#include <QJsonObject>
#include <QMetaObject>
#include <QQmlEngine>
#include <QString>
namespace ZShell::config {
ConfigList::ConfigList(QObject* parent, const QVariantList& defaults)
: ConfigNode(parent), m_defaults(QJsonArray::fromVariantList(defaults)) {}
int ConfigList::count() const {
return static_cast<int>(m_items.size());
}
QVariantList ConfigList::values() const {
QVariantList vals;
vals.reserve(m_items.size());
for (auto* const item : m_items)
vals.append(QVariant::fromValue(item));
return vals;
}
ConfigObject* ConfigList::itemAt(int index) const {
if (index < 0 || index >= m_items.size()) return nullptr;
return m_items.at(index);
}
const QList<ConfigObject*>& ConfigList::items() const {
return m_items;
}
void ConfigList::remove(int index) {
if (index < 0 || index >= m_items.size()) {
qCWarning(lcConfig)
<< metaObject()->className() << "cannot remove index" << index
<< "of" << count();
return;
}
destroyItem(m_items.takeAt(index));
m_loaded = true;
emit countChanged();
emit valuesChanged();
notifyChanged();
}
void ConfigList::move(int from, int to) {
if (from < 0 || from >= m_items.size() || to < 0 || to >= m_items.size()) {
qCWarning(lcConfig) << metaObject()->className() << "cannot move"
<< from << "to" << to << "of" << count();
return;
}
if (from == to) return;
m_items.move(from, to);
m_loaded = true;
emit valuesChanged();
notifyChanged();
}
void ConfigList::clear() {
if (m_items.isEmpty()) return;
destroyItems();
m_loaded = true;
emit countChanged();
emit valuesChanged();
notifyChanged();
}
void ConfigList::loadFromJson(const QJsonValue& json) {
if (!json.isArray()) {
qCWarning(
lcConfig,
"Option '%s' must be a list, resetting to default",
qUtf8Printable(propertyPath()));
resetToDefaults();
materializeDefaults();
return;
}
populate(json.toArray());
m_loaded = true;
}
QJsonValue ConfigList::toJson() const {
if (m_loaded) return elementsToJson();
return QJsonValue::Undefined;
}
void ConfigList::clearLoadedKeys() {
m_loaded = false;
}
QStringList ConfigList::unknownKeys() const {
QStringList keys;
for (int i = 0; i < m_items.size(); ++i) {
const auto childKeys = m_items.at(i)->unknownKeys();
for (const auto& childKey : childKeys)
keys.append(joinPath(QString("[%1]").arg(i), childKey));
}
return keys;
}
void ConfigList::materializeDefaults() {
for (auto* const item : m_items)
item->materializeDefaults();
if (m_global) return;
m_loaded = true;
}
void ConfigList::resyncFromGlobal() {
syncValuesFromGlobal();
}
ConfigObject* ConfigList::insertItem(const QVariantMap& props, int index) {
const auto pos = index < 0 || index > m_items.size() ? m_items.size()
: index;
appendItem(QJsonObject::fromVariantMap(props));
m_items.move(m_items.size() - 1, pos);
auto* const item = m_items.at(pos);
const auto unknown = item->unknownKeys();
for (const auto& key : unknown)
qCWarning(lcConfig) << "Unknown option" << key << "for"
<< item->metaObject()->className();
m_loaded = true;
emit countChanged();
emit valuesChanged();
notifyChanged();
return item;
}
QJsonArray ConfigList::elementsToJson() const {
QJsonArray arr;
for (auto* const item : m_items)
arr.append(item->toJson().toObject());
return arr;
}
void ConfigList::resetToDefaults() {
loadFromJsonQuietly(m_defaults);
m_loaded = false;
}
void ConfigList::syncValuesFromGlobal() {
if (m_loaded) return;
if (auto* const global = qobject_cast<ConfigList*>(m_global))
populate(global->elementsToJson());
}
void ConfigList::onGlobalPropertiesChanged(const QMap<QString, QVariant>&) {
syncValuesFromGlobal();
}
QString ConfigList::childPath(const ConfigNode* child) const {
for (int i = 0; i < m_items.size(); ++i)
if (m_items.at(i) == child) return QString("[%1]").arg(i);
return {};
}
void ConfigList::populate(const QJsonArray& arr) {
const auto current = elementsToJson();
if (current == arr) return;
const auto old = m_items;
QList<bool> reused(old.size(), false);
m_items.clear();
m_items.reserve(arr.size());
for (int i = 0; i < arr.size(); ++i) {
auto match =
i < old.size() && !reused.at(i) && current.at(i) == arr.at(i) ? i
: -1;
for (int j = 0; match < 0 && j < old.size(); ++j)
if (!reused.at(j) && current.at(j) == arr.at(i)) match = j;
auto update = false;
for (int j = 0; match < 0 && j < old.size(); ++j) {
if (!reused.at(j) &&
sameElement(old.at(j), current.at(j), arr.at(i))) {
match = j;
update = true;
}
}
if (match < 0) {
appendItem(arr.at(i));
continue;
}
reused[match] = true;
if (update) old.at(match)->loadFromJsonQuietly(arr.at(i));
m_items.append(old.at(match));
}
for (int i = 0; i < old.size(); ++i)
if (!reused.at(i)) destroyItem(old.at(i));
if (m_items.size() != old.size()) emit countChanged();
emit valuesChanged();
notifyChanged();
}
bool ConfigList::sameElement(
const ConfigObject* item,
const QJsonValue& current,
const QJsonValue& json) {
const auto keys = item->identityKeys();
if (keys.isEmpty()) return false;
const auto currentObj = current.toObject();
const auto newObj = json.toObject();
if (currentObj.keys() != newObj.keys()) return false;
for (const auto& key : keys)
if (!newObj.contains(key) || currentObj.value(key) != newObj.value(key))
return false;
return true;
}
void ConfigList::appendItem(const QJsonValue& json) {
auto* const item = createItem();
QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership);
if (m_items.isEmpty()) rejectGlobalOnlyElement(item);
item->loadFromJsonQuietly(json);
connectItem(item);
m_items.append(item);
}
void ConfigList::rejectGlobalOnlyElement(const ConfigObject* item) const {
const auto keys = item->globalOnlyKeys();
if (keys.isEmpty()) return;
qCCritical(
lcConfig,
"%s declares global-only options (%s) which do not work inside a list, "
"use CONFIG_GLOBAL_LIST on %s instead",
item->metaObject()->className(),
qUtf8Printable(keys.join(", ")),
metaObject()->className());
}
void ConfigList::connectItem(ConfigNode* node) {
connect(
node, &ConfigNode::propertiesChanged, this, &ConfigList::onItemChanged);
const auto children = node->childNodes();
for (auto* const child : children)
connectItem(child);
}
void ConfigList::disconnectItem(ConfigNode* node) {
node->disconnect(this);
const auto children = node->childNodes();
for (auto* const child : children)
disconnectItem(child);
}
void ConfigList::destroyItems() {
for (auto* const item : std::as_const(m_items))
destroyItem(item);
m_items.clear();
}
void ConfigList::destroyItem(ConfigObject* item) {
disconnectItem(item);
item->deleteLater();
}
void ConfigList::onItemChanged() {
m_loaded = true;
notifyChanged();
}
void ConfigList::notifyChanged() {
notifyPropertyChanged("values", count());
}
} // namespace ZShell::config
-139
View File
@@ -1,139 +0,0 @@
#pragma once
#include "configobject.hpp"
#include <QJsonArray>
#include <QJsonObject>
#include <QQmlEngine>
#include <QVariantList>
namespace ZShell::config {
class ConfigList : public ConfigNode {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QVariantList values READ values NOTIFY valuesChanged)
public:
explicit ConfigList(
QObject* parent = nullptr, const QVariantList& defaults = {});
[[nodiscard]] int count() const;
[[nodiscard]] QVariantList values() const;
[[nodiscard]] ConfigObject* itemAt(int index) const;
[[nodiscard]] const QList<ConfigObject*>& items() const;
Q_INVOKABLE void remove(int index);
Q_INVOKABLE void move(int from, int to);
Q_INVOKABLE void clear();
void loadFromJson(const QJsonValue& json) override;
[[nodiscard]] QJsonValue toJson() const override;
void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override;
void materializeDefaults() override;
void resyncFromGlobal() override;
signals:
void countChanged();
void valuesChanged();
protected:
virtual ConfigObject* createItem() = 0;
void resetToDefaults();
ConfigObject* insertItem(const QVariantMap& props, int index = -1);
[[nodiscard]] QJsonArray elementsToJson() const;
void syncValuesFromGlobal() override;
void onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) override;
[[nodiscard]] QString childPath(const ConfigNode* child) const override;
private:
void populate(const QJsonArray& arr);
static bool sameElement(
const ConfigObject* item,
const QJsonValue& current,
const QJsonValue& json);
void appendItem(const QJsonValue& json);
void destroyItems();
void destroyItem(ConfigObject* item);
void rejectGlobalOnlyElement(const ConfigObject* item) const;
void connectItem(ConfigNode* node);
void disconnectItem(ConfigNode* node);
void onItemChanged();
void notifyChanged();
QJsonArray m_defaults;
QList<ConfigObject*> m_items;
bool m_loaded = false;
};
} // namespace ZShell::config
#define CONFIG_LIST_TYPE(Element, Name) \
class Name : public ZShell::config::ConfigList { \
Q_OBJECT \
QML_ANONYMOUS \
\
public: \
explicit Name( \
QObject* parent = nullptr, const QVariantList& defaults = {}) \
: ZShell::config::ConfigList(parent, defaults) { \
resetToDefaults(); \
} \
\
Q_INVOKABLE ZShell::config::Element* at(int index) const { \
return static_cast<ZShell::config::Element*>(itemAt(index)); \
} \
Q_INVOKABLE ZShell::config::Element* insert( \
const QVariantMap& props, int index = -1) { \
return static_cast<ZShell::config::Element*>( \
insertItem(props, index)); \
} \
\
protected: \
[[nodiscard]] ZShell::config::ConfigObject* createItem() override { \
return new ZShell::config::Element(this); \
} \
};
#define CONFIG_LIST(Type, name, ...) \
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
return m_##name; \
} \
\
private: \
Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__));
#define LIST_ENTRY(id, enabled) \
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
namespace ZShell::config {
class ListEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(QString, id)
CFG_PROPERTY(bool, enabled, true)
public:
explicit ListEntry(QObject* parent = nullptr) : ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("id")};
}
};
CONFIG_LIST_TYPE(ListEntry, EntryList)
} // namespace ZShell::config
-89
View File
@@ -1,89 +0,0 @@
#include "confignode.hpp"
#include <qjsonvalue.h>
#include <qloggingcategory.h>
#include <qmetaobject.h>
#include <qstring.h>
namespace ZShell::config {
Q_LOGGING_CATEGORY(lcConfig, "zshell.config")
ConfigNode::ConfigNode(QObject* parent) : QObject(parent) {
m_batchTimer = new QTimer(this);
m_batchTimer->setSingleShot(true);
m_batchTimer->setInterval(0);
connect(
m_batchTimer, &QTimer::timeout, this, &ConfigNode::emitBatchedChanges);
}
void ConfigNode::loadFromJsonQuietly(const QJsonValue& json) {
setNotifySuppressed(true);
loadFromJson(json);
setNotifySuppressed(false);
}
void ConfigNode::setNotifySuppressed(bool suppressed) {
m_suppressNotify = suppressed;
const auto children = childNodes();
for (auto* const child : children)
child->setNotifySuppressed(suppressed);
}
QList<ConfigNode*> ConfigNode::childNodes() const {
return {};
}
void ConfigNode::syncFromGlobal(ConfigNode* global) {
m_global = global;
resyncFromGlobal();
}
bool ConfigNode::isOverlay() const {
return m_global != nullptr && this != m_global;
}
QString ConfigNode::propertyPath(const QString& name) const {
if (name.isEmpty()) return objectName();
QObject* const parentObj = parent();
if (parentObj) {
if (auto* parentNode = qobject_cast<ConfigNode*>(parentObj)) {
const auto childName = parentNode->childPath(this);
if (!childName.isEmpty()) {
return joinPath(parentNode->propertyPath(childName), name);
}
}
}
return name;
}
int ConfigNode::basePropertyOffset() {
return QObject::staticMetaObject.propertyCount();
}
void ConfigNode::notifyPropertyChanged(
const QString& name, const QVariant& value) {
if (m_suppressNotify) return;
m_pendingChanges[name] = value;
if (m_batchTimer && !m_batchTimer->isActive()) m_batchTimer->start();
}
QString ConfigNode::joinPath(const QString& parent, const QString& child) {
// List elements are addressed as parent[i], everything else as parent.child
if (child.startsWith(QLatin1Char('['))) return parent + child;
return parent + QLatin1Char('.') + child;
}
void ConfigNode::emitBatchedChanges() {
if (m_pendingChanges.isEmpty()) return;
QMap<QString, QVariant> changes = m_pendingChanges;
m_pendingChanges.clear();
emit propertiesChanged(changes);
}
} // namespace ZShell::config
-65
View File
@@ -1,65 +0,0 @@
#pragma once
#include <QJsonValue>
#include <QLoggingCategory>
#include <QMap>
#include <QObject>
#include <QStringList>
#include <QTimer>
#include <QVariant>
namespace ZShell::config {
Q_DECLARE_LOGGING_CATEGORY(lcConfig)
class ConfigNode : public QObject {
Q_OBJECT
public:
explicit ConfigNode(QObject* parent = nullptr);
virtual void loadFromJson(const QJsonValue& json) = 0;
void loadFromJsonQuietly(const QJsonValue& json);
[[nodiscard]] virtual QJsonValue toJson() const = 0;
virtual void clearLoadedKeys() = 0;
[[nodiscard]] virtual QStringList unknownKeys() const = 0;
[[nodiscard]] virtual QList<ConfigNode*> childNodes() const;
virtual void materializeDefaults() = 0;
void syncFromGlobal(ConfigNode* global);
virtual void resyncFromGlobal() = 0;
[[nodiscard]] bool isOverlay() const;
[[nodiscard]] QString propertyPath(const QString& name = {}) const;
[[nodiscard]] static int basePropertyOffset();
signals:
void propertiesChanged(const QMap<QString, QVariant>& changed);
protected:
virtual void syncValuesFromGlobal() = 0;
virtual void onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) = 0;
[[nodiscard]] virtual QString childPath(const ConfigNode* child) const = 0;
void notifyPropertyChanged(const QString& name, const QVariant& value);
[[nodiscard]] static QString joinPath(
const QString& parent, const QString& child);
ConfigNode* m_global = nullptr;
private:
void setNotifySuppressed(bool suppressed);
void emitBatchedChanges();
QMap<QString, QVariant> m_pendingChanges;
QTimer* m_batchTimer = nullptr;
bool m_suppressNotify = false;
};
} // namespace ZShell::config
-336
View File
@@ -1,336 +0,0 @@
#include "configobject.hpp"
#include <QJsonArray>
#include <QMetaObject>
#include <QStringList>
#include <QVariant>
namespace ZShell::config {
namespace {
bool isStringArray(const QJsonArray& arr) {
for (const auto& v : arr) {
if (!v.isString()) return false;
}
return true;
}
bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) {
switch (val.type()) {
case QJsonValue::Bool:
return type.id() == QMetaType::Bool;
case QJsonValue::Double:
switch (type.id()) {
case QMetaType::Int:
case QMetaType::UInt:
case QMetaType::LongLong:
case QMetaType::ULongLong:
case QMetaType::Double:
case QMetaType::Float:
return true;
default:
return false;
}
case QJsonValue::String:
return type.id() == QMetaType::QString;
case QJsonValue::Array:
if (type.id() == QMetaType::QStringList)
return isStringArray(val.toArray());
return type.id() == QMetaType::QVariantList;
case QJsonValue::Object:
case QJsonValue::Null:
case QJsonValue::Undefined:
default:
return false;
}
}
} // namespace
ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}
void ConfigObject::loadFromJson(const QJsonValue& json) {
const auto obj = json.toObject();
const auto* meta = metaObject();
QSet<QString> known;
QSet<QString> invalid;
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (!obj.contains(key)) continue;
const auto jsonVal = obj.value(key);
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
known.insert(key);
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->loadFromJson(jsonVal);
continue;
}
}
if (!prop.isWritable()) continue;
known.insert(key);
if (!jsonValueMatchesType(jsonVal, prop.metaType())) {
qWarning() << "Config: type mismatch for" << key << "in"
<< meta->className() << "- expected"
<< prop.metaType().name() << "got value" << jsonVal
<< "- resetting to default";
invalid.insert(key);
continue;
}
if (prop.metaType().id() == QMetaType::QStringList) {
QStringList list;
const auto jsonArr = jsonVal.toArray();
for (const auto& v : jsonArr)
list.append(v.toString());
prop.write(this, QVariant::fromValue(list));
m_loadedKeys.insert(key);
continue;
}
if (prop.metaType().id() == QMetaType::QVariantList) {
prop.write(this, jsonVal.toVariant());
m_loadedKeys.insert(key);
continue;
}
prop.write(this, jsonVal.toVariant());
m_loadedKeys.insert(key);
}
m_extras = {};
for (auto it = obj.begin(); it != obj.end(); ++it) {
if (!known.contains(it.key()) || invalid.contains(it.key())) {
m_extras.insert(it.key(), it.value());
}
}
materializeDefaults();
}
QJsonValue ConfigObject::toJson() const {
QJsonObject obj;
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (!prop.isReadable()) continue;
const auto key = QString::fromUtf8(prop.name());
const auto value = prop.read(this);
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
if (auto* const node = value.value<ConfigNode*>()) {
const auto childJson = node->toJson();
if (!childJson.isUndefined()) obj.insert(key, childJson);
continue;
}
}
if (!m_loadedKeys.contains(key)) continue;
if (!prop.isWritable()) continue;
if (prop.metaType().id() == QMetaType::QStringList) {
QJsonArray arr;
const auto strList = value.toStringList();
for (const auto& s : strList)
arr.append(s);
obj.insert(key, arr);
continue;
}
if (prop.metaType().id() == QMetaType::QVariantList) {
obj.insert(key, QJsonArray::fromVariantList(value.toList()));
continue;
}
obj.insert(key, QJsonValue::fromVariant(value));
}
if (obj.isEmpty()) return QJsonValue::Undefined;
return obj;
}
void ConfigObject::clearLoadedKeys() {
m_loadedKeys.clear();
m_extras = {};
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
if (auto* const node = prop.read(this).value<ConfigNode*>())
node->clearLoadedKeys();
}
}
QStringList ConfigObject::identityKeys() const {
return {};
}
QStringList ConfigObject::unknownKeys() const {
auto keys = m_extras.keys();
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
auto* const node = prop.read(this).value<ConfigNode*>();
if (!node) continue;
const auto childKeys = node->unknownKeys();
for (const auto& childKey : childKeys)
keys.append(joinPath(QString::fromUtf8(prop.name()), childKey));
}
return keys;
}
QList<ConfigNode*> ConfigObject::childNodes() const {
QList<ConfigNode*> nodes;
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (auto* const node = prop.read(this).value<ConfigNode*>())
nodes.append(node);
}
return nodes;
}
void ConfigObject::materializeDefaults() {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (auto* const node = prop.read(this).value<ConfigNode*>())
node->materializeDefaults();
}
}
void ConfigObject::syncValuesFromGlobal() {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
if (auto* const globalNode =
prop.read(m_global).value<ConfigNode*>())
node->syncFromGlobal(globalNode);
continue;
}
if (!prop.isWritable()) continue;
if (!m_loadedKeys.contains(key)) {
auto val = prop.read(m_global);
prop.write(this, val);
m_loadedKeys.remove(key);
}
}
}
void ConfigObject::resyncFromGlobal() {
if (!m_global) return;
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->resyncFromGlobal();
continue;
}
if (!prop.isWritable()) continue;
if (!m_loadedKeys.contains(key)) {
prop.write(this, prop.read(m_global));
m_loadedKeys.remove(key);
}
}
}
bool ConfigObject::isPropertyLoaded(const QString& name) const {
return m_loadedKeys.contains(name);
}
bool ConfigObject::isGlobalOnly(const QString& name) const {
return m_globalOnlyKeys.contains(name);
}
QStringList ConfigObject::globalOnlyKeys() const {
return {m_globalOnlyKeys.begin(), m_globalOnlyKeys.end()};
}
void ConfigObject::resetOption(const QString& name) {
m_loadedKeys.remove(name);
const int idx = metaObject()->indexOfProperty(name.toUtf8().constData());
if (idx < 0) return;
const auto prop = metaObject()->property(idx);
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->clearLoadedKeys();
node->resyncFromGlobal();
return;
}
if (m_global && prop.isWritable()) prop.write(this, prop.read(m_global));
}
void ConfigObject::onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) {
for (auto it = changed.begin(); it != changed.end(); ++it) {
if (m_loadedKeys.contains(it.key()) || isGlobalOnly(it.key())) continue;
int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData());
if (idx >= 0) {
metaObject()->property(idx).write(this, it.value());
m_loadedKeys.remove(it.key());
}
}
}
QString ConfigObject::childPath(const ConfigNode* child) const {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
if (prop.read(this).value<ConfigNode*>() == child)
return QString::fromUtf8(prop.name());
}
return {};
}
void ConfigObject::markPropertyLoaded(const QString& name) {
m_loadedKeys.insert(name);
}
void ConfigObject::markGlobalOnly(const QString& name) {
m_globalOnlyKeys.insert(name);
}
} // namespace ZShell::config
-101
View File
@@ -1,101 +0,0 @@
#pragma once
#include "confignode.hpp"
#include <QJsonObject>
#include <QMetaProperty>
#include <QMetaType>
#include <QObject>
#include <QSet>
#include <QStringList>
#include <QVariant>
#include <QVariantList>
namespace ZShell::config {
inline QVariantMap vmap(
std::initializer_list<std::pair<QString, QVariant>> entries) {
QVariantMap map;
for (const auto& [key, value] : entries)
map.insert(key, value);
return map;
}
#define CFG_PROPERTY(Type, name, ...) \
Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \
public: \
[[nodiscard]] Type name() const { \
return m_##name; \
} \
void set_##name(const Type& value) { \
if (ConfigObject::updateMember(m_##name, value)) { \
markPropertyLoaded(QStringLiteral(#name)); \
Q_EMIT name##Changed(); \
notifyPropertyChanged( \
QStringLiteral(#name), QVariant::fromValue(m_##name)); \
} \
} \
Q_SIGNAL void name##Changed(); \
\
private: \
Type m_##name __VA_OPT__(= __VA_ARGS__);
#define CONFIG_SUBOBJECT(Type, name) \
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
return m_##name; \
} \
\
private: \
Type* m_##name = nullptr;
class ConfigObject : public ConfigNode {
Q_OBJECT
public:
explicit ConfigObject(QObject* parent = nullptr);
void loadFromJson(const QJsonValue& json) override;
[[nodiscard]] QJsonValue toJson() const override;
void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override;
[[nodiscard]] QList<ConfigNode*> childNodes() const override;
void materializeDefaults() override;
void resyncFromGlobal() override;
[[nodiscard]] virtual QStringList identityKeys() const;
[[nodiscard]] bool isPropertyLoaded(const QString& name) const;
[[nodiscard]] bool isGlobalOnly(const QString& name) const;
[[nodiscard]] QStringList globalOnlyKeys() const;
Q_INVOKABLE void resetOption(const QString& name);
template <typename T> static bool updateMember(T& member, const T& value) {
if constexpr (std::is_floating_point_v<T>) {
if (qFuzzyCompare(member + 1.0, value + 1.0)) return false;
} else {
if (member == value) return false;
}
member = value;
return true;
}
protected:
void syncValuesFromGlobal() override;
void onGlobalPropertiesChanged(
const QMap<QString, QVariant>& changed) override;
[[nodiscard]] QString childPath(const ConfigNode* child) const override;
void markPropertyLoaded(const QString& name);
void markGlobalOnly(const QString& name);
private:
QSet<QString> m_loadedKeys;
QSet<QString> m_globalOnlyKeys;
QJsonObject m_extras;
};
} // namespace ZShell::config
+51 -48
View File
@@ -1,65 +1,68 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class Performance : public ConfigObject { class Performance : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Performance, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(bool, showBattery, false) CONFIG_PROPERTY(bool, showBattery, false)
CFG_PROPERTY(bool, showCpu, true) CONFIG_PROPERTY(bool, showCpu, true)
CFG_PROPERTY(bool, showGpu, true) CONFIG_PROPERTY(bool, showGpu, true)
CFG_PROPERTY(bool, showMemory, true) CONFIG_PROPERTY(bool, showMemory, true)
CFG_PROPERTY(bool, showNetwork, true) CONFIG_PROPERTY(bool, showNetwork, true)
CFG_PROPERTY(bool, showStorage, true) CONFIG_PROPERTY(bool, showStorage, true)
CFG_PROPERTY(bool, showVram, true) CONFIG_PROPERTY(bool, showVram, true)
public:
explicit Performance(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class DashboardSizes : public ConfigObject { class DashboardSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(DashboardSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, dateTimeWidth, 110) CONFIG_PROPERTY(int, dateTimeWidth, 110)
CFG_PROPERTY(int, infoIconSize, 25) CONFIG_PROPERTY(int, infoIconSize, 25)
CFG_PROPERTY(int, infoWidth, 200) CONFIG_PROPERTY(int, infoWidth, 200)
CFG_PROPERTY(int, mediaCoverArtSize, 150) CONFIG_PROPERTY(int, mediaCoverArtSize, 150)
CFG_PROPERTY(int, mediaProgressSweep, 180) CONFIG_PROPERTY(int, mediaProgressSweep, 180)
CFG_PROPERTY(int, mediaProgressThickness, 8) CONFIG_PROPERTY(int, mediaProgressThickness, 8)
CFG_PROPERTY(int, mediaVisualizerSize, 200) CONFIG_PROPERTY(int, mediaVisualizerSize, 200)
CFG_PROPERTY(int, mediaWidth, 200) CONFIG_PROPERTY(int, mediaWidth, 200)
CFG_PROPERTY(int, resourceProgessThickness, 10) CONFIG_PROPERTY(int, resourceProgessThickness, 10)
CFG_PROPERTY(int, resourceSize, 200) CONFIG_PROPERTY(int, resourceSize, 200)
CFG_PROPERTY(int, tabIndicatorHeight, 3) CONFIG_PROPERTY(int, tabIndicatorHeight, 3)
CFG_PROPERTY(int, tabIndicatorSpacing, 5) CONFIG_PROPERTY(int, tabIndicatorSpacing, 5)
CFG_PROPERTY(int, weatherWidth, 250) CONFIG_PROPERTY(int, weatherWidth, 250)
public:
explicit DashboardSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Dashboard : public ConfigObject { class ProcessColumnEntry : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(ProcessColumnEntry, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(QString, id, {})
CFG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(double, width, 0)
CFG_PROPERTY(int, dragThreshold, 50) };
CFG_PROPERTY(int, mediaUpdateInterval, 2000)
CONFIG_LIST_TYPE(ProcessColumnEntry, ProcessColumnList)
class Dashboard : public settings::ObjectNode {
CONFIG_NODE(Dashboard, settings::ObjectNode)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(bool, showOnHover, true)
CONFIG_PROPERTY(int, dragThreshold, 50)
CONFIG_PROPERTY(int, mediaUpdateInterval, 2000)
CONFIG_SUBOBJECT(Performance, performance) CONFIG_SUBOBJECT(Performance, performance)
CFG_PROPERTY(int, resourceUpdateInterval, 1000) CONFIG_PROPERTY(int, resourceUpdateInterval, 1000)
CONFIG_SUBOBJECT(DashboardSizes, sizes) CONFIG_SUBOBJECT(DashboardSizes, sizes)
CONFIG_LIST(
public: ProcessColumnList,
explicit Dashboard(QObject* parent = nullptr) processColumns,
: ConfigObject(parent) DEFAULT_ARG({
, m_performance(new Performance(this)) settings::vmap({{"id", "command"}}),
, m_sizes(new DashboardSizes(this)) {} settings::vmap({{"id", "pid"}}),
settings::vmap({{"id", "cpuUsage"}}),
settings::vmap({{"id", "memoryUsage"}}),
}))
}; };
} // namespace ZShell::config } // namespace ZShell::config
+11 -20
View File
@@ -1,33 +1,24 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class Nightlight : public ConfigObject { class Nightlight : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Nightlight, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, schedule, true) CONFIG_PROPERTY(bool, schedule, true)
CFG_PROPERTY(int, scheduleStart, 1200) CONFIG_PROPERTY(int, scheduleStart, 1200)
CFG_PROPERTY(int, scheduleEnd, 570) CONFIG_PROPERTY(int, scheduleEnd, 570)
CFG_PROPERTY(bool, useNative, true) CONFIG_PROPERTY(bool, useNative, true)
CFG_PROPERTY(int, fadeDuration, 2000) CONFIG_PROPERTY(int, fadeDuration, 2000)
CFG_PROPERTY(int, temp, 2600) CONFIG_PROPERTY(int, temp, 2600)
public:
explicit Nightlight(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Display : public ConfigObject { class Display : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Display, settings::ObjectNode)
QML_ANONYMOUS
CONFIG_SUBOBJECT(Nightlight, nightlight) CONFIG_SUBOBJECT(Nightlight, nightlight)
public:
explicit Display(QObject* parent = nullptr)
: ConfigObject(parent), m_nightlight(new Nightlight(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+9 -13
View File
@@ -1,29 +1,25 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <QStringList> #include <QStringList>
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class Dock : public ConfigObject { class Dock : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Dock, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enable, false) CONFIG_PROPERTY(bool, enable, false)
CFG_PROPERTY(int, height, 80) CONFIG_PROPERTY(int, height, 80)
CFG_PROPERTY(bool, hoverToReveal, false) CONFIG_PROPERTY(bool, hoverToReveal, false)
CFG_PROPERTY(QStringList, ignoredAppRegexes, {}) CONFIG_PROPERTY(QStringList, ignoredAppRegexes, {})
CFG_PROPERTY( CONFIG_PROPERTY(
QStringList, QStringList,
pinnedApps, pinnedApps,
(QStringList{ (QStringList{
QStringLiteral("com.ayugram.desktop"), QStringLiteral("com.ayugram.desktop"),
QStringLiteral("com.obsproject.studio"), QStringLiteral("com.obsproject.studio"),
QStringLiteral("librewolf")})) QStringLiteral("librewolf")}))
CFG_PROPERTY(bool, pinnedOnStartup, false) CONFIG_PROPERTY(bool, pinnedOnStartup, false)
public:
explicit Dock(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+8 -10
View File
@@ -1,7 +1,7 @@
#include "font.hpp" #include "font.hpp"
#include "appearance.hpp" #include "appearance.hpp"
#include "config.hpp" #include "rootnodes.hpp"
namespace ZShell::config { namespace ZShell::config {
@@ -59,20 +59,20 @@ void FontStyleBase::bind(FontStyleNode* cfg) {
if (cfg) { if (cfg) {
connect( connect(
cfg, &ConfigNode::propertiesChanged, this, &FontStyleBase::rebuild); cfg, &settings::Node::optionChanged, this, &FontStyleBase::rebuild);
connect( connect(
cfg->largeNode(), cfg->largeNode(),
&ConfigNode::propertiesChanged, &settings::Node::optionChanged,
this, this,
&FontStyleBase::rebuild); &FontStyleBase::rebuild);
connect( connect(
cfg->mediumNode(), cfg->mediumNode(),
&ConfigNode::propertiesChanged, &settings::Node::optionChanged,
this, this,
&FontStyleBase::rebuild); &FontStyleBase::rebuild);
connect( connect(
cfg->smallNode(), cfg->smallNode(),
&ConfigNode::propertiesChanged, &settings::Node::optionChanged,
this, this,
&FontStyleBase::rebuild); &FontStyleBase::rebuild);
} }
@@ -128,7 +128,7 @@ void IconFontStyle::bind(FontStyleNode* cfg) {
if (cfg) if (cfg)
connect( connect(
cfg->extraLargeNode(), cfg->extraLargeNode(),
&ConfigNode::propertiesChanged, &settings::Node::optionChanged,
this, this,
&IconFontStyle::rebuild); &IconFontStyle::rebuild);
} }
@@ -181,16 +181,14 @@ FontBuilder IconFontBuilders::extraLarge() const {
} }
FontTokens::FontTokens(QObject* parent) FontTokens::FontTokens(QObject* parent)
: ConfigObject(parent) : QObject(parent)
, m_size(new FontSizeTokens(this))
, m_headline(new FontStyle(this)) , m_headline(new FontStyle(this))
, m_title(new FontStyle(this)) , m_title(new FontStyle(this))
, m_body(new FontStyle(this)) , m_body(new FontStyle(this))
, m_label(new FontStyle(this)) , m_label(new FontStyle(this))
, m_mono(new FontStyle(this)) , m_mono(new FontStyle(this))
, m_icon(new IconFontStyle(this)) { , m_icon(new IconFontStyle(this)) {
if (!Config::instance()) new Config(); bindFont(ConfigSingleton::instance()->appearance()->font());
bindFont(Config::instance()->appearance()->font());
} }
FontStyle* FontTokens::headline() const { FontStyle* FontTokens::headline() const {
+1 -19
View File
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include "configobject.hpp"
#include "fontbuilder.hpp" #include "fontbuilder.hpp"
#include <qfont.h> #include <qfont.h>
@@ -17,21 +16,6 @@ class FontSizeNode;
class FontStyleNode; class FontStyleNode;
class IconFontStyle; class IconFontStyle;
class FontSizeTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(int, small, 11)
CFG_PROPERTY(int, smaller, 12)
CFG_PROPERTY(int, normal, 13)
CFG_PROPERTY(int, larger, 15)
CFG_PROPERTY(int, large, 18)
CFG_PROPERTY(int, extraLarge, 28)
public:
explicit FontSizeTokens(QObject* parent = nullptr) : ConfigObject(parent) {}
};
class FontBuilders : public QObject { class FontBuilders : public QObject {
Q_OBJECT Q_OBJECT
QML_ANONYMOUS QML_ANONYMOUS
@@ -149,12 +133,10 @@ class IconFontStyle : public FontStyleBase {
IconFontBuilders* m_builders; IconFontBuilders* m_builders;
}; };
class FontTokens : public ConfigObject { class FontTokens : public QObject {
Q_OBJECT Q_OBJECT
QML_ANONYMOUS QML_ANONYMOUS
CONFIG_SUBOBJECT(FontSizeTokens, size)
Q_PROPERTY(FontStyle* headline READ headline CONSTANT FINAL) Q_PROPERTY(FontStyle* headline READ headline CONSTANT FINAL)
Q_PROPERTY(FontStyle* title READ title CONSTANT FINAL) Q_PROPERTY(FontStyle* title READ title CONSTANT FINAL)
Q_PROPERTY(FontStyle* body READ body CONSTANT FINAL) Q_PROPERTY(FontStyle* body READ body CONSTANT FINAL)
+45 -83
View File
@@ -1,141 +1,103 @@
#pragma once #pragma once
#include "configlist.hpp" #include "common.hpp"
#include "configobject.hpp"
#include <QStandardPaths> #include <QStandardPaths>
#include <QStringList> #include <QStringList>
namespace ZShell::config { namespace ZShell::config {
class BatteryThresholdEntry : public ConfigObject { class BatteryThresholdEntry : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(BatteryThresholdEntry, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, icon) CONFIG_PROPERTY(QString, icon, {})
CFG_PROPERTY(QString, message) CONFIG_PROPERTY(QString, message, {})
CFG_PROPERTY(QString, name) CONFIG_PROPERTY(QString, name, {})
CFG_PROPERTY(int, perc) CONFIG_PROPERTY(int, perc, {})
public:
explicit BatteryThresholdEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("perc")};
}
}; };
CONFIG_LIST_TYPE(BatteryThresholdEntry, BatteryThresholdList) CONFIG_LIST_TYPE(BatteryThresholdEntry, BatteryThresholdList)
class IdleTimeoutEntry : public ConfigObject { class IdleTimeoutEntry : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(IdleTimeoutEntry, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, idleAction) CONFIG_PROPERTY(QString, idleAction, {})
CFG_PROPERTY(QString, name) CONFIG_PROPERTY(QString, name, {})
CFG_PROPERTY(int, timeout) CONFIG_PROPERTY(int, timeout, {})
public:
explicit IdleTimeoutEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("name")};
}
}; };
CONFIG_LIST_TYPE(IdleTimeoutEntry, IdleTimeoutList) CONFIG_LIST_TYPE(IdleTimeoutEntry, IdleTimeoutList)
class Apps : public ConfigObject { class Apps : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Apps, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QStringList, archiver, {QStringLiteral("ark")}) CONFIG_PROPERTY(QStringList, archiver, {QStringLiteral("ark")})
CFG_PROPERTY(QStringList, audio, {QStringLiteral("pavucontrol")}) CONFIG_PROPERTY(QStringList, audio, {QStringLiteral("pavucontrol")})
CFG_PROPERTY(QStringList, document, {QStringLiteral("libreoffice")}) CONFIG_PROPERTY(QStringList, document, {QStringLiteral("libreoffice")})
CFG_PROPERTY(QStringList, editor, {QStringLiteral("kate")}) CONFIG_PROPERTY(QStringList, editor, {QStringLiteral("kate")})
CFG_PROPERTY(QStringList, explorer, {QStringLiteral("dolphin")}) CONFIG_PROPERTY(QStringList, explorer, {QStringLiteral("dolphin")})
CFG_PROPERTY(QStringList, image, {QStringLiteral("imv")}) CONFIG_PROPERTY(QStringList, image, {QStringLiteral("imv")})
CFG_PROPERTY(QStringList, playback, {QStringLiteral("vlc")}) CONFIG_PROPERTY(QStringList, playback, {QStringLiteral("vlc")})
CFG_PROPERTY(QStringList, terminal, {QStringLiteral("wezterm")}) CONFIG_PROPERTY(QStringList, terminal, {QStringLiteral("wezterm")})
public:
explicit Apps(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Battery : public ConfigObject { class Battery : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Battery, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, critPerc, 5) CONFIG_PROPERTY(int, critPerc, 5)
CONFIG_LIST( CONFIG_LIST(
BatteryThresholdList, BatteryThresholdList,
popupThresholds, popupThresholds,
{ DEFAULT_ARG({
vmap( settings::vmap(
{{"icon", "battery_android_frame_2"}, {{"icon", "battery_android_frame_2"},
{"message", "Battery low"}, {"message", "Battery low"},
{"name", "Low battery"}, {"name", "Low battery"},
{"perc", 20}}), {"perc", 20}}),
vmap( settings::vmap(
{{"icon", "battery_android_frame_2"}, {{"icon", "battery_android_frame_2"},
{"message", "Battery lower"}, {"message", "Battery lower"},
{"name", "Low battery"}, {"name", "Low battery"},
{"perc", 15}}), {"perc", 15}}),
vmap( settings::vmap(
{{"icon", "battery_android_frame_2"}, {{"icon", "battery_android_frame_2"},
{"message", "Battery lowest"}, {"message", "Battery lowest"},
{"name", "Low battery"}, {"name", "Low battery"},
{"perc", 10}}), {"perc", 10}}),
}) }))
public:
explicit Battery(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Idle : public ConfigObject { class Idle : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Idle, settings::ObjectNode)
QML_ANONYMOUS
CONFIG_LIST( CONFIG_LIST(
IdleTimeoutList, IdleTimeoutList,
timeouts, timeouts,
{ DEFAULT_ARG({
vmap( settings::vmap(
{{"idleAction", QStringLiteral("lock")}, {{"idleAction", QStringLiteral("lock")},
{"name", QStringLiteral("Lock")}, {"name", QStringLiteral("Lock")},
{"timeout", 180}}), {"timeout", 180}}),
}) }))
public:
explicit Idle(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class General : public ConfigObject { class General : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(General, settings::ObjectNode)
QML_ANONYMOUS
CONFIG_SUBOBJECT(Apps, apps) CONFIG_SUBOBJECT(Apps, apps)
CONFIG_SUBOBJECT(Battery, battery) CONFIG_SUBOBJECT(Battery, battery)
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss")) CONFIG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CFG_PROPERTY(bool, desktopIcons, true) CONFIG_PROPERTY(bool, desktopIcons, true)
CONFIG_SUBOBJECT(Idle, idle) CONFIG_SUBOBJECT(Idle, idle)
CFG_PROPERTY( CONFIG_PROPERTY(
QString, QString,
lyricsDir, lyricsDir,
QStandardPaths::writableLocation(QStandardPaths::MusicLocation) + QStringLiteral("/Lyrics/")) QStandardPaths::writableLocation(QStandardPaths::MusicLocation) +
CFG_PROPERTY(QString, logo, QString()) QStringLiteral("/Lyrics/"))
CFG_PROPERTY(bool, showOverFullscreen, true) CONFIG_PROPERTY(QString, logo, QString())
CFG_PROPERTY( CONFIG_PROPERTY(bool, showOverFullscreen, true)
CONFIG_PROPERTY(
QString, QString,
wallpaperPath, wallpaperPath,
QStringLiteral("/mnt/IronWolf/SDImages/SWWW_Wals/")) QStringLiteral("/mnt/IronWolf/SDImages/SWWW_Wals/"))
public:
explicit General(QObject* parent = nullptr)
: ConfigObject(parent)
, m_apps(new Apps(this))
, m_battery(new Battery(this))
, m_idle(new Idle(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+39 -80
View File
@@ -1,68 +1,53 @@
#pragma once #pragma once
#include "configlist.hpp"
#include "configobject.hpp" #include "common.hpp"
#include <QStringList> #include <QStringList>
namespace ZShell::config { namespace ZShell::config {
class LauncherActionEntry : public ConfigObject { using Qt::StringLiterals::operator""_s;
Q_OBJECT using settings::vmap;
QML_ANONYMOUS
CFG_PROPERTY(QVariantList, command) class LauncherActionEntry : public settings::ObjectNode {
CFG_PROPERTY(bool, dangerous, false) CONFIG_NODE(LauncherActionEntry, settings::ObjectNode)
CFG_PROPERTY(QString, description)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(QString, icon)
CFG_PROPERTY(QString, name)
public: CONFIG_PROPERTY(QVariantList, command, {})
explicit LauncherActionEntry(QObject* parent = nullptr) CONFIG_PROPERTY(bool, dangerous, false)
: ConfigObject(parent) {} CONFIG_PROPERTY(QString, description, {})
CONFIG_PROPERTY(bool, enabled, true)
[[nodiscard]] QStringList identityKeys() const override { CONFIG_PROPERTY(QString, icon, {})
return {QStringLiteral("name")}; CONFIG_PROPERTY(QString, name, {})
}
}; };
CONFIG_LIST_TYPE(LauncherActionEntry, LauncherActionList) CONFIG_LIST_TYPE(LauncherActionEntry, LauncherActionList)
class LauncherSizes : public ConfigObject { class LauncherSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(LauncherSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, itemHeight, 50) CONFIG_PROPERTY(int, itemHeight, 50)
CFG_PROPERTY(int, itemWidth, 600) CONFIG_PROPERTY(int, itemWidth, 600)
CFG_PROPERTY(int, wallpaperHeight, 200) CONFIG_PROPERTY(int, wallpaperHeight, 200)
CFG_PROPERTY(int, wallpaperWidth, 280) CONFIG_PROPERTY(int, wallpaperWidth, 280)
public:
explicit LauncherSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class UseFuzzy : public ConfigObject { class UseFuzzy : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(UseFuzzy, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, actions, true) CONFIG_PROPERTY(bool, actions, true)
CFG_PROPERTY(bool, apps, true) CONFIG_PROPERTY(bool, apps, true)
CFG_PROPERTY(bool, schemes, true) CONFIG_PROPERTY(bool, schemes, true)
CFG_PROPERTY(bool, variants, true) CONFIG_PROPERTY(bool, variants, true)
CFG_PROPERTY(bool, wallpapers, true) CONFIG_PROPERTY(bool, wallpapers, true)
public:
explicit UseFuzzy(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Launcher : public ConfigObject { class Launcher : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Launcher, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, actionPrefix, QStringLiteral("<")) CONFIG_PROPERTY(QString, actionPrefix, QStringLiteral("<"))
CONFIG_LIST( CONFIG_LIST(
LauncherActionList, LauncherActionList,
actions, actions,
{ DEFAULT_ARG({
vmap( vmap(
{{"command", QVariantList{"autocomplete", "calc"}}, {{"command", QVariantList{"autocomplete", "calc"}},
{"dangerous", false}, {"dangerous", false},
@@ -134,44 +119,18 @@ class Launcher : public ConfigObject {
{"enabled", true}, {"enabled", true},
{"icon", "bedtime"}, {"icon", "bedtime"},
{"name", "Sleep"}}), {"name", "Sleep"}}),
}) }))
CFG_PROPERTY(int, dragThreshold, 150) CONFIG_PROPERTY(int, dragThreshold, 150)
CFG_PROPERTY(bool, enableDangerousActions, true) CONFIG_PROPERTY(bool, enableDangerousActions, true)
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CFG_PROPERTY( CONFIG_PROPERTY(QStringList, favoriteApps, {})
QStringList, CONFIG_PROPERTY(QStringList, hiddenApps, {})
favoriteApps, CONFIG_PROPERTY(int, maxAppsShown, 18)
{QStringLiteral("Balatro"), CONFIG_PROPERTY(int, maxWallpapers, 7)
QStringLiteral("Slay the Spire 2"),
QStringLiteral("com.obsproject.Studio")})
CFG_PROPERTY(
QStringList,
hiddenApps,
{QStringLiteral("zellij"),
QStringLiteral("vim"),
QStringLiteral("jshell-java-openjdk"),
QStringLiteral("jconsole-java-openjdk"),
QStringLiteral("jshell-java21-openjdk"),
QStringLiteral("jconsole-java21-openjdk"),
QStringLiteral("jshell-java17-openjdk"),
QStringLiteral("jconsole-java17-openjdk"),
QStringLiteral("org.neovim.nvim"),
QStringLiteral("avahi-discover"),
QStringLiteral("bvnc"),
QStringLiteral("bssh"),
QStringLiteral("nm-connection-editor")})
CFG_PROPERTY(int, maxAppsShown, 18)
CFG_PROPERTY(int, maxWallpapers, 7)
CONFIG_SUBOBJECT(LauncherSizes, sizes) CONFIG_SUBOBJECT(LauncherSizes, sizes)
CFG_PROPERTY(QString, specialPrefix, QStringLiteral("@")) CONFIG_PROPERTY(QString, specialPrefix, QStringLiteral("@"))
CONFIG_SUBOBJECT(UseFuzzy, useFuzzy) CONFIG_SUBOBJECT(UseFuzzy, useFuzzy)
CFG_PROPERTY(bool, uwsm, true) CONFIG_PROPERTY(bool, uwsm, true)
public:
explicit Launcher(QObject* parent = nullptr)
: ConfigObject(parent)
, m_sizes(new LauncherSizes(this))
, m_useFuzzy(new UseFuzzy(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+12 -21
View File
@@ -1,36 +1,27 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qhashfunctions.h> #include <qhashfunctions.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class LlmAppearance : public ConfigObject { class LlmAppearance : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(LlmAppearance, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, scheme, QStringLiteral("tokyoNight")) CONFIG_PROPERTY(QString, scheme, QStringLiteral("tokyoNight"))
public:
explicit LlmAppearance(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Llm : public ConfigObject { class Llm : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Llm, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, endpoint, "http://localhost:8080") CONFIG_PROPERTY(QString, endpoint, "http://localhost:8080")
CFG_PROPERTY(QString, model, "") CONFIG_PROPERTY(QString, model, "")
CFG_PROPERTY(double, temperature, 0.7) CONFIG_PROPERTY(double, temperature, 0.7)
CFG_PROPERTY(bool, tools, true) CONFIG_PROPERTY(bool, tools, true)
CFG_PROPERTY(bool, sandbox, true) CONFIG_PROPERTY(bool, sandbox, true)
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CONFIG_SUBOBJECT(LlmAppearance, appearance) CONFIG_SUBOBJECT(LlmAppearance, appearance)
public:
explicit Llm(QObject* parent = nullptr)
: ConfigObject(parent), m_appearance(new LlmAppearance(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+14 -23
View File
@@ -1,36 +1,27 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class LockSizes : public ConfigObject { class LockSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(LockSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, centerWidth, 600) CONFIG_PROPERTY(int, centerWidth, 600)
CFG_PROPERTY(qreal, heightMult, 0.7) CONFIG_PROPERTY(qreal, heightMult, 0.7)
CFG_PROPERTY(qreal, ratio, 1.78) CONFIG_PROPERTY(qreal, ratio, 1.78)
public:
explicit LockSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Lock : public ConfigObject { class Lock : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Lock, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, blurAmount, 10) CONFIG_PROPERTY(int, blurAmount, 10)
CFG_PROPERTY(bool, enableFprint, true) CONFIG_PROPERTY(bool, enableFprint, true)
CFG_PROPERTY(int, maxFprintTries, 3) CONFIG_PROPERTY(int, maxFprintTries, 3)
CFG_PROPERTY(bool, recolorLogo, false) CONFIG_PROPERTY(bool, recolorLogo, false)
CFG_PROPERTY(bool, showNotifContent, true) CONFIG_PROPERTY(bool, showNotifContent, true)
CFG_PROPERTY(bool, showNotifIcon, true) CONFIG_PROPERTY(bool, showNotifIcon, true)
CONFIG_SUBOBJECT(LockSizes, sizes) CONFIG_SUBOBJECT(LockSizes, sizes)
public:
explicit Lock(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new LockSizes(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+17 -26
View File
@@ -1,39 +1,30 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class NotifsSizes : public ConfigObject { class NotifsSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(NotifsSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, badge, 20) CONFIG_PROPERTY(int, badge, 20)
CFG_PROPERTY(int, image, 41) CONFIG_PROPERTY(int, image, 41)
CFG_PROPERTY(int, width, 400) CONFIG_PROPERTY(int, width, 400)
public:
explicit NotifsSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Notifs : public ConfigObject { class Notifs : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Notifs, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, actionOnClick, false) CONFIG_PROPERTY(bool, actionOnClick, false)
CFG_PROPERTY(int, appNotifCooldown, 0) CONFIG_PROPERTY(int, appNotifCooldown, 0)
CFG_PROPERTY(qreal, clearThreshold, 0.3) CONFIG_PROPERTY(qreal, clearThreshold, 0.3)
CFG_PROPERTY(int, defaultExpireTimeout, 5000) CONFIG_PROPERTY(int, defaultExpireTimeout, 5000)
CFG_PROPERTY(int, expandThreshold, 20) CONFIG_PROPERTY(int, expandThreshold, 20)
CFG_PROPERTY(bool, expire, true) CONFIG_PROPERTY(bool, expire, true)
CFG_PROPERTY(int, groupPreviewNum, 5) CONFIG_PROPERTY(int, groupPreviewNum, 5)
CFG_PROPERTY(bool, openExpanded, false) CONFIG_PROPERTY(bool, openExpanded, false)
CFG_PROPERTY(bool, showInFullscreen, false) CONFIG_PROPERTY(bool, showInFullscreen, false)
CONFIG_SUBOBJECT(NotifsSizes, sizes) CONFIG_SUBOBJECT(NotifsSizes, sizes)
public:
explicit Notifs(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new NotifsSizes(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+12 -21
View File
@@ -1,34 +1,25 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class OsdSizes : public ConfigObject { class OsdSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(OsdSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, sliderHeight, 150) CONFIG_PROPERTY(int, sliderHeight, 150)
CFG_PROPERTY(int, sliderWidth, 30) CONFIG_PROPERTY(int, sliderWidth, 30)
public:
explicit OsdSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Osd : public ConfigObject { class Osd : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Osd, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, allMonBrightness, true) CONFIG_PROPERTY(bool, allMonBrightness, true)
CFG_PROPERTY(bool, enableBrightness, true) CONFIG_PROPERTY(bool, enableBrightness, true)
CFG_PROPERTY(bool, enableMicrophone, true) CONFIG_PROPERTY(bool, enableMicrophone, true)
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, hideDelay, 3000) CONFIG_PROPERTY(int, hideDelay, 3000)
CONFIG_SUBOBJECT(OsdSizes, sizes) CONFIG_SUBOBJECT(OsdSizes, sizes)
public:
explicit Osd(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new OsdSizes(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+191
View File
@@ -0,0 +1,191 @@
#include "rootnodes.hpp"
#include <qjsondocument.h>
#include <qsavefile.h>
#include "../toaster.hpp"
#include "common.hpp"
#include "migration.hpp"
namespace ZShell::config {
using Qt::StringLiterals::operator""_s;
namespace {
QString nameFor(const QString& key) {
return key.isEmpty() ? u"global"_s : key;
}
} // namespace
namespace detail {
void loaded(ConfigKind kind, settings::RootNode* layer, const QString& screen) {
if (kind != ConfigKind::Shell || !screen.isEmpty()) return;
auto* const config = static_cast<ConfigRoot*>(layer);
if (!config->utilities()->toasts()->configLoaded()) return;
const auto issues = static_cast<int>(config->diagnostics().count());
const auto message = issues > 0
? QStringLiteral("Config loaded with %1 issue%2")
.arg(issues)
.arg(issues == 1 ? "" : "s")
: QStringLiteral("Config loaded successfully!");
Toaster::instance()->toast(
QStringLiteral("Config loaded"),
message,
issues > 0 ? QStringLiteral("settings_alert")
: QStringLiteral("rule_settings"),
issues > 0 ? Toast::Type::Warning : Toast::Type::Info);
}
void loadFailed(ConfigKind kind, const QString& error, const QString& screen) {
QString title;
if (kind == ConfigKind::Tokens)
title = screen.isEmpty()
? QStringLiteral("Failed to parse token config")
: QStringLiteral("Failed to parse token config for %1")
.arg(screen);
else
title =
screen.isEmpty()
? QStringLiteral("Failed to parse config")
: QStringLiteral("Failed to parse config for %1").arg(screen);
Toaster::instance()->toast(
title, error, QStringLiteral("settings_alert"), Toast::Type::Warning);
}
void saveFailed(ConfigKind kind, const QString& error, const QString& screen) {
if (kind != ConfigKind::Shell) return;
const auto title =
screen.isEmpty()
? QStringLiteral("Failed to save config")
: QStringLiteral("Failed to save config for %1").arg(screen);
Toaster::instance()->toast(
title, error, QStringLiteral("settings_alert"), Toast::Type::Error);
}
} // namespace detail
ConfigRoot::ConfigRoot(
const QString& path, ConfigRoot* fallback, QObject* parent)
: RootNode(path, fallback, parent) {
migrateFile(path);
bindTokens();
qCDebug(lcConfig) << "Created config root for" << nameFor(key());
}
void ConfigRoot::migrateFile(const QString& path) {
QFile file(path);
if (!file.exists() || !file.open(QIODevice::ReadOnly)) return;
const auto doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject()) return;
const QJsonObject raw = doc.object();
const QJsonObject migrated = ConfigMigrations::apply(raw);
if (migrated == raw) return;
file.close();
QSaveFile save(path);
if (!save.open(QIODevice::WriteOnly)) {
qCWarning(lcConfig) << "Failed to open" << path
<< "for migration write:" << save.errorString();
return;
}
if (save.write(QJsonDocument(migrated).toJson(QJsonDocument::Indented)) !=
save.size()) {
qCWarning(lcConfig)
<< "Failed to write migrated config:" << save.errorString();
return;
}
if (!save.commit()) {
qCWarning(lcConfig)
<< "Failed to commit migrated config:" << save.errorString();
return;
}
qCDebug(lcConfig) << "Migrated config file" << path;
}
void ConfigRoot::bindTokens() {
qCDebug(lcConfig) << "Binding appearance to token values for"
<< nameFor(key());
auto* const tokens = TokensSingleton::instance()->appearance();
m_appearance->rounding()->bindTokens(tokens->rounding());
m_appearance->spacing()->bindTokens(tokens->spacing());
m_appearance->padding()->bindTokens(tokens->padding());
m_appearance->anim()->durations()->bindTokens(tokens->animDurations());
}
TokensRoot::TokensRoot(
const QString& path, TokensRoot* fallback, QObject* parent)
: RootNode(path, fallback, parent) {
qCDebug(lcConfig) << "Created tokens root for" << nameFor(key());
}
#define SINGLETON_IMPL(Type, Root, file, kind) \
Type* Type::instance() { \
static Type instance; \
return &instance; \
} \
\
Type* Type::create(QQmlEngine*, QJSEngine*) { \
QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); \
return instance(); \
} \
\
Root* Type::forScreen(const QString& screen) { \
bool created; \
auto* const layer = m_layers.get(screen, this, &created); \
if (created) initLayer(layer); \
return layer; \
} \
\
Type::Type(QObject* parent) \
: Root(configDir() + u'/' + file, nullptr, parent) \
, m_layers(monitorConfigDir(), file, this) { \
initLayer(this); \
} \
\
void Type::onTreeLoaded(settings::RootNode* layer) { \
detail::loaded( \
kind, layer, m_layers.nameFor(static_cast<Root*>(layer))); \
} \
\
void Type::onTreeLoadFailed( \
settings::RootNode* layer, const QString& error) { \
detail::loadFailed( \
kind, error, m_layers.nameFor(static_cast<Root*>(layer))); \
} \
\
void Type::onTreeSaveFailed( \
settings::RootNode* layer, const QString& error) { \
detail::saveFailed( \
kind, error, m_layers.nameFor(static_cast<Root*>(layer))); \
} \
\
void Type::initLayer(Root* layer) { \
QObject::connect(layer, &Root::treeLoaded, this, &Type::onTreeLoaded); \
QObject::connect( \
layer, &Root::treeLoadFailed, this, &Type::onTreeLoadFailed); \
QObject::connect( \
layer, &Root::treeSaveFailed, this, &Type::onTreeSaveFailed); \
layer->load(); \
}
SINGLETON_IMPL(
ConfigSingleton, ConfigRoot, u"config.json"_s, detail::ConfigKind::Shell)
SINGLETON_IMPL(
TokensSingleton, TokensRoot, u"tokens.json"_s, detail::ConfigKind::Tokens)
#undef SINGLETON_IMPL
} // namespace ZShell::config
+168
View File
@@ -0,0 +1,168 @@
#pragma once
#include <qjsengine.h>
#include <qqmlengine.h>
#include "settings/layerregistry.hpp"
#include "settings/rootnode.hpp"
#include "appearance.hpp"
#include "background.hpp"
#include "bar.hpp"
#include "clipboard.hpp"
#include "colors.hpp"
#include "common.hpp"
#include "dashboard.hpp"
#include "display.hpp"
#include "dock.hpp"
#include "general.hpp"
#include "launcher.hpp"
#include "llm.hpp"
#include "lock.hpp"
#include "notifs.hpp"
#include "osd.hpp"
#include "screenshot.hpp"
#include "services.hpp"
#include "sidebar.hpp"
#include "tokens.hpp"
#include "utilities.hpp"
namespace ZShell::config {
class ConfigRoot : public settings::RootNode {
CONFIG_NODE_NO_CTOR(ConfigRoot, settings::RootNode)
QML_ANONYMOUS
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_SUBOBJECT(Appearance, appearance)
CONFIG_SUBOBJECT(Background, background)
CONFIG_SUBOBJECT(Bar, bar)
CONFIG_SUBOBJECT(Clipboard, clipboard)
CONFIG_SUBOBJECT(Colors, colors)
CONFIG_SUBOBJECT(Dashboard, dashboard)
CONFIG_SUBOBJECT(Display, display)
CONFIG_SUBOBJECT(Dock, dock)
CONFIG_SUBOBJECT(General, general)
CONFIG_SUBOBJECT(Launcher, launcher)
CONFIG_SUBOBJECT(Llm, llm)
CONFIG_SUBOBJECT(Lock, lock)
CONFIG_SUBOBJECT(Notifs, notifs)
CONFIG_SUBOBJECT(Osd, osd)
CONFIG_SUBOBJECT(Screenshot, screenshot)
CONFIG_SUBOBJECT(Services, services)
CONFIG_SUBOBJECT(Sidebar, sidebar)
CONFIG_SUBOBJECT(Utilities, utilities)
public:
explicit ConfigRoot(
const QString& path,
ConfigRoot* fallback = nullptr,
QObject* parent = nullptr);
private:
// Binds the computed appearance values to the global token base values
void bindTokens();
// Applies ConfigMigrations to the file before the first load
void migrateFile(const QString& path);
};
class TokensRoot : public settings::RootNode {
CONFIG_NODE_NO_CTOR(TokensRoot, settings::RootNode)
QML_ANONYMOUS
CONFIG_SUBOBJECT(AppearanceTokens, appearance)
CONFIG_SUBOBJECT(SizeTokens, sizes)
public:
explicit TokensRoot(
const QString& path,
TokensRoot* fallback = nullptr,
QObject* parent = nullptr);
};
namespace detail {
enum class ConfigKind : quint8 { Shell, Tokens };
void loaded(ConfigKind kind, settings::RootNode* layer, const QString& screen);
void loadFailed(ConfigKind kind, const QString& error, const QString& screen);
void saveFailed(ConfigKind kind, const QString& error, const QString& screen);
} // namespace detail
#define SINGLETON_DECL(Type, Root, QmlName) \
class Type : public Root { \
Q_OBJECT \
QML_NAMED_ELEMENT(QmlName) \
QML_SINGLETON \
\
public: \
static Type* instance(); \
static Type* create(QQmlEngine*, QJSEngine*); \
\
[[nodiscard]] Q_INVOKABLE Root* forScreen(const QString& screen); \
\
private: \
explicit Type(QObject* parent = nullptr); \
\
void onTreeLoaded(settings::RootNode* layer); \
void onTreeLoadFailed(settings::RootNode* layer, const QString& error); \
void onTreeSaveFailed(settings::RootNode* layer, const QString& error); \
\
void initLayer(Root* layer); \
\
settings::LayerRegistry<Root> m_layers; \
};
SINGLETON_DECL(ConfigSingleton, ConfigRoot, Config)
SINGLETON_DECL(TokensSingleton, TokensRoot, TokenConfig)
#undef SINGLETON_DECL
class Tokens : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(
ZShell::config::AppearanceRounding* rounding READ rounding CONSTANT)
Q_PROPERTY(ZShell::config::AppearanceSpacing* spacing READ spacing CONSTANT)
Q_PROPERTY(ZShell::config::AppearancePadding* padding READ padding CONSTANT)
Q_PROPERTY(ZShell::config::SizeTokens* sizes READ sizes CONSTANT)
Q_PROPERTY(ZShell::config::FontTokens* font READ font CONSTANT)
Q_PROPERTY(ZShell::config::AnimTokens* anim READ anim CONSTANT)
public:
explicit Tokens(QObject* parent = nullptr)
: QObject(parent)
, m_font(new FontTokens(this))
, m_anim(new AnimTokens(this)) {
m_anim->bindCurves(TokensSingleton::instance()->appearance()->curves());
m_anim->bindDurations(
ConfigSingleton::instance()->appearance()->anim()->durations());
m_font->bindFont(ConfigSingleton::instance()->appearance()->font());
}
[[nodiscard]] AppearanceRounding* rounding() const {
return ConfigSingleton::instance()->appearance()->rounding();
}
[[nodiscard]] AppearancePadding* padding() const {
return ConfigSingleton::instance()->appearance()->padding();
}
[[nodiscard]] AppearanceSpacing* spacing() const {
return ConfigSingleton::instance()->appearance()->spacing();
}
[[nodiscard]] SizeTokens* sizes() const {
return TokensSingleton::instance()->sizes();
}
[[nodiscard]] FontTokens* font() const { return m_font; }
[[nodiscard]] AnimTokens* anim() const { return m_anim; }
static Tokens* create(QQmlEngine*, QJSEngine*) { return new Tokens(); }
private:
FontTokens* m_font;
AnimTokens* m_anim;
};
} // namespace ZShell::config
+13 -17
View File
@@ -1,27 +1,23 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <QVariantList> #include <QVariantList>
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class Screenshot : public ConfigObject { class Screenshot : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Screenshot, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enable_pp, false) CONFIG_PROPERTY(bool, enable_pp, false)
CFG_PROPERTY(QString, mode, QStringLiteral("manual")) CONFIG_PROPERTY(QString, mode, QStringLiteral("manual"))
CFG_PROPERTY(int, radius, 49) CONFIG_PROPERTY(int, radius, 49)
CFG_PROPERTY(bool, rounding, false) CONFIG_PROPERTY(bool, rounding, false)
CFG_PROPERTY(bool, saveOnCopy, false) CONFIG_PROPERTY(bool, saveOnCopy, false)
CFG_PROPERTY(bool, shadow, true) CONFIG_PROPERTY(bool, shadow, true)
CFG_PROPERTY(int, shadow_blur, 0) CONFIG_PROPERTY(int, shadow_blur, 0)
CFG_PROPERTY(QVariantList, shadow_color, (QVariantList{0, 0, 0, 160})) CONFIG_PROPERTY(QVariantList, shadow_color, (QVariantList{0, 0, 0, 160}))
CFG_PROPERTY(int, shadow_offset_x, 30) CONFIG_PROPERTY(int, shadow_offset_x, 30)
CFG_PROPERTY(int, shadow_offset_y, 50) CONFIG_PROPERTY(int, shadow_offset_y, 50)
public:
explicit Screenshot(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+23 -37
View File
@@ -1,56 +1,42 @@
#pragma once #pragma once
#include "configlist.hpp" #include "common.hpp"
#include "configobject.hpp"
#include "enums.hpp" #include "enums.hpp"
namespace ZShell::config { namespace ZShell::config {
class PlayerAliasEntry : public ConfigObject { class PlayerAliasEntry : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(PlayerAliasEntry, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QString, from) CONFIG_PROPERTY(QString, from, {})
CFG_PROPERTY(QString, to) CONFIG_PROPERTY(QString, to, {})
public:
explicit PlayerAliasEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("from")};
}
}; };
CONFIG_LIST_TYPE(PlayerAliasEntry, PlayerAliasList) CONFIG_LIST_TYPE(PlayerAliasEntry, PlayerAliasList)
class Services : public ConfigObject { class Services : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Services, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(qreal, audioIncrement, 0.1) CONFIG_PROPERTY(qreal, audioIncrement, 0.1)
CFG_PROPERTY(qreal, brightnessIncrement, 0.1) CONFIG_PROPERTY(qreal, brightnessIncrement, 0.1)
CFG_PROPERTY(bool, ddcutilService, true) CONFIG_PROPERTY(bool, ddcutilService, true)
CFG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify")) CONFIG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
CFG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA")) CONFIG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA"))
CFG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto) CONFIG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto)
CFG_PROPERTY(qreal, maxVolume, 1) CONFIG_PROPERTY(qreal, maxVolume, 1)
CFG_PROPERTY(qreal, minBrightness, 0.01) CONFIG_PROPERTY(qreal, minBrightness, 0.01)
CONFIG_LIST( CONFIG_LIST(
PlayerAliasList, PlayerAliasList,
playerAliases, playerAliases,
{ DEFAULT_ARG({
vmap( settings::vmap(
{{"from", QStringLiteral("com.github.th_ch.youtube_music")}, {{"from", QStringLiteral("com.github.th_ch.youtube_music")},
{"to", QStringLiteral("YT Music")}}), {"to", QStringLiteral("YT Music")}}),
}) }))
CFG_PROPERTY(bool, updates, true) CONFIG_PROPERTY(bool, updates, true)
CFG_PROPERTY(bool, useFahrenheit, false) CONFIG_PROPERTY(bool, useFahrenheit, false)
CFG_PROPERTY(bool, useTwelveHourClock, false) CONFIG_PROPERTY(bool, useTwelveHourClock, false)
CFG_PROPERTY(int, visualizerBars, 50) CONFIG_PROPERTY(int, visualizerBars, 50)
CFG_PROPERTY(QString, weatherLocation, QString()) CONFIG_PROPERTY(QString, weatherLocation, QString())
public:
explicit Services(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+8 -17
View File
@@ -1,30 +1,21 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
class SidebarSizes : public ConfigObject { class SidebarSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(SidebarSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, width, 520) CONFIG_PROPERTY(int, width, 520)
public:
explicit SidebarSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Sidebar : public ConfigObject { class Sidebar : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Sidebar, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, dragThreshold, 30) CONFIG_PROPERTY(int, dragThreshold, 30)
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CONFIG_SUBOBJECT(SidebarSizes, sizes) CONFIG_SUBOBJECT(SidebarSizes, sizes)
public:
explicit Sidebar(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new SidebarSizes(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+127 -171
View File
@@ -1,42 +1,27 @@
#pragma once #pragma once
#include "configobject.hpp" #include <qlist.h>
#include "anim.hpp"
#include "appearance.hpp"
#include "config.hpp"
#include "font.hpp"
#include <QVariantList>
#include <limits>
#include <qobject.h> #include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qqmlregistration.h>
#include <qtypes.h> #include <qtypes.h>
#include <limits>
#include "settings/objectnode.hpp"
#include "anim.hpp"
#include "common.hpp"
#include "font.hpp"
namespace ZShell::config { namespace ZShell::config {
class AnimCurves : public ConfigObject { class AnimCurves : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(AnimCurves, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(QList<qreal>, emphasized) CONFIG_GLOBAL_PROPERTY(
CFG_PROPERTY(QList<qreal>, emphasizedAccel) QList<qreal>,
CFG_PROPERTY(QList<qreal>, emphasizedDecel) emphasized,
CFG_PROPERTY(QList<qreal>, standard) DEFAULT_ARG(
CFG_PROPERTY(QList<qreal>, standardAccel)
CFG_PROPERTY(QList<qreal>, standardDecel)
CFG_PROPERTY(QList<qreal>, expressiveFastSpatial)
CFG_PROPERTY(QList<qreal>, expressiveDefaultSpatial)
CFG_PROPERTY(QList<qreal>, expressiveSlowSpatial)
CFG_PROPERTY(QList<qreal>, expressiveFastEffects)
CFG_PROPERTY(QList<qreal>, expressiveDefaultEffects)
CFG_PROPERTY(QList<qreal>, expressiveSlowEffects)
public:
explicit AnimCurves(QObject* parent = nullptr)
: ConfigObject(parent)
, m_emphasized(
{0.05, {0.05,
0, 0,
2.0 / 15.0, 2.0 / 15.0,
@@ -48,158 +33,129 @@ class AnimCurves : public ConfigObject {
0.25, 0.25,
1, 1,
1, 1,
1}) 1}))
, m_emphasizedAccel({0.3, 0, 0.8, 0.15, 1, 1}) CONFIG_GLOBAL_PROPERTY(
, m_emphasizedDecel({0.05, 0.7, 0.1, 1, 1, 1}) QList<qreal>, emphasizedAccel, DEFAULT_ARG({0.3, 0, 0.8, 0.15, 1, 1}))
, m_standard({0.2, 0, 0, 1, 1, 1}) CONFIG_GLOBAL_PROPERTY(
, m_standardAccel({0.3, 0, 1, 1, 1, 1}) QList<qreal>, emphasizedDecel, DEFAULT_ARG({0.05, 0.7, 0.1, 1, 1, 1}))
, m_standardDecel({0, 0, 0, 1, 1, 1}) CONFIG_GLOBAL_PROPERTY(
, m_expressiveFastSpatial({0.42, 1.67, 0.21, 0.9, 1, 1}) QList<qreal>, standard, DEFAULT_ARG({0.2, 0, 0, 1, 1, 1}))
, m_expressiveDefaultSpatial({0.38, 1.21, 0.22, 1, 1, 1}) CONFIG_GLOBAL_PROPERTY(
, m_expressiveSlowSpatial({0.39, 1.29, 0.35, 0.98, 1, 1}) QList<qreal>, standardAccel, DEFAULT_ARG({0.3, 0, 1, 1, 1, 1}))
, m_expressiveFastEffects({0.31, 0.94, 0.34, 1, 1, 1}) CONFIG_GLOBAL_PROPERTY(
, m_expressiveDefaultEffects({0.34, 0.8, 0.34, 1, 1, 1}) QList<qreal>, standardDecel, DEFAULT_ARG({0, 0, 0, 1, 1, 1}))
, m_expressiveSlowEffects({0.34, 0.88, 0.34, 1, 1, 1}) {} CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveFastSpatial,
DEFAULT_ARG({0.42, 1.67, 0.21, 0.9, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveDefaultSpatial,
DEFAULT_ARG({0.38, 1.21, 0.22, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveSlowSpatial,
DEFAULT_ARG({0.39, 1.29, 0.35, 0.98, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveFastEffects,
DEFAULT_ARG({0.31, 0.94, 0.34, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveDefaultEffects,
DEFAULT_ARG({0.34, 0.8, 0.34, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
expressiveSlowEffects,
DEFAULT_ARG({0.34, 0.88, 0.34, 1, 1, 1}))
}; };
class AnimTokens : public ConfigObject { class RoundingTokens : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(RoundingTokens, settings::ObjectNode)
QML_ANONYMOUS
Q_PROPERTY(ZShell::config::AnimDurations* durations READ durations CONSTANT) CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, small, 8)
Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) CONFIG_PROPERTY(int, medium, 12)
Q_PROPERTY( CONFIG_PROPERTY(int, large, 16)
QEasingCurve emphasizedAccel READ emphasizedAccel NOTIFY curvesChanged) CONFIG_PROPERTY(int, largeIncreased, 20)
Q_PROPERTY( CONFIG_PROPERTY(int, extraLarge, 28)
QEasingCurve emphasizedDecel READ emphasizedDecel NOTIFY curvesChanged) CONFIG_PROPERTY(int, extraLargeIncreased, 32)
Q_PROPERTY(QEasingCurve standard READ standard NOTIFY curvesChanged) CONFIG_PROPERTY(int, extraExtraLarge, 48)
Q_PROPERTY( CONFIG_PROPERTY(int, full, std::numeric_limits<int>::max())
QEasingCurve standardAccel READ standardAccel NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve standardDecel READ standardDecel NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveFastSpatial READ expressiveFastSpatial NOTIFY
curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveDefaultSpatial READ expressiveDefaultSpatial
NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY
curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveFastEffects READ expressiveFastEffects NOTIFY
curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveDefaultEffects READ expressiveDefaultEffects
NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY
curvesChanged)
public:
explicit AnimTokens(QObject* parent = nullptr)
: ConfigObject(parent)
, m_curves(new AnimCurves(this))
, m_easing(new AnimEasing(this)) {
connect(
m_easing,
&AnimEasing::curvesChanged,
this,
&AnimTokens::curvesChanged);
m_easing->bindCurves(m_curves);
}
[[nodiscard]] AnimDurations* durations() const {
return Config::instance()->appearance()->anim()->durations();
}
[[nodiscard]] QEasingCurve emphasized() const {
return m_easing->emphasized();
}
[[nodiscard]] QEasingCurve emphasizedAccel() const {
return m_easing->emphasizedAccel();
}
[[nodiscard]] QEasingCurve emphasizedDecel() const {
return m_easing->emphasizedDecel();
}
[[nodiscard]] QEasingCurve standard() const { return m_easing->standard(); }
[[nodiscard]] QEasingCurve standardAccel() const {
return m_easing->standardAccel();
}
[[nodiscard]] QEasingCurve standardDecel() const {
return m_easing->standardDecel();
}
[[nodiscard]] QEasingCurve expressiveFastSpatial() const {
return m_easing->expressiveFastSpatial();
}
[[nodiscard]] QEasingCurve expressiveDefaultSpatial() const {
return m_easing->expressiveDefaultSpatial();
}
[[nodiscard]] QEasingCurve expressiveSlowSpatial() const {
return m_easing->expressiveSlowSpatial();
}
[[nodiscard]] QEasingCurve expressiveFastEffects() const {
return m_easing->expressiveFastEffects();
}
[[nodiscard]] QEasingCurve expressiveDefaultEffects() const {
return m_easing->expressiveDefaultEffects();
}
[[nodiscard]] QEasingCurve expressiveSlowEffects() const {
return m_easing->expressiveSlowEffects();
}
signals:
void curvesChanged();
private:
AnimCurves* m_curves;
AnimEasing* m_easing;
}; };
class BarTokens : public ConfigObject { class SpacingTokens : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(SpacingTokens, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, innerSize, 28) CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, small, 8)
public: CONFIG_PROPERTY(int, medium, 12)
explicit BarTokens(QObject* parent = nullptr) : ConfigObject(parent) {} CONFIG_PROPERTY(int, large, 16)
CONFIG_PROPERTY(int, largeIncreased, 20)
CONFIG_PROPERTY(int, extraLarge, 28)
CONFIG_PROPERTY(int, extraLargeIncreased, 32)
CONFIG_PROPERTY(int, extraExtraLarge, 48)
}; };
class Tokens : public ConfigObject { class PaddingTokens : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(PaddingTokens, settings::ObjectNode)
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY( CONFIG_PROPERTY(int, extraSmall, 4)
ZShell::config::AppearanceRounding* rounding READ rounding CONSTANT) CONFIG_PROPERTY(int, smaller, 5)
Q_PROPERTY(ZShell::config::AppearancePadding* padding READ padding CONSTANT) CONFIG_PROPERTY(int, small, 8)
Q_PROPERTY(ZShell::config::AppearanceSpacing* spacing READ spacing CONSTANT) CONFIG_PROPERTY(int, medium, 12)
CONFIG_SUBOBJECT(FontTokens, font) CONFIG_PROPERTY(int, large, 16)
CONFIG_SUBOBJECT(AnimTokens, anim) CONFIG_PROPERTY(int, largeIncreased, 20)
CONFIG_PROPERTY(int, extraLarge, 28)
CONFIG_PROPERTY(int, extraLargeIncreased, 32)
CONFIG_PROPERTY(int, extraExtraLarge, 48)
};
class AnimDurationTokens : public settings::ObjectNode {
CONFIG_NODE(AnimDurationTokens, settings::ObjectNode)
CONFIG_GLOBAL_PROPERTY(int, small, 200)
CONFIG_GLOBAL_PROPERTY(int, normal, 400)
CONFIG_GLOBAL_PROPERTY(int, large, 600)
CONFIG_GLOBAL_PROPERTY(int, extraLarge, 1000)
CONFIG_GLOBAL_PROPERTY(int, expressiveFastSpatial, 350)
CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultSpatial, 500)
CONFIG_GLOBAL_PROPERTY(int, expressiveSlowSpatial, 650)
CONFIG_GLOBAL_PROPERTY(int, expressiveFastEffects, 150)
CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultEffects, 200)
CONFIG_GLOBAL_PROPERTY(int, expressiveSlowEffects, 300)
};
class AppearanceTokens : public settings::ObjectNode {
CONFIG_NODE(AppearanceTokens, settings::ObjectNode)
CONFIG_SUBOBJECT(AnimCurves, curves)
CONFIG_SUBOBJECT(RoundingTokens, rounding)
CONFIG_SUBOBJECT(SpacingTokens, spacing)
CONFIG_SUBOBJECT(PaddingTokens, padding)
CONFIG_SUBOBJECT(AnimDurationTokens, animDurations)
};
class BarTokens : public settings::ObjectNode {
CONFIG_NODE(BarTokens, settings::ObjectNode)
CONFIG_PROPERTY(int, innerSize, 28)
};
class DashboardTokens : public settings::ObjectNode {
CONFIG_NODE(DashboardTokens, settings::ObjectNode)
CONFIG_PROPERTY(int, processesWidth, 1200)
CONFIG_PROPERTY(int, processesHeight, 800)
CONFIG_PROPERTY(int, processesHeaderHeight, 40)
CONFIG_PROPERTY(int, processesRowHeight, 40)
};
class SizeTokens : public settings::ObjectNode {
CONFIG_NODE(SizeTokens, settings::ObjectNode)
CONFIG_SUBOBJECT(DashboardTokens, dashboard)
CONFIG_SUBOBJECT(BarTokens, bar) CONFIG_SUBOBJECT(BarTokens, bar)
public:
explicit Tokens(QObject* parent = nullptr)
: ConfigObject(parent)
, m_font(new FontTokens(this))
, m_anim(new AnimTokens(this))
, m_bar(new BarTokens(this)) {
if (!Config::instance()) new Config();
}
[[nodiscard]] AppearanceRounding* rounding() const {
return Config::instance()->appearance()->rounding();
}
[[nodiscard]] AppearancePadding* padding() const {
return Config::instance()->appearance()->padding();
}
[[nodiscard]] AppearanceSpacing* spacing() const {
return Config::instance()->appearance()->spacing();
}
static Tokens* create(QQmlEngine*, QJSEngine*) { return new Tokens(); }
}; };
} // namespace ZShell::config } // namespace ZShell::config
+30 -49
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "configobject.hpp" #include "common.hpp"
#include "configlist.hpp"
#include <QVariantList> #include <QVariantList>
#include <QStringList> #include <QStringList>
@@ -9,75 +8,57 @@
namespace ZShell::config { namespace ZShell::config {
class UtilitiesSizes : public ConfigObject { class UtilitiesSizes : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(UtilitiesSizes, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(int, toastWidth, 430) CONFIG_PROPERTY(int, toastWidth, 430)
CFG_PROPERTY(int, width, 430) CONFIG_PROPERTY(int, width, 430)
public:
explicit UtilitiesSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Toasts : public ConfigObject { class Toasts : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Toasts, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, audioInputChanged, true) CONFIG_PROPERTY(bool, audioInputChanged, true)
CFG_PROPERTY(bool, audioOutputChanged, true) CONFIG_PROPERTY(bool, audioOutputChanged, true)
CFG_PROPERTY(bool, capsLockChanged, true) CONFIG_PROPERTY(bool, capsLockChanged, true)
CFG_PROPERTY(bool, chargingChanged, true) CONFIG_PROPERTY(bool, chargingChanged, true)
CFG_PROPERTY(bool, configLoaded, true) CONFIG_PROPERTY(bool, configLoaded, true)
CFG_PROPERTY(bool, dndChanged, true) CONFIG_PROPERTY(bool, dndChanged, true)
CFG_PROPERTY(bool, gameModeChanged, true) CONFIG_PROPERTY(bool, gameModeChanged, true)
CFG_PROPERTY(bool, kbLayoutChanged, true) CONFIG_PROPERTY(bool, kbLayoutChanged, true)
CFG_PROPERTY(bool, kbLimit, true) CONFIG_PROPERTY(bool, kbLimit, true)
CFG_PROPERTY(bool, nowPlaying, false) CONFIG_PROPERTY(bool, nowPlaying, false)
CFG_PROPERTY(bool, numLockChanged, true) CONFIG_PROPERTY(bool, numLockChanged, true)
CFG_PROPERTY(bool, vpnChanged, true) CONFIG_PROPERTY(bool, vpnChanged, true)
public:
explicit Toasts(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Vpn : public ConfigObject { class Vpn : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Vpn, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, false) CONFIG_PROPERTY(bool, enabled, false)
CFG_PROPERTY(QStringList, provider, (QStringList{QStringLiteral("netbird")})) CONFIG_PROPERTY(
QStringList, provider, (QStringList{QStringLiteral("netbird")}))
public:
explicit Vpn(QObject* parent = nullptr) : ConfigObject(parent) {}
}; };
class Utilities : public ConfigObject { class Utilities : public settings::ObjectNode {
Q_OBJECT CONFIG_NODE(Utilities, settings::ObjectNode)
QML_ANONYMOUS
CFG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, maxToasts, 4) CONFIG_PROPERTY(int, maxToasts, 4)
CONFIG_SUBOBJECT(UtilitiesSizes, sizes) CONFIG_SUBOBJECT(UtilitiesSizes, sizes)
CONFIG_SUBOBJECT(Toasts, toasts) CONFIG_SUBOBJECT(Toasts, toasts)
CONFIG_SUBOBJECT(Vpn, vpn) CONFIG_SUBOBJECT(Vpn, vpn)
CONFIG_LIST( CONFIG_LIST(
EntryList, EntryList,
quickToggles, quickToggles,
DEFAULT_ARG(
{LIST_ENTRY(dnd, true), {LIST_ENTRY(dnd, true),
LIST_ENTRY(wifi, true), LIST_ENTRY(wifi, true),
LIST_ENTRY(settings, true), LIST_ENTRY(settings, true),
LIST_ENTRY(bluetooth, true), LIST_ENTRY(bluetooth, true),
LIST_ENTRY(mic, true), LIST_ENTRY(mic, true),
LIST_ENTRY(audio, true), LIST_ENTRY(audio, true),
LIST_ENTRY(gameMode, true)}) LIST_ENTRY(gameMode, true)}))
public:
explicit Utilities(QObject* parent = nullptr)
: ConfigObject(parent)
, m_sizes(new UtilitiesSizes(this))
, m_toasts(new Toasts(this))
, m_vpn(new Vpn(this)) {}
}; };
} // namespace ZShell::config } // namespace ZShell::config
+2 -1
View File
@@ -1,12 +1,12 @@
qml_module(ZShell-internal qml_module(ZShell-internal
URI ZShell.Internal URI ZShell.Internal
INCLUDE_PREFIX internal
SOURCES SOURCES
hyprextras.hpp hyprextras.cpp hyprextras.hpp hyprextras.cpp
hyprdevices.hpp hyprdevices.cpp hyprdevices.hpp hyprdevices.cpp
cachingimagemanager.hpp cachingimagemanager.cpp cachingimagemanager.hpp cachingimagemanager.cpp
circularindicatormanager.hpp circularindicatormanager.cpp circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp circularbuffer.hpp circularbuffer.cpp
sparklineitem.hpp sparklineitem.cpp
arcgauge.hpp arcgauge.cpp arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp wallpaperimage.hpp wallpaperimage.cpp
lidwatcher.hpp lidwatcher.cpp lidwatcher.hpp lidwatcher.cpp
@@ -14,6 +14,7 @@ qml_module(ZShell-internal
linearindicatormanager.hpp linearindicatormanager.cpp linearindicatormanager.hpp linearindicatormanager.cpp
strokecanvasitem.hpp strokecanvasitem.cpp strokecanvasitem.hpp strokecanvasitem.cpp
strokecanvasrenderer.hpp strokecanvasrenderer.cpp strokecanvasrenderer.hpp strokecanvasrenderer.cpp
cursor.hpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
@@ -19,6 +19,8 @@ void CircularBuffer::setCapacity(int capacity) {
m_capacity = capacity; m_capacity = capacity;
m_data.resize(capacity); m_data.resize(capacity);
m_data.fill(0.0); m_data.fill(0.0);
m_smoothed.resize(capacity);
m_smoothed.fill(0.0);
m_head = 0; m_head = 0;
m_count = 0; m_count = 0;
@@ -30,6 +32,8 @@ void CircularBuffer::setCapacity(int capacity) {
m_count++; m_count++;
} }
recomputeAllSmoothed();
emit capacityChanged(); emit capacityChanged();
emit countChanged(); emit countChanged();
emit valuesChanged(); emit valuesChanged();
@@ -56,6 +60,19 @@ qreal CircularBuffer::maximum() const {
return maxVal; return maxVal;
} }
int CircularBuffer::smoothingRadius() const {
return m_smoothingRadius;
}
void CircularBuffer::setSmoothingRadius(int radius) {
radius = std::max(0, radius);
if (m_smoothingRadius == radius) return;
m_smoothingRadius = radius;
recomputeAllSmoothed();
emit smoothingRadiusChanged();
emit valuesChanged();
}
void CircularBuffer::push(qreal value) { void CircularBuffer::push(qreal value) {
if (m_capacity <= 0) return; if (m_capacity <= 0) return;
@@ -65,6 +82,9 @@ void CircularBuffer::push(qreal value) {
m_count++; m_count++;
emit countChanged(); emit countChanged();
} }
updateSmoothedTail();
emit valuesChanged(); emit valuesChanged();
} }
@@ -85,4 +105,44 @@ qreal CircularBuffer::at(int index) const {
return m_data[actualIndex]; return m_data[actualIndex];
} }
qreal CircularBuffer::smoothedAt(int index) const {
if (index < 0 || index >= m_count) return 0.0;
const int actualIndex =
(m_head - m_count + index + m_capacity) % m_capacity;
return m_smoothed[actualIndex];
}
void CircularBuffer::writeSmoothedAt(int logicalIndex) {
const int actualIndex =
(m_head - m_count + logicalIndex + m_capacity) % m_capacity;
if (m_smoothingRadius <= 0) {
m_smoothed[actualIndex] = m_data[actualIndex];
return;
}
qreal sum = 0.0;
int sampleCount = 0;
for (int j = -m_smoothingRadius; j <= m_smoothingRadius; ++j) {
const int idx = logicalIndex + j;
if (idx < 0 || idx >= m_count) continue;
const int a = (m_head - m_count + idx + m_capacity) % m_capacity;
sum += m_data[a];
sampleCount++;
}
m_smoothed[actualIndex] = sampleCount > 0 ? sum / sampleCount : 0.0;
}
void CircularBuffer::updateSmoothedTail() {
const int start = std::max(0, m_count - 1 - m_smoothingRadius);
for (int i = start; i < m_count; ++i)
writeSmoothedAt(i);
}
void CircularBuffer::recomputeAllSmoothed() {
for (int i = 0; i < m_count; ++i)
writeSmoothedAt(i);
}
} // namespace ZShell::internal } // namespace ZShell::internal
@@ -15,6 +15,9 @@ class CircularBuffer : public QObject {
Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QList<qreal> values READ values NOTIFY valuesChanged) Q_PROPERTY(QList<qreal> values READ values NOTIFY valuesChanged)
Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged) Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged)
Q_PROPERTY(
int smoothingRadius READ smoothingRadius WRITE setSmoothingRadius NOTIFY
smoothingRadiusChanged)
public: public:
explicit CircularBuffer(QObject* parent = nullptr); explicit CircularBuffer(QObject* parent = nullptr);
@@ -26,20 +29,34 @@ class CircularBuffer : public QObject {
[[nodiscard]] QList<qreal> values() const; [[nodiscard]] QList<qreal> values() const;
[[nodiscard]] qreal maximum() const; [[nodiscard]] qreal maximum() const;
[[nodiscard]] int smoothingRadius() const;
void setSmoothingRadius(int radius);
Q_INVOKABLE void push(qreal value); Q_INVOKABLE void push(qreal value);
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE [[nodiscard]] qreal at(int index) const; Q_INVOKABLE [[nodiscard]] qreal at(int index) const;
Q_INVOKABLE [[nodiscard]] qreal smoothedAt(int index) const;
signals: signals:
void capacityChanged(); void capacityChanged();
void countChanged(); void countChanged();
void valuesChanged(); void valuesChanged();
void smoothingRadiusChanged();
private: private:
void writeSmoothedAt(int logicalIndex);
void updateSmoothedTail();
void recomputeAllSmoothed();
QVector<qreal> m_data; QVector<qreal> m_data;
QVector<qreal> m_smoothed;
int m_head = 0; int m_head = 0;
int m_count = 0; int m_count = 0;
int m_capacity = 0; int m_capacity = 0;
int m_smoothingRadius = 0;
}; };
} // namespace ZShell::internal } // namespace ZShell::internal
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <qcursor.h>
#include <qguiapplication.h>
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::internal {
class Cursor : public QObject {
Q_OBJECT
QML_SINGLETON
QML_ELEMENT
public:
Q_INVOKABLE void setOverride(Qt::CursorShape shape) {
if (m_override)
QGuiApplication::changeOverrideCursor(QCursor(shape));
else {
QGuiApplication::setOverrideCursor(QCursor(shape));
m_override = true;
}
}
Q_INVOKABLE void clearOverride() {
if (!m_override) return;
QGuiApplication::restoreOverrideCursor();
m_override = false;
}
private:
bool m_override = false;
};
} // namespace ZShell::internal
-322
View File
@@ -1,322 +0,0 @@
#include "sparklineitem.hpp"
#include <qmath.h>
#include <qnumeric.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
#include <qtypes.h>
namespace ZShell::internal {
SparklineItem::SparklineItem(QQuickItem* parent) : QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void SparklineItem::paint(QPainter* painter) {
const bool has1 = m_line1 && m_line1->count() >= 2;
const bool has2 = m_line2 && m_line2->count() >= 2;
if (!has1 && !has2) return;
painter->setRenderHint(QPainter::Antialiasing, true);
// Draw line1 first (behind), then line2 (in front)
if (has1) drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha);
if (has2) drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha);
}
void SparklineItem::drawLine(
QPainter* painter,
CircularBuffer* buffer,
const QColor& color,
qreal fillAlpha) {
if (m_historyLength < 2) return;
const qreal w = width();
const qreal h = height();
const int len = buffer->count();
if (len < 2 || m_maxValue <= 0.0) return;
qreal stepX = 0.0;
if (m_sliding)
stepX = w / static_cast<qreal>(m_historyLength - 1);
else
stepX = w / static_cast<qreal>(len - 1);
qreal startX = 0.0;
if (m_sliding)
startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
else
startX = 0;
const qreal strokePad = qCeil(m_lineWidth / 2);
const qreal curvePad = 3.0;
const qreal topPad = strokePad + curvePad;
const qreal bottomPad = strokePad;
const qreal plotTop = topPad;
const qreal plotBottom = h - bottomPad;
const qreal fillBottom = h;
const qreal plotH = qMax<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
qreal value = 0.0;
if (m_smoothing > 0.0) {
const int radius = qRound(m_smoothing);
qreal sum = 0.0;
int count = 0;
for (int j = -radius; j <= radius; ++j) {
const int index = i + j;
if (index < 0 || index >= len) continue;
sum += buffer->at(index);
count++;
}
value = sum / static_cast<qreal>(count);
} else {
value = buffer->at(i);
}
value = qBound<qreal>(0.0, value, m_maxValue);
const qreal x = startX + i * stepX;
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
points.append(QPointF(x, y));
}
QPainterPath linePath;
linePath.moveTo(points[0]);
if (m_curveSmoothing && points.size() > 2) {
// 1.0 = standard uniform Catmull-Rom. Lower (e.g. 0.5) = tighter, less overshoot.
const qreal tension = 1.0;
const qreal k =
tension / 6.0; // Catmull-Rom -> Bezier factor (1/6 for uniform)
const int n = points.size();
for (int i = 0; i < n - 1; ++i) {
// Duplicate endpoints so the first/last segments are well-defined
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
const QPointF& p2 = points[i + 1];
const QPointF& p3 = points[qMin(i + 2, n - 1)];
QPointF c1 = p1 + (p2 - p0) * k;
QPointF c2 = p2 - (p3 - p1) * k;
// Keep control points inside the vertical range of the segment's
// endpoints so we never overshoot (no dips below zero, no
// bulges above a peak). Remember: smaller y = higher value.
const qreal segMinY = qMin(p1.y(), p2.y());
const qreal segMaxY = qMax(p1.y(), p2.y());
c1.setY(qBound(segMinY, c1.y(), segMaxY));
c2.setY(qBound(segMinY, c2.y(), segMaxY));
linePath.cubicTo(c1, c2, p2);
}
} else {
// Existing horizontal-tangent (smoothstep-style) curve
for (int i = 0; i < points.size() - 1; ++i) {
const QPointF& p0 = points[i];
const QPointF& p1 = points[i + 1];
const qreal ctrlX = (p0.x() + p1.x()) * 0.5;
linePath.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1);
}
}
QPen pen(color, m_lineWidth);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
painter->setPen(pen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(linePath);
QPainterPath fillPath = linePath;
fillPath.lineTo(startX + (len - 1) * stepX, fillBottom);
fillPath.lineTo(startX, fillBottom);
fillPath.closeSubpath();
QColor fillColor = color;
fillColor.setAlphaF(static_cast<float>(fillAlpha));
painter->setPen(Qt::NoPen);
painter->setBrush(fillColor);
painter->drawPath(fillPath);
}
void SparklineItem::connectBuffer(CircularBuffer* buffer) {
if (!buffer) return;
connect(buffer, &CircularBuffer::valuesChanged, this, [this]() {
update();
});
connect(buffer, &QObject::destroyed, this, [this, buffer]() {
if (m_line1 == buffer) {
m_line1 = nullptr;
emit line1Changed();
}
if (m_line2 == buffer) {
m_line2 = nullptr;
emit line2Changed();
}
update();
});
}
CircularBuffer* SparklineItem::line1() const {
return m_line1;
}
void SparklineItem::setLine1(CircularBuffer* buffer) {
if (m_line1 == buffer) return;
if (m_line1) disconnect(m_line1, nullptr, this, nullptr);
m_line1 = buffer;
connectBuffer(buffer);
emit line1Changed();
update();
}
CircularBuffer* SparklineItem::line2() const {
return m_line2;
}
void SparklineItem::setLine2(CircularBuffer* buffer) {
if (m_line2 == buffer) return;
if (m_line2) disconnect(m_line2, nullptr, this, nullptr);
m_line2 = buffer;
connectBuffer(buffer);
emit line2Changed();
update();
}
QColor SparklineItem::line1Color() const {
return m_line1Color;
}
void SparklineItem::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
emit line1ColorChanged();
update();
}
QColor SparklineItem::line2Color() const {
return m_line2Color;
}
void SparklineItem::setLine2Color(const QColor& color) {
if (m_line2Color == color) return;
m_line2Color = color;
emit line2ColorChanged();
update();
}
qreal SparklineItem::line1FillAlpha() const {
return m_line1FillAlpha;
}
void SparklineItem::setLine1FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line1FillAlpha, alpha)) return;
m_line1FillAlpha = alpha;
emit line1FillAlphaChanged();
update();
}
qreal SparklineItem::line2FillAlpha() const {
return m_line2FillAlpha;
}
void SparklineItem::setLine2FillAlpha(qreal alpha) {
if (qFuzzyCompare(m_line2FillAlpha, alpha)) return;
m_line2FillAlpha = alpha;
emit line2FillAlphaChanged();
update();
}
qreal SparklineItem::maxValue() const {
return m_maxValue;
}
void SparklineItem::setMaxValue(qreal value) {
if (qFuzzyCompare(m_maxValue, value)) return;
m_maxValue = value;
emit maxValueChanged();
update();
}
qreal SparklineItem::slideProgress() const {
return m_slideProgress;
}
void SparklineItem::setSlideProgress(qreal progress) {
if (qFuzzyCompare(m_slideProgress, progress)) return;
m_slideProgress = progress;
emit slideProgressChanged();
update();
}
int SparklineItem::historyLength() const {
return m_historyLength;
}
void SparklineItem::setHistoryLength(int length) {
if (m_historyLength == length) return;
m_historyLength = length;
emit historyLengthChanged();
update();
}
qreal SparklineItem::lineWidth() const {
return m_lineWidth;
}
void SparklineItem::setLineWidth(qreal width) {
if (qFuzzyCompare(m_lineWidth, width)) return;
m_lineWidth = width;
emit lineWidthChanged();
update();
}
void SparklineItem::setSliding(bool sliding) {
if (m_sliding == sliding) return;
m_sliding = sliding;
emit slidingChanged();
}
bool SparklineItem::sliding() const {
return m_sliding;
};
void SparklineItem::setSmoothing(qreal smoothing) {
smoothing = qMax<qreal>(0.0, smoothing);
if (qFuzzyCompare(m_smoothing, smoothing)) return;
m_smoothing = smoothing;
emit smoothingChanged();
}
qreal SparklineItem::smoothing() const {
return m_smoothing;
};
void SparklineItem::setCurveSmoothing(bool sliding) {
if (m_curveSmoothing == sliding) return;
m_curveSmoothing = sliding;
emit curveSmoothingChanged();
}
bool SparklineItem::curveSmoothing() const {
return m_curveSmoothing;
};
} // namespace ZShell::internal
+1 -56
View File
@@ -1,38 +1,12 @@
# cmark-gfm: markdown -> block structure (pkg-config; no CMake config
# package is installed).
pkg_check_modules(CMARK_GFM REQUIRED IMPORTED_TARGET libcmark-gfm) pkg_check_modules(CMARK_GFM REQUIRED IMPORTED_TARGET libcmark-gfm)
# tree-sitter runtime for code block highlighting (grammars are
# dlopen()'d at runtime and optional).
pkg_check_modules(TREE_SITTER REQUIRED IMPORTED_TARGET tree-sitter) pkg_check_modules(TREE_SITTER REQUIRED IMPORTED_TARGET tree-sitter)
# JKQTMathText (JKQtPlotter) for LaTeX rendering. The config files live
# in a shared JKQTPlotter6 directory, not one named after the package.
find_path(JKQTPlotter6_CMAKE_DIR find_path(JKQTPlotter6_CMAKE_DIR
NAMES JKQTMathText6Config.cmake NAMES JKQTMathText6Config.cmake
HINTS /usr/lib/cmake/JKQTPlotter6 /usr/local/lib/cmake/JKQTPlotter6 HINTS /usr/lib/cmake/JKQTPlotter6 /usr/local/lib/cmake/JKQTPlotter6
DOC "Directory containing the JKQtPlotter cmake package files") DOC "Directory containing the JKQtPlotter cmake package files")
find_package(JKQTMathText6 REQUIRED PATHS "${JKQTPlotter6_CMAKE_DIR}") find_package(JKQTMathText6 REQUIRED PATHS "${JKQTPlotter6_CMAKE_DIR}")
# --- tree-sitter grammar discovery (configure time) ---
#
# Discover installed tree-sitter grammars — system packages
# (libtree-sitter-<lang>.so) and the parsers Neovim's nvim-treesitter
# installs (~/.local/share/nvim/site/parser/*.so) — and pair each with
# highlight queries. Query sources, in priority order:
# 1. vendored highlight-queries/*.scm (version-pinned; see the
# per-file source headers, MIT),
# 2. Neovim's own queries (version-matched to its parsers),
# 3. tree-sitter/highlighting from GitHub (cached in the build dir).
# The result is embedded as highlight-queries.hpp. Re-run cmake to pick
# up grammars installed later.
#
# Candidate entry points are read from the .so with `nm` rather than
# assumed to be tree_sitter_<id>; a missing entry point just makes that
# candidate fail at runtime.
function(_ts_entry_point out file) function(_ts_entry_point out file)
# OUTPUT_VARIABLE + OUTPUT_QUIET loses the output on CMake 4, so
# capture through a temp file.
get_filename_component(_ts_nm_base "${file}" NAME) get_filename_component(_ts_nm_base "${file}" NAME)
set(_ts_nm_file "${CMAKE_CURRENT_BINARY_DIR}/ts-entry-${_ts_nm_base}") set(_ts_nm_file "${CMAKE_CURRENT_BINARY_DIR}/ts-entry-${_ts_nm_base}")
execute_process( execute_process(
@@ -44,8 +18,6 @@ function(_ts_entry_point out file)
if(_ts_nm_rc EQUAL 0 AND EXISTS "${_ts_nm_file}") if(_ts_nm_rc EQUAL 0 AND EXISTS "${_ts_nm_file}")
file(READ "${_ts_nm_file}" _ts_nm_out) file(READ "${_ts_nm_file}" _ts_nm_out)
file(REMOVE "${_ts_nm_file}" "${_ts_nm_file}.err") file(REMOVE "${_ts_nm_file}" "${_ts_nm_file}.err")
# The library also exports the external scanner functions; the
# entry point is the one without the _external suffix.
string(REGEX MATCHALL " T tree_sitter_[A-Za-z0-9_]+" _ts_syms "${_ts_nm_out}") string(REGEX MATCHALL " T tree_sitter_[A-Za-z0-9_]+" _ts_syms "${_ts_nm_out}")
foreach(_ts_s IN LISTS _ts_syms) foreach(_ts_s IN LISTS _ts_syms)
string(REPLACE " T " "" _ts_s "${_ts_s}") string(REPLACE " T " "" _ts_s "${_ts_s}")
@@ -58,22 +30,11 @@ function(_ts_entry_point out file)
set(${out} "${_ts_sym}" PARENT_SCOPE) set(${out} "${_ts_sym}" PARENT_SCOPE)
endfunction() endfunction()
# Append a query text to <files> (PARENT_SCOPE) unless it duplicates a
# hash in <hashes>. The text is written to the build tree right away:
# the s-expression `;` comments would split CMake list items and the
# query `(` `)` break bracket arguments, so query text only ever lives
# in variables and files, never in lists.
# Expand Neovim's "; inherits:" directives by appending the inherited
# query set's highlights file (recursively; a visited set prevents
# cycles). Several languages are stubs that inherit the real query
# (html inherits html_tags, qmljs inherits ecma, ...).
function(_ts_expand_inherits query_dir text outVar) function(_ts_expand_inherits query_dir text outVar)
set(result "${text}") set(result "${text}")
set(_visited "") set(_visited "")
set(_depth 0) set(_depth 0)
while(_depth LESS 8) while(_depth LESS 8)
# Do not match the leading `;`: a MATCHALL result that itself
# contains a semicolon is re-split into list items.
string(REGEX MATCHALL "inherits:[ \t]*[A-Za-z0-9_]+" _inh "${result}") string(REGEX MATCHALL "inherits:[ \t]*[A-Za-z0-9_]+" _inh "${result}")
if(NOT _inh) if(NOT _inh)
break() break()
@@ -99,7 +60,6 @@ function(_ts_expand_inherits query_dir text outVar)
endfunction() endfunction()
function(_ts_add_query sid text filesVar hashesVar) function(_ts_add_query sid text filesVar hashesVar)
# filesVar/hashesVar hold the caller's variable names.
set(files "${${filesVar}}") set(files "${${filesVar}}")
set(hashes "${${hashesVar}}") set(hashes "${${hashesVar}}")
string(SHA256 _ts_qh "${text}") string(SHA256 _ts_qh "${text}")
@@ -164,20 +124,13 @@ set(_ts_vendored
foreach(_ts_id IN LISTS _ts_ids) foreach(_ts_id IN LISTS _ts_ids)
string(REPLACE "-" "_" _ts_sid "${_ts_id}") string(REPLACE "-" "_" _ts_sid "${_ts_id}")
# Query candidates in priority order (deduped; identical texts are
# skipped, the nvim site and plugin copies are usually the same
# file).
set(_ts_qfiles "") set(_ts_qfiles "")
set(_ts_qhashes "") set(_ts_qhashes "")
if(_ts_id STREQUAL "cpp") if(_ts_id STREQUAL "cpp")
# The C++ grammar is a superset of C and its query only covers
# the C++ delta; base C coverage comes from the C query.
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/c.scm" _ts_qa) file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/c.scm" _ts_qa)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/cpp.scm" _ts_qb) file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/cpp.scm" _ts_qb)
_ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes) _ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
elseif(_ts_id STREQUAL "typescript" OR _ts_id STREQUAL "tsx") elseif(_ts_id STREQUAL "typescript" OR _ts_id STREQUAL "tsx")
# The TS grammars reuse the JS node names; the JS query usually
# compiles against them and gives full coverage.
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/javascript.scm" _ts_qa) file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/javascript.scm" _ts_qa)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/typescript.scm" _ts_qb) file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/typescript.scm" _ts_qb)
_ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes) _ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
@@ -195,10 +148,6 @@ foreach(_ts_id IN LISTS _ts_ids)
endif() endif()
endforeach() endforeach()
if(NOT _ts_qfiles) if(NOT _ts_qfiles)
# Last resort: fetch from the tree-sitter/highlighting repo.
# Version-skewed against locally installed grammars, so only
# used when nothing local exists. Cached; offline builds simply
# drop the language.
set(_ts_dl "${CMAKE_CURRENT_BINARY_DIR}/ts-query-downloads/${_ts_sid}.scm") set(_ts_dl "${CMAKE_CURRENT_BINARY_DIR}/ts-query-downloads/${_ts_sid}.scm")
if(NOT EXISTS "${_ts_dl}") if(NOT EXISTS "${_ts_dl}")
file(DOWNLOAD file(DOWNLOAD
@@ -219,7 +168,6 @@ foreach(_ts_id IN LISTS _ts_ids)
continue() continue()
endif() endif()
# Emit query sources.
set(_ts_qn 0) set(_ts_qn 0)
set(_ts_q_ptrs "") set(_ts_q_ptrs "")
foreach(_ts_qfile IN LISTS _ts_qfiles) foreach(_ts_qfile IN LISTS _ts_qfiles)
@@ -232,7 +180,6 @@ foreach(_ts_id IN LISTS _ts_ids)
string(APPEND _hl_header string(APPEND _hl_header
"inline constexpr const char* const q_${_ts_sid}[] = { ${_ts_q_ptrs} };\n") "inline constexpr const char* const q_${_ts_sid}[] = { ${_ts_q_ptrs} };\n")
# Emit library candidates (system package first, then Neovim).
string(APPEND _hl_header "inline constexpr Candidate cand_${_ts_sid}[] = {\n") string(APPEND _hl_header "inline constexpr Candidate cand_${_ts_sid}[] = {\n")
set(_ts_nc 0) set(_ts_nc 0)
foreach(_ts_c IN LISTS _ts_candidates) foreach(_ts_c IN LISTS _ts_candidates)
@@ -248,7 +195,6 @@ foreach(_ts_id IN LISTS _ts_ids)
endforeach() endforeach()
string(APPEND _hl_header "};\n") string(APPEND _hl_header "};\n")
if(_ts_nc EQUAL 0) if(_ts_nc EQUAL 0)
# Queries but no library: pointless, drop the language.
string(APPEND _hl_header "") # (arrays stay; grammar row is skipped) string(APPEND _hl_header "") # (arrays stay; grammar row is skipped)
continue() continue()
endif() endif()
@@ -263,8 +209,6 @@ endforeach()
string(APPEND _hl_header "};\n}\n") string(APPEND _hl_header "};\n}\n")
file(WRITE "${HIGHLIGHT_QUERIES_HPP}" "${_hl_header}") file(WRITE "${HIGHLIGHT_QUERIES_HPP}" "${_hl_header}")
# Embed the vendored Latin Modern fonts (GUST Font License; provenance
# in fonts/latinmodern/GUST-FONT-LICENSE.txt) as byte arrays.
set(LM_FONTS set(LM_FONTS
lmroman10-regular lmroman10-regular
lmroman10-italic lmroman10-italic
@@ -308,6 +252,7 @@ qml_module(ZShell-llm
Qt::Gui Qt::Gui
Qt::Widgets Qt::Widgets
ZShell-config ZShell-config
ZShell-settings
JKQTPlotter::JKQTMathText JKQTPlotter::JKQTMathText
PkgConfig::CMARK_GFM PkgConfig::CMARK_GFM
PkgConfig::TREE_SITTER PkgConfig::TREE_SITTER
+5 -7
View File
@@ -1,6 +1,6 @@
#include "chat.hpp" #include "chat.hpp"
#include "config.hpp" #include "config/rootnodes.hpp"
#include "filetool.hpp" #include "filetool.hpp"
#include "llm.hpp" #include "llm.hpp"
#include "llmclient.hpp" #include "llmclient.hpp"
@@ -15,14 +15,12 @@ Chat::Chat(QObject* parent)
: QObject(parent) : QObject(parent)
, m_store(new ChatStore(this)) , m_store(new ChatStore(this))
, m_client(new LlmClient(this)) { , m_client(new LlmClient(this)) {
if (!config::Config::instance()) new config::Config();
m_store->setLlmClient(m_client); m_store->setLlmClient(m_client);
m_client->tools()->registerTool(new WebFetchTool(m_client->tools())); m_client->tools()->registerTool(new WebFetchTool(m_client->tools()));
m_client->tools()->registerTool(new FileReadTool(m_client->tools())); m_client->tools()->registerTool(new FileReadTool(m_client->tools()));
m_client->tools()->registerTool(new ShellTool(m_client, m_client->tools())); m_client->tools()->registerTool(new ShellTool(m_client, m_client->tools()));
const auto* llm = config::Config::instance()->llm(); const auto* llm = config::ConfigSingleton::instance()->llm();
m_client->setEndpoint(llm->endpoint()); m_client->setEndpoint(llm->endpoint());
m_client->setModel(llm->model()); m_client->setModel(llm->model());
m_client->setTemperature(llm->temperature()); m_client->setTemperature(llm->temperature());
@@ -144,7 +142,7 @@ bool Chat::toolsEnabled() const {
void Chat::setToolsEnabled(bool value) { void Chat::setToolsEnabled(bool value) {
m_client->setToolsEnabled(value); m_client->setToolsEnabled(value);
if (auto* config = config::Config::instance()) if (auto* config = config::ConfigSingleton::instance())
config->llm()->set_tools(value); config->llm()->set_tools(value);
} }
@@ -154,7 +152,7 @@ bool Chat::sandboxEnabled() const {
void Chat::setSandboxEnabled(bool value) { void Chat::setSandboxEnabled(bool value) {
m_client->setSandboxEnabled(value); m_client->setSandboxEnabled(value);
if (auto* config = config::Config::instance()) if (auto* config = config::ConfigSingleton::instance())
config->llm()->set_sandbox(value); config->llm()->set_sandbox(value);
} }
@@ -195,7 +193,7 @@ void Chat::refreshFromServer() {
void Chat::selectModel(const QString& id) { void Chat::selectModel(const QString& id) {
if (id.isEmpty()) return; if (id.isEmpty()) return;
m_client->setModel(id); m_client->setModel(id);
if (auto* config = config::Config::instance()) config->llm()->set_model(id); if (auto* config = config::ConfigSingleton::instance()) config->llm()->set_model(id);
} }
} // namespace ZShell::llm } // namespace ZShell::llm
+2
View File
@@ -18,6 +18,7 @@ qml_module(ZShell-services
memory.hpp memory.cpp memory.hpp memory.cpp
diskinfo.hpp diskinfo.cpp diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp storage.hpp storage.cpp
processes.hpp processes.cpp
lyriccandidate.hpp lyriccandidate.cpp lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp lyrics.hpp lyrics.cpp
LIBRARIES LIBRARIES
@@ -28,6 +29,7 @@ qml_module(ZShell-services
PkgConfig::Cava PkgConfig::Cava
Sensors::Sensors Sensors::Sensors
ZShell-config ZShell-config
ZShell-settings
DEPENDENCIES DEPENDENCIES
ZShell.Config ZShell.Config
) )
+5 -5
View File
@@ -7,7 +7,7 @@
#include <qsavefile.h> #include <qsavefile.h>
#include <qurlquery.h> #include <qurlquery.h>
#include "config/config.hpp" #include "config/rootnodes.hpp"
#include "config/general.hpp" #include "config/general.hpp"
#include "config/services.hpp" #include "config/services.hpp"
@@ -75,7 +75,7 @@ Lyrics::Lyrics(QObject* parent)
m_loadDebounce->setInterval(k_loadDebounceMs); m_loadDebounce->setInterval(k_loadDebounceMs);
QObject::connect(m_loadDebounce, &QTimer::timeout, this, &Lyrics::doLoad); QObject::connect(m_loadDebounce, &QTimer::timeout, this, &Lyrics::doLoad);
const auto* cfg = config::Config::instance(); const auto* cfg = config::ConfigSingleton::instance();
const auto* svcCfg = cfg->services(); const auto* svcCfg = cfg->services();
const auto* genCfg = cfg->general(); const auto* genCfg = cfg->general();
@@ -114,7 +114,7 @@ void Lyrics::setPreferredBackend(LyricsBackend value) {
m_preferredBackend = value; m_preferredBackend = value;
emit preferredBackendChanged(); emit preferredBackendChanged();
config::Config::instance()->services()->set_lyricsBackend(value); config::ConfigSingleton::instance()->services()->set_lyricsBackend(value);
scheduleLoad(); scheduleLoad();
} }
@@ -870,7 +870,7 @@ QNetworkReply* Lyrics::getJson(
void Lyrics::onPreferredBackendConfigChanged() { void Lyrics::onPreferredBackendConfigChanged() {
const LyricsBackend desired = const LyricsBackend desired =
config::Config::instance()->services()->lyricsBackend(); config::ConfigSingleton::instance()->services()->lyricsBackend();
if (desired == m_preferredBackend) { if (desired == m_preferredBackend) {
return; return;
} }
@@ -942,7 +942,7 @@ void Lyrics::persistTrackPrefs() {
} }
QString Lyrics::lyricsDir() { QString Lyrics::lyricsDir() {
QString dir = config::Config::instance()->general()->lyricsDir(); QString dir = config::ConfigSingleton::instance()->general()->lyricsDir();
if (dir.isEmpty()) { if (dir.isEmpty()) {
return {}; return {};
} }
+53 -1
View File
@@ -3,6 +3,8 @@
#include <qfile.h> #include <qfile.h>
#include <qregularexpression.h> #include <qregularexpression.h>
#include <unistd.h>
namespace ZShell::services { namespace ZShell::services {
Memory::Memory(QObject* parent) : TickingService(parent) {} Memory::Memory(QObject* parent) : TickingService(parent) {}
@@ -15,6 +17,18 @@ qreal Memory::total() const {
return m_total; return m_total;
} }
qreal Memory::cached() const {
return m_cached;
}
qreal Memory::readTotal() const {
return m_readTotal;
}
qreal Memory::writeTotal() const {
return m_writeTotal;
}
qreal Memory::percentage() const { qreal Memory::percentage() const {
return m_total > 0.0 ? m_used / m_total : 0.0; return m_total > 0.0 ? m_used / m_total : 0.0;
} }
@@ -31,6 +45,8 @@ void Memory::tick() {
QStringLiteral("MemTotal: *(\\d+)")); QStringLiteral("MemTotal: *(\\d+)"));
static const QRegularExpression reAvail( static const QRegularExpression reAvail(
QStringLiteral("MemAvailable: *(\\d+)")); QStringLiteral("MemAvailable: *(\\d+)"));
static const QRegularExpression reCached(QStringLiteral(
"Cached: *(\\d+)"));
const QString text = QString::fromLatin1(data); const QString text = QString::fromLatin1(data);
const auto totalMatch = reTotal.match(text); const auto totalMatch = reTotal.match(text);
@@ -45,14 +61,50 @@ void Memory::tick() {
return; return;
} }
const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0; const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0;
const auto cachedMatch = reCached.match(text);
const quint64 cachedKib =
cachedMatch.hasMatch() ? cachedMatch.captured(1).toULongLong() : 0;
if (totalKib == m_lastTotal && usedKib == m_lastUsed) { static const QRegularExpression rePgin(QStringLiteral("pgpgin (\\d+)"));
static const QRegularExpression rePgout(QStringLiteral("pgpgout (\\d+)"));
quint64 readKib = 0;
quint64 writeKib = 0;
{
QFile vf(QStringLiteral("/proc/vmstat"));
if (vf.open(QIODevice::ReadOnly | QIODevice::Text)) {
const QString vtext = QString::fromLatin1(vf.readAll());
vf.close();
const double pageKib = getpagesize() / 1024.0;
const auto pgin = rePgin.match(vtext);
const auto pgout = rePgout.match(vtext);
if (pgin.hasMatch()) {
readKib = static_cast<quint64>(static_cast<double>(
pgin.captured(1).toULongLong()) *
pageKib);
}
if (pgout.hasMatch()) {
writeKib = static_cast<quint64>(static_cast<double>(
pgout.captured(1).toULongLong()) *
pageKib);
}
}
}
if (totalKib == m_lastTotal && usedKib == m_lastUsed &&
cachedKib == m_lastCached && readKib == m_lastReadKib &&
writeKib == m_lastWriteKib) {
return; return;
} }
m_lastTotal = totalKib; m_lastTotal = totalKib;
m_lastUsed = usedKib; m_lastUsed = usedKib;
m_lastCached = cachedKib;
m_lastReadKib = readKib;
m_lastWriteKib = writeKib;
m_total = static_cast<qreal>(totalKib); m_total = static_cast<qreal>(totalKib);
m_used = static_cast<qreal>(usedKib); m_used = static_cast<qreal>(usedKib);
m_cached = static_cast<qreal>(cachedKib);
m_readTotal = static_cast<qreal>(readKib);
m_writeTotal = static_cast<qreal>(writeKib);
Q_EMIT changed(); Q_EMIT changed();
} }
+12
View File
@@ -14,14 +14,20 @@ class Memory : public TickingService {
Q_PROPERTY(qreal used READ used NOTIFY changed) Q_PROPERTY(qreal used READ used NOTIFY changed)
Q_PROPERTY(qreal total READ total NOTIFY changed) Q_PROPERTY(qreal total READ total NOTIFY changed)
Q_PROPERTY(qreal cached READ cached NOTIFY changed)
Q_PROPERTY(qreal percentage READ percentage NOTIFY changed) Q_PROPERTY(qreal percentage READ percentage NOTIFY changed)
Q_PROPERTY(qreal readTotal READ readTotal NOTIFY changed)
Q_PROPERTY(qreal writeTotal READ writeTotal NOTIFY changed)
public: public:
explicit Memory(QObject* parent = nullptr); explicit Memory(QObject* parent = nullptr);
[[nodiscard]] qreal used() const; [[nodiscard]] qreal used() const;
[[nodiscard]] qreal total() const; [[nodiscard]] qreal total() const;
[[nodiscard]] qreal cached() const;
[[nodiscard]] qreal percentage() const; [[nodiscard]] qreal percentage() const;
[[nodiscard]] qreal readTotal() const;
[[nodiscard]] qreal writeTotal() const;
signals: signals:
void changed(); void changed();
@@ -32,8 +38,14 @@ class Memory : public TickingService {
private: private:
qreal m_used = 0.0; qreal m_used = 0.0;
qreal m_total = 1.0; qreal m_total = 1.0;
qreal m_cached = 0.0;
qreal m_readTotal = 0.0;
qreal m_writeTotal = 0.0;
quint64 m_lastUsed = 0; quint64 m_lastUsed = 0;
quint64 m_lastTotal = 0; quint64 m_lastTotal = 0;
quint64 m_lastCached = 0;
quint64 m_lastReadKib = 0;
quint64 m_lastWriteKib = 0;
}; };
} // namespace ZShell::services } // namespace ZShell::services
File diff suppressed because it is too large Load Diff
+259
View File
@@ -0,0 +1,259 @@
#pragma once
#include "tickingservice.hpp"
#include <optional>
#include <qabstractitemmodel.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <sys/types.h>
template <typename T> class QFutureWatcher;
namespace ZShell::services {
class ProcessModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
enum Roles : quint16 {
PidRole = Qt::UserRole + 1,
PpidRole,
CommandRole,
CommandLineRole,
StateRole,
SleepingRole,
CpuUsageRole,
MemoryUsageRole,
ElapsedRole,
UidRole,
UserRole,
IoReadBytesRole,
IoWriteBytesRole,
IconNameRole,
};
Q_ENUM(Roles)
explicit ProcessModel(QObject* parent = nullptr);
[[nodiscard]] int rowCount(
const QModelIndex& parent = QModelIndex()) const override;
[[nodiscard]] QVariant data(
const QModelIndex& index, int role) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] int count() const;
void refresh();
bool killProcess(qint64 pid, int signal = 15) const;
void setDesktopEntryIcons(
QHash<QString, QString> execPathToIcon,
QHash<QString, QString> execNameToIcon);
[[nodiscard]] QString searchText() const;
void setSearchText(const QString& text);
[[nodiscard]] bool currentUserOnly() const;
void setCurrentUserOnly(bool enabled);
[[nodiscard]] int sortField() const;
void setSortField(int field);
[[nodiscard]] bool sortAscending() const;
void setSortAscending(bool ascending);
signals:
void countChanged();
void searchTextChanged();
void currentUserOnlyChanged();
void sortFieldChanged();
void sortAscendingChanged();
private:
struct RawStat {
pid_t pid = 0;
pid_t ppid = 0;
QString comm;
char state = '?';
quint64 utime = 0;
quint64 stime = 0;
quint64 starttime = 0;
};
struct ProcessEntry {
pid_t pid = 0;
pid_t ppid = 0;
QString command;
QString commandLine;
char state = '?';
double cpuPercent = 0.0;
quint64 rssKb = 0;
double elapsedSecs = 0.0;
uid_t uid = 0;
QString username;
QString iconName;
quint64 ioReadBytes = 0;
quint64 ioWriteBytes = 0;
int searchScore = -1;
quint64 lastUtime = 0;
quint64 lastStime = 0;
quint64 starttime = 0;
};
struct StatusInfo {
quint64 rssKb = 0;
uid_t uid = 0;
};
struct IoInfo {
quint64 readBytes = 0;
quint64 writeBytes = 0;
};
struct ScanResult {
QVector<ProcessEntry> entries;
QHash<uid_t, QString> newUidNames;
quint64 iconVersion = 0;
};
static std::optional<RawStat> readStat(pid_t pid);
static QString readCommandLine(pid_t pid, const QString& fallbackComm);
static StatusInfo readStatus(pid_t pid);
static IoInfo readIo(pid_t pid);
static std::optional<double> readUptimeSeconds();
static QString resolveUsername(uid_t uid, QHash<uid_t, QString>& cache);
static QString resolveProcessExe(pid_t pid);
static ScanResult scanProcesses(
double uptime,
double deltaSecs,
QHash<pid_t, int> prevPidToAll,
QVector<ProcessEntry> prevAll,
QHash<uid_t, QString> uidCache,
long clockTicksPerSec,
QHash<QString, QString> execPathToIcon,
QHash<QString, QString> execNameToIcon,
QStringList pathDirs,
bool desktopEntryIconsChanged,
quint64 iconVersion);
void doRefresh();
void onScanFinished();
void applyView();
void converge(const QVector<ProcessEntry>& desired);
[[nodiscard]] int compareEntries(
const ProcessEntry& a, const ProcessEntry& b) const;
[[nodiscard]] static bool isSleeping(const ProcessEntry& e);
QVector<ProcessEntry> mAll;
QHash<pid_t, int> mPidToAll;
QVector<ProcessEntry> mProcesses;
QHash<uid_t, QString> mUidToName;
double mLastUptimeSecs = -1.0;
long mClockTicksPerSec = 100;
QFutureWatcher<ScanResult>* mWatcher = nullptr;
bool mRefreshPending = false;
double mPendingUptime = -1.0;
QString m_searchText;
bool m_currentUserOnly = true;
uid_t m_currentUid = 0;
int m_sortField = 0;
bool m_sortAscending = true;
QHash<QString, QString> mExecPathToIcon;
QHash<QString, QString> mExecNameToIcon;
quint64 m_iconVersion = 0;
quint64 m_scannedIconVersion = 0;
};
class Processes : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum SortField : int {
Unsorted = 0,
Pid,
Ppid,
Command,
CommandLine,
State,
Sleeping,
CpuUsage,
MemoryUsage,
Elapsed,
Uid,
User,
IoReadBytes,
IoWriteBytes
};
Q_ENUM(SortField)
private:
Q_PROPERTY(QAbstractItemModel* list READ list CONSTANT)
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(
SortField sortField READ sortField WRITE setSortField NOTIFY
sortFieldChanged)
Q_PROPERTY(
bool sortAscending READ sortAscending WRITE setSortAscending NOTIFY
sortAscendingChanged)
Q_PROPERTY(
QString searchText READ searchText WRITE setSearchText NOTIFY
searchTextChanged)
Q_PROPERTY(
bool currentUserOnly READ currentUserOnly WRITE setCurrentUserOnly
NOTIFY currentUserOnlyChanged)
public:
explicit Processes(QObject* parent = nullptr);
[[nodiscard]] QAbstractItemModel* list() const;
[[nodiscard]] int count() const;
[[nodiscard]] SortField sortField() const;
void setSortField(SortField field);
[[nodiscard]] bool sortAscending() const;
void setSortAscending(bool ascending);
[[nodiscard]] QString searchText() const;
void setSearchText(const QString& text);
[[nodiscard]] bool currentUserOnly() const;
void setCurrentUserOnly(bool enabled);
Q_INVOKABLE void sortBy(SortField field);
Q_INVOKABLE void refresh();
Q_INVOKABLE bool killProcess(qint64 pid, int signal = 15) const;
Q_INVOKABLE void setDesktopEntries(const QVariantList& entries);
signals:
void countChanged();
void sortFieldChanged();
void sortAscendingChanged();
void searchTextChanged();
void currentUserOnlyChanged();
protected:
void tick() override;
private:
ProcessModel* m_model;
};
} // namespace ZShell::services
+4 -1
View File
@@ -12,7 +12,10 @@ namespace ZShell::services {
TickingService::TickingService(QObject* parent) TickingService::TickingService(QObject* parent)
: Service(parent), m_timer(new QTimer(this)) { : Service(parent), m_timer(new QTimer(this)) {
m_timer->setSingleShot(false); m_timer->setSingleShot(false);
QObject::connect(m_timer, &QTimer::timeout, this, [this] { tick(); }); QObject::connect(m_timer, &QTimer::timeout, this, [this] {
tick();
updateTriggered();
});
QString configPath = QString configPath =
QDir::homePath() + QStringLiteral("/.config/zshell/config.json"); QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
@@ -18,6 +18,7 @@ class TickingService : public Service {
signals: signals:
void updateIntervalChanged(); void updateIntervalChanged();
void updateTriggered();
protected: protected:
void start() final; void start() final;
+18
View File
@@ -14,6 +14,24 @@ bool finitePositive(qreal v) {
namespace ZShell::services::usagefmt { namespace ZShell::services::usagefmt {
FormatResult UsageFmt::formatKib(qreal kib) const {
if (!finitePositive(kib)) {
return {0.0, 0.0, "KiB"};
}
if (kib >= kGib) {
return {kib / kGib, 0.0, "GiB"};
}
if (kib >= kMib) {
return {kib / kMib, 0.0, "MiB"};
}
if (kib >= kKib) {
return {kib / kKib, 0.0, "KiB"};
}
return {kib, 0.0, "KiB"};
}
FormatResult UsageFmt::formatKib(qreal kib, qreal total) const { FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
if (!finitePositive(kib) || !finitePositive(total)) { if (!finitePositive(kib) || !finitePositive(total)) {
return {0.0, 0.0, "KiB"}; return {0.0, 0.0, "KiB"};
+2
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <qaccessible_base.h>
#include <qobject.h> #include <qobject.h>
#include <qobjectdefs.h> #include <qobjectdefs.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
@@ -27,6 +28,7 @@ class UsageFmt : public QObject {
QML_SINGLETON QML_SINGLETON
public: public:
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib) const;
Q_INVOKABLE [[nodiscard]] FormatResult formatKib( Q_INVOKABLE [[nodiscard]] FormatResult formatKib(
qreal kib, qreal total) const; qreal kib, qreal total) const;
}; };
+17
View File
@@ -0,0 +1,17 @@
qml_module(ZShell-settings
URI ZShell.Settings
INCLUDE_PREFIX settings
SOURCES
common.cpp
schema.cpp
codecs.cpp
quarantine.cpp
node.cpp
objectnode.cpp
listnode.cpp
rootnode.cpp
settingsfile.cpp
changebatcher.cpp
LIBRARIES
zshell-util
)
+25
View File
@@ -0,0 +1,25 @@
#include "changebatcher.hpp"
namespace ZShell::settings {
ChangeBatcher::ChangeBatcher(QObject* parent)
: QObject(parent)
, m_dirty(false) {}
void ChangeBatcher::dirty() {
if (m_dirty)
return;
m_dirty = true;
QMetaObject::invokeMethod(this, &ChangeBatcher::flush, Qt::QueuedConnection);
}
void ChangeBatcher::flush() {
if (!m_dirty)
return;
m_dirty = false;
emit dirtied();
}
} // namespace ZShell::settings
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <qobject.h>
namespace ZShell::settings {
class ChangeBatcher : public QObject {
Q_OBJECT
public:
explicit ChangeBatcher(QObject* parent = nullptr);
void dirty();
signals:
void dirtied();
private:
bool m_dirty;
void flush();
};
} // namespace ZShell::settings
+371
View File
@@ -0,0 +1,371 @@
#include "codecs.hpp"
#include <qjsonarray.h>
#include <qjsonobject.h>
#include <qmetaobject.h>
#include <cmath>
#include "util/metaenum.hpp"
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
namespace {
// We don't know the option here so it isn't set, it should be set by the caller
DecodeResult error(DiagnosticType::Type type, const QString& message) {
Diagnostic error;
error.type = type;
error.message = message;
return { .value = QVariant(), .error = error, .indexPath = {} };
}
DecodeResult mismatch(ExpectedType expected, const QJsonValue& value) {
return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} };
}
DecodeResult mismatch(const QList<ExpectedType>& expected, const QJsonValue& value) {
return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} };
}
template <typename Container> ValueCodec* makeListCodec(const QMetaType& type) {
const auto* elementCodec = ValueCodec::codecFor(QMetaType::fromType<typename Container::value_type>());
return elementCodec ? new ListCodec<Container>(type, elementCodec) : nullptr;
}
// Unions are keyed by their alternatives in order, since order decides which one wins
QString unionKey(const QList<QMetaType>& types) {
QStringList ids;
ids.reserve(types.size());
for (const auto& type : types)
ids << QString::number(type.id());
return ids.join(u","_s);
}
QList<ExpectedType> expectedTypesOf(const QList<const ValueCodec*>& alternatives) {
QList<ExpectedType> types;
types.reserve(alternatives.size());
for (const auto* codec : alternatives)
types << codec->expected();
return types;
}
using ListFactory = ValueCodec* (*)(const QMetaType&);
const QHash<int, ListFactory>& listFactories() {
static const QHash<int, ListFactory> k_factories{
{ QMetaType::fromType<QStringList>().id(), &makeListCodec<QStringList> },
{ QMetaType::fromType<QList<qreal>>().id(), &makeListCodec<QList<qreal>> },
};
return k_factories;
}
} // namespace
ValueCodec::ValueCodec(const QMetaType& type, ExpectedType expected)
: m_type(type)
, m_expected(expected) {}
QMetaType ValueCodec::type() const {
return m_type;
}
ExpectedType ValueCodec::expected() const {
return m_expected;
}
ValueCodec* ValueCodec::codecFor(const QMetaType& type) {
// Cache for codecs, keyed by type id
static QHash<int, ValueCodec*> s_registry;
// Cached lookup
if (const auto it = s_registry.constFind(type.id()); it != s_registry.constEnd())
return *it;
ValueCodec* codec = nullptr;
switch (type.id()) {
case QMetaType::Bool:
codec = new BoolCodec(type);
break;
case QMetaType::Int:
codec = new IntCodec(type);
break;
case QMetaType::Double:
codec = new RealCodec(type);
break;
case QMetaType::QString:
codec = new StringCodec(type);
break;
case QMetaType::QVariantList:
codec = new VariantListCodec(type);
break;
case QMetaType::QVariantMap:
codec = new VariantMapCodec(type);
break;
default:
if (const auto factory = listFactories().constFind(type.id()); factory != listFactories().constEnd())
codec = (*factory)(type);
else if (util::isSupportedEnum(type))
codec = new EnumCodec(type, util::metaEnumFor(type));
break;
}
// Cache codec
if (codec)
s_registry.insert(type.id(), codec);
return codec;
}
ValueCodec* ValueCodec::unionFor(const QList<QMetaType>& types) {
// Cache for union codecs, keyed by their alternatives
static QHash<QString, ValueCodec*> s_registry;
if (types.size() < 2) {
qCCritical(lcSettings, "A union needs at least two types, got %lld", types.size());
return nullptr;
}
const auto key = unionKey(types);
// Cached lookup
if (const auto it = s_registry.constFind(key); it != s_registry.constEnd())
return *it;
QList<const ValueCodec*> alternatives;
alternatives.reserve(types.size());
for (const auto& type : types) {
const auto* codec = codecFor(type);
if (!codec) {
qCCritical(lcSettings, "No codec found for type %s, cannot build union", type.name());
return nullptr;
}
alternatives << codec;
}
auto* const codec = new UnionCodec(alternatives);
s_registry.insert(key, codec);
return codec;
}
QJsonValue BoolCodec::encode(const QVariant& value) const {
return value.toBool();
}
DecodeResult BoolCodec::decode(const QJsonValue& value) const {
// 1 and "true" are not booleans
if (!value.isBool())
return mismatch(ExpectedType::Bool, value);
return { .value = value.toBool(), .error = std::nullopt, .indexPath = {} };
}
QJsonValue IntCodec::encode(const QVariant& value) const {
return value.toInt();
}
DecodeResult IntCodec::decode(const QJsonValue& value) const {
if (!value.isDouble())
return mismatch(ExpectedType::Int, value);
const auto num = value.toDouble();
double integral;
if (std::fpclassify(std::modf(num, &integral)) != FP_ZERO)
return error(
DiagnosticType::InvalidValue, QStringLiteral("Expected an integer, got the real %1").arg(QString::number(num)));
constexpr auto k_min = std::numeric_limits<int>::min();
constexpr auto k_max = std::numeric_limits<int>::max();
if (num < static_cast<double>(k_min) || num > static_cast<double>(k_max)) {
const auto message = QStringLiteral("Integer %1 is out of range, expected between %2 and %3").arg(QString::number(num, 'f', 0), QString::number(k_min), QString::number(k_max));
return error(DiagnosticType::InvalidValue, message);
}
return { .value = static_cast<int>(num), .error = std::nullopt, .indexPath = {} };
}
QJsonValue RealCodec::encode(const QVariant& value) const {
return value.toDouble();
}
DecodeResult RealCodec::decode(const QJsonValue& value) const {
if (!value.isDouble())
return mismatch(ExpectedType::Real, value);
return { .value = QVariant::fromValue<qreal>(value.toDouble()), .error = std::nullopt, .indexPath = {} };
}
QJsonValue StringCodec::encode(const QVariant& value) const {
return value.toString();
}
DecodeResult StringCodec::decode(const QJsonValue& value) const {
if (!value.isString())
return mismatch(ExpectedType::String, value);
return { .value = value.toString(), .error = std::nullopt, .indexPath = {} };
}
QJsonValue VariantListCodec::encode(const QVariant& value) const {
return QJsonArray::fromVariantList(value.toList());
}
DecodeResult VariantListCodec::decode(const QJsonValue& value) const {
if (!value.isArray())
return mismatch(ExpectedType::Array, value);
return { .value = value.toArray().toVariantList(), .error = std::nullopt, .indexPath = {} };
}
QJsonValue VariantMapCodec::encode(const QVariant& value) const {
return QJsonObject::fromVariantMap(value.toMap());
}
DecodeResult VariantMapCodec::decode(const QJsonValue& value) const {
if (!value.isObject())
return mismatch(ExpectedType::Object, value);
return { .value = value.toObject().toVariantMap(), .error = std::nullopt, .indexPath = {} };
}
EnumCodec::EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum)
: ValueCodec(type, ExpectedType::String)
, m_metaEnum(metaEnum) {}
QJsonValue EnumCodec::encode(const QVariant& value) const {
const auto* key = util::enumKeyFor(m_metaEnum, value);
if (!key) {
qCWarning(
lcSettings, "Cannot encode value %lld of enum %s, no such enumerator", value.toLongLong(), m_type.name());
return {};
}
return QString::fromUtf8(key);
}
DecodeResult EnumCodec::decode(const QJsonValue& value) const {
if (!value.isString())
return mismatch(ExpectedType::String, value);
const auto key = value.toString();
for (int i = 0; i < m_metaEnum.keyCount(); ++i) {
if (QString::compare(QString::fromUtf8(m_metaEnum.key(i)), key, Qt::CaseInsensitive) != 0)
continue;
const auto raw = m_metaEnum.value(i);
QVariant decoded(m_type);
if (!QMetaType::convert(QMetaType::fromType<int>(), &raw, m_type, decoded.data())) {
qCCritical(lcSettings, "Failed to convert value %d to enum %s", raw, m_type.name());
return error(DiagnosticType::InvalidValue,
QStringLiteral("Could not convert %1 to %2").arg(key, QString::fromUtf8(m_type.name())));
}
return { .value = decoded, .error = std::nullopt, .indexPath = {} };
}
QStringList options;
options.reserve(m_metaEnum.keyCount());
for (int i = 0; i < m_metaEnum.keyCount(); ++i)
options << QString::fromUtf8(m_metaEnum.key(i));
return error(DiagnosticType::InvalidValue,
QStringLiteral("Invalid enum value %1. Expected one of %2").arg(key, options.join(u", "_s)));
}
template <typename Container>
ListCodec<Container>::ListCodec(const QMetaType& type, const ValueCodec* elementCodec)
: ValueCodec(type, ExpectedType::Array)
, m_elementCodec(elementCodec) {}
template <typename Container> QJsonValue ListCodec<Container>::encode(const QVariant& value) const {
QJsonArray array;
const auto list = value.value<Container>();
for (const auto& element : list)
array.append(m_elementCodec->encode(QVariant::fromValue(element)));
return array;
}
template <typename Container> DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const {
if (!value.isArray())
return mismatch(ExpectedType::Array, value);
const auto array = value.toArray();
Container list;
list.reserve(array.size());
for (qsizetype i = 0; i < array.size(); ++i) {
auto result = m_elementCodec->decode(array.at(i));
// Reject the entire list if any element is invalid
if (result.error) {
result.indexPath.prepend(i);
return result;
}
list.append(result.value.value<Value>());
}
return { .value = QVariant::fromValue(list), .error = std::nullopt, .indexPath = {} };
}
UnionCodec::UnionCodec(const QList<const ValueCodec*>& alternatives)
: ValueCodec(QMetaType::fromType<QVariant>(), alternatives.first()->expected())
, m_alternatives(alternatives)
, m_expectedTypes(expectedTypesOf(alternatives)) {
m_byType.reserve(alternatives.size());
for (const auto* codec : alternatives)
m_byType.insert(codec->type().id(), codec);
}
QJsonValue UnionCodec::encode(const QVariant& value) const {
// An unset union is simply absent from the file
if (!value.isValid())
return QJsonValue::Undefined;
const auto* codec = m_byType.value(value.metaType().id());
if (!codec) {
qCWarning(lcSettings, "Cannot encode value of type %s, it is not one of the allowed types", value.typeName());
return QJsonValue::Undefined;
}
return codec->encode(value);
}
DecodeResult UnionCodec::decode(const QJsonValue& value) const {
std::optional<DecodeResult> best;
for (const auto* codec : m_alternatives) {
auto result = codec->decode(value);
// The first alternative to accept the value wins
if (!result.error)
return result;
// Non-nested type mismatch just means try another alternative
if (result.indexPath.isEmpty() && result.error->type == DiagnosticType::TypeMismatch)
continue;
// Report the deepest error that isn't a type mismatch
if (!best || result.indexPath.size() > best->indexPath.size())
best = std::move(result);
}
if (best)
return *best;
// Nothing matched the shape, so report every alternative
return mismatch(m_expectedTypes, value);
}
// Instantiated for types as needed
template class ListCodec<QStringList>;
template class ListCodec<QList<qreal>>;
} // namespace ZShell::settings
+101
View File
@@ -0,0 +1,101 @@
#pragma once
#include <qhash.h>
#include <qjsonvalue.h>
#include <qlist.h>
#include <qmetaobject.h>
#include <qvariant.h>
#include <optional>
#include "common.hpp"
namespace ZShell::settings {
struct DecodeResult {
QVariant value;
std::optional<Diagnostic> error;
QList<qsizetype> indexPath; // Index path of the failing element, outermost list first
};
class ValueCodec {
public:
explicit ValueCodec(const QMetaType& type, ExpectedType expected);
virtual ~ValueCodec() = default;
// Returns the shared codec for a type, or nullptr if the type is unsupported
static ValueCodec* codecFor(const QMetaType& type);
// Returns the shared codec for a union of types, or nullptr if any of them is unsupported
static ValueCodec* unionFor(const QList<QMetaType>& types);
[[nodiscard]] QMetaType type() const;
[[nodiscard]] ExpectedType expected() const; // The JSON type this decodes from
[[nodiscard]] virtual QJsonValue encode(const QVariant& value) const = 0;
[[nodiscard]] virtual DecodeResult decode(const QJsonValue& value) const = 0;
protected:
const QMetaType m_type;
const ExpectedType m_expected;
Q_DISABLE_COPY_MOVE(ValueCodec)
};
#define CODEC(Type, Expected) \
class Type##Codec : public ValueCodec { \
public: \
explicit Type##Codec(const QMetaType& type) \
: ValueCodec(type, ExpectedType::Expected) {} \
\
[[nodiscard]] QJsonValue encode(const QVariant& value) const override; \
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override; \
};
CODEC(Bool, Bool)
CODEC(Int, Int)
CODEC(Real, Real)
CODEC(String, String)
CODEC(VariantList, Array)
CODEC(VariantMap, Object)
#undef CODEC
class EnumCodec : public ValueCodec {
public:
explicit EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private:
const QMetaEnum m_metaEnum;
};
template <typename Container> class ListCodec : public ValueCodec {
using Value = Container::value_type;
public:
explicit ListCodec(const QMetaType& type, const ValueCodec* elementCodec);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private:
const ValueCodec* m_elementCodec;
};
// Decodes any one of several types, for options that accept more than one shape
class UnionCodec : public ValueCodec {
public:
explicit UnionCodec(const QList<const ValueCodec*>& alternatives);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private:
const QList<const ValueCodec*> m_alternatives; // Tried in order, so the first to accept a value wins
const QList<ExpectedType> m_expectedTypes; // Types of the alternatives, for diagnostics
QHash<int, const ValueCodec*> m_byType; // Type id to alternative, for encoding
};
} // namespace ZShell::settings
+131
View File
@@ -0,0 +1,131 @@
#include "common.hpp"
#include "node.hpp"
using Qt::StringLiterals::operator""_s;
namespace ZShell::settings {
Q_LOGGING_CATEGORY(lcSettings, "zshell.settings", QtInfoMsg)
WriteScope::WriteScope(Node* node, WriteOrigin origin)
: m_root(node->rootNode()), m_previous(m_root->m_writeOrigin) {
m_root->m_writeOrigin = origin;
}
WriteScope::~WriteScope() {
m_root->m_writeOrigin = m_previous;
}
InternalRead::InternalRead(Node* node)
: m_root(node->rootNode()), m_previous(m_root->m_internalRead) {
m_root->m_internalRead = true;
}
InternalRead::~InternalRead() {
m_root->m_internalRead = m_previous;
}
QString DiagnosticType::toString(Type t) {
switch (t) {
case UnknownOption:
return u"UnknownOption"_s;
case GlobalOption:
return u"GlobalOption"_s;
case TypeMismatch:
return u"TypeMismatch"_s;
case InvalidValue:
return u"InvalidValue"_s;
}
Q_UNREACHABLE_RETURN(QString());
}
namespace {
QString expectedStr(ExpectedType expected) {
switch (expected) {
case ExpectedType::Bool:
return QStringLiteral("a boolean");
case ExpectedType::Int:
return QStringLiteral("an integer");
case ExpectedType::Real:
return QStringLiteral("a number");
case ExpectedType::String:
return QStringLiteral("a string");
case ExpectedType::Array:
return QStringLiteral("an array");
case ExpectedType::Object:
return QStringLiteral("an object");
}
Q_UNREACHABLE_RETURN(QString());
}
QString receivedStr(const QJsonValue& value) {
switch (value.type()) {
case QJsonValue::Null:
return QStringLiteral("null");
case QJsonValue::Bool:
return QStringLiteral("a boolean");
case QJsonValue::Double:
return QStringLiteral("a number");
case QJsonValue::String:
return QStringLiteral("a string");
case QJsonValue::Array:
return QStringLiteral("an array");
case QJsonValue::Object:
return QStringLiteral("an object");
default:
return QStringLiteral("nothing");
}
}
QString mismatchStr(
const QList<ExpectedType>& expected, const QJsonValue& value) {
QStringList types;
types.reserve(expected.size());
for (const auto type : expected)
types << expectedStr(type);
const auto received = receivedStr(value);
switch (expected.size()) {
case 2:
return QStringLiteral("Expected %1 or %2, got %3")
.arg(types[0], types[1], received);
case 3:
return QStringLiteral("Expected %1, %2 or %3, got %4")
.arg(types[0], types[1], types[2], received);
case 4:
return QStringLiteral("Expected one of: %1, %2, %3, %4; got %5")
.arg(types[0], types[1], types[2], types[3], received);
default:
// The bounds are checked in macros.hpp `unionType<...T>`
Q_UNREACHABLE_RETURN(QString());
}
}
} // namespace
Diagnostic Diagnostic::mismatch(
ExpectedType expected, const QJsonValue& value, const QString& option) {
return {
.type = DiagnosticType::TypeMismatch,
.option = option,
.message = QStringLiteral("Expected %1, got %2")
.arg(expectedStr(expected), receivedStr(value)),
};
}
Diagnostic Diagnostic::mismatch(
const QList<ExpectedType>& expected,
const QJsonValue& value,
const QString& option) {
return {
.type = DiagnosticType::TypeMismatch,
.option = option,
.message = mismatchStr(expected, value),
};
}
} // namespace ZShell::settings
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include <qjsonvalue.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::settings {
Q_DECLARE_LOGGING_CATEGORY(lcSettings)
class Node;
enum class WriteOrigin : quint8 {
Init, // On init
File, // From the JSON file
FileReset, // When option not present in file
Layer, // From the fallback layer
Qml, // From QML
QmlReset, // On option reset
};
class WriteScope {
public:
explicit WriteScope(Node* node, WriteOrigin origin);
~WriteScope();
private:
Node* const m_root;
const WriteOrigin m_previous;
Q_DISABLE_COPY_MOVE(WriteScope)
};
class InternalRead {
public:
explicit InternalRead(Node* node);
~InternalRead();
private:
Node* const m_root;
const bool m_previous;
Q_DISABLE_COPY_MOVE(InternalRead)
};
class DiagnosticType : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum Type : quint8 {
UnknownOption = 0,
GlobalOption,
TypeMismatch,
InvalidValue,
};
Q_ENUM(Type)
Q_INVOKABLE static QString toString(Type t);
};
enum class ExpectedType : quint8 {
Bool,
Int,
Real,
String,
Array,
Object,
};
struct Diagnostic {
Q_GADGET
QML_VALUE_TYPE(diagnostic)
Q_PROPERTY(DiagnosticType::Type type MEMBER type)
Q_PROPERTY(QString option MEMBER option)
Q_PROPERTY(QString message MEMBER message)
public:
DiagnosticType::Type type = DiagnosticType::UnknownOption;
QString option;
QString message;
static Diagnostic mismatch(
ExpectedType expected,
const QJsonValue& value,
const QString& option = {});
static Diagnostic mismatch(
const QList<ExpectedType>& expected,
const QJsonValue& value,
const QString& option = {});
bool operator==(const Diagnostic& other) const = default;
};
} // namespace ZShell::settings
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <qhash.h>
#include <qobject.h>
#include <qstringview.h>
#include "node.hpp"
namespace ZShell::settings {
// Derived from Node and has ctor(path, fallback, parent)
template <typename T>
concept LayerType = std::derived_from<T, Node> && std::constructible_from<T, const QString&, T*, QObject*>;
template <LayerType T> class LayerRegistry {
public:
explicit LayerRegistry(const QString& prefix, const QString& suffix, QObject* parent);
[[nodiscard]] QString pathFor(const QString& name) const;
[[nodiscard]] QString nameFor(T* layer) const;
[[nodiscard]] T* get(const QString& name, T* fallback, bool* created = nullptr); // Created on demand
private:
const QString m_prefix;
const QString m_suffix;
QObject* const m_parent;
QHash<QString, T*> m_layers;
};
namespace detail {
inline QString stripTrailingSlashes(QStringView str) {
while (str.endsWith(u'/'))
str = str.left(str.length() - 1);
return str.toString();
}
inline QString stripLeadingSlashes(QStringView str) {
while (str.startsWith(u'/'))
str = str.mid(1);
return str.toString();
}
} // namespace detail
template <LayerType T>
LayerRegistry<T>::LayerRegistry(const QString& prefix, const QString& suffix, QObject* parent)
: m_prefix(detail::stripTrailingSlashes(prefix))
, m_suffix(detail::stripLeadingSlashes(suffix))
, m_parent(parent) {}
template <LayerType T> QString LayerRegistry<T>::pathFor(const QString& name) const {
return m_prefix + u'/' + name + u'/' + m_suffix;
}
template <LayerType T> QString LayerRegistry<T>::nameFor(T* layer) const {
return m_layers.key(layer);
}
template <LayerType T> T* LayerRegistry<T>::get(const QString& name, T* fallback, bool* created) {
if (auto* const layer = m_layers.value(name)) {
if (created)
*created = false;
return layer;
}
if (created)
*created = true;
auto* const layer = new T(pathFor(name), fallback, m_parent);
m_layers.insert(name, layer);
return layer;
}
} // namespace ZShell::settings
+445
View File
@@ -0,0 +1,445 @@
#include "listnode.hpp"
#include <qjsonarray.h>
#include <qjsonobject.h>
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
namespace {
QString valuesKey() {
return u"values"_s;
}
void deleteNode(Node* node) {
node->detachFallback();
node->setParent(nullptr);
node->deleteLater();
}
bool isVariantListType(const QVariant& value) {
return value.typeId() == QMetaType::QVariantList || value.typeId() == qMetaTypeId<QList<QVariantMap>>();
}
QList<QVariantMap> toMaps(const QVariant& value) {
if (value.typeId() == qMetaTypeId<QList<QVariantMap>>())
return value.value<QList<QVariantMap>>();
const auto list = value.toList();
QList<QVariantMap> maps;
maps.reserve(list.count());
for (const auto& el : list) {
if (el.typeId() != QMetaType::QVariantMap) {
qCCritical(lcSettings, "Expected a map in list properties, got %s.", el.typeName());
continue;
}
maps << el.toMap();
}
return maps;
}
void syncNodeRecursive(Node* node, const QVariantMap& props) {
const auto& schema = node->schema();
for (const auto& [key, val] : props.asKeyValueRange()) {
const auto* desc = schema.get(key);
if (!desc) {
qCCritical(lcSettings, "Unknown key %s in props for %s, something is wrong.", qUtf8Printable(key),
qUtf8Printable(node->path()));
continue;
}
// `setValue` warns on type mismatches itself
if (!desc->isNode) {
node->setValue(key, val);
continue;
}
auto* const child = node->value(key).value<Node*>();
// Nested lists take their elements straight from props
if (auto* const list = qobject_cast<ListNode*>(child)) {
if (!isVariantListType(val)) {
qCCritical(lcSettings, "Expected a list for node key %s on %s, got %s.", qUtf8Printable(key),
qUtf8Printable(node->path()), val.typeName());
continue;
}
list->setValue(valuesKey(), val);
continue;
}
if (val.typeId() != QMetaType::QVariantMap) {
qCCritical(lcSettings, "Expected a map for node key %s on %s, got %s.", qUtf8Printable(key),
qUtf8Printable(node->path()), val.typeName());
continue;
}
syncNodeRecursive(child, val.toMap());
}
}
} // namespace
ListNode::ListNode(ListNode* fallback, QObject* parent, bool globalOnly)
: Node(fallback, parent, globalOnly) {
if (fallback) {
// Disconnect generic fallback notify, lists use a custom one
QObject::disconnect(fallback, &ListNode::optionChanged, this, nullptr);
if (!m_globalOnly)
QObject::connect(fallback, &ListNode::elementsChanged, this, &ListNode::onFallbackListNotify);
}
}
qsizetype ListNode::count() const {
return m_elements.count();
}
QVariantList ListNode::values() const {
QVariantList list;
list.reserve(m_elements.count());
for (auto* const element : m_elements)
list << QVariant::fromValue(element);
return list;
}
void ListNode::remove(qsizetype index) {
if (rejectGlobalMutation())
return;
const WriteScope scope(this, WriteOrigin::Qml);
if (!validIndex(index)) {
qCWarning(lcSettings, "Attempted to remove invalid index %lld from list node %s, ignoring.", index,
qUtf8Printable(path()));
recordWrite(valuesKey(), false);
return;
}
deleteNode(m_elements.takeAt(index));
recordWrite(valuesKey(), true);
emit countChanged();
emit elementsChanged({}, { index }, {});
}
void ListNode::move(qsizetype from, qsizetype to) {
if (rejectGlobalMutation())
return;
const WriteScope scope(this, WriteOrigin::Qml);
const auto fromInvalid = !validIndex(from);
if (fromInvalid || !validIndex(to)) {
if (fromInvalid)
qCWarning(lcSettings, "Attempted to move invalid index %lld to index %lld on list node %s, ignoring.", from,
to, qUtf8Printable(path()));
else
qCWarning(lcSettings, "Attempted to move index %lld to invalid index %lld on list node %s, ignoring.", from,
to, qUtf8Printable(path()));
recordWrite(valuesKey(), false);
return;
}
m_elements.move(from, to);
recordWrite(valuesKey(), true);
emit elementsChanged({}, {}, { { from, to } });
}
void ListNode::clear() {
if (rejectGlobalMutation())
return;
const WriteScope scope(this, WriteOrigin::Qml);
if (m_elements.isEmpty()) {
recordWrite(valuesKey(), false);
return;
}
const auto len = m_elements.count();
for (auto* const element : std::as_const(m_elements))
deleteNode(element);
m_elements.clear();
recordWrite(valuesKey(), true);
emit countChanged();
NodeChanges removed(len);
std::iota(removed.begin(), removed.end(), 0);
emit elementsChanged({}, removed, {});
}
QString ListNode::pathFor(const QString& key) const {
return elementPath(path(), key);
}
const Schema& ListNode::schema() const {
// Offset +1 so count is not registered (allow read only cause values is read only)
static const auto k_schema = Schema::build(&staticMetaObject, Node::staticMetaObject.propertyCount() + 1, true);
return k_schema;
}
QVariant ListNode::value(const QString& key) const {
if (key != valuesKey()) {
qCCritical(lcSettings,
"Attempted to read %s on list node %s. List nodes only have a 'values' key, something is wrong.",
qUtf8Printable(key), qUtf8Printable(path()));
return {};
}
return QVariant::fromValue(m_elements);
}
bool ListNode::setValue(const QString& key, const QVariant& value) {
return setValue(key, value, nullptr);
}
bool ListNode::setValue(const QString& key, const QVariant& value, QList<Diagnostic>* diagnostics) {
if (key != valuesKey()) {
qCCritical(lcSettings,
"Attempted to write %s on list node %s. List nodes only have a 'values' key, something is wrong.",
qUtf8Printable(key), qUtf8Printable(path()));
return false;
}
const auto len = m_elements.count();
for (auto* const element : std::as_const(m_elements))
deleteNode(element);
m_elements.clear();
if (value.typeId() == qMetaTypeId<QList<Node*>>()) {
// On reset to fallback
const auto nodes = value.value<QList<Node*>>();
for (auto* const node : nodes)
insertNode(m_elements.count(), node);
} else if (isVariantListType(value)) {
// From defaults or QML
const auto list = toMaps(value);
for (qsizetype i = 0; i < list.count(); ++i)
insertNode(i, list.at(i), fallbackFor(i));
} else if (value.typeId() == QMetaType::QJsonArray) {
// From syncJson
const auto array = value.toJsonArray();
for (const auto& json : array)
insertNode(m_elements.count(), json.toObject(), *diagnostics);
} else {
qCCritical(lcSettings, "Unexpected type %s for list node %s, something is wrong.", value.typeName(),
qUtf8Printable(path()));
return false;
}
if (!recordWrite(valuesKey(), true))
return false;
if (len != m_elements.count())
emit countChanged();
NodeChanges added(m_elements.count());
NodeChanges removed(len);
std::iota(added.begin(), added.end(), 0);
std::iota(removed.begin(), removed.end(), 0);
emit elementsChanged(added, removed, {});
return true;
}
void ListNode::resetToDefaults() {
if (isNested()) {
qCCritical(lcSettings, "List node %s has a parent list node, resetToDefaults should never be called on it.",
qUtf8Printable(path()));
return;
}
// Don't reset global only list nodes on overlays
if (fallbackNode() && m_globalOnly)
return;
const WriteScope scope(this, WriteOrigin::FileReset);
setValue(valuesKey(), fallbackNode() ? fallbackNode()->value(valuesKey()) : QVariant::fromValue(defaultValue()));
}
QJsonValue ListNode::toJson(bool sparse) const {
Q_UNUSED(sparse) // Lists can't be sparse
QJsonArray array;
for (auto* const element : m_elements)
array << element->toJson(false);
return array;
}
bool ListNode::syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) {
if (!json.isArray()) {
const auto d = Diagnostic::mismatch(ExpectedType::Array, json, path());
qCWarning(lcSettings, "Error decoding option %s: %s", qUtf8Printable(d.option),
qUtf8Printable(d.message));
diagnostics << d;
return false;
}
// Refuse syncs to global only list nodes on overlays
if (rejectGlobalSync(diagnostics))
return false;
const WriteScope scope(this, WriteOrigin::File);
setValue(valuesKey(), json.toArray(), &diagnostics);
return true;
}
bool ListNode::recordWrite(const QString& key, bool changed) {
const auto wasOverride = isOverride(key);
const auto notify = Node::recordWrite(key, changed);
// Elements inside overridden lists have no fallbacks, so detach here
if (!wasOverride && isOverride(key))
for (auto* const element : std::as_const(m_elements))
element->detachFallback();
return notify;
}
QString ListNode::keyOf(const Node* child) const {
return QString::number(m_elements.indexOf(const_cast<Node*>(child)));
}
Node* ListNode::elementAt(qsizetype index) const {
if (!validIndex(index))
return nullptr;
return m_elements.at(index);
}
Node* ListNode::insertElement(const QVariantMap& props, qsizetype index) {
if (rejectGlobalMutation())
return nullptr;
const WriteScope scope(this, WriteOrigin::Qml);
if (!validIndex(index)) // Invalid index means append
index = m_elements.count();
auto* const element = insertNode(index, props, nullptr);
recordWrite(valuesKey(), true);
emit countChanged();
emit elementsChanged({ index }, {}, {});
return element;
}
QList<QVariantMap> ListNode::defaultValue() const {
const auto* desc = getDescriptor();
if (!desc)
return {};
return desc->defaultValue().value<QList<QVariantMap>>();
}
bool ListNode::isNested() const {
return qobject_cast<ListNode*>(parentNode());
}
bool ListNode::rejectGlobalMutation() const {
if (!m_globalOnly || !fallbackNode())
return false;
qCWarning(lcSettings,
"Attempted to mutate global list %s from an overlay layer, ignoring. "
"This should not be used, mutate global lists from the global layer instead.",
qUtf8Printable(path()));
return true;
}
bool ListNode::validIndex(qsizetype index) const {
return index >= 0 && index < m_elements.count();
}
const Descriptor* ListNode::getDescriptor() const {
if (!parentNode()) { // List nodes should not be root nodes
qCCritical(lcSettings, "List node %s has no parent, something is wrong.", qUtf8Printable(path()));
return nullptr;
}
const auto* desc = parentNode()->schema().get(key());
if (!desc) {
qCCritical(lcSettings, "List node %s is not in parent schema, something is wrong.", qUtf8Printable(path()));
return nullptr;
}
return desc;
}
Node* ListNode::fallbackFor(qsizetype index) const {
if (isOverride(valuesKey()))
return nullptr;
const auto* fallback = static_cast<ListNode*>(fallbackNode());
if (!fallback || !fallback->validIndex(index))
return nullptr;
return fallback->m_elements.at(index);
}
Node* ListNode::insertNode(qsizetype index, const QVariantMap& props, Node* fallback) {
auto* const node = createElement(fallback);
m_elements.insert(index, node);
const WriteScope scope(node, WriteOrigin::Init);
syncNodeRecursive(node, props);
return node;
}
Node* ListNode::insertNode(qsizetype index, Node* node) {
auto* const copy = createElement(node);
m_elements.insert(index, copy);
return copy;
}
Node* ListNode::insertNode(qsizetype index, const QJsonObject& json, QList<Diagnostic>& diagnostics) {
// Write origin will be file/file reset, which is correct.
// This function should only be called from the JSON path of setValue.
auto* const node = createElement(nullptr); // Nodes synced from JSON get no fallback as they are overrides
m_elements.insert(index, node);
node->syncJson(json, diagnostics);
return node;
}
void ListNode::onFallbackListNotify(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved) {
if (isOverride(valuesKey()))
return;
const WriteScope scope(this, WriteOrigin::Layer);
for (const auto index : removed | std::views::reverse)
deleteNode(m_elements.takeAt(index));
for (const auto index : added)
insertNode(index, fallbackFor(index));
for (const auto& move : moved)
m_elements.move(move.first, move.second);
if (!recordWrite(valuesKey(), true))
return;
if (!added.isEmpty() || !removed.isEmpty())
emit countChanged();
emit elementsChanged(added, removed, moved);
}
} // namespace ZShell::settings
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include "node.hpp"
namespace ZShell::settings {
class ListNode : public Node {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(qsizetype count READ count NOTIFY countChanged)
Q_PROPERTY(QVariantList values READ values NOTIFY elementsChanged)
using NodeChanges = QList<qsizetype>;
using MoveChanges = QList<QPair<qsizetype, qsizetype>>;
public:
explicit ListNode(ListNode* fallback, QObject* parent = nullptr, bool globalOnly = false);
[[nodiscard]] qsizetype count() const;
[[nodiscard]] QVariantList values() const;
Q_INVOKABLE void remove(qsizetype index);
Q_INVOKABLE void move(qsizetype from, qsizetype to);
Q_INVOKABLE void clear();
[[nodiscard]] QString pathFor(const QString& key) const override;
[[nodiscard]] const Schema& schema() const override;
[[nodiscard]] QVariant value(const QString& key) const override;
bool setValue(const QString& key, const QVariant& value) override;
bool setValue(const QString& key, const QVariant& value, QList<Diagnostic>* diagnostics);
void resetToDefaults() override;
[[nodiscard]] QJsonValue toJson(bool sparse = true) const override;
bool syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) override;
signals:
void countChanged();
void elementsChanged(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved);
protected:
bool recordWrite(const QString& key, bool changed) override;
[[nodiscard]] QString keyOf(const Node* child) const override;
[[nodiscard]] Node* elementAt(qsizetype index) const;
[[nodiscard]] Node* insertElement(const QVariantMap& props, qsizetype index = -1);
[[nodiscard]] virtual Node* createElement(Node* fallback) = 0;
private:
QList<Node*> m_elements;
[[nodiscard]] QList<QVariantMap> defaultValue() const;
[[nodiscard]] bool isNested() const;
// Returns true if the mutation should be skipped, overlays cannot mutate global lists
[[nodiscard]] bool rejectGlobalMutation() const;
[[nodiscard]] bool validIndex(qsizetype index) const;
[[nodiscard]] const Descriptor* getDescriptor() const;
[[nodiscard]] Node* fallbackFor(qsizetype index) const;
Node* insertNode(qsizetype index, const QVariantMap& props, Node* fallback);
Node* insertNode(qsizetype index, Node* node);
Node* insertNode(qsizetype index, const QJsonObject& json, QList<Diagnostic>& diagnostics);
void onFallbackListNotify(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved);
};
} // namespace ZShell::settings
+193
View File
@@ -0,0 +1,193 @@
#pragma once
#include <qmetatype.h>
#include <qobject.h>
#include <qvariant.h>
#include "listnode.hpp" // IWYU pragma: keep
#include "schema.hpp" // IWYU pragma: keep
namespace ZShell::settings {
inline QVariantMap vmap(std::initializer_list<std::pair<QString, QVariant>> entries) {
QVariantMap map;
for (const auto& [key, value] : entries)
map.insert(key, value);
return map;
}
template <typename... Ts> inline QList<QMetaType> unionTypes() {
static_assert(sizeof...(Ts) >= 2, "A union needs at least two types");
// If the max size is changed, common.cpp `mismatchStr` must be updated
static_assert(sizeof...(Ts) <= 4, "A union cannot have more than 4 types");
static_assert((!std::is_same_v<Ts, QVariant> && ...), "A union cannot contain QVariant");
return { QMetaType::fromType<Ts>()... };
}
namespace detail {
template <typename T> inline bool compare(const T& a, const T& b) {
return a == b;
}
template <std::floating_point T> bool compare(const T& a, const T& b) {
return qFuzzyCompare(a + T(1), b + T(1));
}
template <std::floating_point T> bool compare(const QList<T>& a, const QList<T>& b) {
if (a.size() != b.size())
return false;
for (qsizetype i = 0; i < a.size(); ++i)
if (!compare(a.at(i), b.at(i)))
return false;
return true;
}
} // namespace detail
} // namespace ZShell::settings
// Helper macro to prevent splitting initialiser lists
#define DEFAULT_ARG(...) __VA_ARGS__
// Declares a class to be a node class. This replaces the Q_OBJECT call at the top of the class.
#define SETTINGS_NODE_NO_CTOR(Class, Base) \
Q_OBJECT \
\
public: \
[[nodiscard]] const ZShell::settings::Schema& schema() const override { \
static const auto schema = \
ZShell::settings::Schema::build(&staticMetaObject, Base::staticMetaObject.propertyCount()); \
return schema; \
} \
\
private: \
using Self = Class; // For use in the below macros
#define SETTINGS_NODE(Class, Base) \
SETTINGS_NODE_NO_CTOR(Class, Base) \
QML_ANONYMOUS \
\
public: \
explicit Class(Class* fallback = nullptr, QObject* parent = nullptr, bool globalOnly = false) \
: Base(fallback, parent, globalOnly) {} \
\
private:
// Defines a property on a node.
#define SETTINGS_PROPERTY_IMPL(Type, name, global, defaultVal, ...) \
Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \
\
public: \
[[nodiscard]] Type name() const { \
if (global || m_globalOnly) \
warnGlobalRead(QStringLiteral(#name)); \
return m_##name; \
} \
\
void set_##name(const Type& value) { \
if (!true /* TODO: validation */) \
return; \
\
if (rejectInvalidWrite(QStringLiteral(#name), value)) \
return; /* Skip writes of the wrong type */ \
\
if (rejectGlobalWrite(QStringLiteral(#name))) \
return; /* Skip writes to global only keys, they should be sent to the global layer */ \
\
const auto needsNotify = !ZShell::settings::detail::compare(value, m_##name); \
m_##name = value; \
if (recordWrite(QStringLiteral(#name), needsNotify)) \
Q_EMIT name##Changed(); \
} \
\
Q_SIGNAL void name##Changed(); \
\
private: \
Type m_##name = fallbackValue(&Self::m_##name, defaultVal); \
inline static const bool s_register_##name = \
(ZShell::settings::Schema::annotate(&staticMetaObject, QStringLiteral(#name), \
{ .defaultValue = QVariant::fromValue<Type>(defaultVal), .globalOnly = global, __VA_ARGS__ }), \
true);
#define SETTINGS_PROPERTY(Type, name, defaultVal, ...) \
SETTINGS_PROPERTY_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__)
// Defines a global property on a node. Shorthand for .globalOnly = true.
#define SETTINGS_GLOBAL_PROPERTY(Type, name, defaultVal, ...) \
SETTINGS_PROPERTY_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)
// Defines a subobject property on a node. Subobject properties are CONSTANT.
#define SETTINGS_SUBOBJECT_IMPL(Type, name, global) \
Q_PROPERTY(Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
if (global || m_globalOnly) \
warnGlobalRead(QStringLiteral(#name)); \
return m_##name; \
} \
\
private: \
Type* m_##name = new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \
inline static const bool s_register_##name = \
(ZShell::settings::Schema::annotate( \
&staticMetaObject, QStringLiteral(#name), { .defaultValue = {}, .globalOnly = global }), \
true);
#define SETTINGS_SUBOBJECT(Type, name) SETTINGS_SUBOBJECT_IMPL(Type, name, false)
// Defines a global subobject on a node. Everything inside it is global only.
#define SETTINGS_GLOBAL_SUBOBJECT(Type, name) SETTINGS_SUBOBJECT_IMPL(Type, name, true)
// Defines a list type for use with SETTINGS_LIST.
#define SETTINGS_LIST_TYPE(Element, Name) \
class Name : public ZShell::settings::ListNode { \
Q_OBJECT \
QML_ANONYMOUS \
\
public: \
explicit Name(Name* fallback = nullptr, QObject* parent = nullptr, bool globalOnly = false) \
: ZShell::settings::ListNode(fallback, parent, globalOnly) {} \
\
[[nodiscard]] Q_INVOKABLE Element* at(qsizetype index) const { /* Format ugh */ \
return static_cast<Element*>(elementAt(index)); \
} \
[[nodiscard]] Q_INVOKABLE Element* insert(const QVariantMap& props, qsizetype index = -1) { \
return static_cast<Element*>(insertElement(props, index)); \
} \
\
protected: \
[[nodiscard]] ZShell::settings::Node* createElement(ZShell::settings::Node* fallback) override { \
return new Element(static_cast<Element*>(fallback), this); \
} \
};
// Defines a list property on a node. List properties are CONSTANT.
#define SETTINGS_LIST_IMPL(Type, name, global, defaultVal, ...) \
Q_PROPERTY(Type* name READ name CONSTANT) \
\
public: \
[[nodiscard]] Type* name() const { \
if (global || m_globalOnly) \
warnGlobalRead(QStringLiteral(#name)); \
return m_##name; \
} \
\
private: \
Type* m_##name = new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \
inline static const bool s_register_##name = \
(ZShell::settings::Schema::annotate(&staticMetaObject, QStringLiteral(#name), \
{ .defaultValue = QVariant::fromValue<QList<QVariantMap>>(defaultVal), \
.globalOnly = global, \
__VA_ARGS__ }), \
true);
#define SETTINGS_LIST(Type, name, defaultVal, ...) \
SETTINGS_LIST_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__)
// Defines a global list on a node. Everything inside it is global only.
#define SETTINGS_GLOBAL_LIST(Type, name, defaultVal, ...) \
SETTINGS_LIST_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)
+280
View File
@@ -0,0 +1,280 @@
#include "node.hpp"
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
Node::Node(Node* fallback, QObject* parent, bool globalOnly)
: QObject(parent)
, m_globalOnly(globalOnly || (parentNode() && parentNode()->m_globalOnly))
, m_rootNode(parentNode() ? parentNode()->rootNode() : this)
, m_fallbackNode(fallback)
, m_writeOrigin(WriteOrigin::Qml)
, m_internalRead(false)
, m_batcher(m_rootNode == this ? new ChangeBatcher(this) : nullptr) {
if (fallback && !m_globalOnly)
QObject::connect(fallback, &Node::optionChanged, this, &Node::onFallbackNotify);
}
QString Node::key() const {
return parentNode() ? parentNode()->keyOf(this) : QString();
}
QString Node::path() const {
return parentNode() ? parentNode()->pathFor(key()) : key();
}
QString Node::pathFor(const QString& key) const {
const auto p = path();
return p.isEmpty() ? key : p + u'.' + key;
}
QString Node::elementPath(const QString& path, const QString& index) {
return path + u'[' + index + u']';
}
Node* Node::parentNode() const {
return qobject_cast<Node*>(parent());
}
Node* Node::rootNode() const {
return m_rootNode;
}
Node* Node::fallbackNode() const {
return m_fallbackNode;
}
void Node::detachFallback() {
if (m_fallbackNode) {
QObject::disconnect(m_fallbackNode, nullptr, this, nullptr);
m_fallbackNode = nullptr;
}
const auto childNodes = findChildren<Node*>(Qt::FindDirectChildrenOnly);
for (auto* const child : childNodes)
child->detachFallback();
}
bool Node::isGlobalOnly() const {
return m_globalOnly;
}
bool Node::isOverride(const QString& key) const {
return m_overrides.contains(key);
}
const QSet<QString>& Node::overrides() const {
return m_overrides;
}
bool Node::hasContent() const {
return !m_overrides.isEmpty() || m_quarantine ||
std::ranges::any_of(findChildren<Node*>(Qt::FindDirectChildrenOnly), &Node::hasContent);
}
QVariant Node::value(const QString& key) const {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(
lcSettings, "Attempted to read an unknown key %s, something is wrong.", qUtf8Printable(pathFor(key)));
return {};
}
// Generated getters warn on global reads, this silences them as the warning is only for QML reads
const InternalRead guard(m_rootNode);
return metaObject()->property(desc->metaIndex).read(this);
}
bool Node::setValue(const QString& key, const QVariant& value) {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(lcSettings, "Attempted to set an unknown key %s, something is wrong.", qUtf8Printable(pathFor(key)));
return false;
}
if (desc->isNode) {
qCCritical(lcSettings, "Attempted to set node %s directly, something is wrong.", qUtf8Printable(pathFor(key)));
return false;
}
// Type mismatch, conversion should happen before this function is called
if (rejectInvalidWrite(key, value))
return false;
return metaObject()->property(desc->metaIndex).write(this, value);
}
void Node::resetToDefaults() {
// Don't reset global only nodes on overlays, the whole subtree belongs to the global layer
if (m_globalOnly && m_fallbackNode)
return;
m_quarantine.reset(); // Reset quarantine as well
// No write scope, callers should create the scope
for (const auto& desc : schema().descriptors()) {
if (desc.isNode)
value(desc.key).value<Node*>()->resetToDefaults();
else if (!desc.globalOnly() || !m_fallbackNode) // Skip resetting global options on overlays
setValue(desc.key, m_fallbackNode ? m_fallbackNode->value(desc.key) : desc.defaultValue());
}
}
const Quarantine* Node::quarantine() const {
return m_quarantine.get();
}
bool Node::rejectInvalidWrite(const QString& key, const QVariant& value) const {
const auto* desc = schema().get(key);
if (!desc || desc->accepts(value.metaType()))
return false;
qCWarning(lcSettings, "Type mismatch for %s, expected %s got %s", qUtf8Printable(pathFor(key)),
qUtf8Printable(desc->typeString()), value.metaType().name());
return true;
}
void Node::warnGlobalRead(const QString& key) const {
if (!m_fallbackNode || m_rootNode->m_internalRead)
return;
qCWarning(lcSettings,
"Global option %s was read from an overlay layer. "
"This should not be used, read global options from the global layer instead.",
qUtf8Printable(pathFor(key)));
}
bool Node::rejectGlobalWrite(const QString& key) {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(lcSettings, "Attempted to check a write for an unknown key %s, something is seriously wrong...",
qUtf8Printable(pathFor(key)));
return false;
}
const auto origin = m_rootNode->m_writeOrigin;
const auto fromUser = origin == WriteOrigin::Qml || origin == WriteOrigin::QmlReset;
if ((!m_globalOnly && !desc->globalOnly()) || !fromUser || !m_fallbackNode)
return false;
if (origin == WriteOrigin::QmlReset)
qCWarning(lcSettings,
"Attempted to reset global option %s from an overlay layer, ignoring. "
"This should not be used, reset global options from the global layer instead.",
qUtf8Printable(pathFor(key)));
else
qCWarning(lcSettings,
"Attempted to write global option %s from an overlay layer, ignoring. "
"This should not be used, write global options from the global layer instead.",
qUtf8Printable(pathFor(key)));
return true;
}
void Node::warnGlobalSync(QList<Diagnostic>& diagnostics, const QString& path) {
qCWarning(lcSettings, "Global option definition %s found in overlay file, ignoring.", qUtf8Printable(path));
diagnostics << Diagnostic{
.type = DiagnosticType::GlobalOption,
.option = path,
.message = QStringLiteral("Global options should not be defined in overlay files"),
};
}
bool Node::rejectGlobalSync(QList<Diagnostic>& diagnostics) const {
if (!m_globalOnly || !m_fallbackNode)
return false;
warnGlobalSync(diagnostics, path());
return true;
}
bool Node::recordWrite(const QString& key, bool changed) {
const auto* desc = schema().get(key);
if (!desc) {
qCCritical(lcSettings, "Attempted to record a write for an unknown key %s, something is seriously wrong...",
qUtf8Printable(pathFor(key)));
return false;
}
const auto origin = m_rootNode->m_writeOrigin;
const auto fromUser = origin == WriteOrigin::Qml || origin == WriteOrigin::QmlReset;
bool dirty = changed;
switch (origin) {
// Init does not notify or write to file
case WriteOrigin::Init:
return false;
// File and qml both count as overrides
case WriteOrigin::File:
case WriteOrigin::Qml:
dirty |= !m_overrides.contains(key);
m_overrides << key;
break;
// Layer is not an override, it is a sync with the fallback value
case WriteOrigin::Layer:
break;
// Both resets clear the override
case WriteOrigin::FileReset:
case WriteOrigin::QmlReset:
dirty |= m_overrides.remove(key);
break;
}
// User writes/reset override quarantine
if (fromUser)
dirty |= removeQuarantined(key);
// Both qml and reset write to the file (only write if dirty)
if (fromUser && dirty)
m_rootNode->m_batcher->dirty();
if (changed)
emit optionChanged(key);
return changed;
}
bool Node::removeQuarantined(const QString& key) {
if (!m_quarantine)
return false;
const auto removed = m_quarantine->remove(key);
if (m_quarantine->isEmpty())
m_quarantine.reset();
return removed;
}
ChangeBatcher* Node::batcher() const {
return m_rootNode->m_batcher;
}
QString Node::keyOf(const Node* child) const {
for (const auto& desc : schema().descriptors()) {
if (desc.isNode && child == value(desc.key).value<Node*>())
return desc.key;
}
return {};
}
void Node::onFallbackNotify(const QString& key) {
if (m_overrides.contains(key))
return;
// Don't mirror global options onto overlays
const auto* desc = schema().get(key);
if (desc && desc->globalOnly())
return;
const WriteScope scope(this, WriteOrigin::Layer);
setValue(key, m_fallbackNode->value(key));
}
} // namespace ZShell::settings
+100
View File
@@ -0,0 +1,100 @@
#pragma once
#include <qjsonvalue.h>
#include <qobject.h>
#include <qvariant.h>
#include "changebatcher.hpp"
#include "common.hpp"
#include "quarantine.hpp"
#include "schema.hpp"
namespace ZShell::settings {
class Node : public QObject {
Q_OBJECT
public:
// Global only nodes are inherited, anything inside one is also global only
explicit Node(Node* fallback, QObject* parent = nullptr, bool globalOnly = false);
[[nodiscard]] QString key() const; // The key of this in the parent node
[[nodiscard]] QString path() const;
[[nodiscard]] virtual QString pathFor(const QString& key) const;
[[nodiscard]] static QString elementPath(const QString& path, const QString& index);
[[nodiscard]] Node* parentNode() const;
[[nodiscard]] Node* rootNode() const;
[[nodiscard]] Node* fallbackNode() const;
void detachFallback(); // Recursive, unlinks this and its children from the fallback layer
[[nodiscard]] Q_INVOKABLE bool isGlobalOnly() const;
[[nodiscard]] bool isOverride(const QString& key) const;
[[nodiscard]] const QSet<QString>& overrides() const;
[[nodiscard]] bool hasContent() const; // Recursive
[[nodiscard]] virtual const Schema& schema() const = 0;
[[nodiscard]] virtual QVariant value(const QString& key) const;
virtual bool setValue(const QString& key, const QVariant& value); // Returns whether the write was successful or not
virtual void resetToDefaults(); // Recursive, resets to fallbacks then defaults if not overridden
[[nodiscard]] virtual QJsonValue toJson(bool sparse = true) const = 0;
// Returns false if the entire node was rejected
virtual bool syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) = 0;
[[nodiscard]] const Quarantine* quarantine() const;
signals:
void optionChanged(const QString& key);
protected:
// Null means empty, otherwise it has content
std::unique_ptr<Quarantine> m_quarantine;
const bool m_globalOnly; // Own flag or inherited from the parent node
void warnGlobalRead(const QString& key) const;
// Returns true if the write should be skipped afterwards, the value is not one of the allowed types
[[nodiscard]] bool rejectInvalidWrite(const QString& key, const QVariant& value) const;
template <typename T> [[nodiscard]] bool rejectInvalidWrite(const QString& key, const T& value) const;
// Returns true if the write should be skipped afterwards, overlays cannot write global options
bool rejectGlobalWrite(const QString& key);
static void warnGlobalSync(QList<Diagnostic>& diagnostics, const QString& path);
bool rejectGlobalSync(QList<Diagnostic>& diagnostics) const; // Returns true if the sync should be rejected
// Returns true if the notify signal should be emitted
virtual bool recordWrite(const QString& key, bool changed);
[[nodiscard]] bool removeQuarantined(const QString& key);
[[nodiscard]] ChangeBatcher* batcher() const;
[[nodiscard]] virtual QString keyOf(const Node* child) const;
template <typename C, typename T>
[[nodiscard]] T fallbackValue(T C::* member, std::type_identity_t<T> defaultValue) const;
private:
QSet<QString> m_overrides; // Overridden keys from file/qml writes
Node* const m_rootNode;
Node* m_fallbackNode; // No fallback node either means global tree or inside overridden list
// For root node use only
WriteOrigin m_writeOrigin;
bool m_internalRead;
ChangeBatcher* const m_batcher;
void onFallbackNotify(const QString& key);
friend class WriteScope;
friend class InternalRead;
};
template <typename T> bool Node::rejectInvalidWrite(const QString& key, const T& value) const {
Q_UNUSED(key)
Q_UNUSED(value)
return false; // Only QVariant unions can be given the wrong type
}
template <typename C, typename T> T Node::fallbackValue(T C::* member, std::type_identity_t<T> defaultValue) const {
const auto* fallback = static_cast<const C*>(m_fallbackNode);
return fallback ? fallback->*member : defaultValue;
}
} // namespace ZShell::settings
+219
View File
@@ -0,0 +1,219 @@
#include "objectnode.hpp"
#include <qjsonobject.h>
#include "codecs.hpp"
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
ObjectNode::ObjectNode(ObjectNode* fallback, QObject* parent, bool globalOnly)
: Node(fallback, parent, globalOnly) {}
void ObjectNode::resetOption(const QString& key) {
const auto* desc = schema().get(key);
if (!desc) {
qCWarning(lcSettings)
<< "Attempted to reset unknown option" << pathFor(key);
return;
}
// Warn and ignore if this is a global node property and we are an overlay
if (desc->isNode && (m_globalOnly || desc->globalOnly()) &&
fallbackNode()) {
qCWarning(
lcSettings,
"Attempted to reset global node %s, ignoring. "
"This should not be used, reset global nodes from the global layer "
"instead.",
qUtf8Printable(pathFor(key)));
return;
}
const WriteScope scope(this, WriteOrigin::QmlReset);
if (desc->isNode)
value(key).value<Node*>()->resetToDefaults();
else
setValue(
key,
fallbackNode() ? fallbackNode()->value(key) : desc->defaultValue());
}
Descriptor ObjectNode::descriptorFor(const QString& key) const {
const auto* desc = schema().get(key);
if (!desc) {
qCWarning(lcSettings)
<< "Attempted to get descriptor for unknown option" << pathFor(key);
return {};
}
return *desc;
}
QJsonValue ObjectNode::toJson(bool sparse) const {
QJsonObject json;
for (const auto& desc : schema().descriptors()) {
const auto val = value(desc.key);
if (const auto* node = val.value<Node*>()) {
if (!sparse || node->hasContent())
json.insert(desc.key, node->toJson(sparse));
continue;
}
if (sparse && !isOverride(desc.key)) continue;
if (!desc.codec) { // This should not happen
qCCritical(
lcSettings,
"No codec found for type %s, not serialising %s",
desc.type.name(),
qUtf8Printable(pathFor(desc.key)));
continue;
}
json.insert(desc.key, desc.codec->encode(val));
}
if (m_quarantine) return m_quarantine->apply(json);
return json;
}
bool ObjectNode::syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) {
m_quarantine.reset(); // Clear out old quarantine
if (!json.isObject()) {
const auto d = Diagnostic::mismatch(ExpectedType::Object, json, path());
qCWarning(
lcSettings,
"Error decoding option %s: %s",
qUtf8Printable(d.option),
qUtf8Printable(d.message));
diagnostics << d;
return false;
}
// Refuse syncs to global only nodes on overlays
if (rejectGlobalSync(diagnostics)) return false;
const auto obj = json.toObject();
qCDebug(lcSettings) << "Loading JSON into" << metaObject()->className()
<< "with" << obj.size() << "keys:" << obj.keys();
const auto visited = loadFromJson(obj, diagnostics);
resetUnvisited(visited);
return true;
}
void ObjectNode::quarantineKey(const QString& key, const QJsonValue& value) {
if (!m_quarantine) m_quarantine = std::make_unique<ObjectQuarantine>();
m_quarantine->insert(key, value);
}
QSet<QString> ObjectNode::loadFromJson(
const QJsonObject& json, QList<Diagnostic>& diagnostics) {
const WriteScope scope(this, WriteOrigin::File);
QSet<QString> visited;
visited.reserve(json.size());
// Convenience macro for quarantine then skip
#define SKIP \
quarantineKey(key, v); \
continue
for (const auto [k, v] : json.asKeyValueRange()) {
const auto key = k.toString();
const auto* desc = schema().get(key);
if (!desc) {
const auto path = pathFor(key);
qCWarning(lcSettings) << "Unknown option" << path;
diagnostics << Diagnostic{
.type = DiagnosticType::UnknownOption,
.option = path,
.message = QStringLiteral("Unknown option %1").arg(key),
};
SKIP;
}
// Recurse into child nodes
if (desc->isNode) {
qCDebug(lcSettings) << " Recursing into" << key;
auto* const node = value(key).value<Node*>();
if (node->syncJson(v, diagnostics))
visited << key;
else
quarantineKey(
key, v); // Quarantine entire node cause sync failed
continue;
}
if ((m_globalOnly || desc->globalOnly()) && fallbackNode()) {
warnGlobalSync(diagnostics, pathFor(key));
SKIP;
}
if (!desc->codec) { // This should not happen
qCCritical(
lcSettings,
"No codec found for type %s, not loading %s",
desc->type.name(),
qUtf8Printable(pathFor(key)));
SKIP;
}
auto val = desc->codec->decode(v);
if (val.error) {
auto path = pathFor(key);
for (const auto index : std::as_const(val.indexPath))
path = elementPath(path, QString::number(index));
qCWarning(
lcSettings,
"Error decoding option %s: %s",
qUtf8Printable(path),
qUtf8Printable(val.error->message));
val.error->option = path;
diagnostics << *val.error;
SKIP;
}
visited << key;
setValue(key, val.value);
}
#undef SKIP
return visited;
}
void ObjectNode::resetUnvisited(const QSet<QString>& visited) {
const WriteScope scope(this, WriteOrigin::FileReset);
for (const auto& desc : schema().descriptors()) {
if (visited.contains(desc.key)) continue;
// Reset nodes recursively
if (desc.isNode) {
value(desc.key).value<Node*>()->resetToDefaults();
continue;
}
// Skip global options on overlays
if ((m_globalOnly || desc.globalOnly()) && fallbackNode()) continue;
setValue(
desc.key,
fallbackNode() ? fallbackNode()->value(desc.key)
: desc.defaultValue());
}
}
} // namespace ZShell::settings
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <qjsonobject.h>
#include <qobject.h>
#include <qset.h>
#include "node.hpp"
namespace ZShell::settings {
class ObjectNode : public Node {
Q_OBJECT
public:
explicit ObjectNode(
ObjectNode* fallback,
QObject* parent = nullptr,
bool globalOnly = false);
Q_INVOKABLE void resetOption(const QString& key);
[[nodiscard]] Q_INVOKABLE Descriptor descriptorFor(const QString& key) const;
[[nodiscard]] QJsonValue toJson(bool sparse = true) const override;
bool syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) override;
private:
void quarantineKey(const QString& key, const QJsonValue& value);
QSet<QString> loadFromJson(
const QJsonObject& json, QList<Diagnostic>& diagnostics);
void resetUnvisited(const QSet<QString>& visited);
};
} // namespace ZShell::settings
+27
View File
@@ -0,0 +1,27 @@
#include "quarantine.hpp"
#include <qjsonobject.h>
namespace ZShell::settings {
void ObjectQuarantine::insert(const QString& key, const QJsonValue& value) {
m_quarantine.insert(key, value);
}
bool ObjectQuarantine::remove(const QString& key) {
return m_quarantine.remove(key);
}
bool ObjectQuarantine::isEmpty() const {
return m_quarantine.isEmpty();
}
QJsonValue ObjectQuarantine::apply(const QJsonValue& json) const {
auto result = json.toObject();
for (const auto& [key, value] : m_quarantine.asKeyValueRange())
if (!result.contains(key)) // Don't clobber existing values
result.insert(key, value);
return result;
}
} // namespace ZShell::settings
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <qhash.h>
#include <qjsonvalue.h>
#include <qtclasshelpermacros.h>
namespace ZShell::settings {
class Quarantine {
public:
explicit Quarantine() = default;
virtual ~Quarantine() = default;
virtual void insert(const QString& key, const QJsonValue& value) = 0;
[[nodiscard]] virtual bool remove(const QString& key) = 0;
[[nodiscard]] virtual bool isEmpty() const = 0;
[[nodiscard]] virtual QJsonValue apply(const QJsonValue& json) const = 0;
Q_DISABLE_COPY_MOVE(Quarantine)
};
class ObjectQuarantine : public Quarantine {
public:
void insert(const QString& key, const QJsonValue& value) override;
[[nodiscard]] bool remove(const QString& key) override;
[[nodiscard]] bool isEmpty() const override;
[[nodiscard]] QJsonValue apply(const QJsonValue& json) const override;
private:
QHash<QString, QJsonValue> m_quarantine;
};
} // namespace ZShell::settings
+46
View File
@@ -0,0 +1,46 @@
#include "rootnode.hpp"
#include <qjsonobject.h>
namespace ZShell::settings {
RootNode::RootNode(const QString& path, RootNode* fallback, QObject* parent)
: ObjectNode(fallback, parent)
, m_file(new SettingsFile(path, this)) {
QObject::connect(batcher(), &ChangeBatcher::dirtied, this, &RootNode::saveToFile);
QObject::connect(m_file, &SettingsFile::changed, this, &RootNode::reloadFromFile);
QObject::connect(m_file, &SettingsFile::readFailed, this, [this](const QString& error) {
emit treeLoadFailed(this, error);
});
QObject::connect(m_file, &SettingsFile::writeFailed, this, [this](const QString& error) {
emit treeSaveFailed(this, error);
});
}
void RootNode::load() {
if (m_file->load() != SettingsFile::LoadResult::Changed)
reloadFromFile();
}
QList<Diagnostic> RootNode::diagnostics() const {
return m_diagnostics;
}
void RootNode::reloadFromFile() {
const auto oldDiagnostics = m_diagnostics;
m_diagnostics.clear();
const auto data = m_file->read();
syncJson(data ? data.value() : QJsonObject(), m_diagnostics);
if (m_diagnostics != oldDiagnostics)
emit diagnosticsChanged();
emit treeLoaded(this);
}
void RootNode::saveToFile() {
m_file->write(toJson());
}
} // namespace ZShell::settings
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "common.hpp"
#include "objectnode.hpp"
#include "settingsfile.hpp"
namespace ZShell::settings {
class RootNode : public ObjectNode {
Q_OBJECT
Q_PROPERTY(QList<ZShell::settings::Diagnostic> diagnostics READ diagnostics NOTIFY diagnosticsChanged)
public:
explicit RootNode(const QString& path, RootNode* fallback, QObject* parent = nullptr);
[[nodiscard]] QList<Diagnostic> diagnostics() const;
void load();
signals:
void diagnosticsChanged();
void treeLoaded(RootNode* self);
void treeLoadFailed(RootNode* self, const QString& error);
void treeSaveFailed(RootNode* self, const QString& error);
private:
SettingsFile* const m_file;
QList<Diagnostic> m_diagnostics;
void reloadFromFile();
void saveToFile();
};
} // namespace ZShell::settings
+133
View File
@@ -0,0 +1,133 @@
#include "schema.hpp"
#include "codecs.hpp"
#include "node.hpp"
namespace {
Q_LOGGING_CATEGORY(lcSchema, "zshell.settings.schema", QtInfoMsg)
} // namespace
namespace ZShell::settings {
namespace {
// Descriptor annotations are stored here by Schema::annotate, which is called in the CONFIG_XXX macros.
// Schema::build then takes them from here.
QHash<const QMetaObject*, QHash<QString, Annotation>>& annotationCache() {
static QHash<const QMetaObject*, QHash<QString, Annotation>> s_cache;
return s_cache;
}
// Union options carry their alternatives in the annotation, everything else goes by property type
const ValueCodec* resolveCodec(Descriptor& desc) {
auto& allowed = desc.annotation.allowedTypes;
if (allowed.isEmpty())
return ValueCodec::codecFor(desc.type);
if (desc.type.id() != QMetaType::QVariant) {
qCCritical(lcSchema, "Allowed types are only valid for QVariant properties, ignoring them for %s",
qUtf8Printable(desc.key));
allowed.clear();
return ValueCodec::codecFor(desc.type);
}
if (const auto* codec = ValueCodec::unionFor(allowed))
return codec;
allowed.clear();
return nullptr;
}
bool isNodeType(const QMetaType& type) {
if (!type.flags().testFlag(QMetaType::PointerToQObject))
return false;
const auto* meta = type.metaObject();
return meta && meta->inherits(&Node::staticMetaObject);
}
} // namespace
QString Descriptor::typeString() const {
if (annotation.allowedTypes.isEmpty())
return QString::fromUtf8(type.name());
QStringList names;
names.reserve(annotation.allowedTypes.size());
for (const auto& allowed : annotation.allowedTypes)
names << QString::fromUtf8(allowed.name());
return names.join(QStringLiteral(" | "));
}
bool Descriptor::accepts(const QMetaType& valueType) const {
// Unset is a valid state for QVariant options, they are simply absent from the file
if (type.id() == QMetaType::QVariant && !valueType.isValid())
return true;
if (annotation.allowedTypes.isEmpty())
return type == valueType;
return annotation.allowedTypes.contains(valueType);
}
Schema Schema::build(const QMetaObject* meta, int baseOffset, bool includeReadOnly) {
Schema schema;
schema.m_descriptors.reserve(meta->propertyCount() - baseOffset);
// Descriptors are taken from cache since this should be called exactly once per class
const auto annotations = annotationCache().take(meta);
qCDebug(lcSchema) << "Building schema for" << meta->className();
for (int i = baseOffset; i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
const auto isNode = isNodeType(prop.metaType());
// Skip read only properties (unless includeReadOnly)
if (!isNode && !includeReadOnly && !prop.isWritable()) {
qCDebug(lcSchema) << " Skipping computed property" << key;
continue;
}
qCDebug(lcSchema) << " Adding property" << key;
Descriptor desc{
.key = key,
.type = prop.metaType(),
.metaIndex = i,
.isNode = isNode,
.annotation = annotations.value(key),
};
if (!isNode) {
desc.codec = resolveCodec(desc);
if (!desc.codec)
qCCritical(lcSchema, "No codec for %s of type %s, it will not be loaded or saved", qUtf8Printable(key),
desc.type.name());
}
schema.m_descriptors.append(std::move(desc));
schema.m_keyToIndex.insert(key, schema.m_descriptors.size() - 1);
}
return schema;
}
void Schema::annotate(const QMetaObject* meta, const QString& key, Annotation annotation) {
annotationCache()[meta].insert(key, std::move(annotation));
}
const QList<Descriptor>& Schema::descriptors() const {
return m_descriptors;
}
const Descriptor* Schema::get(const QString& key) const {
const auto it = m_keyToIndex.find(key);
return it != m_keyToIndex.end() ? &m_descriptors[it.value()] : nullptr;
}
} // namespace ZShell::settings
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <qhash.h>
#include <qlist.h>
#include <qmetaobject.h>
#include <qmetatype.h>
#include <qqmlintegration.h>
#include <qstring.h>
#include <qvariant.h>
namespace ZShell::settings {
class ValueCodec;
struct Annotation {
QVariant defaultValue;
bool globalOnly = false;
// NOLINTNEXTLINE(readability-redundant-member-init) callers will get missing field init warnings without
QList<QMetaType> allowedTypes = {}; // For QVariant properties
};
namespace detail {
// Return trivially copyable types (e.g. bool) by value and others (e.g. QVariant) by const ref
template <typename T>
using AnnotationReturn = std::conditional_t<std::is_trivially_copyable_v<T> && sizeof(T) <= sizeof(void*), T, const T&>;
} // namespace detail
#define ANNOTATION(Type, name) \
Q_PROPERTY(Type name READ name) \
\
public: \
[[nodiscard]] detail::AnnotationReturn<Type> name() const { \
return annotation.name; \
} \
\
private:
struct Descriptor {
Q_GADGET
QML_VALUE_TYPE(descriptor)
Q_PROPERTY(QString key MEMBER key)
Q_PROPERTY(QString type READ typeString)
Q_PROPERTY(int metaIndex MEMBER metaIndex)
Q_PROPERTY(bool isNode MEMBER isNode)
ANNOTATION(QVariant, defaultValue)
ANNOTATION(bool, globalOnly)
public:
QString key;
QMetaType type;
int metaIndex;
bool isNode;
Annotation annotation;
const ValueCodec* codec = nullptr; // Null for nodes and unsupported types
[[nodiscard]] QString typeString() const;
[[nodiscard]] bool accepts(const QMetaType& valueType) const;
};
#undef ANNOTATION
class Schema {
public:
static Schema build(const QMetaObject* meta, int baseOffset, bool includeReadOnly = false);
static void annotate(const QMetaObject* meta, const QString& key, Annotation annotation);
[[nodiscard]] const QList<Descriptor>& descriptors() const;
[[nodiscard]] const Descriptor* get(const QString& key) const;
private:
explicit Schema() = default;
QList<Descriptor> m_descriptors;
QHash<QString, qsizetype> m_keyToIndex;
};
} // namespace ZShell::settings
+213
View File
@@ -0,0 +1,213 @@
#include "settingsfile.hpp"
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qloggingcategory.h>
#include <qsavefile.h>
#include <utility>
namespace {
Q_LOGGING_CATEGORY(lcSettingsFile, "zshell.settings.file", QtInfoMsg)
} // namespace
namespace ZShell::settings {
using Qt::StringLiterals::operator""_s;
namespace {
// Max retries for loads which fail due to malformed JSON, e.g. partial writes
constexpr int k_maxLoadRetries = 3;
} // namespace
SettingsFile::SettingsFile(QString path, QObject* parent)
: QObject(parent)
, m_path(std::move(path))
, m_watcher(new QFileSystemWatcher(this))
, m_saveDebounce(new QTimer(this))
, m_loadDebounce(new QTimer(this))
, m_loadRetries(0) {
m_saveDebounce->setSingleShot(true);
m_saveDebounce->setInterval(500); // Save at most once every 500ms
m_loadDebounce->setSingleShot(true);
m_loadDebounce->setInterval(50); // Coalesce watcher events within 50ms
QObject::connect(m_loadDebounce, &QTimer::timeout, this, &SettingsFile::onLoadDebounced);
QObject::connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &SettingsFile::onFileChanged);
QObject::connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &SettingsFile::onDirChanged);
initWatcher();
}
std::optional<QJsonValue> SettingsFile::read() const {
return m_lastData;
}
void SettingsFile::write(const QJsonValue& json) {
m_pendingWrite = json;
save();
}
void SettingsFile::onFileChanged() {
initWatcher(); // Re-add path in case the file was replaced
scheduleLoad();
}
void SettingsFile::onDirChanged() {
// Ignore if the file is already watched
if (m_watcher->files().contains(m_path))
return;
initWatcher();
if (QFile::exists(m_path))
scheduleLoad();
}
void SettingsFile::initWatcher() {
const QFileInfo info(m_path);
const auto dir = info.absolutePath();
if (QDir(dir).exists() && !m_watcher->directories().contains(dir))
m_watcher->addPath(dir);
if (info.exists() && !m_watcher->files().contains(m_path))
m_watcher->addPath(m_path);
}
void SettingsFile::scheduleLoad() {
// Ignore if a load is already pending
if (m_loadDebounce->isActive())
return;
m_loadRetries = 0;
m_loadDebounce->start();
}
void SettingsFile::onLoadDebounced() {
const auto isFinalTry = m_loadRetries >= k_maxLoadRetries;
if (load(isFinalTry) == LoadResult::ParseError && !isFinalTry) {
// Likely a partial write, retry after another debounce
++m_loadRetries;
qCDebug(lcSettingsFile, "Retrying load of %s (%d/%d)", qUtf8Printable(m_path), m_loadRetries, k_maxLoadRetries);
m_loadDebounce->start();
}
}
SettingsFile::LoadResult SettingsFile::load(bool reportErrors) {
QFile file(m_path);
if (!file.exists()) {
if (m_lastData) {
m_lastData = std::nullopt;
emit changed();
return LoadResult::Changed;
}
return LoadResult::Unchanged;
}
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcSettingsFile, "Failed to open %s for reading: %s", qUtf8Printable(m_path),
qUtf8Printable(file.errorString()));
emit readFailed(QStringLiteral("Failed to open: %1").arg(file.errorString()));
return LoadResult::Error;
}
const auto data = file.readAll();
file.close();
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
if (reportErrors) {
qCWarning(lcSettingsFile, "Failed to parse %s as JSON: %s", qUtf8Printable(m_path),
qUtf8Printable(error.errorString()));
emit readFailed(QStringLiteral("Failed to parse: %1").arg(error.errorString()));
} else {
qCDebug(lcSettingsFile, "Failed to parse %s as JSON: %s", qUtf8Printable(m_path),
qUtf8Printable(error.errorString()));
}
return LoadResult::ParseError;
}
const auto json = doc.isObject() ? QJsonValue(doc.object()) : QJsonValue(doc.array());
if (json == m_lastData)
return LoadResult::Unchanged;
// Clear pending write, stuff loaded from file should take precedence
m_pendingWrite = std::nullopt;
m_lastData = json;
emit changed();
qCDebug(lcSettingsFile) << "Read JSON from" << m_path;
qCDebug(lcSettingsFile) << " " << json;
return LoadResult::Changed;
}
void SettingsFile::save() {
if (!m_pendingWrite)
return;
if (m_saveDebounce->isActive()) {
// Queue save for debounce end
QObject::connect(m_saveDebounce, &QTimer::timeout, this, &SettingsFile::save,
// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) ConnectionType is OR-able by design
static_cast<Qt::ConnectionType>(Qt::UniqueConnection | Qt::SingleShotConnection));
return;
}
// Debounce regardless of whether the save actually succeeded
m_saveDebounce->start();
const auto json = m_pendingWrite.value();
const auto data = (json.isObject() ? QJsonDocument(json.toObject()) : QJsonDocument(json.toArray())).toJson();
m_pendingWrite = std::nullopt;
const auto dir = QFileInfo(m_path).absolutePath();
if (!QDir().mkpath(dir)) {
qCWarning(lcSettingsFile) << "Failed to create dir" << dir;
emit writeFailed(QStringLiteral("Failed to create parent directory"));
return;
}
// Write atomically
QSaveFile file(m_path);
if (!file.open(QIODevice::WriteOnly)) {
qCWarning(lcSettingsFile, "Failed to open %s for writing: %s", qUtf8Printable(m_path),
qUtf8Printable(file.errorString()));
emit writeFailed(QStringLiteral("Failed to open: %1").arg(file.errorString()));
return;
}
file.write(data);
if (!file.commit()) {
qCWarning(lcSettingsFile, "Failed to write %s: %s", qUtf8Printable(m_path), qUtf8Printable(file.errorString()));
emit writeFailed(QStringLiteral("Failed to write: %1").arg(file.errorString()));
return;
}
m_lastData = json;
qCDebug(lcSettingsFile) << "Saved" << m_path;
initWatcher(); // The file may have just been created, or replaced by the rename
}
} // namespace ZShell::settings

Some files were not shown because too many files have changed in this diff Show More