Feat: Dashboard tabs and repositioning. New processes view. Settings backend changes #158

Merged
zach merged 21 commits from feat/dashboard-tabs-processes-and-settings-backend into main 2026-09-30 16:51:16 +02:00
98 changed files with 8633 additions and 2983 deletions
Showing only changes of commit 359f832609 - Show all commits
+1 -1
View File
@@ -51,7 +51,7 @@ CustomRect {
if (internalChecked)
return checkedRadius;
if (isRound)
return (height || implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale);
return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale);
return defaultRadius;
}
+2 -2
View File
@@ -99,13 +99,13 @@ MouseArea {
id: base
anchors.fill: parent
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: Colors.palette.m3onSurface
opacity: root.stateOpacity
// Pick up radius from parent if it has one (parent can be anything with radius props)
// qmllint disable missing-property
radius: root.parent?.radius ?? 0
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property
+16 -1
View File
@@ -145,8 +145,23 @@ CustomWindow {
HyprlandFocusGrab {
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]
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: {
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
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
property real extent: fullscreen ? 0 : Config.bar.border
required property bool fullscreen
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Services
CustomRect {
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
color: Colors.tPalette.m3surfaceContainer
+3 -5
View File
@@ -17,7 +17,7 @@ Item {
required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.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
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
@@ -30,14 +30,12 @@ Item {
Behavior on implicitHeight {
enabled: !root.horizontal
Anim {
}
Anim {}
}
Behavior on implicitWidth {
enabled: root.horizontal
Anim {
}
Anim {}
}
CustomRect {
+109 -101
View File
@@ -11,8 +11,8 @@ import qs.Components
Item {
id: root
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + clipWrapper.anchors.margins * 2
readonly property real nonAnimHeight: tabs.implicitHeight + clipWrapper.anchors.topMargin + view.implicitHeight + clipWrapper.anchors.margins * 2
required property ScreenState screenState
@@ -36,12 +36,12 @@ Item {
text: qsTr("Media"),
enabled: true
},
// {
// component: testingComponent,
// iconName: "speed",
// text: qsTr("Testing"),
// enabled: true
// },
{
component: procsComponent,
iconName: "speed",
text: qsTr("Processes"),
enabled: true
},
];
return allTabs.filter(tab => tab.enabled);
}
@@ -50,130 +50,138 @@ Item {
implicitHeight: nonAnimHeight
Behavior on implicitHeight {
Anim {}
Anim {
type: Anim.SlowEffects
}
}
Behavior on implicitWidth {
Anim {}
Anim {
type: Anim.SlowEffects
}
}
Tabs {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
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
screenState: root.screenState
tabs: root.dashboardTabs
}
ClippingRectangle {
id: viewWrapper
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Item {
id: clipWrapper
anchors.fill: parent
anchors.margins: Tokens.padding.small
color: "transparent"
radius: Tokens.rounding.extraLarge - anchors.margins
// radius: Tokens.rounding.extraLarge - anchors.margins
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
Flickable {
id: view
Tabs {
id: tabs
readonly property int currentIndex: root.screenState.dashboardTab
readonly property Item currentItem: {
repeater.count;
return repeater.itemAt(currentIndex);
}
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.fill: parent
nonAnimWidth: view.implicitWidth
screenState: root.screenState
tabs: root.dashboardTabs
}
implicitWidth: currentItem?.implicitWidth ?? 0
implicitHeight: currentItem?.implicitHeight ?? 0
Item {
id: viewWrapper
contentX: currentItem?.x ?? 0
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small
interactive: false
Flickable {
id: view
onContentXChanged: {
if (!moving || !currentItem)
return;
readonly property int currentIndex: root.screenState.dashboardTab
readonly property Item currentItem: {
repeater.count;
return repeater.itemAt(currentIndex);
}
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
}
anchors.fill: parent
RowLayout {
id: row
implicitWidth: currentItem?.implicitWidth ?? 0
implicitHeight: currentItem?.implicitHeight ?? 0
Repeater {
id: repeater
contentX: currentItem?.x ?? 0
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
model: ScriptModel {
values: root.dashboardTabs
}
interactive: false
delegate: Loader {
id: paneLoader
onContentXChanged: {
if (!moving || !currentItem)
return;
required property int index
required property var modelData
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
}
Layout.alignment: Qt.AlignTop
RowLayout {
id: row
sourceComponent: modelData.component
Repeater {
id: repeater
Component.onCompleted: active = Qt.binding(() => {
if (index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
model: ScriptModel {
values: root.dashboardTabs
}
delegate: Loader {
id: paneLoader
required property int index
required property var modelData
Layout.alignment: Qt.AlignTop
sourceComponent: modelData.component
Component.onCompleted: active = Qt.binding(() => {
if (index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
}
}
}
}
Component {
id: dashComponent
Component {
id: dashComponent
Dash {
screenState: root.screenState
Dash {
screenState: root.screenState
}
}
}
Component {
id: weatherComponent
Component {
id: weatherComponent
WeatherTab {}
}
Component {
id: mediaComponent
Media {
screenState: root.screenState
WeatherTab {}
}
}
// Component {
// id: testingComponent
//
// WeatherTest {}
// }
Component {
id: mediaComponent
Behavior on contentX {
Anim {
type: Anim.DefaultEffects
Media {
screenState: root.screenState
}
}
Component {
id: procsComponent
ProcessesTab {}
}
Behavior on contentX {
Anim {
type: Anim.DefaultEffects
}
}
}
}
@@ -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.Layouts
import ZShell.Internal
import ZShell.Components
import ZShell.Config
import qs.Helpers
import qs.Components
@@ -66,8 +66,7 @@ CustomRect {
Behavior on smoothMax {
enabled: sparkline.initialized
Anim {
}
Anim {}
}
Component.onCompleted: {
+11
View File
@@ -73,6 +73,17 @@ qml_module(ZShell
target_compile_definitions(ZShell PRIVATE ZSHELL_VERSION="${VERSION}")
# 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(Internal)
add_subdirectory(Services)
+8 -2
View File
@@ -5,6 +5,12 @@ qml_module(ZShell-components
wavyline.hpp wavyline.cpp
buttonrow.hpp buttonrow.cpp
carouselview.hpp carouselview.cpp
LIBRARIES
Qt::Quick
wheelinverter.hpp
sparklinecontent.hpp sparklinecontent.cpp
sparklineitem.hpp sparklineitem.cpp
sparklinesceneitem.hpp sparklinesceneitem.cpp
LIBRARIES
Qt::Quick
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 <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <qquickitem.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
QML_ELEMENT
Q_PROPERTY(
CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed)
internal::CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY
line1Changed)
Q_PROPERTY(
CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed)
internal::CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY
line2Changed)
Q_PROPERTY(
QColor line1Color READ line1Color WRITE setLine1Color NOTIFY
line1ColorChanged)
@@ -52,13 +56,11 @@ class SparklineItem : public QQuickPaintedItem {
public:
explicit SparklineItem(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
[[nodiscard]] internal::CircularBuffer* line1() const;
void setLine1(internal::CircularBuffer* buffer);
[[nodiscard]] CircularBuffer* line1() const;
void setLine1(CircularBuffer* buffer);
[[nodiscard]] CircularBuffer* line2() const;
void setLine2(CircularBuffer* buffer);
[[nodiscard]] internal::CircularBuffer* line2() const;
void setLine2(internal::CircularBuffer* buffer);
[[nodiscard]] QColor line1Color() const;
void setLine1Color(const QColor& color);
@@ -108,16 +110,17 @@ class SparklineItem : public QQuickPaintedItem {
void smoothingChanged();
void curveSmoothingChanged();
private:
void drawLine(
QPainter* painter,
CircularBuffer* buffer,
const QColor& color,
qreal fillAlpha);
void connectBuffer(CircularBuffer* buffer);
protected:
void geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) override;
CircularBuffer* m_line1 = nullptr;
CircularBuffer* m_line2 = nullptr;
private:
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_line2Color;
qreal m_line1FillAlpha = 0.15;
@@ -131,4 +134,4 @@ class SparklineItem : public QQuickPaintedItem {
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
+13 -7
View File
@@ -1,16 +1,17 @@
qml_module(ZShell-config
URI ZShell.Config
INCLUDE_PREFIX config
DEPENDENCIES
ZShell.Settings
SOURCES
confignode.hpp confignode.cpp
configobject.hpp configobject.cpp
configlist.hpp configlist.cpp
config.hpp config.cpp
common.hpp common.cpp
rootnodes.hpp rootnodes.cpp
migration.hpp migration.cpp
anim.hpp anim.cpp
fontbuilder.hpp fontbuilder.cpp
font.hpp font.cpp
tokens.hpp
appearance.hpp
appearance.hpp appearance.cpp
background.hpp
bar.hpp
clipboard.hpp
@@ -27,6 +28,11 @@ qml_module(ZShell-config
services.hpp
sidebar.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 "tokens.hpp"
#include <qpoint.h>
#include "appearance.hpp"
#include "tokens.hpp"
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);
// 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) {
QPointF c1(points[i], points[i + 1]);
QPointF c2(points[i + 2], points[i + 3]);
QPointF end(points[i + 4], points[i + 5]);
const QPointF c1(points[i], points[i + 1]);
const QPointF c2(points[i + 2], points[i + 3]);
const QPointF end(points[i + 4], points[i + 5]);
curve.addCubicBezierSegment(c1, c2, end);
}
return curve;
}
void AnimEasing::bindCurves(AnimCurves* curves) {
m_curves = curves;
connect(
curves,
&AnimCurves::propertiesChanged,
this,
&AnimEasing::rebuildCurves);
rebuildCurves();
}
void AnimEasing::rebuildCurves() {
void AnimTokens::rebuildCurves() {
if (!m_curves) return;
m_emphasized = buildCurve(m_curves->emphasized());
m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel());
m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel());
@@ -41,7 +94,25 @@ void AnimEasing::rebuildCurves() {
m_expressiveDefaultEffects =
buildCurve(m_curves->expressiveDefaultEffects());
m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects());
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
+38 -37
View File
@@ -9,8 +9,10 @@ namespace ZShell::config {
class AnimCurves;
class AnimDurations;
class AnimEasing : public QObject {
class AnimTokens : public QObject {
Q_OBJECT
Q_MOC_INCLUDE("tokens.hpp") // AnimCurves
Q_MOC_INCLUDE("appearance.hpp")
QML_ANONYMOUS
Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged)
@@ -42,54 +44,53 @@ class AnimEasing : public QObject {
QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY
curvesChanged)
Q_PROPERTY(
ZShell::config::AnimDurations* durations READ durations NOTIFY
durationsChanged)
public:
explicit AnimEasing(QObject* parent = nullptr) : QObject(parent) {}
explicit AnimTokens(QObject* parent = nullptr);
void bindCurves(AnimCurves* curves);
void bindDurations(AnimDurations* durations);
[[nodiscard]] QEasingCurve emphasized() const { return m_emphasized; }
[[nodiscard]] QEasingCurve emphasizedAccel() const {
return m_emphasizedAccel;
}
[[nodiscard]] QEasingCurve emphasizedDecel() const {
return m_emphasizedDecel;
}
[[nodiscard]] QEasingCurve standard() const { return m_standard; }
[[nodiscard]] QEasingCurve standardAccel() const { return m_standardAccel; }
[[nodiscard]] QEasingCurve standardDecel() const { return m_standardDecel; }
[[nodiscard]] QEasingCurve expressiveFastSpatial() const {
return m_expressiveFastSpatial;
}
[[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;
}
[[nodiscard]] QEasingCurve emphasized() const;
[[nodiscard]] QEasingCurve emphasizedAccel() const;
[[nodiscard]] QEasingCurve emphasizedDecel() const;
[[nodiscard]] QEasingCurve standard() const;
[[nodiscard]] QEasingCurve standardAccel() const;
[[nodiscard]] QEasingCurve standardDecel() const;
[[nodiscard]] QEasingCurve expressiveFastSpatial() const;
[[nodiscard]] QEasingCurve expressiveDefaultSpatial() const;
[[nodiscard]] QEasingCurve expressiveSlowSpatial() const;
[[nodiscard]] QEasingCurve expressiveFastEffects() const;
[[nodiscard]] QEasingCurve expressiveDefaultEffects() const;
[[nodiscard]] QEasingCurve expressiveSlowEffects() const;
[[nodiscard]] AnimDurations* durations() const;
signals:
void curvesChanged();
void durationsChanged();
private:
static QEasingCurve buildCurve(const QList<qreal>& points);
void rebuildCurves();
static QEasingCurve buildCurve(const QList<qreal>& points);
AnimCurves* m_curves = nullptr;
QEasingCurve m_emphasized, m_emphasizedAccel, m_emphasizedDecel;
QEasingCurve m_standard, m_standardAccel, m_standardDecel;
QEasingCurve m_expressiveFastSpatial, m_expressiveDefaultSpatial,
m_expressiveSlowSpatial;
QEasingCurve m_expressiveFastEffects, m_expressiveDefaultEffects,
m_expressiveSlowEffects;
AnimDurations* m_durations = nullptr;
QEasingCurve m_emphasized;
QEasingCurve m_emphasizedAccel;
QEasingCurve m_emphasizedDecel;
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
+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
+206 -276
View File
@@ -1,101 +1,126 @@
#pragma once
#include "configobject.hpp"
#include <QFont>
#include <QVariantMap>
#include <limits>
#include <qqmlregistration.h>
#include <qfont.h>
#include <qstring.h>
#include <qvariantmap.h>
#include "settings/objectnode.hpp"
#include "common.hpp"
namespace ZShell::config {
class AnimDurations : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
using Qt::StringLiterals::operator""_s;
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(
int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY
scaleChanged)
Q_PROPERTY(
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)
int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
Q_PROPERTY(int full READ full NOTIFY valuesChanged)
public:
explicit AnimDurations(QObject* parent = nullptr) : ConfigObject(parent) {}
void bindTokens(RoundingTokens* tokens);
[[nodiscard]] int small() const { return static_cast<int>(200 * scale()); }
[[nodiscard]] int normal() const { return static_cast<int>(400 * scale()); }
[[nodiscard]] int large() const { return static_cast<int>(600 * scale()); }
[[nodiscard]] int extraLarge() const {
return static_cast<int>(1000 * scale());
}
[[nodiscard]] int expressiveFastSpatial() const {
return static_cast<int>(350 * scale());
}
[[nodiscard]] int expressiveDefaultSpatial() const {
return static_cast<int>(500 * scale());
}
[[nodiscard]] int expressiveSlowSpatial() const {
return static_cast<int>(650 * scale());
}
[[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());
}
[[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;
[[nodiscard]] int full() const;
signals:
void valuesChanged();
private:
RoundingTokens* m_tokens = nullptr;
};
class Anim : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AppearanceSpacing : public settings::ObjectNode {
CONFIG_NODE(AppearanceSpacing, settings::ObjectNode)
CFG_PROPERTY(int, mediaGifSpeedAdjustment, 300)
CFG_PROPERTY(qreal, sessionGifSpeed, 0.7)
CONFIG_SUBOBJECT(AnimDurations, durations)
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(
int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
public:
explicit Anim(QObject* parent = nullptr)
: ConfigObject(parent), m_durations(new AnimDurations(this)) {}
void bindTokens(SpacingTokens* tokens);
[[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 {
Q_OBJECT
QML_ANONYMOUS
class AppearancePadding : public settings::ObjectNode {
CONFIG_NODE(AppearancePadding, settings::ObjectNode)
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:
explicit Deform(QObject* parent = nullptr) : ConfigObject(parent) {}
};
void bindTokens(PaddingTokens* tokens);
class FontFamily : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
[[nodiscard]] int extraSmall() const;
[[nodiscard]] int smaller() 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;
CFG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CFG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded"))
CFG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF"))
CFG_PROPERTY(QString, sans, QStringLiteral("Segoe UI Variable Text"))
signals:
void valuesChanged();
public:
explicit FontFamily(QObject* parent = nullptr) : ConfigObject(parent) {}
private:
PaddingTokens* m_tokens = nullptr;
};
namespace detail {
@@ -105,20 +130,19 @@ struct FontConfig {
int size;
int weight = QFont::Normal;
bool italic = false;
QVariantMap vaxes = {{QStringLiteral("ROND"), 25}};
QVariantMap vaxes = {{u"ROND"_s, 25}};
};
} // namespace detail
class FontSizeNode : public ConfigObject {
public:
explicit FontSizeNode(QObject* parent = nullptr) : ConfigObject(parent) {}
class FontSizeNode : public settings::ObjectNode {
CONFIG_NODE(FontSizeNode, settings::ObjectNode)
};
class FontStyleNode : public ConfigObject {
public:
explicit FontStyleNode(QObject* parent = nullptr) : ConfigObject(parent) {}
class FontStyleNode : public settings::ObjectNode {
CONFIG_NODE(FontStyleNode, settings::ObjectNode)
public:
virtual FontSizeNode* largeNode() const = 0;
virtual FontSizeNode* mediumNode() 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_CONFIG(Style, Size, props) \
class FontConfig##Style##Size : public FontSizeNode { \
Q_OBJECT QML_ANONYMOUS \
CONFIG_NODE(FontConfig##Style##Size, FontSizeNode) \
\
CFG_PROPERTY(QString, family, ARG(props).family) \
CFG_PROPERTY(int, size, ARG(props).size) \
CFG_PROPERTY(int, weight, ARG(props).weight) \
CFG_PROPERTY(bool, italic, ARG(props).italic) \
CFG_PROPERTY(QVariantMap, vaxes, ARG(props).vaxes) \
\
public \
: explicit FontConfig##Style##Size(QObject* parent = nullptr) \
: FontSizeNode(parent) {} \
CONFIG_PROPERTY( \
QString, \
family, \
ARG(props).family) /* Empty inherits the style family */ \
CONFIG_PROPERTY(int, size, ARG(props).size) \
CONFIG_PROPERTY(int, weight, ARG(props).weight) \
CONFIG_PROPERTY(bool, italic, ARG(props).italic) \
CONFIG_PROPERTY(QVariantMap, vaxes, ARG(props).vaxes) \
};
#define FONT_STYLE(Style, family_, large_, medium_, small_) \
FONT_CONFIG(Style, Large, ARG(large_)) \
@@ -147,47 +170,41 @@ class FontStyleNode : public ConfigObject {
FONT_CONFIG(Style, Small, ARG(small_)) \
\
class FontStyle##Style : public FontStyleNode { \
Q_OBJECT QML_ANONYMOUS \
CONFIG_NODE(FontStyle##Style, FontStyleNode) \
\
CFG_PROPERTY(QString, family, family_) \
CONFIG_SUBOBJECT(FontConfig##Style##Large, large) \
CONFIG_SUBOBJECT(FontConfig##Style##Medium, medium) \
CONFIG_SUBOBJECT(FontConfig##Style##Small, small) \
\
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)) {} \
CONFIG_PROPERTY(QString, family, family_) \
CONFIG_SUBOBJECT(FontConfig##Style##Large, large) \
CONFIG_SUBOBJECT(FontConfig##Style##Medium, medium) \
CONFIG_SUBOBJECT(FontConfig##Style##Small, small) \
\
public: \
FontSizeNode* largeNode() const override { return m_large; } \
FontSizeNode* mediumNode() const override { return m_medium; } \
FontSizeNode* smallNode() const override { return m_small; } \
};
// clang-format off
FONT_STYLE(Headline, QStringLiteral("GoogleSansFlex"),
FONT_STYLE(Headline, u"GoogleSansFlex"_s,
FONT({ .size = 32, .weight = QFont::Medium }),
FONT({ .size = 28, .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 = 16, .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 = 14 }),
FONT({ .size = 12 })
)
FONT_STYLE(Label, QStringLiteral("GoogleSansFlex"),
FONT_STYLE(Label, u"GoogleSansFlex"_s,
FONT({ .size = 14, .weight = QFont::Medium }),
FONT({ .size = 12, .weight = QFont::Medium }),
FONT({ .size = 11 })
)
FONT_STYLE(Mono, QStringLiteral("CaskaydiaCove NF"),
FONT_STYLE(Mono, u"CaskaydiaCove NF"_s,
FONT({ .size = 16, .vaxes = {} }),
FONT({ .size = 14, .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)}))
class FontStyleIcon : public FontStyleNode {
Q_OBJECT
QML_ANONYMOUS
CONFIG_NODE(FontStyleIcon, FontStyleNode)
CFG_PROPERTY(QString, family, QStringLiteral("Material Symbols Rounded"))
CONFIG_PROPERTY(QString, family, u"Material Symbols Rounded"_s)
CONFIG_SUBOBJECT(FontConfigIconExtraLarge, extraLarge)
CONFIG_SUBOBJECT(FontConfigIconLarge, large)
CONFIG_SUBOBJECT(FontConfigIconMedium, medium)
CONFIG_SUBOBJECT(FontConfigIconSmall, small)
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* largeNode() const override { return m_large; }
FontSizeNode* mediumNode() const override { return m_medium; }
@@ -228,11 +237,20 @@ class FontStyleIcon : public FontStyleNode {
#undef FONT_CONFIG
#undef FONT_STYLE
class AppearanceFont : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class FontFamily : public settings::ObjectNode {
CONFIG_NODE(FontFamily, settings::ObjectNode)
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(FontStyleHeadline, headline)
CONFIG_SUBOBJECT(FontStyleTitle, title)
@@ -240,160 +258,83 @@ class AppearanceFont : public ConfigObject {
CONFIG_SUBOBJECT(FontStyleLabel, label)
CONFIG_SUBOBJECT(FontStyleMono, mono)
CONFIG_SUBOBJECT(FontStyleIcon, icon)
CFG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CFG_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)) {}
CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik"))
CONFIG_PROPERTY(QString, workspaces, QStringLiteral("Rubik"))
};
class Transparency : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AnimDurations : public settings::ObjectNode {
CONFIG_NODE(AnimDurations, settings::ObjectNode)
CFG_PROPERTY(qreal, base, 0.85)
CFG_PROPERTY(bool, enabled, false)
CFG_PROPERTY(qreal, layers, 0.4)
CONFIG_GLOBAL_PROPERTY(qreal, scale, 1)
public:
explicit Transparency(QObject* parent = nullptr) : ConfigObject(parent) {}
};
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(int small READ small NOTIFY valuesChanged)
Q_PROPERTY(int normal READ normal NOTIFY valuesChanged)
Q_PROPERTY(int large READ large NOTIFY valuesChanged)
Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY scaleChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY scaleChanged)
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)
int expressiveFastSpatial READ expressiveFastSpatial NOTIFY
valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY scaleChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY scaleChanged)
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)
int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY
valuesChanged)
Q_PROPERTY(
int extraLargeIncreased READ extraLargeIncreased NOTIFY scaleChanged)
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY scaleChanged)
int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY
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:
explicit AppearancePadding(QObject* parent = nullptr)
: ConfigObject(parent) {}
void bindTokens(AnimDurationTokens* tokens);
[[nodiscard]] int extraSmall() const {
return static_cast<int>(4 * scale());
}
[[nodiscard]] int smaller() const { return static_cast<int>(5 * 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 small() const;
[[nodiscard]] int normal() const;
[[nodiscard]] int large() const;
[[nodiscard]] int extraLarge() const;
[[nodiscard]] int expressiveFastSpatial() const;
[[nodiscard]] int expressiveDefaultSpatial() const;
[[nodiscard]] int expressiveSlowSpatial() const;
[[nodiscard]] int expressiveFastEffects() const;
[[nodiscard]] int expressiveDefaultEffects() const;
[[nodiscard]] int expressiveSlowEffects() const;
signals:
void valuesChanged();
private:
AnimDurationTokens* m_tokens = nullptr;
};
class Appearance : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Anim : public settings::ObjectNode {
CONFIG_NODE(Anim, settings::ObjectNode)
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(Deform, deform)
@@ -401,18 +342,7 @@ class Appearance : public ConfigObject {
CONFIG_SUBOBJECT(AppearanceRounding, rounding)
CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
CONFIG_SUBOBJECT(AppearancePadding, padding)
CONFIG_SUBOBJECT(Transparency, 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)) {};
CONFIG_SUBOBJECT(AppearanceTransparency, transparency)
};
} // namespace ZShell::config
+5 -9
View File
@@ -1,18 +1,14 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Background : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Background : public settings::ObjectNode {
CONFIG_NODE(Background, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, wallFadeDuration, 300)
public:
explicit Background(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, wallFadeDuration, 300)
};
} // namespace ZShell::config
+26 -46
View File
@@ -1,7 +1,6 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include <qcontainerfwd.h>
#include <qhashfunctions.h>
#include <qqmlregistration.h>
@@ -9,66 +8,53 @@
namespace ZShell::config {
class BarEntry : public ListEntry {
Q_OBJECT
QML_ANONYMOUS
public:
explicit BarEntry(QObject* parent = nullptr) : ListEntry(parent) {}
CONFIG_NODE(BarEntry, ListEntry)
};
class Popouts : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Popouts : public settings::ObjectNode {
CONFIG_NODE(Popouts, settings::ObjectNode)
CFG_PROPERTY(bool, statusIcons, true)
CFG_PROPERTY(bool, tray, true)
public:
explicit Popouts(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, statusIcons, true)
CONFIG_PROPERTY(bool, tray, true)
};
class Tray : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Tray : public settings::ObjectNode {
CONFIG_NODE(Tray, settings::ObjectNode)
CFG_PROPERTY(bool, recolorIcons, false)
CFG_PROPERTY(bool, showOnHover, true)
CFG_PROPERTY(int, trayIconSize, 24)
CONFIG_PROPERTY(bool, recolorIcons, false)
CONFIG_PROPERTY(bool, showOnHover, true)
CONFIG_PROPERTY(int, trayIconSize, 24)
CONFIG_LIST(
EntryList,
statusIcons,
{
DEFAULT_ARG({
LIST_ENTRY(audio, true),
LIST_ENTRY(microphone, true),
LIST_ENTRY(bluetooth, false),
LIST_ENTRY(network, false),
LIST_ENTRY(wifi, false),
LIST_ENTRY(power, true),
})
public:
explicit Tray(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
};
class Bar : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Bar : public settings::ObjectNode {
CONFIG_NODE(Bar, settings::ObjectNode)
CFG_PROPERTY(bool, autoHide, false)
CFG_PROPERTY(int, border, 4)
CFG_PROPERTY(bool, fullscreenReveal, true)
CFG_PROPERTY(int, height, 24)
CFG_PROPERTY(bool, hideWhenNotif, true)
CONFIG_PROPERTY(bool, autoHide, false)
CONFIG_PROPERTY(int, border, 4)
CONFIG_PROPERTY(bool, fullscreenReveal, true)
CONFIG_PROPERTY(int, height, 24)
CONFIG_PROPERTY(bool, hideWhenNotif, true)
CONFIG_SUBOBJECT(Popouts, popouts)
CFG_PROPERTY(int, revealDelay, 100)
CFG_PROPERTY(int, rounding, 14)
CFG_PROPERTY(int, smoothing, 32)
CFG_PROPERTY(QString, position, QStringLiteral("top"))
CONFIG_PROPERTY(int, revealDelay, 100)
CONFIG_PROPERTY(int, rounding, 14)
CONFIG_PROPERTY(int, smoothing, 32)
CONFIG_PROPERTY(QString, position, QStringLiteral("top"))
CONFIG_SUBOBJECT(Tray, tray)
CONFIG_LIST(
EntryList,
entries,
{
DEFAULT_ARG({
LIST_ENTRY(workspaces, true),
LIST_ENTRY(audio, true),
LIST_ENTRY(media, true),
@@ -85,13 +71,7 @@ class Bar : public ConfigObject {
LIST_ENTRY(upower, true),
LIST_ENTRY(clock, 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
+11 -20
View File
@@ -1,33 +1,24 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class ClipboardSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class ClipboardSizes : public settings::ObjectNode {
CONFIG_NODE(ClipboardSizes, settings::ObjectNode)
CFG_PROPERTY(int, itemHeight, 60)
CFG_PROPERTY(int, minPreviewWidth, 200)
CFG_PROPERTY(int, previewWidth, 800)
CFG_PROPERTY(int, width, 500)
public:
explicit ClipboardSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, itemHeight, 60)
CONFIG_PROPERTY(int, minPreviewWidth, 200)
CONFIG_PROPERTY(int, previewWidth, 800)
CONFIG_PROPERTY(int, width, 500)
};
class Clipboard : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Clipboard : public settings::ObjectNode {
CONFIG_NODE(Clipboard, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, maxEntriesShown, 10)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, maxEntriesShown, 10)
CONFIG_SUBOBJECT(ClipboardSizes, sizes)
public:
explicit Clipboard(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new ClipboardSizes(this)) {}
};
} // namespace ZShell::config
+16 -18
View File
@@ -1,31 +1,29 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Presets : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Presets : public settings::ObjectNode {
CONFIG_NODE(Presets, settings::ObjectNode)
CFG_PROPERTY(QString, accent, QString())
CFG_PROPERTY(QString, name, QStringLiteral("Catppuccin"))
CFG_PROPERTY(QString, variant, QStringLiteral("latte"))
public:
explicit Presets(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(QString, accent, QString())
CONFIG_PROPERTY(QString, name, QStringLiteral("Catppuccin"))
CONFIG_PROPERTY(QString, variant, QStringLiteral("latte"))
};
class Colors : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Colors : public settings::ObjectNode {
CONFIG_NODE(Colors, settings::ObjectNode)
CONFIG_SUBOBJECT(Presets, presets)
CFG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
public:
explicit Colors(QObject* parent = nullptr)
: ConfigObject(parent), m_presets(new Presets(this)) {}
CONFIG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
CONFIG_PROPERTY(bool, scheduleDark, false)
CONFIG_PROPERTY(int, scheduleDarkEnd, 600)
CONFIG_PROPERTY(int, scheduleDarkStart, 1140)
CONFIG_PROPERTY(bool, schemeGen, true)
CONFIG_PROPERTY(bool, neovimColors, false)
CONFIG_PROPERTY(QString, mode, QStringLiteral("dark"))
CONFIG_PROPERTY(bool, smart, false)
};
} // 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}, \
})
-278
View File
@@ -1,278 +0,0 @@
#include "config.hpp"
#include "appearance.hpp"
#include "background.hpp"
#include "bar.hpp"
#include "clipboard.hpp"
#include "colors.hpp"
#include "dashboard.hpp"
#include "dock.hpp"
#include "general.hpp"
#include "launcher.hpp"
#include "lock.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(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_dock(new Dock(this))
, m_general(new General(this))
, m_launcher(new Launcher(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)) {
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::create(QQmlEngine*, QJSEngine*) {
return new Config();
}
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;
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
if (doc.isObject()) {
before = doc.object();
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 || 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 before = valid ? doc.object() : QJsonObject();
QMetaObject::invokeMethod(
this,
[this, valid, before]() {
loadFromJson(QJsonValue(before));
m_loading = false;
const auto after = toJson().toObject();
if (!valid || 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
-108
View File
@@ -1,108 +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 Dock;
class General;
class Launcher;
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("dock.hpp")
Q_MOC_INCLUDE("general.hpp")
Q_MOC_INCLUDE("launcher.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(Dock, dock)
CONFIG_SUBOBJECT(General, general)
CONFIG_SUBOBJECT(Launcher, launcher)
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*);
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;
};
} // 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
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Performance : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Performance : public settings::ObjectNode {
CONFIG_NODE(Performance, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(bool, showBattery, false)
CFG_PROPERTY(bool, showCpu, true)
CFG_PROPERTY(bool, showGpu, true)
CFG_PROPERTY(bool, showMemory, true)
CFG_PROPERTY(bool, showNetwork, true)
CFG_PROPERTY(bool, showStorage, true)
CFG_PROPERTY(bool, showVram, true)
public:
explicit Performance(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(bool, showBattery, false)
CONFIG_PROPERTY(bool, showCpu, true)
CONFIG_PROPERTY(bool, showGpu, true)
CONFIG_PROPERTY(bool, showMemory, true)
CONFIG_PROPERTY(bool, showNetwork, true)
CONFIG_PROPERTY(bool, showStorage, true)
CONFIG_PROPERTY(bool, showVram, true)
};
class DashboardSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class DashboardSizes : public settings::ObjectNode {
CONFIG_NODE(DashboardSizes, settings::ObjectNode)
CFG_PROPERTY(int, dateTimeWidth, 110)
CFG_PROPERTY(int, infoIconSize, 25)
CFG_PROPERTY(int, infoWidth, 200)
CFG_PROPERTY(int, mediaCoverArtSize, 150)
CFG_PROPERTY(int, mediaProgressSweep, 180)
CFG_PROPERTY(int, mediaProgressThickness, 8)
CFG_PROPERTY(int, mediaVisualizerSize, 200)
CFG_PROPERTY(int, mediaWidth, 200)
CFG_PROPERTY(int, resourceProgessThickness, 10)
CFG_PROPERTY(int, resourceSize, 200)
CFG_PROPERTY(int, tabIndicatorHeight, 3)
CFG_PROPERTY(int, tabIndicatorSpacing, 5)
CFG_PROPERTY(int, weatherWidth, 250)
public:
explicit DashboardSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, dateTimeWidth, 110)
CONFIG_PROPERTY(int, infoIconSize, 25)
CONFIG_PROPERTY(int, infoWidth, 200)
CONFIG_PROPERTY(int, mediaCoverArtSize, 150)
CONFIG_PROPERTY(int, mediaProgressSweep, 180)
CONFIG_PROPERTY(int, mediaProgressThickness, 8)
CONFIG_PROPERTY(int, mediaVisualizerSize, 200)
CONFIG_PROPERTY(int, mediaWidth, 200)
CONFIG_PROPERTY(int, resourceProgessThickness, 10)
CONFIG_PROPERTY(int, resourceSize, 200)
CONFIG_PROPERTY(int, tabIndicatorHeight, 3)
CONFIG_PROPERTY(int, tabIndicatorSpacing, 5)
CONFIG_PROPERTY(int, weatherWidth, 250)
};
class Dashboard : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class ProcessColumnEntry : public settings::ObjectNode {
CONFIG_NODE(ProcessColumnEntry, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(bool, showOnHover, true)
CFG_PROPERTY(int, dragThreshold, 50)
CFG_PROPERTY(int, mediaUpdateInterval, 2000)
CONFIG_PROPERTY(QString, id, {})
CONFIG_PROPERTY(double, width, 0)
};
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)
CFG_PROPERTY(int, resourceUpdateInterval, 1000)
CONFIG_PROPERTY(int, resourceUpdateInterval, 1000)
CONFIG_SUBOBJECT(DashboardSizes, sizes)
public:
explicit Dashboard(QObject* parent = nullptr)
: ConfigObject(parent)
, m_performance(new Performance(this))
, m_sizes(new DashboardSizes(this)) {}
CONFIG_LIST(
ProcessColumnList,
processColumns,
DEFAULT_ARG({
settings::vmap({{"id", "command"}}),
settings::vmap({{"id", "pid"}}),
settings::vmap({{"id", "cpuUsage"}}),
settings::vmap({{"id", "memoryUsage"}}),
}))
};
} // namespace ZShell::config
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class Nightlight : public settings::ObjectNode {
CONFIG_NODE(Nightlight, settings::ObjectNode)
CONFIG_PROPERTY(bool, schedule, true)
CONFIG_PROPERTY(int, scheduleStart, 1200)
CONFIG_PROPERTY(int, scheduleEnd, 570)
CONFIG_PROPERTY(bool, useNative, true)
CONFIG_PROPERTY(int, fadeDuration, 2000)
CONFIG_PROPERTY(int, temp, 2600)
};
class Display : public settings::ObjectNode {
CONFIG_NODE(Display, settings::ObjectNode)
CONFIG_SUBOBJECT(Nightlight, nightlight)
};
} // namespace ZShell::config
+9 -13
View File
@@ -1,29 +1,25 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <QStringList>
#include <qqmlintegration.h>
namespace ZShell::config {
class Dock : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Dock : public settings::ObjectNode {
CONFIG_NODE(Dock, settings::ObjectNode)
CFG_PROPERTY(bool, enable, false)
CFG_PROPERTY(int, height, 80)
CFG_PROPERTY(bool, hoverToReveal, false)
CFG_PROPERTY(QStringList, ignoredAppRegexes, {})
CFG_PROPERTY(
CONFIG_PROPERTY(bool, enable, false)
CONFIG_PROPERTY(int, height, 80)
CONFIG_PROPERTY(bool, hoverToReveal, false)
CONFIG_PROPERTY(QStringList, ignoredAppRegexes, {})
CONFIG_PROPERTY(
QStringList,
pinnedApps,
(QStringList{
QStringLiteral("com.ayugram.desktop"),
QStringLiteral("com.obsproject.studio"),
QStringLiteral("librewolf")}))
CFG_PROPERTY(bool, pinnedOnStartup, false)
public:
explicit Dock(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, pinnedOnStartup, false)
};
} // namespace ZShell::config
+8 -10
View File
@@ -1,7 +1,7 @@
#include "font.hpp"
#include "appearance.hpp"
#include "config.hpp"
#include "rootnodes.hpp"
namespace ZShell::config {
@@ -59,20 +59,20 @@ void FontStyleBase::bind(FontStyleNode* cfg) {
if (cfg) {
connect(
cfg, &ConfigNode::propertiesChanged, this, &FontStyleBase::rebuild);
cfg, &settings::Node::optionChanged, this, &FontStyleBase::rebuild);
connect(
cfg->largeNode(),
&ConfigNode::propertiesChanged,
&settings::Node::optionChanged,
this,
&FontStyleBase::rebuild);
connect(
cfg->mediumNode(),
&ConfigNode::propertiesChanged,
&settings::Node::optionChanged,
this,
&FontStyleBase::rebuild);
connect(
cfg->smallNode(),
&ConfigNode::propertiesChanged,
&settings::Node::optionChanged,
this,
&FontStyleBase::rebuild);
}
@@ -128,7 +128,7 @@ void IconFontStyle::bind(FontStyleNode* cfg) {
if (cfg)
connect(
cfg->extraLargeNode(),
&ConfigNode::propertiesChanged,
&settings::Node::optionChanged,
this,
&IconFontStyle::rebuild);
}
@@ -181,16 +181,14 @@ FontBuilder IconFontBuilders::extraLarge() const {
}
FontTokens::FontTokens(QObject* parent)
: ConfigObject(parent)
, m_size(new FontSizeTokens(this))
: QObject(parent)
, m_headline(new FontStyle(this))
, m_title(new FontStyle(this))
, m_body(new FontStyle(this))
, m_label(new FontStyle(this))
, m_mono(new FontStyle(this))
, m_icon(new IconFontStyle(this)) {
if (!Config::instance()) new Config();
bindFont(Config::instance()->appearance()->font());
bindFont(ConfigSingleton::instance()->appearance()->font());
}
FontStyle* FontTokens::headline() const {
+1 -19
View File
@@ -1,6 +1,5 @@
#pragma once
#include "configobject.hpp"
#include "fontbuilder.hpp"
#include <qfont.h>
@@ -17,21 +16,6 @@ class FontSizeNode;
class FontStyleNode;
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 {
Q_OBJECT
QML_ANONYMOUS
@@ -149,12 +133,10 @@ class IconFontStyle : public FontStyleBase {
IconFontBuilders* m_builders;
};
class FontTokens : public ConfigObject {
class FontTokens : public QObject {
Q_OBJECT
QML_ANONYMOUS
CONFIG_SUBOBJECT(FontSizeTokens, size)
Q_PROPERTY(FontStyle* headline READ headline CONSTANT FINAL)
Q_PROPERTY(FontStyle* title READ title CONSTANT FINAL)
Q_PROPERTY(FontStyle* body READ body CONSTANT FINAL)
+45 -107
View File
@@ -1,165 +1,103 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include <QStandardPaths>
#include <QStringList>
namespace ZShell::config {
class BatteryThresholdEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class BatteryThresholdEntry : public settings::ObjectNode {
CONFIG_NODE(BatteryThresholdEntry, settings::ObjectNode)
CFG_PROPERTY(QString, icon)
CFG_PROPERTY(QString, message)
CFG_PROPERTY(QString, name)
CFG_PROPERTY(int, perc)
public:
explicit BatteryThresholdEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("perc")};
}
CONFIG_PROPERTY(QString, icon, {})
CONFIG_PROPERTY(QString, message, {})
CONFIG_PROPERTY(QString, name, {})
CONFIG_PROPERTY(int, perc, {})
};
CONFIG_LIST_TYPE(BatteryThresholdEntry, BatteryThresholdList)
class IdleTimeoutEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class IdleTimeoutEntry : public settings::ObjectNode {
CONFIG_NODE(IdleTimeoutEntry, settings::ObjectNode)
CFG_PROPERTY(QString, idleAction)
CFG_PROPERTY(QString, name)
CFG_PROPERTY(int, timeout)
public:
explicit IdleTimeoutEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("name")};
}
CONFIG_PROPERTY(QString, idleAction, {})
CONFIG_PROPERTY(QString, name, {})
CONFIG_PROPERTY(int, timeout, {})
};
CONFIG_LIST_TYPE(IdleTimeoutEntry, IdleTimeoutList)
class Apps : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Apps : public settings::ObjectNode {
CONFIG_NODE(Apps, settings::ObjectNode)
CFG_PROPERTY(QStringList, archiver, {QStringLiteral("ark")})
CFG_PROPERTY(QStringList, audio, {QStringLiteral("pavucontrol")})
CFG_PROPERTY(QStringList, document, {QStringLiteral("libreoffice")})
CFG_PROPERTY(QStringList, editor, {QStringLiteral("kate")})
CFG_PROPERTY(QStringList, explorer, {QStringLiteral("dolphin")})
CFG_PROPERTY(QStringList, image, {QStringLiteral("imv")})
CFG_PROPERTY(QStringList, playback, {QStringLiteral("vlc")})
CFG_PROPERTY(QStringList, terminal, {QStringLiteral("wezterm")})
public:
explicit Apps(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(QStringList, archiver, {QStringLiteral("ark")})
CONFIG_PROPERTY(QStringList, audio, {QStringLiteral("pavucontrol")})
CONFIG_PROPERTY(QStringList, document, {QStringLiteral("libreoffice")})
CONFIG_PROPERTY(QStringList, editor, {QStringLiteral("kate")})
CONFIG_PROPERTY(QStringList, explorer, {QStringLiteral("dolphin")})
CONFIG_PROPERTY(QStringList, image, {QStringLiteral("imv")})
CONFIG_PROPERTY(QStringList, playback, {QStringLiteral("vlc")})
CONFIG_PROPERTY(QStringList, terminal, {QStringLiteral("wezterm")})
};
class Battery : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Battery : public settings::ObjectNode {
CONFIG_NODE(Battery, settings::ObjectNode)
CFG_PROPERTY(int, critPerc, 5)
CONFIG_PROPERTY(int, critPerc, 5)
CONFIG_LIST(
BatteryThresholdList,
popupThresholds,
{
vmap(
DEFAULT_ARG({
settings::vmap(
{{"icon", "battery_android_frame_2"},
{"message", "Battery low"},
{"name", "Low battery"},
{"perc", 20}}),
vmap(
settings::vmap(
{{"icon", "battery_android_frame_2"},
{"message", "Battery lower"},
{"name", "Low battery"},
{"perc", 15}}),
vmap(
settings::vmap(
{{"icon", "battery_android_frame_2"},
{"message", "Battery lowest"},
{"name", "Low battery"},
{"perc", 10}}),
})
public:
explicit Battery(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
};
class ColorSettings : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(int, hyprsunsetTemp, 2600)
CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
CFG_PROPERTY(bool, neovimColors, false)
CFG_PROPERTY(bool, useNativeNightlight, true)
CFG_PROPERTY(int, nativeFadeDuration, 2000)
CFG_PROPERTY(bool, scheduleDark, false)
CFG_PROPERTY(int, scheduleDarkEnd, 600)
CFG_PROPERTY(int, scheduleDarkStart, 1140)
CFG_PROPERTY(bool, scheduleHyprsunset, true)
CFG_PROPERTY(int, scheduleHyprsunsetEnd, 570)
CFG_PROPERTY(int, scheduleHyprsunsetStart, 1200)
CFG_PROPERTY(bool, schemeGeneration, true)
CFG_PROPERTY(bool, smart, false)
public:
explicit ColorSettings(QObject* parent = nullptr) : ConfigObject(parent) {}
};
class Idle : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Idle : public settings::ObjectNode {
CONFIG_NODE(Idle, settings::ObjectNode)
CONFIG_LIST(
IdleTimeoutList,
timeouts,
{
vmap(
DEFAULT_ARG({
settings::vmap(
{{"idleAction", QStringLiteral("lock")},
{"name", QStringLiteral("Lock")},
{"timeout", 180}}),
})
public:
explicit Idle(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
};
class General : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class General : public settings::ObjectNode {
CONFIG_NODE(General, settings::ObjectNode)
CONFIG_SUBOBJECT(Apps, apps)
CONFIG_SUBOBJECT(Battery, battery)
CONFIG_SUBOBJECT(ColorSettings, color)
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CFG_PROPERTY(bool, desktopIcons, true)
CONFIG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CONFIG_PROPERTY(bool, desktopIcons, true)
CONFIG_SUBOBJECT(Idle, idle)
CFG_PROPERTY(
CONFIG_PROPERTY(
QString,
lyricsDir,
QStandardPaths::writableLocation(QStandardPaths::MusicLocation) + QStringLiteral("/Lyrics/"))
CFG_PROPERTY(QString, logo, QString())
CFG_PROPERTY(bool, showOverFullscreen, true)
CFG_PROPERTY(
QStandardPaths::writableLocation(QStandardPaths::MusicLocation) +
QStringLiteral("/Lyrics/"))
CONFIG_PROPERTY(QString, logo, QString())
CONFIG_PROPERTY(bool, showOverFullscreen, true)
CONFIG_PROPERTY(
QString,
wallpaperPath,
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_color(new ColorSettings(this))
, m_idle(new Idle(this)) {}
};
} // namespace ZShell::config
+39 -80
View File
@@ -1,68 +1,53 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include <QStringList>
namespace ZShell::config {
class LauncherActionEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
using Qt::StringLiterals::operator""_s;
using settings::vmap;
CFG_PROPERTY(QVariantList, command)
CFG_PROPERTY(bool, dangerous, false)
CFG_PROPERTY(QString, description)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(QString, icon)
CFG_PROPERTY(QString, name)
class LauncherActionEntry : public settings::ObjectNode {
CONFIG_NODE(LauncherActionEntry, settings::ObjectNode)
public:
explicit LauncherActionEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("name")};
}
CONFIG_PROPERTY(QVariantList, command, {})
CONFIG_PROPERTY(bool, dangerous, false)
CONFIG_PROPERTY(QString, description, {})
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(QString, icon, {})
CONFIG_PROPERTY(QString, name, {})
};
CONFIG_LIST_TYPE(LauncherActionEntry, LauncherActionList)
class LauncherSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class LauncherSizes : public settings::ObjectNode {
CONFIG_NODE(LauncherSizes, settings::ObjectNode)
CFG_PROPERTY(int, itemHeight, 50)
CFG_PROPERTY(int, itemWidth, 600)
CFG_PROPERTY(int, wallpaperHeight, 200)
CFG_PROPERTY(int, wallpaperWidth, 280)
public:
explicit LauncherSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, itemHeight, 50)
CONFIG_PROPERTY(int, itemWidth, 600)
CONFIG_PROPERTY(int, wallpaperHeight, 200)
CONFIG_PROPERTY(int, wallpaperWidth, 280)
};
class UseFuzzy : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class UseFuzzy : public settings::ObjectNode {
CONFIG_NODE(UseFuzzy, settings::ObjectNode)
CFG_PROPERTY(bool, actions, true)
CFG_PROPERTY(bool, apps, true)
CFG_PROPERTY(bool, schemes, true)
CFG_PROPERTY(bool, variants, true)
CFG_PROPERTY(bool, wallpapers, true)
public:
explicit UseFuzzy(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, actions, true)
CONFIG_PROPERTY(bool, apps, true)
CONFIG_PROPERTY(bool, schemes, true)
CONFIG_PROPERTY(bool, variants, true)
CONFIG_PROPERTY(bool, wallpapers, true)
};
class Launcher : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Launcher : public settings::ObjectNode {
CONFIG_NODE(Launcher, settings::ObjectNode)
CFG_PROPERTY(QString, actionPrefix, QStringLiteral("<"))
CONFIG_PROPERTY(QString, actionPrefix, QStringLiteral("<"))
CONFIG_LIST(
LauncherActionList,
actions,
{
DEFAULT_ARG({
vmap(
{{"command", QVariantList{"autocomplete", "calc"}},
{"dangerous", false},
@@ -134,44 +119,18 @@ class Launcher : public ConfigObject {
{"enabled", true},
{"icon", "bedtime"},
{"name", "Sleep"}}),
})
CFG_PROPERTY(int, dragThreshold, 150)
CFG_PROPERTY(bool, enableDangerousActions, true)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(
QStringList,
favoriteApps,
{QStringLiteral("Balatro"),
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_PROPERTY(int, dragThreshold, 150)
CONFIG_PROPERTY(bool, enableDangerousActions, true)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(QStringList, favoriteApps, {})
CONFIG_PROPERTY(QStringList, hiddenApps, {})
CONFIG_PROPERTY(int, maxAppsShown, 18)
CONFIG_PROPERTY(int, maxWallpapers, 7)
CONFIG_SUBOBJECT(LauncherSizes, sizes)
CFG_PROPERTY(QString, specialPrefix, QStringLiteral("@"))
CONFIG_PROPERTY(QString, specialPrefix, QStringLiteral("@"))
CONFIG_SUBOBJECT(UseFuzzy, useFuzzy)
CFG_PROPERTY(bool, uwsm, true)
public:
explicit Launcher(QObject* parent = nullptr)
: ConfigObject(parent)
, m_sizes(new LauncherSizes(this))
, m_useFuzzy(new UseFuzzy(this)) {}
CONFIG_PROPERTY(bool, uwsm, true)
};
} // namespace ZShell::config
+14 -23
View File
@@ -1,36 +1,27 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class LockSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class LockSizes : public settings::ObjectNode {
CONFIG_NODE(LockSizes, settings::ObjectNode)
CFG_PROPERTY(int, centerWidth, 600)
CFG_PROPERTY(qreal, heightMult, 0.7)
CFG_PROPERTY(qreal, ratio, 1.78)
public:
explicit LockSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, centerWidth, 600)
CONFIG_PROPERTY(qreal, heightMult, 0.7)
CONFIG_PROPERTY(qreal, ratio, 1.78)
};
class Lock : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Lock : public settings::ObjectNode {
CONFIG_NODE(Lock, settings::ObjectNode)
CFG_PROPERTY(int, blurAmount, 10)
CFG_PROPERTY(bool, enableFprint, true)
CFG_PROPERTY(int, maxFprintTries, 3)
CFG_PROPERTY(bool, recolorLogo, false)
CFG_PROPERTY(bool, showNotifContent, true)
CFG_PROPERTY(bool, showNotifIcon, true)
CONFIG_PROPERTY(int, blurAmount, 10)
CONFIG_PROPERTY(bool, enableFprint, true)
CONFIG_PROPERTY(int, maxFprintTries, 3)
CONFIG_PROPERTY(bool, recolorLogo, false)
CONFIG_PROPERTY(bool, showNotifContent, true)
CONFIG_PROPERTY(bool, showNotifIcon, true)
CONFIG_SUBOBJECT(LockSizes, sizes)
public:
explicit Lock(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new LockSizes(this)) {}
};
} // namespace ZShell::config
+17 -26
View File
@@ -1,39 +1,30 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class NotifsSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class NotifsSizes : public settings::ObjectNode {
CONFIG_NODE(NotifsSizes, settings::ObjectNode)
CFG_PROPERTY(int, badge, 20)
CFG_PROPERTY(int, image, 41)
CFG_PROPERTY(int, width, 400)
public:
explicit NotifsSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, badge, 20)
CONFIG_PROPERTY(int, image, 41)
CONFIG_PROPERTY(int, width, 400)
};
class Notifs : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Notifs : public settings::ObjectNode {
CONFIG_NODE(Notifs, settings::ObjectNode)
CFG_PROPERTY(bool, actionOnClick, false)
CFG_PROPERTY(int, appNotifCooldown, 0)
CFG_PROPERTY(qreal, clearThreshold, 0.3)
CFG_PROPERTY(int, defaultExpireTimeout, 5000)
CFG_PROPERTY(int, expandThreshold, 20)
CFG_PROPERTY(bool, expire, true)
CFG_PROPERTY(int, groupPreviewNum, 5)
CFG_PROPERTY(bool, openExpanded, false)
CFG_PROPERTY(bool, showInFullscreen, false)
CONFIG_PROPERTY(bool, actionOnClick, false)
CONFIG_PROPERTY(int, appNotifCooldown, 0)
CONFIG_PROPERTY(qreal, clearThreshold, 0.3)
CONFIG_PROPERTY(int, defaultExpireTimeout, 5000)
CONFIG_PROPERTY(int, expandThreshold, 20)
CONFIG_PROPERTY(bool, expire, true)
CONFIG_PROPERTY(int, groupPreviewNum, 5)
CONFIG_PROPERTY(bool, openExpanded, false)
CONFIG_PROPERTY(bool, showInFullscreen, false)
CONFIG_SUBOBJECT(NotifsSizes, sizes)
public:
explicit Notifs(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new NotifsSizes(this)) {}
};
} // namespace ZShell::config
+12 -21
View File
@@ -1,34 +1,25 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class OsdSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class OsdSizes : public settings::ObjectNode {
CONFIG_NODE(OsdSizes, settings::ObjectNode)
CFG_PROPERTY(int, sliderHeight, 150)
CFG_PROPERTY(int, sliderWidth, 30)
public:
explicit OsdSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, sliderHeight, 150)
CONFIG_PROPERTY(int, sliderWidth, 30)
};
class Osd : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Osd : public settings::ObjectNode {
CONFIG_NODE(Osd, settings::ObjectNode)
CFG_PROPERTY(bool, allMonBrightness, true)
CFG_PROPERTY(bool, enableBrightness, true)
CFG_PROPERTY(bool, enableMicrophone, true)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, hideDelay, 3000)
CONFIG_PROPERTY(bool, allMonBrightness, true)
CONFIG_PROPERTY(bool, enableBrightness, true)
CONFIG_PROPERTY(bool, enableMicrophone, true)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, hideDelay, 3000)
CONFIG_SUBOBJECT(OsdSizes, sizes)
public:
explicit Osd(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new OsdSizes(this)) {}
};
} // 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
#include "configobject.hpp"
#include "common.hpp"
#include <QVariantList>
#include <qqmlintegration.h>
namespace ZShell::config {
class Screenshot : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Screenshot : public settings::ObjectNode {
CONFIG_NODE(Screenshot, settings::ObjectNode)
CFG_PROPERTY(bool, enable_pp, false)
CFG_PROPERTY(QString, mode, QStringLiteral("manual"))
CFG_PROPERTY(int, radius, 49)
CFG_PROPERTY(bool, rounding, false)
CFG_PROPERTY(bool, saveOnCopy, false)
CFG_PROPERTY(bool, shadow, true)
CFG_PROPERTY(int, shadow_blur, 0)
CFG_PROPERTY(QVariantList, shadow_color, (QVariantList{0, 0, 0, 160}))
CFG_PROPERTY(int, shadow_offset_x, 30)
CFG_PROPERTY(int, shadow_offset_y, 50)
public:
explicit Screenshot(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enable_pp, false)
CONFIG_PROPERTY(QString, mode, QStringLiteral("manual"))
CONFIG_PROPERTY(int, radius, 49)
CONFIG_PROPERTY(bool, rounding, false)
CONFIG_PROPERTY(bool, saveOnCopy, false)
CONFIG_PROPERTY(bool, shadow, true)
CONFIG_PROPERTY(int, shadow_blur, 0)
CONFIG_PROPERTY(QVariantList, shadow_color, (QVariantList{0, 0, 0, 160}))
CONFIG_PROPERTY(int, shadow_offset_x, 30)
CONFIG_PROPERTY(int, shadow_offset_y, 50)
};
} // namespace ZShell::config
+23 -37
View File
@@ -1,56 +1,42 @@
#pragma once
#include "configlist.hpp"
#include "configobject.hpp"
#include "common.hpp"
#include "enums.hpp"
namespace ZShell::config {
class PlayerAliasEntry : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class PlayerAliasEntry : public settings::ObjectNode {
CONFIG_NODE(PlayerAliasEntry, settings::ObjectNode)
CFG_PROPERTY(QString, from)
CFG_PROPERTY(QString, to)
public:
explicit PlayerAliasEntry(QObject* parent = nullptr)
: ConfigObject(parent) {}
[[nodiscard]] QStringList identityKeys() const override {
return {QStringLiteral("from")};
}
CONFIG_PROPERTY(QString, from, {})
CONFIG_PROPERTY(QString, to, {})
};
CONFIG_LIST_TYPE(PlayerAliasEntry, PlayerAliasList)
class Services : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Services : public settings::ObjectNode {
CONFIG_NODE(Services, settings::ObjectNode)
CFG_PROPERTY(qreal, audioIncrement, 0.1)
CFG_PROPERTY(qreal, brightnessIncrement, 0.1)
CFG_PROPERTY(bool, ddcutilService, true)
CFG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
CFG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA"))
CFG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto)
CFG_PROPERTY(qreal, maxVolume, 1)
CFG_PROPERTY(qreal, minBrightness, 0.01)
CONFIG_PROPERTY(qreal, audioIncrement, 0.1)
CONFIG_PROPERTY(qreal, brightnessIncrement, 0.1)
CONFIG_PROPERTY(bool, ddcutilService, true)
CONFIG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify"))
CONFIG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA"))
CONFIG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto)
CONFIG_PROPERTY(qreal, maxVolume, 1)
CONFIG_PROPERTY(qreal, minBrightness, 0.01)
CONFIG_LIST(
PlayerAliasList,
playerAliases,
{
vmap(
DEFAULT_ARG({
settings::vmap(
{{"from", QStringLiteral("com.github.th_ch.youtube_music")},
{"to", QStringLiteral("YT Music")}}),
})
CFG_PROPERTY(bool, updates, true)
CFG_PROPERTY(bool, useFahrenheit, false)
CFG_PROPERTY(bool, useTwelveHourClock, false)
CFG_PROPERTY(int, visualizerBars, 50)
CFG_PROPERTY(QString, weatherLocation, QString())
public:
explicit Services(QObject* parent = nullptr) : ConfigObject(parent) {}
}))
CONFIG_PROPERTY(bool, updates, true)
CONFIG_PROPERTY(bool, useFahrenheit, false)
CONFIG_PROPERTY(bool, useTwelveHourClock, false)
CONFIG_PROPERTY(int, visualizerBars, 50)
CONFIG_PROPERTY(QString, weatherLocation, QString())
};
} // namespace ZShell::config
+8 -17
View File
@@ -1,30 +1,21 @@
#pragma once
#include "configobject.hpp"
#include "common.hpp"
#include <qqmlintegration.h>
namespace ZShell::config {
class SidebarSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class SidebarSizes : public settings::ObjectNode {
CONFIG_NODE(SidebarSizes, settings::ObjectNode)
CFG_PROPERTY(int, width, 520)
public:
explicit SidebarSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, width, 520)
};
class Sidebar : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Sidebar : public settings::ObjectNode {
CONFIG_NODE(Sidebar, settings::ObjectNode)
CFG_PROPERTY(int, dragThreshold, 30)
CFG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, dragThreshold, 30)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_SUBOBJECT(SidebarSizes, sizes)
public:
explicit Sidebar(QObject* parent = nullptr)
: ConfigObject(parent), m_sizes(new SidebarSizes(this)) {}
};
} // namespace ZShell::config
+138 -182
View File
@@ -1,205 +1,161 @@
#pragma once
#include "configobject.hpp"
#include "anim.hpp"
#include "appearance.hpp"
#include "config.hpp"
#include "font.hpp"
#include <QVariantList>
#include <limits>
#include <qlist.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
#include <qqmlregistration.h>
#include <qtypes.h>
#include <limits>
#include "settings/objectnode.hpp"
#include "anim.hpp"
#include "common.hpp"
#include "font.hpp"
namespace ZShell::config {
class AnimCurves : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class AnimCurves : public settings::ObjectNode {
CONFIG_NODE(AnimCurves, settings::ObjectNode)
CFG_PROPERTY(QList<qreal>, emphasized)
CFG_PROPERTY(QList<qreal>, emphasizedAccel)
CFG_PROPERTY(QList<qreal>, emphasizedDecel)
CFG_PROPERTY(QList<qreal>, standard)
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,
2.0 / 15.0,
0.06,
1.0 / 6.0,
0.4,
5.0 / 24.0,
0.82,
0.25,
1,
1,
1})
, m_emphasizedAccel({0.3, 0, 0.8, 0.15, 1, 1})
, m_emphasizedDecel({0.05, 0.7, 0.1, 1, 1, 1})
, m_standard({0.2, 0, 0, 1, 1, 1})
, m_standardAccel({0.3, 0, 1, 1, 1, 1})
, m_standardDecel({0, 0, 0, 1, 1, 1})
, m_expressiveFastSpatial({0.42, 1.67, 0.21, 0.9, 1, 1})
, m_expressiveDefaultSpatial({0.38, 1.21, 0.22, 1, 1, 1})
, m_expressiveSlowSpatial({0.39, 1.29, 0.35, 0.98, 1, 1})
, m_expressiveFastEffects({0.31, 0.94, 0.34, 1, 1, 1})
, m_expressiveDefaultEffects({0.34, 0.8, 0.34, 1, 1, 1})
, m_expressiveSlowEffects({0.34, 0.88, 0.34, 1, 1, 1}) {}
CONFIG_GLOBAL_PROPERTY(
QList<qreal>,
emphasized,
DEFAULT_ARG(
{0.05,
0,
2.0 / 15.0,
0.06,
1.0 / 6.0,
0.4,
5.0 / 24.0,
0.82,
0.25,
1,
1,
1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, emphasizedAccel, DEFAULT_ARG({0.3, 0, 0.8, 0.15, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, emphasizedDecel, DEFAULT_ARG({0.05, 0.7, 0.1, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, standard, DEFAULT_ARG({0.2, 0, 0, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, standardAccel, DEFAULT_ARG({0.3, 0, 1, 1, 1, 1}))
CONFIG_GLOBAL_PROPERTY(
QList<qreal>, standardDecel, DEFAULT_ARG({0, 0, 0, 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 {
Q_OBJECT
QML_ANONYMOUS
class RoundingTokens : public settings::ObjectNode {
CONFIG_NODE(RoundingTokens, settings::ObjectNode)
Q_PROPERTY(ZShell::config::AnimDurations* durations READ durations CONSTANT)
Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve emphasizedAccel READ emphasizedAccel NOTIFY curvesChanged)
Q_PROPERTY(
QEasingCurve emphasizedDecel READ emphasizedDecel NOTIFY curvesChanged)
Q_PROPERTY(QEasingCurve standard READ standard NOTIFY curvesChanged)
Q_PROPERTY(
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;
CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, small, 8)
CONFIG_PROPERTY(int, medium, 12)
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)
CONFIG_PROPERTY(int, full, std::numeric_limits<int>::max())
};
class BarTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class SpacingTokens : public settings::ObjectNode {
CONFIG_NODE(SpacingTokens, settings::ObjectNode)
CFG_PROPERTY(int, innerSize, 28)
public:
explicit BarTokens(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, small, 8)
CONFIG_PROPERTY(int, medium, 12)
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 {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
class PaddingTokens : public settings::ObjectNode {
CONFIG_NODE(PaddingTokens, settings::ObjectNode)
Q_PROPERTY(
ZShell::config::AppearanceRounding* rounding READ rounding CONSTANT)
Q_PROPERTY(ZShell::config::AppearancePadding* padding READ padding CONSTANT)
Q_PROPERTY(ZShell::config::AppearanceSpacing* spacing READ spacing CONSTANT)
CONFIG_SUBOBJECT(FontTokens, font)
CONFIG_SUBOBJECT(AnimTokens, anim)
CONFIG_PROPERTY(int, extraSmall, 4)
CONFIG_PROPERTY(int, smaller, 5)
CONFIG_PROPERTY(int, small, 8)
CONFIG_PROPERTY(int, medium, 12)
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 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)
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
+36 -55
View File
@@ -1,7 +1,6 @@
#pragma once
#include "configobject.hpp"
#include "configlist.hpp"
#include "common.hpp"
#include <QVariantList>
#include <QStringList>
@@ -9,75 +8,57 @@
namespace ZShell::config {
class UtilitiesSizes : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class UtilitiesSizes : public settings::ObjectNode {
CONFIG_NODE(UtilitiesSizes, settings::ObjectNode)
CFG_PROPERTY(int, toastWidth, 430)
CFG_PROPERTY(int, width, 430)
public:
explicit UtilitiesSizes(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(int, toastWidth, 430)
CONFIG_PROPERTY(int, width, 430)
};
class Toasts : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Toasts : public settings::ObjectNode {
CONFIG_NODE(Toasts, settings::ObjectNode)
CFG_PROPERTY(bool, audioInputChanged, true)
CFG_PROPERTY(bool, audioOutputChanged, true)
CFG_PROPERTY(bool, capsLockChanged, true)
CFG_PROPERTY(bool, chargingChanged, true)
CFG_PROPERTY(bool, configLoaded, true)
CFG_PROPERTY(bool, dndChanged, true)
CFG_PROPERTY(bool, gameModeChanged, true)
CFG_PROPERTY(bool, kbLayoutChanged, true)
CFG_PROPERTY(bool, kbLimit, true)
CFG_PROPERTY(bool, nowPlaying, false)
CFG_PROPERTY(bool, numLockChanged, true)
CFG_PROPERTY(bool, vpnChanged, true)
public:
explicit Toasts(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, audioInputChanged, true)
CONFIG_PROPERTY(bool, audioOutputChanged, true)
CONFIG_PROPERTY(bool, capsLockChanged, true)
CONFIG_PROPERTY(bool, chargingChanged, true)
CONFIG_PROPERTY(bool, configLoaded, true)
CONFIG_PROPERTY(bool, dndChanged, true)
CONFIG_PROPERTY(bool, gameModeChanged, true)
CONFIG_PROPERTY(bool, kbLayoutChanged, true)
CONFIG_PROPERTY(bool, kbLimit, true)
CONFIG_PROPERTY(bool, nowPlaying, false)
CONFIG_PROPERTY(bool, numLockChanged, true)
CONFIG_PROPERTY(bool, vpnChanged, true)
};
class Vpn : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Vpn : public settings::ObjectNode {
CONFIG_NODE(Vpn, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, false)
CFG_PROPERTY(QStringList, provider, (QStringList{QStringLiteral("netbird")}))
public:
explicit Vpn(QObject* parent = nullptr) : ConfigObject(parent) {}
CONFIG_PROPERTY(bool, enabled, false)
CONFIG_PROPERTY(
QStringList, provider, (QStringList{QStringLiteral("netbird")}))
};
class Utilities : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
class Utilities : public settings::ObjectNode {
CONFIG_NODE(Utilities, settings::ObjectNode)
CFG_PROPERTY(bool, enabled, true)
CFG_PROPERTY(int, maxToasts, 4)
CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, maxToasts, 4)
CONFIG_SUBOBJECT(UtilitiesSizes, sizes)
CONFIG_SUBOBJECT(Toasts, toasts)
CONFIG_SUBOBJECT(Vpn, vpn)
CONFIG_LIST(
EntryList,
quickToggles,
{LIST_ENTRY(dnd, true),
LIST_ENTRY(wifi, true),
LIST_ENTRY(settings, true),
LIST_ENTRY(bluetooth, true),
LIST_ENTRY(mic, true),
LIST_ENTRY(audio, 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)) {}
DEFAULT_ARG(
{LIST_ENTRY(dnd, true),
LIST_ENTRY(wifi, true),
LIST_ENTRY(settings, true),
LIST_ENTRY(bluetooth, true),
LIST_ENTRY(mic, true),
LIST_ENTRY(audio, true),
LIST_ENTRY(gameMode, true)}))
};
} // namespace ZShell::config
+2 -1
View File
@@ -1,12 +1,12 @@
qml_module(ZShell-internal
URI ZShell.Internal
INCLUDE_PREFIX internal
SOURCES
hyprextras.hpp hyprextras.cpp
hyprdevices.hpp hyprdevices.cpp
cachingimagemanager.hpp cachingimagemanager.cpp
circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp
sparklineitem.hpp sparklineitem.cpp
arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
lidwatcher.hpp lidwatcher.cpp
@@ -14,6 +14,7 @@ qml_module(ZShell-internal
linearindicatormanager.hpp linearindicatormanager.cpp
strokecanvasitem.hpp strokecanvasitem.cpp
strokecanvasrenderer.hpp strokecanvasrenderer.cpp
cursor.hpp
LIBRARIES
Qt::Gui
Qt::Quick
@@ -19,6 +19,8 @@ void CircularBuffer::setCapacity(int capacity) {
m_capacity = capacity;
m_data.resize(capacity);
m_data.fill(0.0);
m_smoothed.resize(capacity);
m_smoothed.fill(0.0);
m_head = 0;
m_count = 0;
@@ -30,6 +32,8 @@ void CircularBuffer::setCapacity(int capacity) {
m_count++;
}
recomputeAllSmoothed();
emit capacityChanged();
emit countChanged();
emit valuesChanged();
@@ -56,6 +60,19 @@ qreal CircularBuffer::maximum() const {
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) {
if (m_capacity <= 0) return;
@@ -65,6 +82,9 @@ void CircularBuffer::push(qreal value) {
m_count++;
emit countChanged();
}
updateSmoothedTail();
emit valuesChanged();
}
@@ -85,4 +105,44 @@ qreal CircularBuffer::at(int index) const {
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
@@ -15,6 +15,9 @@ class CircularBuffer : public QObject {
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(QList<qreal> values READ values NOTIFY valuesChanged)
Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged)
Q_PROPERTY(
int smoothingRadius READ smoothingRadius WRITE setSmoothingRadius NOTIFY
smoothingRadiusChanged)
public:
explicit CircularBuffer(QObject* parent = nullptr);
@@ -26,20 +29,34 @@ class CircularBuffer : public QObject {
[[nodiscard]] QList<qreal> values() const;
[[nodiscard]] qreal maximum() const;
[[nodiscard]] int smoothingRadius() const;
void setSmoothingRadius(int radius);
Q_INVOKABLE void push(qreal value);
Q_INVOKABLE void clear();
Q_INVOKABLE [[nodiscard]] qreal at(int index) const;
Q_INVOKABLE [[nodiscard]] qreal smoothedAt(int index) const;
signals:
void capacityChanged();
void countChanged();
void valuesChanged();
void smoothingRadiusChanged();
private:
void writeSmoothedAt(int logicalIndex);
void updateSmoothedTail();
void recomputeAllSmoothed();
QVector<qreal> m_data;
QVector<qreal> m_smoothed;
int m_head = 0;
int m_count = 0;
int m_capacity = 0;
int m_smoothingRadius = 0;
};
} // 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
+2
View File
@@ -18,6 +18,7 @@ qml_module(ZShell-services
memory.hpp memory.cpp
diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp
processes.hpp processes.cpp
lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp
LIBRARIES
@@ -28,6 +29,7 @@ qml_module(ZShell-services
PkgConfig::Cava
Sensors::Sensors
ZShell-config
ZShell-settings
DEPENDENCIES
ZShell.Config
)
+5 -5
View File
@@ -7,7 +7,7 @@
#include <qsavefile.h>
#include <qurlquery.h>
#include "config/config.hpp"
#include "config/rootnodes.hpp"
#include "config/general.hpp"
#include "config/services.hpp"
@@ -75,7 +75,7 @@ Lyrics::Lyrics(QObject* parent)
m_loadDebounce->setInterval(k_loadDebounceMs);
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* genCfg = cfg->general();
@@ -114,7 +114,7 @@ void Lyrics::setPreferredBackend(LyricsBackend value) {
m_preferredBackend = value;
emit preferredBackendChanged();
config::Config::instance()->services()->set_lyricsBackend(value);
config::ConfigSingleton::instance()->services()->set_lyricsBackend(value);
scheduleLoad();
}
@@ -870,7 +870,7 @@ QNetworkReply* Lyrics::getJson(
void Lyrics::onPreferredBackendConfigChanged() {
const LyricsBackend desired =
config::Config::instance()->services()->lyricsBackend();
config::ConfigSingleton::instance()->services()->lyricsBackend();
if (desired == m_preferredBackend) {
return;
}
@@ -942,7 +942,7 @@ void Lyrics::persistTrackPrefs() {
}
QString Lyrics::lyricsDir() {
QString dir = config::Config::instance()->general()->lyricsDir();
QString dir = config::ConfigSingleton::instance()->general()->lyricsDir();
if (dir.isEmpty()) {
return {};
}
+53 -1
View File
@@ -3,6 +3,8 @@
#include <qfile.h>
#include <qregularexpression.h>
#include <unistd.h>
namespace ZShell::services {
Memory::Memory(QObject* parent) : TickingService(parent) {}
@@ -15,6 +17,18 @@ qreal Memory::total() const {
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 {
return m_total > 0.0 ? m_used / m_total : 0.0;
}
@@ -31,6 +45,8 @@ void Memory::tick() {
QStringLiteral("MemTotal: *(\\d+)"));
static const QRegularExpression reAvail(
QStringLiteral("MemAvailable: *(\\d+)"));
static const QRegularExpression reCached(QStringLiteral(
"Cached: *(\\d+)"));
const QString text = QString::fromLatin1(data);
const auto totalMatch = reTotal.match(text);
@@ -45,14 +61,50 @@ void Memory::tick() {
return;
}
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;
}
m_lastTotal = totalKib;
m_lastUsed = usedKib;
m_lastCached = cachedKib;
m_lastReadKib = readKib;
m_lastWriteKib = writeKib;
m_total = static_cast<qreal>(totalKib);
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();
}
+12
View File
@@ -14,14 +14,20 @@ class Memory : public TickingService {
Q_PROPERTY(qreal used READ used 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 readTotal READ readTotal NOTIFY changed)
Q_PROPERTY(qreal writeTotal READ writeTotal NOTIFY changed)
public:
explicit Memory(QObject* parent = nullptr);
[[nodiscard]] qreal used() const;
[[nodiscard]] qreal total() const;
[[nodiscard]] qreal cached() const;
[[nodiscard]] qreal percentage() const;
[[nodiscard]] qreal readTotal() const;
[[nodiscard]] qreal writeTotal() const;
signals:
void changed();
@@ -32,8 +38,14 @@ class Memory : public TickingService {
private:
qreal m_used = 0.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_lastTotal = 0;
quint64 m_lastCached = 0;
quint64 m_lastReadKib = 0;
quint64 m_lastWriteKib = 0;
};
} // 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)
: Service(parent), m_timer(new QTimer(this)) {
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 =
QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
@@ -18,6 +18,7 @@ class TickingService : public Service {
signals:
void updateIntervalChanged();
void updateTriggered();
protected:
void start() final;
+18
View File
@@ -14,6 +14,24 @@ bool finitePositive(qreal v) {
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 {
if (!finitePositive(kib) || !finitePositive(total)) {
return {0.0, 0.0, "KiB"};
+2
View File
@@ -1,5 +1,6 @@
#pragma once
#include <qaccessible_base.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlintegration.h>
@@ -27,6 +28,7 @@ class UsageFmt : public QObject {
QML_SINGLETON
public:
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib) const;
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(
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
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qjsonvalue.h>
#include <qobject.h>
#include <qtimer.h>
namespace ZShell::settings {
class SettingsFile : public QObject {
Q_OBJECT
public:
enum class LoadResult : quint8 {
Unchanged,
Changed,
ParseError,
Error
};
explicit SettingsFile(QString path, QObject* parent = nullptr);
[[nodiscard]] std::optional<QJsonValue> read() const;
void write(const QJsonValue& json);
LoadResult load(bool reportErrors = true);
signals:
void changed(); // Data changed, not file watcher event
void readFailed(const QString& error);
void writeFailed(const QString& error);
private:
QString m_path;
QFileSystemWatcher* m_watcher;
QTimer* m_saveDebounce;
QTimer* m_loadDebounce;
int m_loadRetries;
std::optional<QJsonValue> m_lastData;
std::optional<QJsonValue> m_pendingWrite;
void onFileChanged();
void onDirChanged();
void initWatcher();
void scheduleLoad();
void onLoadDebounced();
void save();
};
} // namespace ZShell::settings
+5
View File
@@ -102,6 +102,11 @@ void Toast::unlock(QObject* sender) {
Toaster::Toaster(QObject* parent) : QObject(parent) {}
Toaster* Toaster::instance() {
static Toaster toaster;
return &toaster;
}
QQmlListProperty<Toast> Toaster::toasts() {
return QQmlListProperty<Toast>(this, &m_toasts);
}
+1
View File
@@ -67,6 +67,7 @@ class Toaster : public QObject {
public:
explicit Toaster(QObject* parent = nullptr);
static Toaster* instance();
[[nodiscard]] QQmlListProperty<Toast> toasts();