Feat: Dashboard tabs and repositioning. New processes view. Settings backend changes #158
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+25
-22
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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}, \
|
||||
})
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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"};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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__)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ class Toaster : public QObject {
|
||||
|
||||
public:
|
||||
explicit Toaster(QObject* parent = nullptr);
|
||||
static Toaster* instance();
|
||||
|
||||
[[nodiscard]] QQmlListProperty<Toast> toasts();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user