Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 3s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 27s
Rust / clippy (pull_request) Successful in 46s
C++ / fmt (pull_request) Failing after 3s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 27s
Rust / clippy (pull_request) Successful in 46s
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
qml_module(ZShell
|
||||
URI ZShell
|
||||
INCLUDE_PREFIX core
|
||||
SOURCES
|
||||
writefile.hpp writefile.cpp
|
||||
appdb.hpp appdb.cpp
|
||||
@@ -23,6 +24,17 @@ target_compile_definitions(ZShell PRIVATE
|
||||
GIT_REVISION="${GIT_REVISION}"
|
||||
)
|
||||
|
||||
# Node macros expand to Q_OBJECT, automoc can't see it otherwise
|
||||
list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
|
||||
CONFIG_NODE
|
||||
CONFIG_NODE_NO_CTOR
|
||||
CONFIG_LIST_TYPE
|
||||
SETTINGS_NODE
|
||||
SETTINGS_NODE_NO_CTOR
|
||||
SETTINGS_LIST_TYPE
|
||||
)
|
||||
|
||||
add_subdirectory(Settings)
|
||||
add_subdirectory(Models)
|
||||
add_subdirectory(Internal)
|
||||
add_subdirectory(Services)
|
||||
@@ -30,3 +42,11 @@ add_subdirectory(Components)
|
||||
add_subdirectory(Blobs)
|
||||
add_subdirectory(Config)
|
||||
add_subdirectory(Llm)
|
||||
|
||||
pkg_check_modules(HYPRLAND_PROBE hyprland)
|
||||
if(HYPRLAND_PROBE_FOUND)
|
||||
add_subdirectory(HyprPlugins)
|
||||
else()
|
||||
message(STATUS
|
||||
"hyprland development headers not found; skipping zshell-nightlight plugin")
|
||||
endif()
|
||||
|
||||
@@ -7,7 +7,11 @@ qml_module(ZShell-components
|
||||
buttonrow.hpp buttonrow.cpp
|
||||
carouselview.hpp carouselview.cpp
|
||||
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
|
||||
+47
-21
@@ -3,20 +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)
|
||||
@@ -40,17 +45,22 @@ class SparklineItem : public QQuickPaintedItem {
|
||||
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 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);
|
||||
@@ -76,6 +86,15 @@ class SparklineItem : public QQuickPaintedItem {
|
||||
[[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();
|
||||
@@ -87,17 +106,21 @@ class SparklineItem : public QQuickPaintedItem {
|
||||
void slideProgressChanged();
|
||||
void historyLengthChanged();
|
||||
void lineWidthChanged();
|
||||
void slidingChanged();
|
||||
void smoothingChanged();
|
||||
void curveSmoothingChanged();
|
||||
|
||||
protected:
|
||||
void geometryChange(
|
||||
const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
||||
|
||||
private:
|
||||
void drawLine(
|
||||
QPainter* painter,
|
||||
CircularBuffer* buffer,
|
||||
const QColor& color,
|
||||
qreal fillAlpha);
|
||||
void connectBuffer(CircularBuffer* buffer);
|
||||
void updateContentOffset();
|
||||
void applySmoothingRadius(internal::CircularBuffer* buffer) const;
|
||||
|
||||
CircularBuffer* m_line1 = nullptr;
|
||||
CircularBuffer* m_line2 = nullptr;
|
||||
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;
|
||||
@@ -106,6 +129,9 @@ class SparklineItem : public QQuickPaintedItem {
|
||||
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;
|
||||
};
|
||||
|
||||
} // 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,19 +1,27 @@
|
||||
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
|
||||
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
|
||||
colors.hpp
|
||||
dashboard.hpp
|
||||
display.hpp
|
||||
dock.hpp
|
||||
enums.hpp
|
||||
general.hpp
|
||||
launcher.hpp
|
||||
llm.hpp
|
||||
@@ -25,3 +33,10 @@ qml_module(ZShell-config
|
||||
sidebar.hpp
|
||||
utilities.hpp
|
||||
)
|
||||
|
||||
target_link_libraries(ZShell-config PUBLIC
|
||||
Qt::Gui
|
||||
zshell-util
|
||||
ZShell
|
||||
ZShell-settings
|
||||
)
|
||||
|
||||
@@ -1,32 +1,82 @@
|
||||
#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);
|
||||
|
||||
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 +91,24 @@ 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;
|
||||
|
||||
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")
|
||||
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,219 @@
|
||||
#include "appearance.hpp"
|
||||
|
||||
#include "tokens.hpp"
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
namespace {
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,83 +1,347 @@
|
||||
#pragma once
|
||||
|
||||
#include "configobject.hpp"
|
||||
#include <qqmlregistration.h>
|
||||
#include <qfont.h>
|
||||
#include <qstring.h>
|
||||
#include <qvariantmap.h>
|
||||
|
||||
#include "settings/objectnode.hpp"
|
||||
#include "common.hpp"
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
class Anim : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
using Qt::StringLiterals::operator""_s;
|
||||
|
||||
CFG_PROPERTY(int, mediaGifSpeedAdjustment, 300)
|
||||
CFG_PROPERTY(qreal, sessionGifSpeed, 0.7)
|
||||
class RoundingTokens;
|
||||
class SpacingTokens;
|
||||
class PaddingTokens;
|
||||
class AnimDurationTokens;
|
||||
|
||||
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 extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged)
|
||||
Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged)
|
||||
Q_PROPERTY(int full READ full NOTIFY valuesChanged)
|
||||
|
||||
public:
|
||||
explicit Anim(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||
void bindTokens(RoundingTokens* 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;
|
||||
[[nodiscard]] int full() const;
|
||||
|
||||
signals:
|
||||
void valuesChanged();
|
||||
|
||||
private:
|
||||
RoundingTokens* m_tokens = nullptr;
|
||||
};
|
||||
|
||||
class Deform : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class AppearanceSpacing : public settings::ObjectNode {
|
||||
CONFIG_NODE(AppearanceSpacing, settings::ObjectNode)
|
||||
|
||||
CFG_PROPERTY(qreal, scale, 1)
|
||||
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 Deform(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||
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 FontFamily : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class AppearancePadding : public settings::ObjectNode {
|
||||
CONFIG_NODE(AppearancePadding, settings::ObjectNode)
|
||||
|
||||
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"))
|
||||
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 FontFamily(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||
void bindTokens(PaddingTokens* tokens);
|
||||
|
||||
[[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;
|
||||
|
||||
signals:
|
||||
void valuesChanged();
|
||||
|
||||
private:
|
||||
PaddingTokens* m_tokens = nullptr;
|
||||
};
|
||||
|
||||
class Font : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
namespace detail {
|
||||
|
||||
struct FontConfig {
|
||||
QString family = {};
|
||||
int size;
|
||||
int weight = QFont::Normal;
|
||||
bool italic = false;
|
||||
QVariantMap vaxes = {{u"ROND"_s, 25}};
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class FontSizeNode : public settings::ObjectNode {
|
||||
CONFIG_NODE(FontSizeNode, settings::ObjectNode)
|
||||
};
|
||||
|
||||
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;
|
||||
virtual FontSizeNode* extraLargeNode() const { return nullptr; }
|
||||
};
|
||||
|
||||
#define ARG(...) __VA_ARGS__
|
||||
#define FONT(...) ZShell::config::detail::FontConfig __VA_ARGS__
|
||||
#define FONT_CONFIG(Style, Size, props) \
|
||||
class FontConfig##Style##Size : public FontSizeNode { \
|
||||
CONFIG_NODE(FontConfig##Style##Size, FontSizeNode) \
|
||||
\
|
||||
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_)) \
|
||||
FONT_CONFIG(Style, Medium, ARG(medium_)) \
|
||||
FONT_CONFIG(Style, Small, ARG(small_)) \
|
||||
\
|
||||
class FontStyle##Style : public FontStyleNode { \
|
||||
CONFIG_NODE(FontStyle##Style, FontStyleNode) \
|
||||
\
|
||||
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, u"GoogleSansFlex"_s,
|
||||
FONT({ .size = 32, .weight = QFont::Medium }),
|
||||
FONT({ .size = 28, .weight = QFont::Medium }),
|
||||
FONT({ .size = 24, .weight = QFont::Medium })
|
||||
)
|
||||
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, u"GoogleSansFlex"_s,
|
||||
FONT({ .size = 16 }),
|
||||
FONT({ .size = 14 }),
|
||||
FONT({ .size = 12 })
|
||||
)
|
||||
FONT_STYLE(Label, u"GoogleSansFlex"_s,
|
||||
FONT({ .size = 14, .weight = QFont::Medium }),
|
||||
FONT({ .size = 12, .weight = QFont::Medium }),
|
||||
FONT({ .size = 11 })
|
||||
)
|
||||
FONT_STYLE(Mono, u"CaskaydiaCove NF"_s,
|
||||
FONT({ .size = 16, .vaxes = {} }),
|
||||
FONT({ .size = 14, .vaxes = {} }),
|
||||
FONT({ .size = 12, .vaxes = {} })
|
||||
)
|
||||
// clang-format on
|
||||
|
||||
FONT_CONFIG(Icon, ExtraLarge, FONT({.size = static_cast<int>(48 / 1.33)}))
|
||||
FONT_CONFIG(Icon, Large, FONT({.size = static_cast<int>(32 / 1.33)}))
|
||||
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 {
|
||||
CONFIG_NODE(FontStyleIcon, FontStyleNode)
|
||||
|
||||
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:
|
||||
FontSizeNode* extraLargeNode() const override { return m_extraLarge; }
|
||||
FontSizeNode* largeNode() const override { return m_large; }
|
||||
FontSizeNode* mediumNode() const override { return m_medium; }
|
||||
FontSizeNode* smallNode() const override { return m_small; }
|
||||
};
|
||||
|
||||
#undef ARG
|
||||
#undef FONT
|
||||
#undef FONT_CONFIG
|
||||
#undef FONT_STYLE
|
||||
|
||||
class FontFamily : public settings::ObjectNode {
|
||||
CONFIG_NODE(FontFamily, settings::ObjectNode)
|
||||
|
||||
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)
|
||||
|
||||
public:
|
||||
explicit Font(QObject* parent = nullptr)
|
||||
: ConfigObject(parent), m_family(new FontFamily(this)) {}
|
||||
CONFIG_SUBOBJECT(FontStyleHeadline, headline)
|
||||
CONFIG_SUBOBJECT(FontStyleTitle, title)
|
||||
CONFIG_SUBOBJECT(FontStyleBody, body)
|
||||
CONFIG_SUBOBJECT(FontStyleLabel, label)
|
||||
CONFIG_SUBOBJECT(FontStyleMono, mono)
|
||||
CONFIG_SUBOBJECT(FontStyleIcon, icon)
|
||||
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)
|
||||
|
||||
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 expressiveFastSpatial READ expressiveFastSpatial NOTIFY
|
||||
valuesChanged)
|
||||
Q_PROPERTY(
|
||||
int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY
|
||||
valuesChanged)
|
||||
Q_PROPERTY(
|
||||
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 Transparency(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||
void bindTokens(AnimDurationTokens* tokens);
|
||||
|
||||
[[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)
|
||||
CONFIG_SUBOBJECT(Font, font)
|
||||
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 Font(this))
|
||||
, m_transparency(new Transparency(this)) {}
|
||||
CONFIG_SUBOBJECT(AppearanceFont, font)
|
||||
CONFIG_SUBOBJECT(AppearanceRounding, rounding)
|
||||
CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
|
||||
CONFIG_SUBOBJECT(AppearancePadding, padding)
|
||||
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),
|
||||
@@ -78,20 +64,15 @@ class Bar : public ConfigObject {
|
||||
LIST_ENTRY(spacer, true),
|
||||
LIST_ENTRY(activeWindow, true),
|
||||
LIST_ENTRY(spacer, true),
|
||||
LIST_ENTRY(hyprsunset, true),
|
||||
LIST_ENTRY(nightlight, true),
|
||||
LIST_ENTRY(tray, true),
|
||||
LIST_ENTRY(statusIcons, true),
|
||||
LIST_ENTRY(network, false),
|
||||
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)) {}
|
||||
}),
|
||||
.identityKey = QStringLiteral("id"))
|
||||
};
|
||||
|
||||
} // 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,288 +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 "llm.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::s_instance = nullptr;
|
||||
|
||||
Config::Config(QObject* parent)
|
||||
: ConfigObject(parent)
|
||||
, m_appearance(new Appearance(this))
|
||||
, m_background(new Background(this))
|
||||
, m_bar(new Bar(this))
|
||||
, m_clipboard(new Clipboard(this))
|
||||
, m_colors(new Colors(this))
|
||||
, m_dashboard(new Dashboard(this))
|
||||
, m_dock(new Dock(this))
|
||||
, m_general(new General(this))
|
||||
, m_launcher(new Launcher(this))
|
||||
, m_llm(new Llm(this))
|
||||
, m_lock(new Lock(this))
|
||||
, m_notifs(new Notifs(this))
|
||||
, m_osd(new Osd(this))
|
||||
, m_screenshot(new Screenshot(this))
|
||||
, m_services(new Services(this))
|
||||
, m_sidebar(new Sidebar(this))
|
||||
, m_utilities(new Utilities(this)) {
|
||||
s_instance = this;
|
||||
connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave);
|
||||
|
||||
m_saveTimer.setSingleShot(true);
|
||||
m_saveTimer.setInterval(300);
|
||||
connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync);
|
||||
|
||||
m_reloadTimer.setSingleShot(true);
|
||||
m_reloadTimer.setInterval(50);
|
||||
connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync);
|
||||
|
||||
connectAutoSave(this);
|
||||
|
||||
connect(
|
||||
&m_watcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&Config::onWatcherEvent);
|
||||
|
||||
connect(
|
||||
&m_watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
&Config::onWatcherEvent);
|
||||
|
||||
loadSync();
|
||||
updateWatch();
|
||||
|
||||
m_firstLoadDone = true;
|
||||
}
|
||||
|
||||
Config* Config::instance() {
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
Config* Config::create(QQmlEngine*, QJSEngine*) {
|
||||
if (!s_instance) s_instance = new Config();
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
QString Config::filePath() const {
|
||||
const QString dir =
|
||||
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) +
|
||||
QStringLiteral("/zshell");
|
||||
QDir().mkpath(dir);
|
||||
return dir + QStringLiteral("/config.json");
|
||||
}
|
||||
|
||||
void Config::loadSync() {
|
||||
m_loading = true;
|
||||
|
||||
QFile f(filePath());
|
||||
QJsonObject before;
|
||||
|
||||
bool existed = false;
|
||||
|
||||
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,114 +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 Llm;
|
||||
class Lock;
|
||||
class Notifs;
|
||||
class Osd;
|
||||
class Screenshot;
|
||||
class Services;
|
||||
class Sidebar;
|
||||
class Utilities;
|
||||
|
||||
class Config : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_MOC_INCLUDE("appearance.hpp")
|
||||
Q_MOC_INCLUDE("background.hpp")
|
||||
Q_MOC_INCLUDE("bar.hpp")
|
||||
Q_MOC_INCLUDE("clipboard.hpp")
|
||||
Q_MOC_INCLUDE("colors.hpp")
|
||||
Q_MOC_INCLUDE("dashboard.hpp")
|
||||
Q_MOC_INCLUDE("dock.hpp")
|
||||
Q_MOC_INCLUDE("general.hpp")
|
||||
Q_MOC_INCLUDE("launcher.hpp")
|
||||
Q_MOC_INCLUDE("llm.hpp")
|
||||
Q_MOC_INCLUDE("lock.hpp")
|
||||
Q_MOC_INCLUDE("notifs.hpp")
|
||||
Q_MOC_INCLUDE("osd.hpp")
|
||||
Q_MOC_INCLUDE("screenshot.hpp")
|
||||
Q_MOC_INCLUDE("services.hpp")
|
||||
Q_MOC_INCLUDE("sidebar.hpp")
|
||||
Q_MOC_INCLUDE("utilities.hpp")
|
||||
|
||||
CONFIG_SUBOBJECT(Appearance, appearance)
|
||||
CONFIG_SUBOBJECT(Background, background)
|
||||
CONFIG_SUBOBJECT(Bar, bar)
|
||||
CONFIG_SUBOBJECT(Clipboard, clipboard)
|
||||
CONFIG_SUBOBJECT(Colors, colors)
|
||||
CONFIG_SUBOBJECT(Dashboard, dashboard)
|
||||
CONFIG_SUBOBJECT(Dock, dock)
|
||||
CONFIG_SUBOBJECT(General, general)
|
||||
CONFIG_SUBOBJECT(Launcher, launcher)
|
||||
CONFIG_SUBOBJECT(Llm, llm)
|
||||
CONFIG_SUBOBJECT(Lock, lock)
|
||||
CONFIG_SUBOBJECT(Notifs, notifs)
|
||||
CONFIG_SUBOBJECT(Osd, osd)
|
||||
CONFIG_SUBOBJECT(Screenshot, screenshot)
|
||||
CONFIG_SUBOBJECT(Services, services)
|
||||
CONFIG_SUBOBJECT(Sidebar, sidebar)
|
||||
CONFIG_SUBOBJECT(Utilities, utilities)
|
||||
|
||||
public:
|
||||
explicit Config(QObject* parent = nullptr);
|
||||
static Config* create(QQmlEngine*, QJSEngine*);
|
||||
[[nodiscard]] static Config* instance();
|
||||
|
||||
Q_INVOKABLE void load();
|
||||
Q_INVOKABLE void saveNow();
|
||||
Q_INVOKABLE void reloadAsync();
|
||||
|
||||
private Q_SLOTS:
|
||||
void scheduleSave();
|
||||
void flushAsync();
|
||||
void onWatcherEvent(const QString& path);
|
||||
|
||||
private:
|
||||
QString filePath() const;
|
||||
bool writeAtomically(const QByteArray& data);
|
||||
void updateWatch();
|
||||
void loadSync();
|
||||
void loadAsync();
|
||||
void connectAutoSave(ConfigNode* node);
|
||||
|
||||
QTimer m_saveTimer;
|
||||
QTimer m_reloadTimer;
|
||||
QFileSystemWatcher m_watcher;
|
||||
|
||||
QMutex m_writeMutex;
|
||||
QByteArray m_lastWriteHash;
|
||||
|
||||
bool m_reloadPending = false;
|
||||
bool m_loading = false;
|
||||
bool m_firstLoadDone = false;
|
||||
QFuture<void> m_loadFuture;
|
||||
|
||||
static Config* s_instance;
|
||||
};
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -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,344 +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());
|
||||
|
||||
known.insert(key);
|
||||
|
||||
if (!obj.contains(key)) continue;
|
||||
|
||||
const auto jsonVal = obj.value(key);
|
||||
|
||||
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
|
||||
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
||||
node->loadFromJson(jsonVal);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!prop.isWritable()) continue;
|
||||
|
||||
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);
|
||||
const auto key = QString::fromUtf8(prop.name());
|
||||
|
||||
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
|
||||
node->materializeDefaults();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!prop.isWritable()) continue;
|
||||
|
||||
if (m_global) continue;
|
||||
|
||||
m_loadedKeys.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
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,63 +1,53 @@
|
||||
#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 ProcessColumnEntry : public settings::ObjectNode {
|
||||
CONFIG_NODE(ProcessColumnEntry, 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(QString, id, {})
|
||||
CONFIG_PROPERTY(double, width, 0)
|
||||
};
|
||||
|
||||
class Dashboard : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
CONFIG_LIST_TYPE(ProcessColumnEntry, ProcessColumnList)
|
||||
|
||||
CFG_PROPERTY(bool, enabled, true)
|
||||
CFG_PROPERTY(int, mediaUpdateInterval, 2000)
|
||||
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_PROPERTY(bool, showWeather, true)
|
||||
CONFIG_PROPERTY(bool, showDashboard, true)
|
||||
CONFIG_PROPERTY(bool, showMedia, true)
|
||||
CONFIG_PROPERTY(bool, showProcesses, false)
|
||||
CONFIG_SUBOBJECT(Performance, performance)
|
||||
CFG_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_PROPERTY(int, resourceUpdateInterval, 1000)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
#define ENUM(Name, ...) \
|
||||
namespace Name { \
|
||||
\
|
||||
Q_NAMESPACE \
|
||||
QML_ELEMENT \
|
||||
\
|
||||
enum Enum : quint8 { __VA_ARGS__ }; \
|
||||
Q_ENUM_NS(Enum) \
|
||||
};
|
||||
|
||||
ENUM(BarWorkspaceDisplay, Shapes, Text, Icons)
|
||||
ENUM(BarWorkspaceCapitalisation, Preserve, Upper, Lower)
|
||||
ENUM(LyricsBackend, Auto, Local, LRCLIB, NetEase)
|
||||
ENUM(GpuType, Auto, Nvidia, Generic, None)
|
||||
ENUM(NotifsFullscreen, On, Off)
|
||||
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
|
||||
ENUM(DataUnit, Binary, Decimal)
|
||||
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
|
||||
|
||||
#undef ENUM
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -0,0 +1,288 @@
|
||||
#include "font.hpp"
|
||||
|
||||
#include "appearance.hpp"
|
||||
#include "rootnodes.hpp"
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
FontStyleBase::FontStyleBase(QObject* parent) : QObject(parent) {}
|
||||
|
||||
QFont FontStyleBase::large() const {
|
||||
return m_large;
|
||||
}
|
||||
|
||||
QFont FontStyleBase::medium() const {
|
||||
return m_medium;
|
||||
}
|
||||
|
||||
QFont FontStyleBase::small() const {
|
||||
return m_small;
|
||||
}
|
||||
|
||||
QFont FontStyleBase::buildFont(
|
||||
const FontSizeNode* cfg, const QString& fallbackFamily, qreal scale) {
|
||||
const auto family = cfg->property("family").toString();
|
||||
const auto size = cfg->property("size").toInt();
|
||||
const auto weight = cfg->property("weight").value<QFont::Weight>();
|
||||
const auto italic = cfg->property("italic").toBool();
|
||||
const auto vaxes = cfg->property("vaxes").toMap();
|
||||
|
||||
QFont font;
|
||||
font.setFamily(family.isEmpty() ? fallbackFamily : family);
|
||||
const int scaledSize = static_cast<int>(size * scale);
|
||||
const int cappedSize = scaledSize > 0 ? scaledSize : 1;
|
||||
font.setPointSize(cappedSize);
|
||||
font.setVariableAxis("opsz", static_cast<float>(cappedSize));
|
||||
font.setWeight(weight);
|
||||
font.setVariableAxis("wght", static_cast<float>(weight));
|
||||
font.setItalic(italic);
|
||||
|
||||
for (auto it = vaxes.constBegin(); it != vaxes.constEnd(); ++it) {
|
||||
if (auto tag = QFont::Tag::fromString(it.key()))
|
||||
font.setVariableAxis(*tag, it.value().toFloat());
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
void FontStyleBase::bind(FontStyleNode* cfg) {
|
||||
if (m_cfg == cfg) return;
|
||||
|
||||
if (m_cfg) {
|
||||
disconnect(m_cfg, nullptr, this, nullptr);
|
||||
disconnect(m_cfg->largeNode(), nullptr, this, nullptr);
|
||||
disconnect(m_cfg->mediumNode(), nullptr, this, nullptr);
|
||||
disconnect(m_cfg->smallNode(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
m_cfg = cfg;
|
||||
|
||||
if (cfg) {
|
||||
connect(
|
||||
cfg, &settings::Node::optionChanged, this, &FontStyleBase::rebuild);
|
||||
connect(
|
||||
cfg->largeNode(),
|
||||
&settings::Node::optionChanged,
|
||||
this,
|
||||
&FontStyleBase::rebuild);
|
||||
connect(
|
||||
cfg->mediumNode(),
|
||||
&settings::Node::optionChanged,
|
||||
this,
|
||||
&FontStyleBase::rebuild);
|
||||
connect(
|
||||
cfg->smallNode(),
|
||||
&settings::Node::optionChanged,
|
||||
this,
|
||||
&FontStyleBase::rebuild);
|
||||
}
|
||||
|
||||
rebuild();
|
||||
}
|
||||
|
||||
void FontStyleBase::rebuild() {
|
||||
if (m_cfg) {
|
||||
const auto family = m_cfg->property("family").toString();
|
||||
m_large = buildFont(m_cfg->largeNode(), family, m_scale);
|
||||
m_medium = buildFont(m_cfg->mediumNode(), family, m_scale);
|
||||
m_small = buildFont(m_cfg->smallNode(), family, m_scale);
|
||||
} else {
|
||||
m_large = QFont();
|
||||
m_medium = QFont();
|
||||
m_small = QFont();
|
||||
}
|
||||
emit fontsChanged();
|
||||
}
|
||||
|
||||
void FontStyleBase::setScale(qreal scale) {
|
||||
if (qFuzzyCompare(m_scale + 1.0, scale + 1.0)) return;
|
||||
m_scale = scale;
|
||||
rebuild();
|
||||
}
|
||||
|
||||
FontStyle::FontStyle(QObject* parent)
|
||||
: FontStyleBase(parent), m_builders(new FontBuilders(this, this)) {}
|
||||
|
||||
FontBuilders* FontStyle::builders() const {
|
||||
return m_builders;
|
||||
}
|
||||
|
||||
IconFontStyle::IconFontStyle(QObject* parent)
|
||||
: FontStyleBase(parent), m_builders(new IconFontBuilders(this, this)) {}
|
||||
|
||||
FontBuilder IconFontStyle::size(int pointSize) {
|
||||
return FontBuilder(m_small).size(pointSize);
|
||||
}
|
||||
|
||||
FontBuilder IconFontStyle::pixelSize(int pixelSize) {
|
||||
return FontBuilder(m_small).pixelSize(pixelSize);
|
||||
}
|
||||
|
||||
void IconFontStyle::bind(FontStyleNode* cfg) {
|
||||
if (m_cfg == cfg) return;
|
||||
|
||||
if (m_cfg) disconnect(m_cfg->extraLargeNode(), nullptr, this, nullptr);
|
||||
|
||||
FontStyleBase::bind(cfg);
|
||||
|
||||
if (cfg)
|
||||
connect(
|
||||
cfg->extraLargeNode(),
|
||||
&settings::Node::optionChanged,
|
||||
this,
|
||||
&IconFontStyle::rebuild);
|
||||
}
|
||||
|
||||
QFont IconFontStyle::extraLarge() const {
|
||||
return m_extraLarge;
|
||||
}
|
||||
|
||||
IconFontBuilders* IconFontStyle::builders() const {
|
||||
return m_builders;
|
||||
}
|
||||
|
||||
void IconFontStyle::rebuild() {
|
||||
if (m_cfg) {
|
||||
const auto family = m_cfg->property("family").toString();
|
||||
m_extraLarge = buildFont(m_cfg->extraLargeNode(), family, m_scale);
|
||||
} else {
|
||||
m_extraLarge = QFont();
|
||||
}
|
||||
FontStyleBase::rebuild();
|
||||
}
|
||||
|
||||
FontBuilders::FontBuilders(const FontStyleBase* style, QObject* parent)
|
||||
: QObject(parent), m_style(style) {
|
||||
connect(
|
||||
style,
|
||||
&FontStyleBase::fontsChanged,
|
||||
this,
|
||||
&FontBuilders::buildersChanged);
|
||||
}
|
||||
|
||||
FontBuilder FontBuilders::large() const {
|
||||
return FontBuilder(m_style->large());
|
||||
}
|
||||
|
||||
FontBuilder FontBuilders::medium() const {
|
||||
return FontBuilder(m_style->medium());
|
||||
}
|
||||
|
||||
FontBuilder FontBuilders::small() const {
|
||||
return FontBuilder(m_style->small());
|
||||
}
|
||||
|
||||
IconFontBuilders::IconFontBuilders(const IconFontStyle* style, QObject* parent)
|
||||
: FontBuilders(style, parent) {}
|
||||
|
||||
FontBuilder IconFontBuilders::extraLarge() const {
|
||||
return FontBuilder(
|
||||
static_cast<const IconFontStyle*>(m_style)->extraLarge());
|
||||
}
|
||||
|
||||
FontTokens::FontTokens(QObject* parent)
|
||||
: 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)) {
|
||||
bindFont(ConfigSingleton::instance()->appearance()->font());
|
||||
}
|
||||
|
||||
FontStyle* FontTokens::headline() const {
|
||||
return m_headline;
|
||||
}
|
||||
|
||||
FontStyle* FontTokens::title() const {
|
||||
return m_title;
|
||||
}
|
||||
|
||||
FontStyle* FontTokens::body() const {
|
||||
return m_body;
|
||||
}
|
||||
|
||||
FontStyle* FontTokens::label() const {
|
||||
return m_label;
|
||||
}
|
||||
|
||||
FontStyle* FontTokens::mono() const {
|
||||
return m_mono;
|
||||
}
|
||||
|
||||
IconFontStyle* FontTokens::icon() const {
|
||||
return m_icon;
|
||||
}
|
||||
|
||||
FontBuilder FontTokens::clock() const {
|
||||
return FontBuilder(m_clock);
|
||||
}
|
||||
|
||||
QString FontTokens::workspaces() const {
|
||||
return m_font ? m_font->workspaces() : QString();
|
||||
}
|
||||
|
||||
void FontTokens::bindFont(AppearanceFont* font) {
|
||||
if (m_font == font) return;
|
||||
|
||||
if (m_font) disconnect(m_font, nullptr, this, nullptr);
|
||||
|
||||
m_font = font;
|
||||
|
||||
if (font) {
|
||||
rebuildScale();
|
||||
m_headline->bind(font->headline());
|
||||
m_title->bind(font->title());
|
||||
m_body->bind(font->body());
|
||||
m_label->bind(font->label());
|
||||
m_mono->bind(font->mono());
|
||||
m_icon->bind(font->icon());
|
||||
|
||||
connect(
|
||||
font,
|
||||
&AppearanceFont::clockChanged,
|
||||
this,
|
||||
&FontTokens::rebuildClock);
|
||||
connect(
|
||||
font,
|
||||
&AppearanceFont::scaleChanged,
|
||||
this,
|
||||
&FontTokens::rebuildScale);
|
||||
connect(
|
||||
font,
|
||||
&AppearanceFont::workspacesChanged,
|
||||
this,
|
||||
&FontTokens::workspacesChanged);
|
||||
} else {
|
||||
rebuildScale();
|
||||
m_headline->bind(nullptr);
|
||||
m_title->bind(nullptr);
|
||||
m_body->bind(nullptr);
|
||||
m_label->bind(nullptr);
|
||||
m_mono->bind(nullptr);
|
||||
m_icon->bind(nullptr);
|
||||
}
|
||||
|
||||
rebuildClock();
|
||||
emit workspacesChanged();
|
||||
}
|
||||
|
||||
void FontTokens::rebuildScale() {
|
||||
const qreal s = m_font ? m_font->scale() : 1;
|
||||
m_headline->setScale(s);
|
||||
m_title->setScale(s);
|
||||
m_body->setScale(s);
|
||||
m_label->setScale(s);
|
||||
m_mono->setScale(s);
|
||||
m_icon->setScale(s);
|
||||
}
|
||||
|
||||
void FontTokens::rebuildClock() {
|
||||
QFont f;
|
||||
if (m_font) f.setFamily(m_font->clock());
|
||||
m_clock = f;
|
||||
emit clockChanged();
|
||||
}
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -0,0 +1,182 @@
|
||||
#pragma once
|
||||
|
||||
#include "fontbuilder.hpp"
|
||||
|
||||
#include <qfont.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qvariantmap.h>
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
class AppearanceFont;
|
||||
class FontBuilders;
|
||||
class FontStyleBase;
|
||||
class FontSizeNode;
|
||||
class FontStyleNode;
|
||||
class IconFontStyle;
|
||||
|
||||
class FontBuilders : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
|
||||
Q_PROPERTY(
|
||||
ZShell::config::FontBuilder large READ large NOTIFY buildersChanged
|
||||
FINAL)
|
||||
Q_PROPERTY(
|
||||
ZShell::config::FontBuilder medium READ medium NOTIFY buildersChanged
|
||||
FINAL)
|
||||
Q_PROPERTY(
|
||||
ZShell::config::FontBuilder small READ small NOTIFY buildersChanged
|
||||
FINAL)
|
||||
|
||||
public:
|
||||
explicit FontBuilders(const FontStyleBase* style, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] FontBuilder large() const;
|
||||
[[nodiscard]] FontBuilder medium() const;
|
||||
[[nodiscard]] FontBuilder small() const;
|
||||
|
||||
signals:
|
||||
void buildersChanged();
|
||||
|
||||
protected:
|
||||
const FontStyleBase* m_style;
|
||||
};
|
||||
|
||||
class IconFontBuilders : public FontBuilders {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
|
||||
Q_PROPERTY(
|
||||
ZShell::config::FontBuilder extraLarge READ extraLarge NOTIFY
|
||||
buildersChanged FINAL)
|
||||
|
||||
public:
|
||||
explicit IconFontBuilders(
|
||||
const IconFontStyle* style, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] FontBuilder extraLarge() const;
|
||||
};
|
||||
|
||||
class FontStyleBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QFont large READ large NOTIFY fontsChanged FINAL)
|
||||
Q_PROPERTY(QFont medium READ medium NOTIFY fontsChanged FINAL)
|
||||
Q_PROPERTY(QFont small READ small NOTIFY fontsChanged FINAL)
|
||||
|
||||
public:
|
||||
explicit FontStyleBase(QObject* parent = nullptr);
|
||||
|
||||
virtual void bind(FontStyleNode* cfg);
|
||||
void setScale(qreal scale);
|
||||
|
||||
[[nodiscard]] QFont large() const;
|
||||
[[nodiscard]] QFont medium() const;
|
||||
[[nodiscard]] QFont small() const;
|
||||
|
||||
signals:
|
||||
void fontsChanged();
|
||||
|
||||
protected:
|
||||
virtual void rebuild();
|
||||
|
||||
static QFont buildFont(
|
||||
const FontSizeNode* cfg, const QString& fallbackFamily, qreal scale);
|
||||
|
||||
FontStyleNode* m_cfg = nullptr;
|
||||
qreal m_scale = 1;
|
||||
QFont m_large;
|
||||
QFont m_medium;
|
||||
QFont m_small;
|
||||
};
|
||||
|
||||
class FontStyle : public FontStyleBase {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
|
||||
Q_PROPERTY(FontBuilders* builders READ builders CONSTANT FINAL)
|
||||
|
||||
public:
|
||||
explicit FontStyle(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] FontBuilders* builders() const;
|
||||
|
||||
private:
|
||||
FontBuilders* m_builders;
|
||||
};
|
||||
|
||||
class IconFontStyle : public FontStyleBase {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
|
||||
Q_PROPERTY(QFont extraLarge READ extraLarge NOTIFY fontsChanged FINAL)
|
||||
Q_PROPERTY(IconFontBuilders* builders READ builders CONSTANT FINAL)
|
||||
|
||||
public:
|
||||
explicit IconFontStyle(QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE FontBuilder size(int pointSize);
|
||||
Q_INVOKABLE FontBuilder pixelSize(int pixelSize);
|
||||
|
||||
void bind(FontStyleNode* cfg) override;
|
||||
|
||||
[[nodiscard]] QFont extraLarge() const;
|
||||
[[nodiscard]] IconFontBuilders* builders() const;
|
||||
|
||||
protected:
|
||||
void rebuild() override;
|
||||
|
||||
private:
|
||||
QFont m_extraLarge;
|
||||
IconFontBuilders* m_builders;
|
||||
};
|
||||
|
||||
class FontTokens : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
|
||||
Q_PROPERTY(FontStyle* headline READ headline CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* title READ title CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* body READ body CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* label READ label CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* mono READ mono CONSTANT FINAL)
|
||||
Q_PROPERTY(IconFontStyle* icon READ icon CONSTANT FINAL)
|
||||
Q_PROPERTY(
|
||||
ZShell::config::FontBuilder clock READ clock NOTIFY clockChanged FINAL)
|
||||
Q_PROPERTY(QString workspaces READ workspaces NOTIFY workspacesChanged FINAL)
|
||||
|
||||
public:
|
||||
explicit FontTokens(QObject* parent = nullptr);
|
||||
|
||||
void bindFont(AppearanceFont* font);
|
||||
|
||||
[[nodiscard]] FontStyle* headline() const;
|
||||
[[nodiscard]] FontStyle* title() const;
|
||||
[[nodiscard]] FontStyle* body() const;
|
||||
[[nodiscard]] FontStyle* label() const;
|
||||
[[nodiscard]] FontStyle* mono() const;
|
||||
[[nodiscard]] IconFontStyle* icon() const;
|
||||
[[nodiscard]] FontBuilder clock() const;
|
||||
[[nodiscard]] QString workspaces() const;
|
||||
|
||||
signals:
|
||||
void clockChanged();
|
||||
void workspacesChanged();
|
||||
|
||||
private:
|
||||
void rebuildClock();
|
||||
void rebuildScale();
|
||||
|
||||
AppearanceFont* m_font = nullptr;
|
||||
FontStyle* m_headline;
|
||||
FontStyle* m_title;
|
||||
FontStyle* m_body;
|
||||
FontStyle* m_label;
|
||||
FontStyle* m_mono;
|
||||
IconFontStyle* m_icon;
|
||||
QFont m_clock;
|
||||
};
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "fontbuilder.hpp"
|
||||
|
||||
#include <qloggingcategory.h>
|
||||
|
||||
namespace {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcFontBuilder, "zshell.fontbuilder", QtInfoMsg)
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
FontBuilder::FontBuilder(const QFont& font) : m_font(font) {}
|
||||
|
||||
FontBuilder FontBuilder::family(const QString& family) {
|
||||
m_font.setFamily(family);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::size(int pointSize) {
|
||||
if (pointSize <= 0) pointSize = 1;
|
||||
m_font.setPointSize(pointSize);
|
||||
m_font.setVariableAxis("opsz", static_cast<float>(pointSize));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::pixelSize(int pixelSize) {
|
||||
if (pixelSize <= 0) pixelSize = 1;
|
||||
m_font.setPixelSize(pixelSize);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::weight(QFont::Weight weight) {
|
||||
m_font.setWeight(weight);
|
||||
m_font.setVariableAxis("wght", static_cast<float>(weight));
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::italic(bool on) {
|
||||
m_font.setItalic(on);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::stretch(int stretch) {
|
||||
m_font.setStretch(stretch);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) {
|
||||
m_font.setLetterSpacing(
|
||||
absolute ? QFont::AbsoluteSpacing : QFont::PercentageSpacing, spacing);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) {
|
||||
m_font.setCapitalization(cap);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::vaxis(const QString& tag, float value) {
|
||||
if (auto t = QFont::Tag::fromString(tag))
|
||||
m_font.setVariableAxis(*t, value);
|
||||
else
|
||||
qCWarning(lcFontBuilder)
|
||||
<< "Unable to convert tag" << tag << "to QFont::Tag";
|
||||
return *this;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::vaxes(const QVariantMap& axes) {
|
||||
for (auto it = axes.constBegin(); it != axes.constEnd(); ++it) {
|
||||
if (it.value().canConvert<float>()) {
|
||||
if (auto tag = QFont::Tag::fromString(it.key()))
|
||||
m_font.setVariableAxis(*tag, it.value().toFloat());
|
||||
else
|
||||
qCWarning(lcFontBuilder)
|
||||
<< "Unable to convert tag" << it.key() << "to QFont::Tag";
|
||||
} else {
|
||||
qCWarning(lcFontBuilder)
|
||||
<< "Unable to convert value" << it.value() << "to float";
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
QFont FontBuilder::build() const {
|
||||
return m_font;
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::fill(float value) {
|
||||
return vaxis(QStringLiteral("FILL"), value);
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::grade(float value) {
|
||||
return vaxis(QStringLiteral("GRAD"), value);
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::width(float value) {
|
||||
return vaxis(QStringLiteral("wdth"), value);
|
||||
}
|
||||
|
||||
FontBuilder FontBuilder::scale(qreal factor) {
|
||||
return size(static_cast<int>(m_font.pointSize() * factor));
|
||||
}
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <qfont.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
class FontBuilder {
|
||||
Q_GADGET
|
||||
QML_ANONYMOUS
|
||||
|
||||
public:
|
||||
FontBuilder() = default;
|
||||
explicit FontBuilder(const QFont& font);
|
||||
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder family(const QString& family);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder size(int pointSize);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder pixelSize(int pixelSize);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder weight(QFont::Weight weight);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder italic(bool on = true);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder stretch(int stretch);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder
|
||||
letterSpacing(qreal spacing, bool absolute = true);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder
|
||||
capitalisation(QFont::Capitalization cap);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes);
|
||||
[[nodiscard]] Q_INVOKABLE QFont build() const;
|
||||
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder fill(float value);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder grade(float value);
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder width(float value);
|
||||
|
||||
[[nodiscard]] Q_INVOKABLE FontBuilder scale(qreal factor);
|
||||
|
||||
private:
|
||||
QFont m_font;
|
||||
};
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -1,158 +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, 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(QString, logo, QString())
|
||||
CFG_PROPERTY(bool, showOverFullscreen, true)
|
||||
CFG_PROPERTY(
|
||||
CONFIG_PROPERTY(
|
||||
QString,
|
||||
lyricsDir,
|
||||
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
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "migration.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
namespace {
|
||||
|
||||
// A stored value is "missing" if the path does not resolve to an entry.
|
||||
// Note: default-constructed QJsonValue is Null, not Undefined.
|
||||
bool isMissing(const QJsonValue& v) {
|
||||
return v.isUndefined() || v.isNull();
|
||||
}
|
||||
|
||||
QJsonValue valueAt(const QJsonObject& obj, const QStringList& path) {
|
||||
QJsonObject cur = obj;
|
||||
|
||||
for (int i = 0; i < path.size() - 1; ++i) {
|
||||
if (!cur.value(path.at(i)).isObject()) return QJsonValue::Undefined;
|
||||
cur = cur.value(path.at(i)).toObject();
|
||||
}
|
||||
|
||||
const QString key = path.last();
|
||||
if (!cur.contains(key)) return QJsonValue::Undefined;
|
||||
return cur.value(key);
|
||||
}
|
||||
|
||||
QJsonObject placeAt(QJsonObject obj, QStringList path, const QJsonValue& value) {
|
||||
const QString head = path.takeFirst();
|
||||
|
||||
if (path.isEmpty()) {
|
||||
obj[head] = value;
|
||||
return obj;
|
||||
}
|
||||
|
||||
QJsonObject child = obj.value(head).isObject() ? obj.value(head).toObject()
|
||||
: QJsonObject();
|
||||
obj[head] = placeAt(child, path, value);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Removes `path` from `obj`. Returns true if a value was removed.
|
||||
// Parent objects left empty are removed as well.
|
||||
bool removeAt(QJsonObject& obj, const QStringList& path) {
|
||||
const QString head = path.first();
|
||||
|
||||
if (path.size() == 1) {
|
||||
if (!obj.contains(head)) return false;
|
||||
obj.remove(head);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!obj.value(head).isObject()) return false;
|
||||
|
||||
QJsonObject child = obj.value(head).toObject();
|
||||
const bool removed = removeAt(child, path.mid(1));
|
||||
|
||||
if (child.isEmpty())
|
||||
obj.remove(head);
|
||||
else
|
||||
obj[head] = child;
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const QList<ConfigMigrationRule>& ConfigMigrations::rules() {
|
||||
static const QList<ConfigMigrationRule> s_rules = {
|
||||
migrate(
|
||||
"general.color.scheduleHyprsunset", "display.nightlight.schedule"),
|
||||
migrate(
|
||||
"general.color.scheduleHyprsunsetStart",
|
||||
"display.nightlight.scheduleStart"),
|
||||
migrate(
|
||||
"general.color.scheduleHyprsunsetEnd",
|
||||
"display.nightlight.scheduleEnd"),
|
||||
migrate("general.color.scheduleDark", "colors.scheduleDark"),
|
||||
migrate("general.color.scheduleDarkStart", "colors.scheduleDarkStart"),
|
||||
migrate("general.color.scheduleDarkEnd", "colors.scheduleDarkEnd"),
|
||||
migrate("general.color.schemeGeneration", "colors.schemeGen"),
|
||||
migrate(
|
||||
"general.color.useNativeNightlight",
|
||||
"display.nightlight.useNative"),
|
||||
migrate(
|
||||
"general.color.nativeFadeDuration",
|
||||
"display.nightlight.fadeDuration"),
|
||||
migrate("general.color.hyprsunsetTemp", "display.nightlight.temp"),
|
||||
migrate("general.color.neovimColors", "colors.neovimColors"),
|
||||
migrate("general.color.mode", "colors.mode"),
|
||||
migrate("general.color.smart", "colors.smart"),
|
||||
};
|
||||
return s_rules;
|
||||
}
|
||||
|
||||
QJsonObject ConfigMigrations::apply(const QJsonObject& json) {
|
||||
QJsonObject obj = json;
|
||||
|
||||
for (const auto& rule : rules()) {
|
||||
const QStringList from =
|
||||
rule.from.split(QLatin1Char('.'), Qt::SkipEmptyParts);
|
||||
const QStringList to =
|
||||
rule.to.split(QLatin1Char('.'), Qt::SkipEmptyParts);
|
||||
|
||||
if (from.isEmpty() || to.isEmpty()) continue;
|
||||
|
||||
const QJsonValue oldValue = valueAt(obj, from);
|
||||
if (isMissing(oldValue)) continue;
|
||||
|
||||
const bool targetExists = !isMissing(valueAt(obj, to));
|
||||
if (!targetExists) obj = placeAt(obj, to, oldValue);
|
||||
|
||||
if (removeAt(obj, from)) {
|
||||
qInfo() << "Config migration:" << rule.from << "->" << rule.to
|
||||
<< (targetExists ? "(target already set, old value dropped)"
|
||||
: "(value moved)");
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
} // namespace ZShell::config
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace ZShell::config {
|
||||
|
||||
// A single migration rule: move the value stored at the dotted `from` path
|
||||
// to the dotted `to` path, then delete the old key (and any parent objects
|
||||
// left empty).
|
||||
//
|
||||
// Rules are applied to the raw JSON file on every load, before the schema
|
||||
// loads it. They are idempotent: once the old key is gone the rule never
|
||||
// fires again, so no version stamping is needed. Old rules can stay in the
|
||||
// table forever.
|
||||
//
|
||||
// WARNING: the target path must exist in the schema (i.e. a ConfigObject
|
||||
// property somewhere in the tree). If it does not, the migrated value is
|
||||
// loaded as an unknown key and silently dropped on the next save.
|
||||
struct ConfigMigrationRule {
|
||||
QString from;
|
||||
QString to;
|
||||
};
|
||||
|
||||
// Helper for writing rules: migrate("general.color.foo", "display.bar.foo")
|
||||
inline ConfigMigrationRule migrate(const QString& from, const QString& to) {
|
||||
return {from, to};
|
||||
}
|
||||
|
||||
class ConfigMigrations {
|
||||
public:
|
||||
[[nodiscard]] static const QList<ConfigMigrationRule>& rules();
|
||||
|
||||
// Applies all rules to `json`, returning the migrated object.
|
||||
[[nodiscard]] static QJsonObject apply(const QJsonObject& json);
|
||||
};
|
||||
|
||||
} // 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,54 +1,43 @@
|
||||
#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(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_ENUM_PROPERTY(GpuType, gpuType, GpuType::Auto)
|
||||
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
|
||||
|
||||
+136
-254
@@ -1,292 +1,174 @@
|
||||
#pragma once
|
||||
|
||||
#include "configobject.hpp"
|
||||
#include "anim.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 AppearanceRounding : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class AnimCurves : public settings::ObjectNode {
|
||||
CONFIG_NODE(AnimCurves, settings::ObjectNode)
|
||||
|
||||
CFG_PROPERTY(qreal, extraSmall, 4)
|
||||
CFG_PROPERTY(qreal, full, std::numeric_limits<int>::max())
|
||||
CFG_PROPERTY(qreal, large, 16)
|
||||
CFG_PROPERTY(qreal, largeIncreased, 20)
|
||||
CFG_PROPERTY(qreal, extraLarge, 28)
|
||||
CFG_PROPERTY(qreal, extraLargeIncreased, 32)
|
||||
CFG_PROPERTY(qreal, extraExtraLarge, 48)
|
||||
CFG_PROPERTY(qreal, medium, 12)
|
||||
CFG_PROPERTY(qreal, scale, 1)
|
||||
CFG_PROPERTY(qreal, small, 8)
|
||||
|
||||
public:
|
||||
explicit AppearanceRounding(QObject* parent = nullptr)
|
||||
: ConfigObject(parent) {}
|
||||
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 AppearancePadding : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class RoundingTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(RoundingTokens, settings::ObjectNode)
|
||||
|
||||
CFG_PROPERTY(int, extraSmall, 4)
|
||||
CFG_PROPERTY(int, smaller, 5)
|
||||
CFG_PROPERTY(int, small, 8)
|
||||
CFG_PROPERTY(int, medium, 12)
|
||||
CFG_PROPERTY(int, large, 16)
|
||||
CFG_PROPERTY(int, largeIncreased, 20)
|
||||
CFG_PROPERTY(int, extraLarge, 28)
|
||||
CFG_PROPERTY(int, extraLargeIncreased, 32)
|
||||
CFG_PROPERTY(int, extraExtraLarge, 48)
|
||||
|
||||
public:
|
||||
explicit AppearancePadding(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)
|
||||
CONFIG_PROPERTY(int, full, std::numeric_limits<int>::max())
|
||||
};
|
||||
|
||||
class AppearanceSpacing : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class SpacingTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(SpacingTokens, settings::ObjectNode)
|
||||
|
||||
CFG_PROPERTY(int, extraSmall, 4)
|
||||
CFG_PROPERTY(int, small, 8)
|
||||
CFG_PROPERTY(int, medium, 12)
|
||||
CFG_PROPERTY(int, large, 16)
|
||||
CFG_PROPERTY(int, largeIncreased, 20)
|
||||
CFG_PROPERTY(int, extraLarge, 28)
|
||||
CFG_PROPERTY(int, extraLargeIncreased, 32)
|
||||
CFG_PROPERTY(int, extraExtraLarge, 48)
|
||||
|
||||
public:
|
||||
explicit AppearanceSpacing(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 FontSizeTokens : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class PaddingTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(PaddingTokens, settings::ObjectNode)
|
||||
|
||||
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) {}
|
||||
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 FontTokens : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class AnimDurationTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(AnimDurationTokens, settings::ObjectNode)
|
||||
|
||||
CONFIG_SUBOBJECT(FontSizeTokens, size)
|
||||
|
||||
public:
|
||||
explicit FontTokens(QObject* parent = nullptr)
|
||||
: ConfigObject(parent), m_size(new FontSizeTokens(this)) {}
|
||||
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 AnimCurves : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class AppearanceTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(AppearanceTokens, 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_SUBOBJECT(AnimCurves, curves)
|
||||
CONFIG_SUBOBJECT(RoundingTokens, rounding)
|
||||
CONFIG_SUBOBJECT(SpacingTokens, spacing)
|
||||
CONFIG_SUBOBJECT(PaddingTokens, padding)
|
||||
CONFIG_SUBOBJECT(AnimDurationTokens, animDurations)
|
||||
};
|
||||
|
||||
class AnimDurations : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class BarTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(BarTokens, settings::ObjectNode)
|
||||
|
||||
CFG_PROPERTY(int, small, 200)
|
||||
CFG_PROPERTY(int, scale, 1)
|
||||
CFG_PROPERTY(int, normal, 400)
|
||||
CFG_PROPERTY(int, large, 600)
|
||||
CFG_PROPERTY(int, extraLarge, 1000)
|
||||
CFG_PROPERTY(int, expressiveFastSpatial, 350)
|
||||
CFG_PROPERTY(int, expressiveDefaultSpatial, 500)
|
||||
CFG_PROPERTY(int, expressiveSlowSpatial, 650)
|
||||
CFG_PROPERTY(int, expressiveFastEffects, 150)
|
||||
CFG_PROPERTY(int, expressiveDefaultEffects, 200)
|
||||
CFG_PROPERTY(int, expressiveSlowEffects, 300)
|
||||
|
||||
public:
|
||||
explicit AnimDurations(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||
CONFIG_PROPERTY(int, innerSize, 28)
|
||||
};
|
||||
|
||||
class AnimTokens : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class DashboardTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(DashboardTokens, settings::ObjectNode)
|
||||
|
||||
CONFIG_SUBOBJECT(AnimDurations, durations)
|
||||
|
||||
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_durations(new AnimDurations(this))
|
||||
, m_easing(new AnimEasing(this)) {
|
||||
connect(
|
||||
m_easing,
|
||||
&AnimEasing::curvesChanged,
|
||||
this,
|
||||
&AnimTokens::curvesChanged);
|
||||
m_easing->bindCurves(m_curves);
|
||||
}
|
||||
|
||||
[[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, processesWidth, 1200)
|
||||
CONFIG_PROPERTY(int, processesHeight, 800)
|
||||
CONFIG_PROPERTY(int, processesHeaderHeight, 40)
|
||||
CONFIG_PROPERTY(int, processesRowHeight, 40)
|
||||
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 BarTokens : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
class SizeTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(SizeTokens, settings::ObjectNode)
|
||||
|
||||
CFG_PROPERTY(int, innerSize, 28)
|
||||
|
||||
public:
|
||||
explicit BarTokens(QObject* parent = nullptr) : ConfigObject(parent) {}
|
||||
};
|
||||
|
||||
class Tokens : public ConfigObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
CONFIG_SUBOBJECT(AppearanceRounding, rounding)
|
||||
CONFIG_SUBOBJECT(AppearancePadding, padding)
|
||||
CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
|
||||
CONFIG_SUBOBJECT(FontTokens, font)
|
||||
CONFIG_SUBOBJECT(AnimTokens, anim)
|
||||
CONFIG_SUBOBJECT(DashboardTokens, dashboard)
|
||||
CONFIG_SUBOBJECT(BarTokens, bar)
|
||||
|
||||
public:
|
||||
explicit Tokens(QObject* parent = nullptr)
|
||||
: ConfigObject(parent)
|
||||
, m_rounding(new AppearanceRounding(this))
|
||||
, m_padding(new AppearancePadding(this))
|
||||
, m_spacing(new AppearanceSpacing(this))
|
||||
, m_font(new FontTokens(this))
|
||||
, m_anim(new AnimTokens(this))
|
||||
, m_bar(new BarTokens(this)) {}
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
pkg_check_modules(HYPRLAND REQUIRED hyprland)
|
||||
|
||||
add_library(zshell-nightlight SHARED main.cpp)
|
||||
|
||||
target_compile_features(zshell-nightlight PRIVATE cxx_std_23)
|
||||
target_include_directories(zshell-nightlight PRIVATE ${HYPRLAND_INCLUDE_DIRS})
|
||||
target_link_directories(zshell-nightlight PRIVATE ${HYPRLAND_LIBRARY_DIRS})
|
||||
target_compile_options(zshell-nightlight PRIVATE ${HYPRLAND_CFLAGS_OTHER})
|
||||
|
||||
target_compile_options(zshell-nightlight PRIVATE -fno-gnu-unique)
|
||||
|
||||
install(TARGETS zshell-nightlight LIBRARY DESTINATION "${INSTALL_LIBDIR}/plugins")
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
inline std::array<float, 3> kelvinToGain(int kelvin) {
|
||||
const double t = std::clamp(kelvin, 1000, 40000) / 100.0;
|
||||
double r, g, b;
|
||||
|
||||
r = t <= 66.0
|
||||
? 1.0
|
||||
: std::clamp(
|
||||
1.29293618606 * std::pow(t - 60.0, -0.1332047592), 0.0, 1.0);
|
||||
g = t <= 66.0
|
||||
? std::clamp(0.39008157876 * std::log(t) - 0.63184144378, 0.0, 1.0)
|
||||
: std::clamp(
|
||||
1.12989086089 * std::pow(t - 60.0, -0.0755148492), 0.0, 1.0);
|
||||
b = t >= 66.0
|
||||
? 1.0
|
||||
: (t <= 19.0
|
||||
? 0.0
|
||||
: std::clamp(
|
||||
0.54320678911 * std::log(t - 10.0) - 1.19625408914,
|
||||
0.0,
|
||||
1.0));
|
||||
|
||||
return {static_cast<float>(r), static_cast<float>(g), static_cast<float>(b)};
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,455 @@
|
||||
#include <any>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#define private public
|
||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||
#include <hyprland/src/render/OpenGL.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#undef private
|
||||
|
||||
#include <hyprland/src/managers/EventManager.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
}
|
||||
|
||||
#include "colortemp.hpp"
|
||||
|
||||
using ZShell::services::kelvinToGain;
|
||||
|
||||
using Render::IFramebuffer;
|
||||
using Render::GL::CHyprOpenGLImpl;
|
||||
using Render::GL::g_pHyprOpenGL;
|
||||
|
||||
inline HANDLE PHANDLE = nullptr;
|
||||
|
||||
static std::filesystem::path g_runtimeDir{"/tmp"};
|
||||
static std::filesystem::path g_cacheBase{"/tmp/.cache"};
|
||||
|
||||
static void initEnvPaths() {
|
||||
if (const char* env = getenv("XDG_RUNTIME_DIR"))
|
||||
g_runtimeDir = env;
|
||||
|
||||
if (const char* env = getenv("XDG_CACHE_HOME")) {
|
||||
if (*env)
|
||||
g_cacheBase = env;
|
||||
} else if (const char* home = getenv("HOME")) {
|
||||
g_cacheBase = std::filesystem::path(home) / ".cache";
|
||||
}
|
||||
}
|
||||
|
||||
static std::ofstream& debugLog() {
|
||||
static std::ofstream log(
|
||||
g_runtimeDir / "zshell-nightlight-debug.log",
|
||||
std::ios::app);
|
||||
return log;
|
||||
}
|
||||
|
||||
static std::mutex g_logMutex;
|
||||
static std::atomic<uint64_t> g_logSeq{0};
|
||||
|
||||
static void dlog(const std::string& msg) {
|
||||
std::lock_guard<std::mutex> lk(g_logMutex);
|
||||
auto& log = debugLog();
|
||||
log << "[" << g_logSeq++ << "][tid=" << std::this_thread::get_id() << "] "
|
||||
<< msg << std::endl;
|
||||
}
|
||||
|
||||
static constexpr int BAKED_STEPS = 24;
|
||||
|
||||
static std::string glslFloat(float v) {
|
||||
std::ostringstream ss;
|
||||
ss << std::fixed << std::setprecision(6) << v;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static std::filesystem::path cacheDir() {
|
||||
return g_cacheBase / "zshell" / "night-light";
|
||||
}
|
||||
|
||||
static std::filesystem::path shaderPath() {
|
||||
return cacheDir() / "nightlight.frag";
|
||||
}
|
||||
|
||||
static std::filesystem::path bakedShaderPath(const std::array<float, 3>& gain) {
|
||||
return cacheDir() /
|
||||
("nightlight-" + glslFloat(gain[0]) + "-" + glslFloat(gain[1]) +
|
||||
"-" + glslFloat(gain[2]) + ".frag");
|
||||
}
|
||||
|
||||
static std::string defaultShaderSource() {
|
||||
return "#version 100\n"
|
||||
"precision highp float;\n"
|
||||
"varying vec2 v_texcoord;\n"
|
||||
"uniform sampler2D tex;\n"
|
||||
"uniform vec3 tint;\n"
|
||||
"void main() {\n"
|
||||
" vec4 pixColor = texture2D(tex, v_texcoord);\n"
|
||||
" gl_FragColor = vec4(pixColor.rgb * tint, pixColor.a);\n"
|
||||
"}\n";
|
||||
}
|
||||
|
||||
static std::string bakedShaderSource(const std::array<float, 3>& gain) {
|
||||
std::ostringstream src;
|
||||
src << "#version 100\n"
|
||||
"precision highp float;\n"
|
||||
"varying vec2 v_texcoord;\n"
|
||||
"uniform sampler2D tex;\n"
|
||||
"const vec3 GAIN = vec3("
|
||||
<< glslFloat(gain[0]) << "," << glslFloat(gain[1]) << ","
|
||||
<< glslFloat(gain[2])
|
||||
<< ");\n"
|
||||
"void main() {\n"
|
||||
" vec4 pixColor = texture2D(tex, v_texcoord);\n"
|
||||
" gl_FragColor = vec4(pixColor.rgb * GAIN, pixColor.a);\n"
|
||||
"}\n";
|
||||
return src.str();
|
||||
}
|
||||
|
||||
static void ensureShaderFile(
|
||||
const std::filesystem::path& path, const std::string& source) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_regular_file(path, ec)) return;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
std::ofstream out(path, std::ios::trunc);
|
||||
out << source;
|
||||
}
|
||||
|
||||
static bool g_active = false;
|
||||
static bool g_uniformMode = false;
|
||||
static bool g_modeDecided = false;
|
||||
static std::array<float, 3> g_currentGain{1.f, 1.f, 1.f};
|
||||
|
||||
static int g_currentKelvin = 6500;
|
||||
static int g_lastKelvin = 6500;
|
||||
|
||||
struct STransition {
|
||||
bool active = false;
|
||||
std::chrono::steady_clock::time_point start;
|
||||
float durationSec = 0.4f;
|
||||
std::array<float, 3> fromGain{1.f, 1.f, 1.f};
|
||||
std::array<float, 3> toGain{1.f, 1.f, 1.f};
|
||||
int lastBakedStep = -1;
|
||||
};
|
||||
static STransition g_transition;
|
||||
|
||||
static void postStateEvent(bool enabled, bool animating);
|
||||
|
||||
static bool gainIsNeutral(const std::array<float, 3>& gain) {
|
||||
for (float v : gain)
|
||||
if (std::abs(v - 1.f) > 0.001f) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static CFunctionHook* g_pBeginHook = nullptr;
|
||||
using origBegin = void (*)(
|
||||
CHyprOpenGLImpl*,
|
||||
PHLMONITOR,
|
||||
const CRegion&,
|
||||
SP<IFramebuffer>,
|
||||
std::optional<CRegion>);
|
||||
|
||||
static CFunctionHook* g_pSaveMirrorHook = nullptr;
|
||||
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
|
||||
|
||||
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
|
||||
const bool saved = thisptr->m_applyFinalShader;
|
||||
thisptr->m_applyFinalShader = false;
|
||||
const bool result = (*reinterpret_cast<origSaveMirror>(
|
||||
g_pSaveMirrorHook->m_original))(thisptr, box);
|
||||
thisptr->m_applyFinalShader = saved;
|
||||
return result;
|
||||
}
|
||||
|
||||
static void installShader() {
|
||||
auto* gl = g_pHyprOpenGL.get();
|
||||
if (!gl)
|
||||
return;
|
||||
|
||||
ensureShaderFile(shaderPath(), defaultShaderSource());
|
||||
gl->applyScreenShader(shaderPath().string());
|
||||
|
||||
auto* shader = gl->m_finalScreenShader.get();
|
||||
if (shader && shader->program() >= 1 &&
|
||||
shader->getUniformLocation(SHADER_TINT) != -1) {
|
||||
g_uniformMode = true;
|
||||
} else {
|
||||
g_uniformMode = false;
|
||||
if (!g_modeDecided)
|
||||
dlog(
|
||||
"shader has no 'tint' uniform, falling back to "
|
||||
"per-step recompiled shaders");
|
||||
}
|
||||
g_modeDecided = true;
|
||||
}
|
||||
|
||||
static void setUniformGain(const std::array<float, 3>& gain) {
|
||||
auto* gl = g_pHyprOpenGL.get();
|
||||
auto* shader = gl ? gl->m_finalScreenShader.get() : nullptr;
|
||||
if (!gl || !shader)
|
||||
return;
|
||||
|
||||
gl->useShader(gl->m_finalScreenShader);
|
||||
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
|
||||
}
|
||||
|
||||
static void hkBegin(
|
||||
CHyprOpenGLImpl* thisptr,
|
||||
PHLMONITOR mon,
|
||||
const CRegion& damage,
|
||||
SP<IFramebuffer> fb,
|
||||
std::optional<CRegion> finalDamage) {
|
||||
(*reinterpret_cast<origBegin>(
|
||||
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
|
||||
|
||||
if (fb) {
|
||||
auto* renderer = g_pHyprRenderer.get();
|
||||
if (renderer)
|
||||
renderer->m_renderData.blockScreenShader = true;
|
||||
}
|
||||
|
||||
if (!g_active) return;
|
||||
|
||||
auto* gl = g_pHyprOpenGL.get();
|
||||
if (!gl) return;
|
||||
|
||||
if (gl->m_finalScreenShader.get() &&
|
||||
gl->m_finalScreenShader->program() < 1)
|
||||
installShader();
|
||||
if (!gl->m_finalScreenShader.get() ||
|
||||
gl->m_finalScreenShader->program() < 1)
|
||||
return;
|
||||
|
||||
if (g_transition.active) {
|
||||
auto* monPtr = mon.get();
|
||||
if (!monPtr)
|
||||
return;
|
||||
const float elapsed =
|
||||
std::chrono::duration<float>(
|
||||
std::chrono::steady_clock::now() - g_transition.start)
|
||||
.count();
|
||||
const float t =
|
||||
std::clamp(elapsed / g_transition.durationSec, 0.f, 1.f);
|
||||
|
||||
for (size_t i = 0; i < 3; ++i)
|
||||
g_currentGain[i] =
|
||||
g_transition.fromGain[i] +
|
||||
(g_transition.toGain[i] - g_transition.fromGain[i]) * t;
|
||||
|
||||
if (g_uniformMode) {
|
||||
setUniformGain(g_currentGain);
|
||||
} else {
|
||||
const int step = static_cast<int>(t * BAKED_STEPS);
|
||||
if (step != g_transition.lastBakedStep) {
|
||||
g_transition.lastBakedStep = step;
|
||||
ensureShaderFile(
|
||||
bakedShaderPath(g_currentGain),
|
||||
bakedShaderSource(g_currentGain));
|
||||
gl->applyScreenShader(bakedShaderPath(g_currentGain).string());
|
||||
}
|
||||
}
|
||||
|
||||
if (t >= 1.f) {
|
||||
g_transition.active = false;
|
||||
if (gainIsNeutral(g_currentGain)) {
|
||||
g_active = false;
|
||||
gl->applyScreenShader("");
|
||||
}
|
||||
postStateEvent(g_active, false);
|
||||
} else {
|
||||
monPtr->m_forceFullFrames = 2;
|
||||
monPtr->scheduleFrame();
|
||||
}
|
||||
} else if (g_uniformMode) {
|
||||
setUniformGain(g_currentGain);
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr int KELVIN_MIN = 2000;
|
||||
static constexpr int KELVIN_MAX = 6500;
|
||||
|
||||
static int luaNightlightSet(lua_State* L) {
|
||||
const int rawKelvin = static_cast<int>(luaL_checknumber(L, 1));
|
||||
const float durationSec =
|
||||
lua_gettop(L) >= 2 ? static_cast<float>(luaL_checknumber(L, 2)) : 0.4f;
|
||||
|
||||
const int targetKelvin = std::clamp(rawKelvin, KELVIN_MIN, KELVIN_MAX);
|
||||
if (targetKelvin != rawKelvin)
|
||||
dlog(
|
||||
"clamped " + std::to_string(rawKelvin) + "K to " +
|
||||
std::to_string(targetKelvin) + "K");
|
||||
|
||||
g_transition.fromGain = g_currentGain;
|
||||
g_transition.toGain = kelvinToGain(targetKelvin);
|
||||
g_transition.active = true;
|
||||
g_transition.start = std::chrono::steady_clock::now();
|
||||
g_transition.durationSec = durationSec;
|
||||
g_transition.lastBakedStep = -1;
|
||||
g_active = true;
|
||||
|
||||
g_lastKelvin = g_currentKelvin;
|
||||
g_currentKelvin = targetKelvin;
|
||||
|
||||
postStateEvent(true, true);
|
||||
|
||||
lua_pushinteger(L, targetKelvin);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaNightlightDisable(lua_State* L) {
|
||||
if (!g_active) return 0;
|
||||
|
||||
const float durationSec =
|
||||
lua_gettop(L) >= 1 ? static_cast<float>(luaL_checknumber(L, 1)) : 0.4f;
|
||||
|
||||
g_transition.fromGain = g_currentGain;
|
||||
g_transition.toGain = {1.f, 1.f, 1.f};
|
||||
g_transition.active = true;
|
||||
g_transition.start = std::chrono::steady_clock::now();
|
||||
g_transition.durationSec = durationSec;
|
||||
g_transition.lastBakedStep = -1;
|
||||
|
||||
g_lastKelvin = g_currentKelvin;
|
||||
g_currentKelvin = 6500;
|
||||
|
||||
postStateEvent(false, true);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaNightlightState(lua_State* L) {
|
||||
lua_pushstring(
|
||||
L,
|
||||
std::format(
|
||||
"{{\"enabled\":{},\"animating\":{},\"last\":{},\"current\":{}}}",
|
||||
g_active ? "true" : "false",
|
||||
g_transition.active ? "true" : "false",
|
||||
g_lastKelvin,
|
||||
g_currentKelvin)
|
||||
.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void postStateEvent(bool enabled, bool animating) {
|
||||
if (!g_pEventManager) return;
|
||||
g_pEventManager->postEvent(
|
||||
SHyprIPCEvent{
|
||||
.event = "zshell-nightlight",
|
||||
.data = std::format(
|
||||
"{},{},{},{}",
|
||||
enabled ? 1 : 0,
|
||||
animating ? 1 : 0,
|
||||
g_lastKelvin,
|
||||
g_currentKelvin)});
|
||||
}
|
||||
|
||||
static const SFunctionMatch* findExactMethod(
|
||||
const std::vector<SFunctionMatch>& methods,
|
||||
const std::string& classAndMethod) {
|
||||
for (const auto& m : methods) {
|
||||
if (m.signature.find(classAndMethod) != std::string::npos) return &m;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static std::string manglePrefix(
|
||||
const std::string& className, const std::string& methodName) {
|
||||
return std::to_string(className.size()) + className +
|
||||
std::to_string(methodName.size()) + methodName;
|
||||
}
|
||||
|
||||
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
|
||||
PHANDLE = handle;
|
||||
initEnvPaths();
|
||||
dlog("=== PLUGIN_INIT, build timestamp " __DATE__ " " __TIME__ " ===");
|
||||
|
||||
const std::string serverHash = __hyprland_api_get_hash();
|
||||
const std::string clientHash = __hyprland_api_get_client_hash();
|
||||
|
||||
if (serverHash != clientHash) {
|
||||
HyprlandAPI::addNotification(
|
||||
PHANDLE,
|
||||
"[zshell-nightlight] Mismatched headers! Can't proceed.",
|
||||
CHyprColor{1.0f, 0.2f, 0.2f, 1.0f},
|
||||
5000);
|
||||
throw std::runtime_error("[zshell-nightlight] version mismatch");
|
||||
}
|
||||
|
||||
static const auto SAVEMIRROR_METHODS =
|
||||
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
|
||||
const SFunctionMatch* saveMirrorTarget = findExactMethod(
|
||||
SAVEMIRROR_METHODS,
|
||||
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
|
||||
|
||||
if (saveMirrorTarget) {
|
||||
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
|
||||
PHANDLE,
|
||||
saveMirrorTarget->address,
|
||||
reinterpret_cast<void*>(&hkSaveBufferForMirror));
|
||||
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
|
||||
dlog(
|
||||
"saveBufferForMirror hook installed: " +
|
||||
saveMirrorTarget->signature);
|
||||
}
|
||||
|
||||
if (!g_pSaveMirrorHook)
|
||||
HyprlandAPI::addNotification(
|
||||
PHANDLE,
|
||||
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
|
||||
"captures may still show the nightlight",
|
||||
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
|
||||
8000);
|
||||
|
||||
static const auto METHODS =
|
||||
HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
|
||||
const SFunctionMatch* target =
|
||||
findExactMethod(METHODS, manglePrefix("CHyprOpenGLImpl", "begin"));
|
||||
if (!target)
|
||||
throw std::runtime_error(
|
||||
"[zshell-nightlight] couldn't uniquely resolve "
|
||||
"CHyprOpenGLImpl::begin");
|
||||
|
||||
g_pBeginHook = HyprlandAPI::createFunctionHook(
|
||||
PHANDLE, target->address, reinterpret_cast<void*>(&hkBegin));
|
||||
if (!g_pBeginHook || !g_pBeginHook->hook())
|
||||
throw std::runtime_error("[zshell-nightlight] failed to hook begin()");
|
||||
|
||||
if (!HyprlandAPI::addLuaFunction(
|
||||
PHANDLE, "zshell", "nlSet", luaNightlightSet))
|
||||
throw std::runtime_error(
|
||||
"[zshell-nightlight] failed to register Lua function");
|
||||
|
||||
if (!HyprlandAPI::addLuaFunction(
|
||||
PHANDLE, "zshell", "nlDisable", luaNightlightDisable))
|
||||
throw std::runtime_error(
|
||||
"[zshell-nightlight] failed to register Lua function");
|
||||
|
||||
if (!HyprlandAPI::addLuaFunction(
|
||||
PHANDLE, "zshell", "nlState", luaNightlightState))
|
||||
throw std::runtime_error(
|
||||
"[zshell-nightlight] failed to register Lua function");
|
||||
|
||||
return {
|
||||
"zshell-nightlight",
|
||||
"Animated nightlight, excluded from screencopy",
|
||||
"ZShell",
|
||||
"1.2"};
|
||||
}
|
||||
|
||||
|
||||
APICALL EXPORT std::string PLUGIN_API_VERSION() {
|
||||
return HYPRLAND_API_VERSION;
|
||||
}
|
||||
|
||||
|
||||
APICALL EXPORT void PLUGIN_EXIT() {}
|
||||
@@ -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,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcursor.h>
|
||||
#include <qguiapplication.h>
|
||||
#include <qlogging.h>
|
||||
#include <qnamespace.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_shape == shape) return;
|
||||
|
||||
if (m_shape)
|
||||
QGuiApplication::changeOverrideCursor(QCursor(shape));
|
||||
else {
|
||||
QGuiApplication::setOverrideCursor(QCursor(shape));
|
||||
}
|
||||
m_shape = shape;
|
||||
}
|
||||
|
||||
Q_INVOKABLE void clearOverride() {
|
||||
if (!m_shape) return;
|
||||
|
||||
QGuiApplication::restoreOverrideCursor();
|
||||
m_shape.reset();
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<Qt::CursorShape> m_shape;
|
||||
};
|
||||
|
||||
} // namespace ZShell::internal
|
||||
@@ -1,227 +0,0 @@
|
||||
#include "sparklineitem.hpp"
|
||||
|
||||
#include <qmath.h>
|
||||
#include <qpainter.h>
|
||||
#include <qpainterpath.h>
|
||||
#include <qpen.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;
|
||||
|
||||
const qreal stepX = w / static_cast<qreal>(m_historyLength - 1);
|
||||
const qreal startX =
|
||||
w - (len - 1) * stepX - stepX * m_slideProgress + stepX;
|
||||
|
||||
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) {
|
||||
const qreal x = startX + i * stepX;
|
||||
const qreal value = qBound<qreal>(0.0, buffer->at(i), m_maxValue);
|
||||
const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH;
|
||||
points.append(QPointF(x, y));
|
||||
}
|
||||
|
||||
QPainterPath linePath;
|
||||
linePath.moveTo(points[0]);
|
||||
|
||||
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;
|
||||
const QPointF c1(ctrlX, p0.y());
|
||||
const QPointF c2(ctrlX, p1.y());
|
||||
|
||||
linePath.cubicTo(c1, c2, 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();
|
||||
}
|
||||
|
||||
} // namespace ZShell::internal
|
||||
@@ -9,7 +9,7 @@ qml_module(ZShell-services
|
||||
cavaprovider.hpp cavaprovider.cpp
|
||||
desktopmodel.hpp desktopmodel.cpp
|
||||
desktopstatemanager.hpp desktopstatemanager.cpp
|
||||
hyprsunsetmanager.hpp hyprsunsetmanager.cpp
|
||||
nightlightmanager.hpp nightlightmanager.cpp
|
||||
tickingservice.hpp tickingservice.cpp
|
||||
sensorslib.hpp sensorslib.cpp
|
||||
usagefmt.hpp usagefmt.cpp
|
||||
@@ -18,6 +18,10 @@ qml_module(ZShell-services
|
||||
memory.hpp memory.cpp
|
||||
diskinfo.hpp diskinfo.cpp
|
||||
storage.hpp storage.cpp
|
||||
processes.hpp processes.cpp
|
||||
networkusage.hpp networkusage.cpp
|
||||
lyriccandidate.hpp lyriccandidate.cpp
|
||||
lyrics.hpp lyrics.cpp
|
||||
LIBRARIES
|
||||
Qt6::Core
|
||||
Qt6::Qml
|
||||
@@ -25,4 +29,13 @@ qml_module(ZShell-services
|
||||
PkgConfig::Aubio
|
||||
PkgConfig::Cava
|
||||
Sensors::Sensors
|
||||
ZShell-config
|
||||
ZShell-settings
|
||||
ZShell-internal
|
||||
ZShell
|
||||
zshell-util
|
||||
DEPENDENCIES
|
||||
ZShell
|
||||
ZShell.Config
|
||||
ZShell.Internal
|
||||
)
|
||||
|
||||
+345
-247
@@ -2,97 +2,187 @@
|
||||
|
||||
#include "sensorslib.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <qcontainerfwd.h>
|
||||
#include "config/rootnodes.hpp"
|
||||
#include "config/services.hpp"
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qfile.h>
|
||||
#include <qobject.h>
|
||||
#include <qdiriterator.h>
|
||||
#include <qregularexpression.h>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QTimer>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
using Qt::StringLiterals::operator""_s;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kTypeDetectScript =
|
||||
"if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null "
|
||||
"2>&1; then echo NVIDIA;"
|
||||
" elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep "
|
||||
"-q .; then echo GENERIC;"
|
||||
" else echo NONE; fi";
|
||||
QStringList cardFiles(const QString& relative) {
|
||||
static const QRegularExpression k_cardRe(u"^card\\d+$"_s);
|
||||
|
||||
constexpr const char* kNameDetectScript =
|
||||
"nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null"
|
||||
" || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' "
|
||||
"-f1"
|
||||
" || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1";
|
||||
QStringList files;
|
||||
QDirIterator it(u"/sys/class/drm"_s, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
const QString path = it.next();
|
||||
if (!k_cardRe.match(it.fileName()).hasMatch()) continue;
|
||||
const QString file = path + relative;
|
||||
if (QFile::exists(file)) files << file;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
std::optional<qreal> readNumber(const QString& path) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return std::nullopt;
|
||||
bool ok = false;
|
||||
const qreal v = f.readAll().trimmed().toDouble(&ok);
|
||||
if (!ok) return std::nullopt;
|
||||
return v;
|
||||
}
|
||||
|
||||
QString cleanName(QString s) {
|
||||
static const QRegularExpression k_noise(
|
||||
u"\\(R\\)|\\(TM\\)|Graphics"_s,
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression k_spaces(u"\\s+"_s);
|
||||
s.replace(k_noise, {});
|
||||
s.replace(k_spaces, u" "_s);
|
||||
return s.trimmed();
|
||||
}
|
||||
|
||||
QString parseNvidiaName(const QByteArray& out) {
|
||||
const QString first =
|
||||
QString::fromUtf8(out).split(u'\n').value(0).trimmed();
|
||||
return first.isEmpty() ? QString() : cleanName(first);
|
||||
}
|
||||
|
||||
QString parseGlxinfoName(const QByteArray& out) {
|
||||
const QStringList lines = QString::fromUtf8(out).split(u'\n');
|
||||
for (const QString& line : lines) {
|
||||
const qsizetype idx = line.indexOf(u"Device:"_s);
|
||||
if (idx < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString rest = line.mid(idx + 7);
|
||||
const qsizetype paren = rest.indexOf(u'(');
|
||||
if (paren >= 0) {
|
||||
rest = rest.left(paren);
|
||||
}
|
||||
|
||||
const QString cleaned = cleanName(rest);
|
||||
if (!cleaned.isEmpty()) {
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QString parseLspciName(const QByteArray& out) {
|
||||
static const QRegularExpression k_lineRe(
|
||||
u"vga|3d controller|display"_s,
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
const QStringList lines = QString::fromUtf8(out).split(u'\n');
|
||||
QString match;
|
||||
for (const QString& line : lines) {
|
||||
if (k_lineRe.match(line).hasMatch()) {
|
||||
match = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
static const QRegularExpression k_bracketRe(u"\\[([^\\]]+)\\][^\\[]*$"_s);
|
||||
const auto bracket = k_bracketRe.match(match);
|
||||
if (bracket.hasMatch()) {
|
||||
return cleanName(bracket.captured(1));
|
||||
}
|
||||
|
||||
static const QRegularExpression k_colonRe(u":\\s+(.+)"_s);
|
||||
const auto colon = k_colonRe.match(match);
|
||||
if (colon.hasMatch()) {
|
||||
return cleanName(colon.captured(1));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
struct NameSource {
|
||||
QString program;
|
||||
QStringList args;
|
||||
QString (*parse)(const QByteArray&);
|
||||
};
|
||||
|
||||
const std::array<NameSource, 3>& nameSources() {
|
||||
static const std::array<NameSource, 3> k_sources = {{
|
||||
{
|
||||
.program = u"nvidia-smi"_s,
|
||||
.args = {u"--query-gpu=name"_s, u"--format=csv,noheader"_s},
|
||||
.parse = &parseNvidiaName,
|
||||
},
|
||||
{
|
||||
.program = u"glxinfo"_s,
|
||||
.args = {u"-B"_s},
|
||||
.parse = &parseGlxinfoName,
|
||||
},
|
||||
{
|
||||
.program = u"lspci"_s,
|
||||
.args = {},
|
||||
.parse = &parseLspciName,
|
||||
},
|
||||
}};
|
||||
return k_sources;
|
||||
}
|
||||
|
||||
constexpr int k_nvidiaSource = 0;
|
||||
constexpr int k_firstGenericSource = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
Gpu::Gpu(QObject* parent) : TickingService(parent) {
|
||||
QString configPath =
|
||||
QDir::homePath() + QStringLiteral("/.config/zshell/config.json");
|
||||
auto* svc = config::ConfigSingleton::instance()->services();
|
||||
m_userType = svc->gpuType();
|
||||
QObject::connect(svc, &config::Services::gpuTypeChanged, this, [this, svc] {
|
||||
const GpuType value = svc->gpuType();
|
||||
if (value == m_userType) return;
|
||||
m_userType = value;
|
||||
resolveGpu();
|
||||
});
|
||||
|
||||
auto reloadConfig = [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (!doc.isNull()) {
|
||||
QJsonObject services =
|
||||
doc.object().value("services").toObject();
|
||||
setUserType(parseType(services.value("gpuType").toString()));
|
||||
}
|
||||
}
|
||||
};
|
||||
resolveGpu();
|
||||
|
||||
reloadConfig();
|
||||
m_restartTimer = new QTimer(this);
|
||||
m_restartTimer->setSingleShot(true);
|
||||
m_restartTimer->setInterval(500);
|
||||
QObject::connect(m_restartTimer, &QTimer::timeout, this, [this] {
|
||||
if (!m_nvidiaProc || m_type != GpuType::Nvidia) return;
|
||||
|
||||
static QFileSystemWatcher* watcher = new QFileSystemWatcher();
|
||||
if (!watcher->files().contains(configPath)) {
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
[this, configPath]() {
|
||||
QTimer::singleShot(100, this, [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (file.exists()) {
|
||||
QJsonDocument doc =
|
||||
QJsonDocument::fromJson(file.readAll());
|
||||
if (!doc.isNull()) {
|
||||
QJsonObject services =
|
||||
doc.object().value("services").toObject();
|
||||
setUserType(parseType(
|
||||
services.value("gpuType").toString()));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
watcher->addPath(configPath);
|
||||
stopNvidiaUsage();
|
||||
startNvidiaUsage();
|
||||
});
|
||||
|
||||
QObject::connect(this, &TickingService::updateIntervalChanged, this, [this] {
|
||||
if (!m_nvidiaProc) return;
|
||||
m_restartTimer->start();
|
||||
});
|
||||
}
|
||||
|
||||
Gpu::~Gpu() {
|
||||
if (!m_nvidiaProc) return;
|
||||
m_nvidiaProc->disconnect(this);
|
||||
m_nvidiaProc->terminate();
|
||||
if (!m_nvidiaProc->waitForFinished(500)) {
|
||||
m_nvidiaProc->kill();
|
||||
m_nvidiaProc->waitForFinished(200);
|
||||
}
|
||||
|
||||
// Detection must run before any ServiceRef appears: callers may gate the ref on
|
||||
// `type !== Gpu.None`, which would otherwise deadlock the detection.
|
||||
if (m_userType == Auto) {
|
||||
detectTypeOnce();
|
||||
}
|
||||
detectNameOnce();
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::type() const {
|
||||
return m_userType == Auto ? m_autoType : m_userType;
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::userType() const {
|
||||
return m_userType;
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::autoType() const {
|
||||
return m_autoType;
|
||||
GpuType Gpu::type() const {
|
||||
return m_type;
|
||||
}
|
||||
|
||||
QString Gpu::name() const {
|
||||
@@ -115,28 +205,17 @@ qreal Gpu::memoryTotal() const {
|
||||
return m_memoryTotal;
|
||||
}
|
||||
|
||||
void Gpu::setUserType(Type value) {
|
||||
if (value == m_userType) {
|
||||
return;
|
||||
}
|
||||
const Type prevDerived = type();
|
||||
m_userType = value;
|
||||
Q_EMIT userTypeChanged();
|
||||
if (type() != prevDerived) {
|
||||
Q_EMIT typeChanged();
|
||||
}
|
||||
bool Gpu::detecting() const {
|
||||
return m_detecting;
|
||||
}
|
||||
|
||||
void Gpu::setAutoType(Type value) {
|
||||
if (value == m_userType) {
|
||||
return;
|
||||
}
|
||||
const Type prevDerived = type();
|
||||
m_autoType = value;
|
||||
Q_EMIT autoTypeChanged();
|
||||
if (type() != prevDerived) {
|
||||
Q_EMIT typeChanged();
|
||||
}
|
||||
void Gpu::setType(GpuType value) {
|
||||
if (value == m_type) return;
|
||||
|
||||
m_type = value;
|
||||
if (m_type != GpuType::Nvidia) stopNvidiaUsage();
|
||||
if (m_type == GpuType::None) resetUsage();
|
||||
emit typeChanged();
|
||||
}
|
||||
|
||||
void Gpu::setName(QString value) {
|
||||
@@ -147,6 +226,12 @@ void Gpu::setName(QString value) {
|
||||
Q_EMIT nameChanged();
|
||||
}
|
||||
|
||||
void Gpu::setDetecting(bool value) {
|
||||
if (value == m_detecting) return;
|
||||
m_detecting = value;
|
||||
emit detectingChanged();
|
||||
}
|
||||
|
||||
void Gpu::setMemoryUsed(qreal value) {
|
||||
if (std::abs(m_memoryUsed - value) < 0.001) return;
|
||||
m_memoryUsed = value;
|
||||
@@ -160,168 +245,192 @@ void Gpu::setMemoryTotal(qreal value) {
|
||||
}
|
||||
|
||||
void Gpu::tick() {
|
||||
const Type t = type();
|
||||
if (t == Generic) {
|
||||
if (m_type == GpuType::Generic) {
|
||||
readGenericUsage();
|
||||
readGenericMemory();
|
||||
readGpuTemperature();
|
||||
} else if (t == Nvidia) {
|
||||
} else if (m_type == GpuType::Nvidia) {
|
||||
startNvidiaUsage();
|
||||
} else {
|
||||
if (std::abs(m_percentage) > 0.0001) {
|
||||
m_percentage = 0.0;
|
||||
Q_EMIT percentageChanged();
|
||||
}
|
||||
if (std::abs(m_temperature) > 0.05) {
|
||||
m_temperature = 0.0;
|
||||
Q_EMIT temperatureChanged();
|
||||
}
|
||||
resetUsage();
|
||||
}
|
||||
}
|
||||
|
||||
void Gpu::detectTypeOnce() {
|
||||
if (m_typeProc) {
|
||||
return;
|
||||
void Gpu::refreshSysfsCache() {
|
||||
m_busyFiles = cardFiles(u"/device/gpu_busy_percent"_s);
|
||||
|
||||
m_vramFiles.clear();
|
||||
const QStringList totals = cardFiles(u"/device/mem_info_vram_total"_s);
|
||||
for (const QString& total : totals) {
|
||||
QString used = total;
|
||||
used.replace(u"mem_info_vram_total"_s, u"mem_info_vram_used"_s);
|
||||
if (QFile::exists(used)) m_vramFiles.append({used, total});
|
||||
}
|
||||
m_typeProc = new QProcess(this);
|
||||
QObject::connect(
|
||||
m_typeProc,
|
||||
&QProcess::finished,
|
||||
this,
|
||||
[this](int, QProcess::ExitStatus) {
|
||||
const QByteArray out =
|
||||
m_typeProc->readAllStandardOutput().trimmed();
|
||||
if (!out.isEmpty()) {
|
||||
setAutoType(parseType(QString::fromLatin1(out)));
|
||||
}
|
||||
m_typeProc->deleteLater();
|
||||
m_typeProc = nullptr;
|
||||
});
|
||||
m_typeProc->start(
|
||||
QStringLiteral("sh"),
|
||||
{QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript)});
|
||||
}
|
||||
|
||||
void Gpu::detectNameOnce() {
|
||||
if (m_nameProc) {
|
||||
void Gpu::resolveGpu() {
|
||||
refreshSysfsCache();
|
||||
const int generation = ++m_generation;
|
||||
|
||||
if (m_userType != GpuType::Auto) setType(m_userType);
|
||||
|
||||
if (m_userType == GpuType::None) {
|
||||
setName({});
|
||||
setDetecting(false);
|
||||
return;
|
||||
}
|
||||
m_nameProc = new QProcess(this);
|
||||
|
||||
setName({});
|
||||
setDetecting(true);
|
||||
tryNameSource(
|
||||
m_userType == GpuType::Generic ? k_firstGenericSource : k_nvidiaSource,
|
||||
generation);
|
||||
}
|
||||
|
||||
int Gpu::probeEnd() const {
|
||||
return m_userType == GpuType::Nvidia
|
||||
? k_firstGenericSource
|
||||
: static_cast<int>(nameSources().size());
|
||||
}
|
||||
|
||||
void Gpu::tryNameSource(int index, int generation) {
|
||||
const NameSource& src = nameSources().at(static_cast<std::size_t>(index));
|
||||
runProcess(
|
||||
src.program,
|
||||
src.args,
|
||||
[this, index, generation, parse = src.parse](const QByteArray& out) {
|
||||
finishNameSource(index, generation, parse(out));
|
||||
});
|
||||
}
|
||||
|
||||
void Gpu::finishNameSource(int index, int generation, QString name) {
|
||||
if (generation != m_generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_userType == GpuType::Auto && index == k_nvidiaSource) {
|
||||
if (!name.isEmpty())
|
||||
setType(GpuType::Nvidia);
|
||||
else
|
||||
setType(m_busyFiles.isEmpty() ? GpuType::None : GpuType::Generic);
|
||||
|
||||
if (m_type == GpuType::None) {
|
||||
setName({});
|
||||
setDetecting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name.isEmpty()) {
|
||||
setName(std::move(name));
|
||||
setDetecting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const int next = index + 1;
|
||||
if (next < probeEnd()) {
|
||||
tryNameSource(next, generation);
|
||||
} else {
|
||||
setName({});
|
||||
setDetecting(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Gpu::runProcess(
|
||||
const QString& program,
|
||||
const QStringList& args,
|
||||
std::function<void(const QByteArray&)> callback) {
|
||||
auto* proc = new QProcess(this);
|
||||
proc->setStandardErrorFile(QProcess::nullDevice());
|
||||
|
||||
const auto finish =
|
||||
[proc, callback = std::move(callback)](const QByteArray& out) {
|
||||
callback(out);
|
||||
proc->deleteLater();
|
||||
};
|
||||
|
||||
QObject::connect(
|
||||
m_nameProc,
|
||||
proc,
|
||||
&QProcess::finished,
|
||||
this,
|
||||
[this](int, QProcess::ExitStatus) {
|
||||
const QString output =
|
||||
QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed();
|
||||
if (!output.isEmpty()) {
|
||||
const QString lower = output.toLower();
|
||||
if (lower.contains(QStringLiteral("nvidia")) ||
|
||||
lower.contains(QStringLiteral("geforce")) ||
|
||||
lower.contains(QStringLiteral("rtx")) ||
|
||||
lower.contains(QStringLiteral("gtx")) ||
|
||||
lower.contains(QStringLiteral("rx"))) {
|
||||
setName(cleanName(output));
|
||||
} else {
|
||||
static const QRegularExpression bracketRe(
|
||||
QStringLiteral("\\[([^\\]]+)\\][^\\[]*$"));
|
||||
const auto bracket = bracketRe.match(output);
|
||||
if (bracket.hasMatch()) {
|
||||
setName(cleanName(bracket.captured(1)));
|
||||
} else {
|
||||
static const QRegularExpression colonRe(
|
||||
QStringLiteral(":\\s*(.+)"));
|
||||
const auto colon = colonRe.match(output);
|
||||
if (colon.hasMatch()) {
|
||||
setName(cleanName(colon.captured(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m_nameProc->deleteLater();
|
||||
m_nameProc = nullptr;
|
||||
[finish, proc](int code, QProcess::ExitStatus status) {
|
||||
const bool ok = status == QProcess::NormalExit && code == 0;
|
||||
finish(ok ? proc->readAllStandardOutput() : QByteArray());
|
||||
});
|
||||
m_nameProc->start(
|
||||
QStringLiteral("sh"),
|
||||
{QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript)});
|
||||
QObject::connect(
|
||||
proc,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
[finish](QProcess::ProcessError err) {
|
||||
if (err == QProcess::FailedToStart) {
|
||||
finish(QByteArray());
|
||||
}
|
||||
});
|
||||
|
||||
proc->start(program, args);
|
||||
}
|
||||
|
||||
void Gpu::onStopped() {
|
||||
stopNvidiaUsage();
|
||||
}
|
||||
|
||||
void Gpu::readGenericMemory() {
|
||||
const QStringList paths = QDir(QStringLiteral("/sys/class/drm"))
|
||||
.entryList(
|
||||
QStringList() << QStringLiteral("card*"),
|
||||
QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
qreal totalMem = 0.0;
|
||||
qreal usedMem = 0.0;
|
||||
for (const QString& card : paths) {
|
||||
QFile total(
|
||||
QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_total")
|
||||
.arg(card));
|
||||
if (!total.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const qreal v = total.readAll().trimmed().toDouble(&ok);
|
||||
total.close();
|
||||
if (ok) {
|
||||
totalMem += v;
|
||||
}
|
||||
|
||||
QFile used(QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_used")
|
||||
.arg(card));
|
||||
if (!used.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
bool ok1 = false;
|
||||
const qreal v1 = used.readAll().trimmed().toDouble(&ok1);
|
||||
used.close();
|
||||
if (ok1) {
|
||||
usedMem += v1;
|
||||
}
|
||||
qreal total = 0.0;
|
||||
qreal used = 0.0;
|
||||
for (const VramFiles& v : std::as_const(m_vramFiles)) {
|
||||
const auto t = readNumber(v.total);
|
||||
const auto u = readNumber(v.used);
|
||||
if (!t || !u) continue;
|
||||
total += *t;
|
||||
used += *u;
|
||||
}
|
||||
|
||||
setMemoryTotal(totalMem);
|
||||
setMemoryUsed(usedMem);
|
||||
setMemoryTotal(total);
|
||||
setMemoryUsed(used);
|
||||
}
|
||||
|
||||
void Gpu::readGenericUsage() {
|
||||
const QStringList paths = QDir(QStringLiteral("/sys/class/drm"))
|
||||
.entryList(
|
||||
QStringList() << QStringLiteral("card*"),
|
||||
QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
qreal sum = 0.0;
|
||||
int count = 0;
|
||||
for (const QString& card : paths) {
|
||||
QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent")
|
||||
.arg(card));
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const qreal v = f.readAll().trimmed().toDouble(&ok);
|
||||
f.close();
|
||||
if (ok) {
|
||||
sum += v;
|
||||
for (const QString& path : std::as_const(m_busyFiles)) {
|
||||
if (const auto v = readNumber(path)) {
|
||||
sum += *v;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0;
|
||||
if (std::abs(newPerc - m_percentage) > 0.0001) {
|
||||
m_percentage = newPerc;
|
||||
Q_EMIT percentageChanged();
|
||||
emit percentageChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Gpu::startNvidiaUsage() {
|
||||
if (m_nvidiaProc) {
|
||||
void Gpu::stopNvidiaUsage() {
|
||||
m_restartTimer->stop();
|
||||
|
||||
if (!m_nvidiaProc) return;
|
||||
|
||||
QProcess* proc = m_nvidiaProc;
|
||||
m_nvidiaProc = nullptr;
|
||||
|
||||
proc->disconnect(this);
|
||||
if (proc->state() == QProcess::NotRunning) {
|
||||
proc->deleteLater();
|
||||
return;
|
||||
}
|
||||
m_nvidiaProc = new QProcess(this);
|
||||
QObject::connect(proc, &QProcess::finished, proc, &QObject::deleteLater);
|
||||
proc->terminate();
|
||||
}
|
||||
|
||||
void Gpu::startNvidiaUsage() {
|
||||
if (m_nvidiaProc) return;
|
||||
|
||||
auto* proc = new QProcess(this);
|
||||
m_nvidiaProc = proc;
|
||||
|
||||
QObject::connect(
|
||||
m_nvidiaProc, &QProcess::readyReadStandardOutput, this, [this]() {
|
||||
while (m_nvidiaProc->canReadLine()) {
|
||||
const QByteArray out = m_nvidiaProc->readLine();
|
||||
proc, &QProcess::readyReadStandardOutput, this, [this, proc]() {
|
||||
while (proc->canReadLine()) {
|
||||
const QByteArray out = proc->readLine();
|
||||
const QString output = QString::fromUtf8(out).trimmed();
|
||||
if (output.isEmpty()) continue;
|
||||
|
||||
@@ -357,21 +466,21 @@ void Gpu::startNvidiaUsage() {
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this]() {
|
||||
m_nvidiaProc->deleteLater();
|
||||
m_nvidiaProc = nullptr;
|
||||
});
|
||||
auto cleanup = [this, proc]() {
|
||||
if (m_nvidiaProc == proc) m_nvidiaProc = nullptr;
|
||||
|
||||
proc->deleteLater();
|
||||
};
|
||||
|
||||
QObject::connect(proc, &QProcess::finished, this, cleanup);
|
||||
|
||||
QObject::connect(
|
||||
m_nvidiaProc,
|
||||
proc,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
[this](QProcess::ProcessError) {
|
||||
m_nvidiaProc->deleteLater();
|
||||
m_nvidiaProc = nullptr;
|
||||
});
|
||||
[cleanup](QProcess::ProcessError) { cleanup(); });
|
||||
|
||||
m_nvidiaProc->start(
|
||||
proc->start(
|
||||
QStringLiteral("nvidia-smi"),
|
||||
{QStringLiteral(
|
||||
"--query-gpu=utilization.gpu,temperature.gpu,"
|
||||
@@ -386,32 +495,21 @@ void Gpu::readGpuTemperature() {
|
||||
const qreal newTemp = t.value_or(0.0);
|
||||
if (std::abs(newTemp - m_temperature) > 0.05) {
|
||||
m_temperature = newTemp;
|
||||
Q_EMIT temperatureChanged();
|
||||
emit temperatureChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Gpu::Type Gpu::parseType(const QString& s) {
|
||||
const QString u = s.trimmed().toUpper();
|
||||
if (u.isEmpty()) {
|
||||
return Auto;
|
||||
void Gpu::resetUsage() {
|
||||
if (std::abs(m_percentage) > 0.0001) {
|
||||
m_percentage = 0.0;
|
||||
emit percentageChanged();
|
||||
}
|
||||
if (u == QStringLiteral("NVIDIA")) {
|
||||
return Nvidia;
|
||||
if (std::abs(m_temperature) > 0.05) {
|
||||
m_temperature = 0.0;
|
||||
emit temperatureChanged();
|
||||
}
|
||||
if (u == QStringLiteral("GENERIC")) {
|
||||
return Generic;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
QString Gpu::cleanName(QString s) {
|
||||
static const QRegularExpression noise(
|
||||
QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression spaces(QStringLiteral("\\s+"));
|
||||
s.replace(noise, QString());
|
||||
s.replace(spaces, QStringLiteral(" "));
|
||||
return s.trimmed();
|
||||
setMemoryUsed(0.0);
|
||||
setMemoryTotal(0.0);
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "tickingservice.hpp"
|
||||
#include "config/enums.hpp"
|
||||
|
||||
#include <qprocess.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
using GpuType = config::GpuType::Enum;
|
||||
|
||||
struct VramFiles {
|
||||
QString used;
|
||||
QString total;
|
||||
};
|
||||
|
||||
class Gpu : public TickingService {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
enum Type {
|
||||
Auto, // user override is empty (config "") — defer to detected autoType
|
||||
None, // no usable GPU
|
||||
Nvidia, // queried via nvidia-smi
|
||||
Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent
|
||||
};
|
||||
Q_ENUM(Type)
|
||||
|
||||
private:
|
||||
Q_PROPERTY(Type type READ type NOTIFY typeChanged)
|
||||
Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged)
|
||||
Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged)
|
||||
Q_PROPERTY(GpuType type READ type NOTIFY typeChanged)
|
||||
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
|
||||
Q_PROPERTY(bool detecting READ detecting NOTIFY detectingChanged)
|
||||
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
|
||||
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
|
||||
Q_PROPERTY(qreal memoryUsed READ memoryUsed NOTIFY memoryUsedChanged)
|
||||
@@ -33,11 +31,11 @@ class Gpu : public TickingService {
|
||||
|
||||
public:
|
||||
explicit Gpu(QObject* parent = nullptr);
|
||||
~Gpu() override;
|
||||
|
||||
[[nodiscard]] Type type() const;
|
||||
[[nodiscard]] Type userType() const;
|
||||
[[nodiscard]] Type autoType() const;
|
||||
[[nodiscard]] GpuType type() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] bool detecting() const;
|
||||
[[nodiscard]] qreal percentage() const;
|
||||
[[nodiscard]] qreal temperature() const;
|
||||
[[nodiscard]] qreal memoryUsed() const;
|
||||
@@ -45,9 +43,8 @@ class Gpu : public TickingService {
|
||||
|
||||
signals:
|
||||
void typeChanged();
|
||||
void userTypeChanged();
|
||||
void autoTypeChanged();
|
||||
void nameChanged();
|
||||
void detectingChanged();
|
||||
void percentageChanged();
|
||||
void temperatureChanged();
|
||||
void memoryUsedChanged();
|
||||
@@ -55,35 +52,52 @@ class Gpu : public TickingService {
|
||||
|
||||
protected:
|
||||
void tick() override;
|
||||
void onStopped() override;
|
||||
|
||||
private:
|
||||
void detectTypeOnce();
|
||||
void detectNameOnce();
|
||||
void resolveGpu();
|
||||
|
||||
[[nodiscard]] int probeEnd() const;
|
||||
|
||||
void tryNameSource(int index, int generation);
|
||||
void finishNameSource(int index, int generation, QString name);
|
||||
|
||||
void readGenericUsage();
|
||||
void stopNvidiaUsage();
|
||||
void startNvidiaUsage();
|
||||
void readGpuTemperature();
|
||||
void readGenericMemory();
|
||||
void resetUsage();
|
||||
|
||||
void setUserType(Type value);
|
||||
void setAutoType(Type value);
|
||||
void runProcess(
|
||||
const QString& program,
|
||||
const QStringList& args,
|
||||
std::function<void(const QByteArray&)> callback);
|
||||
|
||||
void setType(GpuType value);
|
||||
void setName(QString value);
|
||||
void setDetecting(bool value);
|
||||
void setMemoryUsed(qreal value);
|
||||
void setMemoryTotal(qreal value);
|
||||
|
||||
[[nodiscard]] static Type parseType(const QString& s);
|
||||
[[nodiscard]] static QString cleanName(QString s);
|
||||
void refreshSysfsCache();
|
||||
|
||||
Type m_userType = Auto;
|
||||
Type m_autoType = None;
|
||||
GpuType m_userType = GpuType::Auto;
|
||||
GpuType m_type = GpuType::None;
|
||||
QString m_name;
|
||||
bool m_detecting = false;
|
||||
qreal m_percentage = 0.0;
|
||||
qreal m_temperature = 0.0;
|
||||
qreal m_memoryUsed = 0.0;
|
||||
qreal m_memoryTotal = 0.0;
|
||||
|
||||
QProcess* m_typeProc = nullptr;
|
||||
QProcess* m_nameProc = nullptr;
|
||||
QStringList m_busyFiles;
|
||||
QList<VramFiles> m_vramFiles;
|
||||
int m_generation = 0;
|
||||
bool m_nvidiaQuerying = false;
|
||||
|
||||
QProcess* m_nvidiaProc = nullptr;
|
||||
QTimer* m_restartTimer = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
#include "hyprsunsetmanager.hpp"
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qprocess.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
HyprsunsetManager::HyprsunsetManager(QObject* parent) : QObject(parent) {
|
||||
connect(&m_timer, &QTimer::timeout, this, &HyprsunsetManager::apply);
|
||||
connect(&m_manualTimer, &QTimer::timeout, this, [this] {
|
||||
m_manualToggle = false;
|
||||
emit manualToggleChanged();
|
||||
apply();
|
||||
});
|
||||
connect(&m_startCooldown, &QTimer::timeout, this, [this] {
|
||||
m_startAllowed = true;
|
||||
apply();
|
||||
});
|
||||
|
||||
m_startCooldown.start(2000);
|
||||
m_manualTimer.setSingleShot(true);
|
||||
m_timer.start(60000);
|
||||
|
||||
m_process.setStandardInputFile(QProcess::nullDevice());
|
||||
m_process.setStandardOutputFile(QProcess::nullDevice());
|
||||
}
|
||||
|
||||
int HyprsunsetManager::startTime() const {
|
||||
return m_startTime;
|
||||
}
|
||||
|
||||
int HyprsunsetManager::endTime() const {
|
||||
return m_endTime;
|
||||
}
|
||||
|
||||
bool HyprsunsetManager::manualToggle() const {
|
||||
return m_manualToggle;
|
||||
}
|
||||
|
||||
bool HyprsunsetManager::enabled() const {
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
bool HyprsunsetManager::activeAuto() const {
|
||||
return m_activeAuto;
|
||||
}
|
||||
|
||||
int HyprsunsetManager::temp() const {
|
||||
return m_temp;
|
||||
}
|
||||
|
||||
void HyprsunsetManager::setActiveAuto(bool activeAuto) {
|
||||
if (activeAuto == m_activeAuto) return;
|
||||
|
||||
m_activeAuto = activeAuto;
|
||||
emit activeAutoChanged();
|
||||
}
|
||||
|
||||
void HyprsunsetManager::setManualToggle(bool toggle) {
|
||||
if (toggle == m_manualToggle) return;
|
||||
|
||||
m_manualToggle = toggle;
|
||||
emit manualToggleChanged();
|
||||
|
||||
m_manualTimer.start(60 * 60 * 1000);
|
||||
}
|
||||
|
||||
void HyprsunsetManager::setEndTime(const int& time) {
|
||||
if (time == m_endTime) return;
|
||||
|
||||
m_endTime = time;
|
||||
emit endTimeChanged();
|
||||
apply();
|
||||
}
|
||||
|
||||
void HyprsunsetManager::setStartTime(const int& time) {
|
||||
if (time == m_startTime) return;
|
||||
|
||||
m_startTime = time;
|
||||
emit startTimeChanged();
|
||||
apply();
|
||||
}
|
||||
|
||||
void HyprsunsetManager::setTemp(const int& temp) {
|
||||
if (temp == m_temp) return;
|
||||
|
||||
m_temp = temp;
|
||||
emit tempChanged();
|
||||
apply();
|
||||
}
|
||||
|
||||
void HyprsunsetManager::toggle() {
|
||||
if (m_enabled) {
|
||||
end();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void HyprsunsetManager::start() {
|
||||
if (m_enabled && m_initialized) return;
|
||||
|
||||
m_initialized = true;
|
||||
m_enabled = true;
|
||||
|
||||
emit enabledChanged();
|
||||
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments(
|
||||
{"hyprsunset", "temperature", QString::number(m_temp)});
|
||||
m_process.startDetached();
|
||||
}
|
||||
|
||||
void HyprsunsetManager::end() {
|
||||
if (!m_enabled && m_initialized) return;
|
||||
|
||||
m_initialized = true;
|
||||
m_enabled = false;
|
||||
|
||||
emit enabledChanged();
|
||||
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments({"hyprsunset", "identity"});
|
||||
m_process.startDetached();
|
||||
}
|
||||
|
||||
void HyprsunsetManager::apply() {
|
||||
if (m_manualToggle || !m_activeAuto || !m_startAllowed) return;
|
||||
|
||||
const auto current = QTime::currentTime();
|
||||
const auto currentMin = current.hour() * 60 + current.minute();
|
||||
bool isDarkTime = false;
|
||||
|
||||
if (m_startTime <= m_endTime) {
|
||||
isDarkTime = (currentMin >= m_startTime && currentMin < m_endTime);
|
||||
} else {
|
||||
isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime);
|
||||
}
|
||||
|
||||
if (isDarkTime) {
|
||||
start();
|
||||
} else {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace ZShell::services
|
||||
@@ -1,71 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QTime>
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class HyprsunsetManager : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged)
|
||||
Q_PROPERTY(
|
||||
int startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
|
||||
Q_PROPERTY(int endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged)
|
||||
Q_PROPERTY(int temp READ temp WRITE setTemp NOTIFY tempChanged)
|
||||
Q_PROPERTY(
|
||||
bool activeAuto READ activeAuto WRITE setActiveAuto NOTIFY
|
||||
activeAutoChanged)
|
||||
Q_PROPERTY(
|
||||
bool manualToggle READ manualToggle WRITE setManualToggle NOTIFY
|
||||
manualToggleChanged)
|
||||
|
||||
public:
|
||||
explicit HyprsunsetManager(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] int startTime() const;
|
||||
[[nodiscard]] int endTime() const;
|
||||
[[nodiscard]] bool enabled() const;
|
||||
[[nodiscard]] int temp() const;
|
||||
[[nodiscard]] bool activeAuto() const;
|
||||
[[nodiscard]] bool manualToggle() const;
|
||||
|
||||
Q_INVOKABLE void toggle();
|
||||
Q_INVOKABLE void apply();
|
||||
|
||||
void setStartTime(const int& time);
|
||||
void setEndTime(const int& time);
|
||||
void setTemp(const int& temp);
|
||||
void setActiveAuto(bool activeAuto);
|
||||
void setManualToggle(bool toggle);
|
||||
|
||||
signals:
|
||||
void enabledChanged();
|
||||
void startTimeChanged();
|
||||
void activeAutoChanged();
|
||||
void endTimeChanged();
|
||||
void tempChanged();
|
||||
void manualToggleChanged();
|
||||
|
||||
private:
|
||||
int m_startTime;
|
||||
int m_endTime;
|
||||
bool m_enabled = false;
|
||||
bool m_manualToggle = false;
|
||||
bool m_activeAuto;
|
||||
bool m_startAllowed = false;
|
||||
bool m_initialized = false;
|
||||
QTimer m_startCooldown;
|
||||
int m_temp;
|
||||
QProcess m_process;
|
||||
QTimer m_timer;
|
||||
QTimer m_manualTimer;
|
||||
void start();
|
||||
void end();
|
||||
};
|
||||
|
||||
}; // namespace ZShell::services
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "lyriccandidate.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
LyricCandidate::LyricCandidate(
|
||||
LyricsBackend backend,
|
||||
QString id,
|
||||
QString title,
|
||||
QString artist,
|
||||
QString album,
|
||||
qreal duration)
|
||||
: m_backend(backend)
|
||||
, m_id(std::move(id))
|
||||
, m_title(std::move(title))
|
||||
, m_artist(std::move(artist))
|
||||
, m_album(std::move(album))
|
||||
, m_duration(duration) {}
|
||||
|
||||
LyricsBackend LyricCandidate::backend() const {
|
||||
return m_backend;
|
||||
}
|
||||
|
||||
QString LyricCandidate::id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
QString LyricCandidate::title() const {
|
||||
return m_title;
|
||||
}
|
||||
|
||||
QString LyricCandidate::artist() const {
|
||||
return m_artist;
|
||||
}
|
||||
|
||||
QString LyricCandidate::album() const {
|
||||
return m_album;
|
||||
}
|
||||
|
||||
qreal LyricCandidate::duration() const {
|
||||
return m_duration;
|
||||
}
|
||||
|
||||
bool LyricCandidate::isValid() const {
|
||||
return !m_id.isEmpty();
|
||||
}
|
||||
|
||||
bool LyricCandidate::operator==(const LyricCandidate& o) const noexcept {
|
||||
return m_backend == o.m_backend && m_id == o.m_id;
|
||||
}
|
||||
|
||||
bool LyricCandidate::operator!=(const LyricCandidate& o) const noexcept {
|
||||
return !(*this == o);
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
#include "config/enums.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
using LyricsBackend = config::LyricsBackend::Enum;
|
||||
|
||||
class LyricCandidate {
|
||||
Q_GADGET
|
||||
QML_VALUE_TYPE(lyricCandidate)
|
||||
|
||||
Q_PROPERTY(ZShell::config::LyricsBackend::Enum backend READ backend)
|
||||
Q_PROPERTY(QString id READ id)
|
||||
Q_PROPERTY(QString title READ title)
|
||||
Q_PROPERTY(QString artist READ artist)
|
||||
Q_PROPERTY(QString album READ album)
|
||||
Q_PROPERTY(qreal duration READ duration)
|
||||
|
||||
public:
|
||||
LyricCandidate() = default;
|
||||
LyricCandidate(
|
||||
LyricsBackend backend,
|
||||
QString id,
|
||||
QString title,
|
||||
QString artist,
|
||||
QString album = {},
|
||||
qreal duration = 0.0);
|
||||
|
||||
[[nodiscard]] LyricsBackend backend() const;
|
||||
[[nodiscard]] QString id() const;
|
||||
[[nodiscard]] QString title() const;
|
||||
[[nodiscard]] QString artist() const;
|
||||
[[nodiscard]] QString album() const;
|
||||
[[nodiscard]] qreal duration() const;
|
||||
|
||||
[[nodiscard]] bool isValid() const;
|
||||
bool operator==(const LyricCandidate& o) const noexcept;
|
||||
bool operator!=(const LyricCandidate& o) const noexcept;
|
||||
|
||||
private:
|
||||
LyricsBackend m_backend = LyricsBackend::Auto;
|
||||
QString m_id;
|
||||
QString m_title;
|
||||
QString m_artist;
|
||||
QString m_album;
|
||||
qreal m_duration = 0.0;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include <qhash.h>
|
||||
#include <qjsonobject.h>
|
||||
#include <qnetworkaccessmanager.h>
|
||||
#include <qnetworkreply.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
#include "lyriccandidate.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
struct LyricLine {
|
||||
qreal time = 0.0;
|
||||
QString text;
|
||||
};
|
||||
|
||||
class Lyrics : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(QStringList lyrics READ lyrics NOTIFY lyricsChanged)
|
||||
Q_PROPERTY(
|
||||
ZShell::config::LyricsBackend::Enum backend READ backend NOTIFY
|
||||
backendChanged)
|
||||
Q_PROPERTY(
|
||||
ZShell::config::LyricsBackend::Enum preferredBackend READ
|
||||
preferredBackend WRITE setPreferredBackend NOTIFY
|
||||
preferredBackendChanged)
|
||||
Q_PROPERTY(
|
||||
QList<ZShell::services::LyricCandidate> lyricCandidates READ
|
||||
lyricCandidates NOTIFY lyricCandidatesChanged)
|
||||
Q_PROPERTY(
|
||||
ZShell::services::LyricCandidate selectedCandidate READ selectedCandidate
|
||||
WRITE setSelectedCandidate NOTIFY selectedCandidateChanged)
|
||||
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
|
||||
Q_PROPERTY(bool hasLyrics READ hasLyrics NOTIFY hasLyricsChanged)
|
||||
Q_PROPERTY(qreal offset READ offset WRITE setOffset NOTIFY offsetChanged)
|
||||
Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY trackChanged)
|
||||
Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackChanged)
|
||||
|
||||
public:
|
||||
explicit Lyrics(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QStringList lyrics() const;
|
||||
[[nodiscard]] LyricsBackend backend() const;
|
||||
[[nodiscard]] LyricsBackend preferredBackend() const;
|
||||
void setPreferredBackend(LyricsBackend value);
|
||||
[[nodiscard]] QList<LyricCandidate> lyricCandidates() const;
|
||||
[[nodiscard]] LyricCandidate selectedCandidate() const;
|
||||
void setSelectedCandidate(const LyricCandidate& value);
|
||||
[[nodiscard]] bool loading() const;
|
||||
[[nodiscard]] bool hasLyrics() const;
|
||||
[[nodiscard]] qreal offset() const;
|
||||
void setOffset(qreal value);
|
||||
[[nodiscard]] QString trackArtist() const;
|
||||
[[nodiscard]] QString trackTitle() const;
|
||||
|
||||
[[nodiscard]] Q_INVOKABLE int indexForTime(qreal time) const;
|
||||
[[nodiscard]] Q_INVOKABLE qreal timeForIndex(int index) const;
|
||||
Q_INVOKABLE void setTrack(
|
||||
const QString& artist,
|
||||
const QString& title,
|
||||
const QString& album = {},
|
||||
qreal duration = 0.0);
|
||||
Q_INVOKABLE void clearTrack();
|
||||
Q_INVOKABLE void refresh();
|
||||
|
||||
signals:
|
||||
void lyricsChanged();
|
||||
void backendChanged();
|
||||
void preferredBackendChanged();
|
||||
void lyricCandidatesChanged();
|
||||
void selectedCandidateChanged();
|
||||
void loadingChanged();
|
||||
void hasLyricsChanged();
|
||||
void offsetChanged();
|
||||
void trackChanged();
|
||||
|
||||
private:
|
||||
void setBackend(LyricsBackend value);
|
||||
void setLoading(bool value);
|
||||
void setLines(QVector<LyricLine> lines, LyricsBackend source);
|
||||
void clearLines();
|
||||
void appendCandidates(const QList<LyricCandidate>& add);
|
||||
void clearCandidates();
|
||||
|
||||
void scheduleLoad();
|
||||
void doLoad();
|
||||
void cancelInFlight();
|
||||
int newRequestId();
|
||||
|
||||
void tryLocal(int reqId);
|
||||
void tryLrclib(int reqId);
|
||||
void tryNetEase(int reqId);
|
||||
void chainNext(LyricsBackend justFailed, int reqId);
|
||||
|
||||
void searchLrclibCandidates(int reqId);
|
||||
void searchNetEaseCandidates(int reqId);
|
||||
|
||||
void fetchLrclibById(const QString& id, int reqId);
|
||||
void fetchNetEaseLyricsById(const QString& id, int reqId);
|
||||
|
||||
QNetworkReply* getJson(
|
||||
const QUrl& url, const QHash<QByteArray, QByteArray>& headers = {});
|
||||
void trackReply(int reqId, QNetworkReply* reply);
|
||||
|
||||
void onPreferredBackendConfigChanged();
|
||||
void onLyricsDirChanged();
|
||||
|
||||
void loadLyricsMap();
|
||||
void persistTrackPrefs();
|
||||
|
||||
[[nodiscard]] static QString lyricsDir();
|
||||
[[nodiscard]] static QString lyricsMapPath();
|
||||
[[nodiscard]] QString trackKey() const;
|
||||
[[nodiscard]] static QString backendKey(LyricsBackend value);
|
||||
[[nodiscard]] static LyricsBackend backendFromKey(const QString& key);
|
||||
|
||||
[[nodiscard]] static const QString& stateDir();
|
||||
[[nodiscard]] static const QString& cacheDir();
|
||||
[[nodiscard]] static QString cachePathFor(
|
||||
LyricsBackend backend, const QString& id);
|
||||
[[nodiscard]] static QString readCachedLrc(
|
||||
LyricsBackend backend, const QString& id);
|
||||
static void writeCachedLrc(
|
||||
LyricsBackend backend, const QString& id, const QString& text);
|
||||
|
||||
[[nodiscard]] static QVector<LyricLine> parseLrc(const QString& text);
|
||||
[[nodiscard]] static QString tryReadLocalLrc(
|
||||
const QString& dir, const QString& artist, const QString& title);
|
||||
[[nodiscard]] static QString findLocalLrcRecursive(
|
||||
const QString& dir, const QString& artist, const QString& title);
|
||||
|
||||
QNetworkAccessManager* m_nam;
|
||||
QTimer* m_loadDebounce;
|
||||
|
||||
QVector<LyricLine> m_lines;
|
||||
QStringList m_lyrics;
|
||||
LyricsBackend m_backend = LyricsBackend::Auto;
|
||||
LyricsBackend m_preferredBackend = LyricsBackend::Auto;
|
||||
QList<LyricCandidate> m_candidates;
|
||||
LyricCandidate m_selected;
|
||||
bool m_loading = false;
|
||||
bool m_hasLyrics = false;
|
||||
qreal m_offset = 0.0;
|
||||
|
||||
QString m_artist;
|
||||
QString m_title;
|
||||
QString m_album;
|
||||
qreal m_duration = 0.0;
|
||||
|
||||
int m_currentRequestId = 0;
|
||||
QHash<int, QList<QPointer<QNetworkReply>>> m_pendingReplies;
|
||||
|
||||
QJsonObject m_lyricsMap;
|
||||
bool m_lyricsMapLoaded = false;
|
||||
bool m_settingFromPrefs = false;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -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,7 @@ 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 +60,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
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "networkusage.hpp"
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qfile.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <system_error>
|
||||
|
||||
#include "config/rootnodes.hpp"
|
||||
#include "config/dashboard.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
using Qt::StringLiterals::operator""_s;
|
||||
|
||||
NetworkUsage::NetworkUsage(QObject* parent)
|
||||
: TickingService(parent)
|
||||
, m_downloadBuffer(new internal::CircularBuffer(this))
|
||||
, m_uploadBuffer(new internal::CircularBuffer(this)) {
|
||||
m_downloadBuffer->setSmoothingRadius(2);
|
||||
m_uploadBuffer->setSmoothingRadius(2);
|
||||
|
||||
auto* resources =
|
||||
config::ConfigSingleton::instance()->dashboard()->performance();
|
||||
setHistoryLength(resources->showVram() ? 70 : 30);
|
||||
QObject::connect(
|
||||
resources,
|
||||
&ZShell::config::Performance::showVramChanged,
|
||||
this,
|
||||
[this, resources] {
|
||||
setHistoryLength(resources->showVram() ? 70 : 30);
|
||||
});
|
||||
}
|
||||
|
||||
qreal NetworkUsage::downloadSpeed() const {
|
||||
return m_downloadSpeed;
|
||||
}
|
||||
|
||||
qreal NetworkUsage::uploadSpeed() const {
|
||||
return m_uploadSpeed;
|
||||
}
|
||||
|
||||
qreal NetworkUsage::downloadTotal() const {
|
||||
return m_downloadTotal;
|
||||
}
|
||||
|
||||
qreal NetworkUsage::uploadTotal() const {
|
||||
return m_uploadTotal;
|
||||
}
|
||||
|
||||
int NetworkUsage::historyLength() const {
|
||||
return m_historyLength;
|
||||
}
|
||||
|
||||
void NetworkUsage::setHistoryLength(int length) {
|
||||
if (length <= 0 || length == m_historyLength) {
|
||||
return;
|
||||
}
|
||||
m_historyLength = length;
|
||||
m_downloadBuffer->setCapacity(m_historyLength + 1);
|
||||
m_uploadBuffer->setCapacity(m_historyLength + 1);
|
||||
Q_EMIT historyLengthChanged();
|
||||
}
|
||||
|
||||
internal::CircularBuffer* NetworkUsage::downloadBuffer() const {
|
||||
return m_downloadBuffer;
|
||||
}
|
||||
|
||||
internal::CircularBuffer* NetworkUsage::uploadBuffer() const {
|
||||
return m_uploadBuffer;
|
||||
}
|
||||
|
||||
void NetworkUsage::tick() {
|
||||
QFile f(u"/proc/net/dev"_s);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
// Skip headers
|
||||
f.readLine();
|
||||
f.readLine();
|
||||
|
||||
quint64 totalRx = 0;
|
||||
quint64 totalTx = 0;
|
||||
|
||||
while (!f.atEnd()) {
|
||||
const QByteArray line = f.readLine();
|
||||
const qsizetype splitIdx = line.indexOf(':');
|
||||
if (splitIdx == -1) {
|
||||
continue;
|
||||
}
|
||||
const QByteArray iface = line.left(splitIdx).trimmed();
|
||||
if (iface == QByteArrayView("lo")) {
|
||||
continue; // Skip loopback interface
|
||||
}
|
||||
|
||||
// Parse every counter through tx bytes to validate the row
|
||||
const char* pos = line.constData() + splitIdx + 1;
|
||||
const char* const end = line.constData() + line.size();
|
||||
|
||||
std::array<unsigned long long, 9> fields{};
|
||||
bool valid = true;
|
||||
for (unsigned long long& field : fields) {
|
||||
while (pos < end && (*pos == ' ' || *pos == '\t'))
|
||||
++pos;
|
||||
|
||||
const auto [next, ec] = std::from_chars(pos, end, field);
|
||||
if (ec != std::errc{}) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
pos = next;
|
||||
}
|
||||
|
||||
if (!valid) continue;
|
||||
|
||||
totalRx += static_cast<quint64>(fields[0]);
|
||||
totalTx += static_cast<quint64>(fields[8]);
|
||||
}
|
||||
f.close();
|
||||
|
||||
if (!m_initialized) {
|
||||
m_prevRx = totalRx;
|
||||
m_prevTx = totalTx;
|
||||
m_timer.start();
|
||||
m_initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal elapsed = static_cast<qreal>(m_timer.restart()) / 1000.0;
|
||||
const quint64 rxDelta = totalRx >= m_prevRx ? totalRx - m_prevRx : 0;
|
||||
const quint64 txDelta = totalTx >= m_prevTx ? totalTx - m_prevTx : 0;
|
||||
|
||||
m_downloadTotal += static_cast<qreal>(rxDelta);
|
||||
m_uploadTotal += static_cast<qreal>(txDelta);
|
||||
|
||||
if (elapsed > 0.0) {
|
||||
// Calculate speeds
|
||||
m_downloadSpeed = static_cast<qreal>(rxDelta) / elapsed;
|
||||
m_uploadSpeed = static_cast<qreal>(txDelta) / elapsed;
|
||||
|
||||
m_downloadBuffer->push(m_downloadSpeed);
|
||||
m_uploadBuffer->push(m_uploadSpeed);
|
||||
}
|
||||
|
||||
m_prevRx = totalRx;
|
||||
m_prevTx = totalTx;
|
||||
|
||||
emit changed();
|
||||
}
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <qelapsedtimer.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
#include "internal/circularbuffer.hpp"
|
||||
#include "tickingservice.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class NetworkUsage : public TickingService {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(qreal downloadSpeed READ downloadSpeed NOTIFY changed)
|
||||
Q_PROPERTY(qreal uploadSpeed READ uploadSpeed NOTIFY changed)
|
||||
Q_PROPERTY(qreal downloadTotal READ downloadTotal NOTIFY changed)
|
||||
Q_PROPERTY(qreal uploadTotal READ uploadTotal NOTIFY changed)
|
||||
Q_PROPERTY(int historyLength READ historyLength NOTIFY historyLengthChanged)
|
||||
|
||||
Q_PROPERTY(
|
||||
ZShell::internal::CircularBuffer* downloadBuffer READ downloadBuffer
|
||||
CONSTANT)
|
||||
Q_PROPERTY(
|
||||
ZShell::internal::CircularBuffer* uploadBuffer READ uploadBuffer
|
||||
CONSTANT)
|
||||
|
||||
public:
|
||||
explicit NetworkUsage(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] qreal downloadSpeed() const;
|
||||
[[nodiscard]] qreal uploadSpeed() const;
|
||||
[[nodiscard]] qreal downloadTotal() const;
|
||||
[[nodiscard]] qreal uploadTotal() const;
|
||||
[[nodiscard]] int historyLength() const;
|
||||
|
||||
[[nodiscard]] internal::CircularBuffer* downloadBuffer() const;
|
||||
[[nodiscard]] internal::CircularBuffer* uploadBuffer() const;
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
void historyLengthChanged();
|
||||
|
||||
protected:
|
||||
void tick() override;
|
||||
|
||||
private:
|
||||
void setHistoryLength(int length);
|
||||
|
||||
qreal m_downloadSpeed = 0.0;
|
||||
qreal m_uploadSpeed = 0.0;
|
||||
qreal m_downloadTotal = 0.0;
|
||||
qreal m_uploadTotal = 0.0;
|
||||
int m_historyLength = 30;
|
||||
|
||||
internal::CircularBuffer* m_downloadBuffer = nullptr;
|
||||
internal::CircularBuffer* m_uploadBuffer = nullptr;
|
||||
|
||||
quint64 m_prevRx = 0;
|
||||
quint64 m_prevTx = 0;
|
||||
bool m_initialized = false;
|
||||
QElapsedTimer m_timer;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services
|
||||
@@ -0,0 +1,442 @@
|
||||
#include "nightlightmanager.hpp"
|
||||
|
||||
#include <qfileinfo.h>
|
||||
#include <qjsondocument.h>
|
||||
#include <qjsonobject.h>
|
||||
#include <qlogging.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
namespace {
|
||||
constexpr const char* NL_EVENT = "zshell-nightlight";
|
||||
constexpr int RECONNECT_MS = 2000;
|
||||
} // namespace
|
||||
|
||||
NightlightManager::NightlightManager(QObject* parent) : QObject(parent) {
|
||||
connect(&m_timer, &QTimer::timeout, this, &NightlightManager::apply);
|
||||
connect(&m_manualTimer, &QTimer::timeout, this, [this] {
|
||||
m_manualToggle = false;
|
||||
emit manualToggleChanged();
|
||||
apply();
|
||||
});
|
||||
connect(&m_startCooldown, &QTimer::timeout, this, [this] {
|
||||
m_startAllowed = true;
|
||||
apply();
|
||||
});
|
||||
connect(&m_reconnectTimer, &QTimer::timeout, this, [this] {
|
||||
if (!m_useNativeNightlight) return;
|
||||
|
||||
connectEventSocket();
|
||||
m_pluginLoadAttempted = false;
|
||||
m_pluginLoadedByUs = false;
|
||||
bootstrapState();
|
||||
});
|
||||
|
||||
m_startCooldown.start(2000);
|
||||
m_manualTimer.setSingleShot(true);
|
||||
m_reconnectTimer.setSingleShot(true);
|
||||
m_timer.start(60000);
|
||||
|
||||
m_process.setStandardInputFile(QProcess::nullDevice());
|
||||
m_process.setStandardOutputFile(QProcess::nullDevice());
|
||||
}
|
||||
|
||||
int NightlightManager::startTime() const {
|
||||
return m_startTime;
|
||||
}
|
||||
|
||||
int NightlightManager::endTime() const {
|
||||
return m_endTime;
|
||||
}
|
||||
|
||||
bool NightlightManager::manualToggle() const {
|
||||
return m_manualToggle;
|
||||
}
|
||||
|
||||
bool NightlightManager::enabled() const {
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
bool NightlightManager::activeAuto() const {
|
||||
return m_activeAuto;
|
||||
}
|
||||
|
||||
int NightlightManager::temp() const {
|
||||
return m_temp;
|
||||
}
|
||||
|
||||
double NightlightManager::fadeDuration() const {
|
||||
return m_fadeDuration;
|
||||
}
|
||||
|
||||
bool NightlightManager::animating() const {
|
||||
return m_animating;
|
||||
}
|
||||
|
||||
int NightlightManager::lastTemp() const {
|
||||
return m_lastTemp;
|
||||
}
|
||||
|
||||
int NightlightManager::currentTemp() const {
|
||||
return m_currentTemp;
|
||||
}
|
||||
|
||||
void NightlightManager::setActiveAuto(bool activeAuto) {
|
||||
if (activeAuto == m_activeAuto) return;
|
||||
|
||||
m_activeAuto = activeAuto;
|
||||
emit activeAutoChanged();
|
||||
}
|
||||
|
||||
void NightlightManager::setManualToggle(bool toggle) {
|
||||
if (toggle == m_manualToggle) return;
|
||||
|
||||
m_manualToggle = toggle;
|
||||
emit manualToggleChanged();
|
||||
|
||||
m_manualTimer.start(60 * 60 * 1000);
|
||||
}
|
||||
|
||||
void NightlightManager::setEndTime(const int& time) {
|
||||
if (time == m_endTime) return;
|
||||
|
||||
m_endTime = time;
|
||||
emit endTimeChanged();
|
||||
apply();
|
||||
}
|
||||
|
||||
void NightlightManager::setStartTime(const int& time) {
|
||||
if (time == m_startTime) return;
|
||||
|
||||
m_startTime = time;
|
||||
emit startTimeChanged();
|
||||
apply();
|
||||
}
|
||||
|
||||
void NightlightManager::setTemp(const int& temp) {
|
||||
if (temp == m_temp) return;
|
||||
|
||||
m_temp = temp;
|
||||
emit tempChanged();
|
||||
apply();
|
||||
}
|
||||
|
||||
void NightlightManager::setFadeDuration(double duration) {
|
||||
if (duration < 0.0) duration = 0.0;
|
||||
|
||||
m_fadeDuration = duration;
|
||||
emit fadeDurationChanged();
|
||||
}
|
||||
|
||||
void NightlightManager::toggle() {
|
||||
if (m_enabled) {
|
||||
end();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void NightlightManager::start() {
|
||||
if (m_enabled && m_initialized) return;
|
||||
|
||||
m_initialized = true;
|
||||
m_enabled = true;
|
||||
|
||||
emit enabledChanged();
|
||||
|
||||
if (m_useNativeNightlight) {
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments(
|
||||
{"dispatch",
|
||||
QString("hl.plugin.zshell.nlSet(%1, %2)")
|
||||
.arg(m_temp)
|
||||
.arg(m_fadeDuration)});
|
||||
m_process.startDetached();
|
||||
} else {
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments(
|
||||
{"hyprsunset", "temperature", QString::number(m_temp)});
|
||||
m_process.startDetached();
|
||||
}
|
||||
}
|
||||
|
||||
void NightlightManager::end() {
|
||||
if (!m_enabled && m_initialized) return;
|
||||
|
||||
m_initialized = true;
|
||||
m_enabled = false;
|
||||
|
||||
emit enabledChanged();
|
||||
|
||||
if (m_useNativeNightlight) {
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments(
|
||||
{"dispatch",
|
||||
QString("hl.plugin.zshell.nlDisable(%1)").arg(m_fadeDuration)});
|
||||
m_process.startDetached();
|
||||
} else {
|
||||
m_process.setProgram("hyprctl");
|
||||
m_process.setArguments({"hyprsunset", "identity"});
|
||||
m_process.startDetached();
|
||||
}
|
||||
}
|
||||
|
||||
void NightlightManager::apply() {
|
||||
if (m_manualToggle || !m_activeAuto || !m_startAllowed) return;
|
||||
|
||||
const auto current = QTime::currentTime();
|
||||
const auto currentMin = current.hour() * 60 + current.minute();
|
||||
bool isDarkTime = false;
|
||||
|
||||
if (m_startTime <= m_endTime) {
|
||||
isDarkTime = (currentMin >= m_startTime && currentMin < m_endTime);
|
||||
} else {
|
||||
isDarkTime = (currentMin >= m_startTime || currentMin < m_endTime);
|
||||
}
|
||||
|
||||
if (isDarkTime) {
|
||||
start();
|
||||
} else {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
bool NightlightManager::useNativeNightlight() const {
|
||||
return m_useNativeNightlight;
|
||||
}
|
||||
|
||||
QString NightlightManager::pluginPath() const {
|
||||
return m_pluginPath;
|
||||
}
|
||||
|
||||
void NightlightManager::setPluginPath(const QString& path) {
|
||||
if (path == m_pluginPath) return;
|
||||
|
||||
m_pluginPath = path;
|
||||
emit pluginPathChanged();
|
||||
}
|
||||
|
||||
void NightlightManager::setUseNativeNightlight(bool native) {
|
||||
if (native == m_useNativeNightlight) return;
|
||||
|
||||
m_useNativeNightlight = native;
|
||||
emit useNativeNightlightChanged();
|
||||
|
||||
if (native) {
|
||||
disconnect(&m_eventSocket, nullptr, this, nullptr);
|
||||
connect(
|
||||
&m_eventSocket,
|
||||
&QLocalSocket::readyRead,
|
||||
this,
|
||||
&NightlightManager::handleEventLines);
|
||||
connect(
|
||||
&m_eventSocket,
|
||||
&QLocalSocket::errorOccurred,
|
||||
this,
|
||||
&NightlightManager::scheduleReconnect);
|
||||
connectEventSocket();
|
||||
bootstrapState();
|
||||
} else {
|
||||
m_reconnectTimer.stop();
|
||||
disconnect(&m_eventSocket, nullptr, this, nullptr);
|
||||
if (m_eventSocket.state() != QLocalSocket::UnconnectedState)
|
||||
m_eventSocket.disconnectFromServer();
|
||||
m_socketBuffer.clear();
|
||||
m_animating = false;
|
||||
emit animatingChanged();
|
||||
m_pluginLoadAttempted = false;
|
||||
m_pluginLoadedByUs = false;
|
||||
}
|
||||
}
|
||||
|
||||
QString NightlightManager::hyprlandDir() const {
|
||||
const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
|
||||
if (his.isEmpty()) return QString();
|
||||
|
||||
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||
auto dir = runtimeDir + "/hypr/" + his;
|
||||
if (!QFileInfo(dir).isDir()) dir = "/tmp/hypr/" + his;
|
||||
|
||||
if (!QFileInfo(dir).isDir()) return QString();
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
QString NightlightManager::eventSocketPath() const {
|
||||
const auto dir = hyprlandDir();
|
||||
return dir.isEmpty() ? QString() : dir + "/.socket2.sock";
|
||||
}
|
||||
|
||||
QString NightlightManager::resolvePluginPath() const {
|
||||
if (!m_pluginPath.isEmpty()) return m_pluginPath;
|
||||
|
||||
const QString name = QStringLiteral("libzshell-nightlight.so");
|
||||
const QStringList dirs = {
|
||||
"/usr/lib/ZShell/plugins",
|
||||
"/usr/local/lib/ZShell/plugins",
|
||||
};
|
||||
for (const auto& dir : dirs) {
|
||||
const QString candidate = dir + "/" + name;
|
||||
if (QFileInfo::exists(candidate)) return candidate;
|
||||
}
|
||||
|
||||
const QString home = qEnvironmentVariable("HOME");
|
||||
if (!home.isEmpty()) {
|
||||
const QString candidate = home + "/.local/lib/ZShell/plugins/" + name;
|
||||
if (QFileInfo::exists(candidate)) return candidate;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
void NightlightManager::tryLoadPlugin() {
|
||||
if (m_pluginLoadAttempted) return;
|
||||
|
||||
const auto path = resolvePluginPath();
|
||||
if (path.isEmpty()) {
|
||||
qWarning() << "NightlightManager: plugin not found; searched "
|
||||
"pluginPath and <prefix>/usr/lib/ZShell/plugins";
|
||||
m_pluginLoadAttempted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
m_pluginLoadAttempted = true;
|
||||
m_pluginLoadedByUs = true;
|
||||
qInfo() << "NightlightManager: plugin not loaded, loading" << path;
|
||||
QProcess::startDetached("hyprctl", {"plugin", "load", path});
|
||||
|
||||
QTimer::singleShot(1500, this, [this] { bootstrapState(); });
|
||||
}
|
||||
|
||||
void NightlightManager::bootstrapState() {
|
||||
if (!m_useNativeNightlight) return;
|
||||
|
||||
const auto dir = hyprlandDir();
|
||||
if (dir.isEmpty()) return;
|
||||
|
||||
auto* sock = new QLocalSocket(this);
|
||||
auto* watchdog = new QTimer(this);
|
||||
watchdog->setSingleShot(true);
|
||||
|
||||
const auto finish = [sock, watchdog]() {
|
||||
watchdog->stop();
|
||||
sock->abort();
|
||||
sock->deleteLater();
|
||||
};
|
||||
|
||||
connect(sock, &QLocalSocket::connected, sock, [sock] {
|
||||
sock->write("repl return hl.plugin.zshell.nlState()");
|
||||
sock->flush();
|
||||
});
|
||||
connect(sock, &QLocalSocket::readyRead, sock, [this, sock, finish]() {
|
||||
const auto raw = sock->readAll();
|
||||
const auto doc = QJsonDocument::fromJson(raw);
|
||||
finish();
|
||||
if (!doc.isObject()) {
|
||||
if (m_pluginLoadAttempted)
|
||||
qWarning() << "NightlightManager: plugin load failed, state "
|
||||
<< "query still answered:" << raw;
|
||||
|
||||
tryLoadPlugin();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pluginLoadedByUs) {
|
||||
qInfo() << "NightlightManager: plugin was not loaded, manager "
|
||||
<< "loaded it; state:" << raw;
|
||||
m_pluginLoadedByUs = false;
|
||||
} else {
|
||||
qInfo() << "NightlightManager: plugin already loaded, state:"
|
||||
<< raw;
|
||||
}
|
||||
|
||||
const auto obj = doc.object();
|
||||
updateState(
|
||||
obj.value("enabled").toBool(),
|
||||
obj.value("animating").toBool(),
|
||||
obj.value("last").toInt(),
|
||||
obj.value("current").toInt());
|
||||
});
|
||||
connect(sock, &QLocalSocket::errorOccurred, sock, finish);
|
||||
connect(watchdog, &QTimer::timeout, sock, finish);
|
||||
watchdog->start(2500);
|
||||
|
||||
sock->connectToServer(dir + "/.socket.sock");
|
||||
}
|
||||
|
||||
void NightlightManager::connectEventSocket() {
|
||||
if (!m_useNativeNightlight) return;
|
||||
|
||||
if (m_eventSocket.state() != QLocalSocket::UnconnectedState) return;
|
||||
|
||||
const auto path = eventSocketPath();
|
||||
if (path.isEmpty()) {
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
m_socketBuffer.clear();
|
||||
m_eventSocket.connectToServer(path, QLocalSocket::ReadOnly);
|
||||
}
|
||||
|
||||
void NightlightManager::scheduleReconnect() {
|
||||
if (!m_useNativeNightlight) return;
|
||||
|
||||
m_reconnectTimer.start(RECONNECT_MS);
|
||||
}
|
||||
|
||||
void NightlightManager::handleEventLines() {
|
||||
m_socketBuffer.append(m_eventSocket.readAll());
|
||||
|
||||
qsizetype idx = 0;
|
||||
while ((idx = m_socketBuffer.indexOf('\n')) >= 0) {
|
||||
const auto line = m_socketBuffer.left(idx);
|
||||
m_socketBuffer.remove(0, idx + 1);
|
||||
handleEventLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
void NightlightManager::handleEventLine(const QByteArray& line) {
|
||||
const qsizetype sep = line.indexOf(">>");
|
||||
if (sep <= 0) return;
|
||||
|
||||
if (line.left(sep) != NL_EVENT) return;
|
||||
|
||||
const auto fields = line.mid(sep + 2).split(',');
|
||||
if (fields.size() < 4) return;
|
||||
|
||||
bool ok = false;
|
||||
const auto enabled = fields.at(0).toInt(&ok);
|
||||
if (!ok) return;
|
||||
const auto animating = fields.at(1).toInt(&ok);
|
||||
if (!ok) return;
|
||||
const auto last = fields.at(2).toInt(&ok);
|
||||
if (!ok) return;
|
||||
const auto current = fields.at(3).toInt(&ok);
|
||||
if (!ok) return;
|
||||
|
||||
updateState(enabled == 1, animating == 1, last, current);
|
||||
}
|
||||
|
||||
void NightlightManager::updateState(
|
||||
bool enabled, bool animating, int last, int current) {
|
||||
if (enabled != m_enabled) {
|
||||
m_enabled = enabled;
|
||||
emit enabledChanged();
|
||||
}
|
||||
if (animating != m_animating) {
|
||||
m_animating = animating;
|
||||
emit animatingChanged();
|
||||
}
|
||||
if (last != m_lastTemp) {
|
||||
m_lastTemp = last;
|
||||
emit lastTempChanged();
|
||||
}
|
||||
if (current != m_currentTemp) {
|
||||
m_currentTemp = current;
|
||||
emit currentTempChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace ZShell::services
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QLocalSocket>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QTime>
|
||||
#include <QTimer>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class NightlightManager : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged)
|
||||
Q_PROPERTY(
|
||||
int startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
|
||||
Q_PROPERTY(int endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged)
|
||||
Q_PROPERTY(int temp READ temp WRITE setTemp NOTIFY tempChanged)
|
||||
Q_PROPERTY(
|
||||
double fadeDuration READ fadeDuration WRITE setFadeDuration NOTIFY
|
||||
fadeDurationChanged)
|
||||
Q_PROPERTY(
|
||||
bool useNativeNightlight READ useNativeNightlight WRITE
|
||||
setUseNativeNightlight NOTIFY useNativeNightlightChanged)
|
||||
Q_PROPERTY(
|
||||
QString pluginPath READ pluginPath WRITE setPluginPath NOTIFY
|
||||
pluginPathChanged)
|
||||
Q_PROPERTY(
|
||||
bool activeAuto READ activeAuto WRITE setActiveAuto NOTIFY
|
||||
activeAutoChanged)
|
||||
Q_PROPERTY(
|
||||
bool manualToggle READ manualToggle WRITE setManualToggle NOTIFY
|
||||
manualToggleChanged)
|
||||
Q_PROPERTY(bool animating READ animating NOTIFY animatingChanged)
|
||||
Q_PROPERTY(int lastTemp READ lastTemp NOTIFY lastTempChanged)
|
||||
Q_PROPERTY(int currentTemp READ currentTemp NOTIFY currentTempChanged)
|
||||
|
||||
public:
|
||||
explicit NightlightManager(QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] int startTime() const;
|
||||
[[nodiscard]] int endTime() const;
|
||||
[[nodiscard]] bool enabled() const;
|
||||
[[nodiscard]] int temp() const;
|
||||
[[nodiscard]] double fadeDuration() const;
|
||||
[[nodiscard]] bool useNativeNightlight() const;
|
||||
[[nodiscard]] QString pluginPath() const;
|
||||
[[nodiscard]] bool activeAuto() const;
|
||||
[[nodiscard]] bool manualToggle() const;
|
||||
[[nodiscard]] bool animating() const;
|
||||
[[nodiscard]] int lastTemp() const;
|
||||
[[nodiscard]] int currentTemp() const;
|
||||
|
||||
Q_INVOKABLE void toggle();
|
||||
Q_INVOKABLE void apply();
|
||||
|
||||
void setStartTime(const int& time);
|
||||
void setEndTime(const int& time);
|
||||
void setTemp(const int& temp);
|
||||
void setFadeDuration(double duration);
|
||||
void setUseNativeNightlight(bool native);
|
||||
void setPluginPath(const QString& path);
|
||||
void setActiveAuto(bool activeAuto);
|
||||
void setManualToggle(bool toggle);
|
||||
|
||||
signals:
|
||||
void enabledChanged();
|
||||
void startTimeChanged();
|
||||
void activeAutoChanged();
|
||||
void endTimeChanged();
|
||||
void tempChanged();
|
||||
void fadeDurationChanged();
|
||||
void useNativeNightlightChanged();
|
||||
void pluginPathChanged();
|
||||
void manualToggleChanged();
|
||||
void animatingChanged();
|
||||
void lastTempChanged();
|
||||
void currentTempChanged();
|
||||
|
||||
private:
|
||||
int m_startTime;
|
||||
int m_endTime;
|
||||
bool m_enabled = false;
|
||||
bool m_manualToggle = false;
|
||||
bool m_activeAuto;
|
||||
bool m_startAllowed = false;
|
||||
bool m_initialized = false;
|
||||
QTimer m_startCooldown;
|
||||
int m_temp;
|
||||
double m_fadeDuration = 0.4;
|
||||
bool m_useNativeNightlight = false;
|
||||
QString m_pluginPath;
|
||||
bool m_pluginLoadAttempted = false;
|
||||
bool m_pluginLoadedByUs = false;
|
||||
bool m_animating = false;
|
||||
int m_lastTemp = 6500;
|
||||
int m_currentTemp = 6500;
|
||||
QProcess m_process;
|
||||
QTimer m_timer;
|
||||
QTimer m_manualTimer;
|
||||
QLocalSocket m_eventSocket;
|
||||
QByteArray m_socketBuffer;
|
||||
QTimer m_reconnectTimer;
|
||||
|
||||
void start();
|
||||
void end();
|
||||
[[nodiscard]] QString hyprlandDir() const;
|
||||
[[nodiscard]] QString eventSocketPath() const;
|
||||
[[nodiscard]] QString resolvePluginPath() const;
|
||||
void tryLoadPlugin();
|
||||
void bootstrapState();
|
||||
void connectEventSocket();
|
||||
void scheduleReconnect();
|
||||
void handleEventLines();
|
||||
void handleEventLine(const QByteArray& line);
|
||||
void updateState(bool enabled, bool animating, int last, int current);
|
||||
};
|
||||
|
||||
}; // namespace ZShell::services
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,288 @@
|
||||
#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 FuzzyCacheEntry {
|
||||
quint64 starttime = 0;
|
||||
QString command;
|
||||
QString commandChars;
|
||||
QVector<quint8> commandClasses;
|
||||
QString commandLineChars;
|
||||
QVector<quint8> commandLineClasses;
|
||||
bool hasCommandLine = false;
|
||||
};
|
||||
|
||||
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 ScanContext {
|
||||
double uptime = 0.0;
|
||||
double deltaSecs = -1.0;
|
||||
const QHash<pid_t, int>* prevPidToAll = nullptr;
|
||||
const QVector<ProcessEntry>* prevAll = nullptr;
|
||||
long clockTicksPerSec = 100;
|
||||
const QHash<QString, QString>* execPathToIcon = nullptr;
|
||||
const QHash<QString, QString>* execNameToIcon = nullptr;
|
||||
bool iconsChanged = false;
|
||||
uid_t selfEuid = 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 void scanChunk(
|
||||
const ScanContext& ctx,
|
||||
const QVector<pid_t>& pids,
|
||||
QVector<ProcessEntry>& out,
|
||||
QHash<uid_t, QString>& uidCache);
|
||||
|
||||
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;
|
||||
QHash<pid_t, FuzzyCacheEntry> mFuzzyCache;
|
||||
|
||||
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
|
||||
@@ -1,66 +1,25 @@
|
||||
#include "tickingservice.hpp"
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QTimer>
|
||||
|
||||
#include "config/dashboard.hpp"
|
||||
#include "config/rootnodes.hpp"
|
||||
|
||||
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");
|
||||
|
||||
auto reloadConfig = [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (doc.isNull()) {
|
||||
return;
|
||||
}
|
||||
QJsonObject dashboard = doc.object().value("dashboard").toObject();
|
||||
if (dashboard.contains("resourceUpdateInterval")) {
|
||||
applyInterval(
|
||||
dashboard.value("resourceUpdateInterval").toInt(1000));
|
||||
}
|
||||
};
|
||||
|
||||
reloadConfig();
|
||||
|
||||
static auto* watcher = new QFileSystemWatcher();
|
||||
if (!watcher->files().contains(configPath)) {
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
[this, configPath]() {
|
||||
QTimer::singleShot(100, this, [this, configPath]() {
|
||||
QFile file(configPath);
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (doc.isNull()) {
|
||||
return;
|
||||
}
|
||||
QJsonObject dashboard =
|
||||
doc.object().value("dashboard").toObject();
|
||||
if (dashboard.contains("resourceUpdateInterval")) {
|
||||
applyInterval(dashboard.value("resourceUpdateInterval")
|
||||
.toInt(1000));
|
||||
}
|
||||
});
|
||||
});
|
||||
watcher->addPath(configPath);
|
||||
}
|
||||
auto* dash = ZShell::config::ConfigSingleton::instance()->dashboard();
|
||||
applyInterval(dash->resourceUpdateInterval());
|
||||
QObject::connect(
|
||||
dash,
|
||||
&ZShell::config::Dashboard::resourceUpdateIntervalChanged,
|
||||
this,
|
||||
[this, dash] { applyInterval(dash->resourceUpdateInterval()); });
|
||||
}
|
||||
|
||||
int TickingService::updateInterval() const {
|
||||
@@ -78,6 +37,7 @@ void TickingService::start() {
|
||||
void TickingService::stop() {
|
||||
m_running = false;
|
||||
m_timer->stop();
|
||||
onStopped();
|
||||
}
|
||||
|
||||
void TickingService::applyInterval(int ms) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "service.hpp"
|
||||
#include <qtimer.h>
|
||||
|
||||
#include "service.hpp"
|
||||
|
||||
namespace ZShell::services {
|
||||
|
||||
class TickingService : public Service {
|
||||
@@ -18,12 +19,14 @@ class TickingService : public Service {
|
||||
|
||||
signals:
|
||||
void updateIntervalChanged();
|
||||
void updateTriggered();
|
||||
|
||||
protected:
|
||||
void start() final;
|
||||
void stop() final;
|
||||
|
||||
virtual void tick() = 0;
|
||||
virtual void onStopped() {}
|
||||
|
||||
private:
|
||||
void applyInterval(int ms);
|
||||
|
||||
@@ -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"};
|
||||
@@ -30,4 +48,20 @@ FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
|
||||
return {kib, total, "KiB"};
|
||||
}
|
||||
|
||||
FormatResult UsageFmt::formatBytes(qreal bytes) const {
|
||||
static constexpr std::array<const char*, 5> units = {
|
||||
"B", "KB", "MB", "GB", "TB"};
|
||||
|
||||
qreal value = finitePositive(bytes) ? bytes : 0.0;
|
||||
const qreal k = 1024.0;
|
||||
std::size_t i = 0;
|
||||
|
||||
while (value >= k && i < units.size() - 1) {
|
||||
value /= k;
|
||||
i++;
|
||||
}
|
||||
|
||||
return {value, 0.0, units[i]};
|
||||
}
|
||||
|
||||
} // namespace ZShell::services::usagefmt
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <qaccessible_base.h>
|
||||
#include <qobject.h>
|
||||
#include <qobjectdefs.h>
|
||||
#include <qqmlintegration.h>
|
||||
@@ -27,8 +28,10 @@ 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;
|
||||
Q_INVOKABLE [[nodiscard]] FormatResult formatBytes(qreal bytes) const;
|
||||
};
|
||||
|
||||
} // namespace ZShell::services::usagefmt
|
||||
|
||||
@@ -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,22 @@
|
||||
#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,420 @@
|
||||
#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,107 @@
|
||||
#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,81 @@
|
||||
#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,535 @@
|
||||
#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);
|
||||
insertMissingDefaults();
|
||||
} 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::insertMissingDefaults() {
|
||||
if (fallbackNode()) return;
|
||||
|
||||
const auto* desc = getDescriptor();
|
||||
if (!desc) return;
|
||||
|
||||
const auto idKey = desc->identityKey();
|
||||
if (idKey.isEmpty()) return;
|
||||
|
||||
if (!m_elements.isEmpty() && !m_elements.first()->schema().get(idKey)) {
|
||||
qCCritical(
|
||||
lcSettings,
|
||||
"List %s has identityKey '%s' which its elements don't define.",
|
||||
qUtf8Printable(path()),
|
||||
qUtf8Printable(idKey));
|
||||
return;
|
||||
}
|
||||
|
||||
QHash<QString, QList<Node*>> unmatched;
|
||||
for (auto* const element : std::as_const(m_elements))
|
||||
unmatched[element->value(idKey).toString()] << element;
|
||||
|
||||
Node* anchor = nullptr;
|
||||
for (const auto& props : defaultValue()) {
|
||||
const auto id = props.value(idKey).toString();
|
||||
if (id.isEmpty()) continue;
|
||||
|
||||
auto& candidates = unmatched[id];
|
||||
if (!candidates.isEmpty()) {
|
||||
anchor = candidates.takeFirst();
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto index = anchor ? m_elements.indexOf(anchor) + 1 : 0;
|
||||
anchor = insertNode(index, props, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
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,86 @@
|
||||
#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;
|
||||
|
||||
void insertMissingDefaults();
|
||||
|
||||
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,208 @@
|
||||
#pragma once
|
||||
|
||||
#include <qmetatype.h>
|
||||
#include <qobject.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "listnode.hpp"
|
||||
#include "schema.hpp"
|
||||
|
||||
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");
|
||||
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
|
||||
|
||||
#define DEFAULT_ARG(...) __VA_ARGS__
|
||||
|
||||
#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:
|
||||
|
||||
#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 */ \
|
||||
\
|
||||
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)
|
||||
|
||||
#define SETTINGS_GLOBAL_SUBOBJECT(Type, name) \
|
||||
SETTINGS_SUBOBJECT_IMPL(Type, name, true)
|
||||
|
||||
#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); \
|
||||
} \
|
||||
};
|
||||
|
||||
#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__)
|
||||
|
||||
#define SETTINGS_GLOBAL_LIST(Type, name, defaultVal, ...) \
|
||||
SETTINGS_LIST_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)
|
||||
@@ -0,0 +1,308 @@
|
||||
#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,116 @@
|
||||
#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,47 @@
|
||||
#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,38 @@
|
||||
#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,137 @@
|
||||
#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,89 @@
|
||||
#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
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
QString identityKey = {};
|
||||
};
|
||||
|
||||
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)
|
||||
ANNOTATION(QString, identityKey)
|
||||
|
||||
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,250 @@
|
||||
#include "settingsfile.hpp"
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qfile.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qjsonarray.h>
|
||||
#include <qjsondocument.h>
|
||||
#include <qjsonobject.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qsavefile.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcSettingsFile, "zshell.settings.file", QtInfoMsg)
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ZShell::settings {
|
||||
|
||||
using Qt::StringLiterals::operator""_s;
|
||||
|
||||
namespace {
|
||||
|
||||
// Max retries for loads which fail due to malformed JSON, e.g. partial writes
|
||||
constexpr int k_maxLoadRetries = 3;
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsFile::SettingsFile(QString path, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_path(std::move(path))
|
||||
, m_watcher(new QFileSystemWatcher(this))
|
||||
, m_saveDebounce(new QTimer(this))
|
||||
, m_loadDebounce(new QTimer(this))
|
||||
, m_loadRetries(0) {
|
||||
m_saveDebounce->setSingleShot(true);
|
||||
m_saveDebounce->setInterval(500); // Save at most once every 500ms
|
||||
|
||||
m_loadDebounce->setSingleShot(true);
|
||||
m_loadDebounce->setInterval(50); // Coalesce watcher events within 50ms
|
||||
QObject::connect(
|
||||
m_loadDebounce, &QTimer::timeout, this, &SettingsFile::onLoadDebounced);
|
||||
|
||||
QObject::connect(
|
||||
m_watcher,
|
||||
&QFileSystemWatcher::fileChanged,
|
||||
this,
|
||||
&SettingsFile::onFileChanged);
|
||||
QObject::connect(
|
||||
m_watcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&SettingsFile::onDirChanged);
|
||||
|
||||
initWatcher();
|
||||
}
|
||||
|
||||
std::optional<QJsonValue> SettingsFile::read() const {
|
||||
return m_lastData;
|
||||
}
|
||||
|
||||
void SettingsFile::write(const QJsonValue& json) {
|
||||
m_pendingWrite = json;
|
||||
save();
|
||||
}
|
||||
|
||||
void SettingsFile::onFileChanged() {
|
||||
initWatcher(); // Re-add path in case the file was replaced
|
||||
scheduleLoad();
|
||||
}
|
||||
|
||||
void SettingsFile::onDirChanged() {
|
||||
// Ignore if the file is already watched
|
||||
if (m_watcher->files().contains(m_path)) return;
|
||||
|
||||
initWatcher();
|
||||
|
||||
if (QFile::exists(m_path)) scheduleLoad();
|
||||
}
|
||||
|
||||
void SettingsFile::initWatcher() {
|
||||
const QFileInfo info(m_path);
|
||||
const auto dir = info.absolutePath();
|
||||
|
||||
if (QDir(dir).exists() && !m_watcher->directories().contains(dir))
|
||||
m_watcher->addPath(dir);
|
||||
|
||||
if (info.exists() && !m_watcher->files().contains(m_path))
|
||||
m_watcher->addPath(m_path);
|
||||
}
|
||||
|
||||
void SettingsFile::scheduleLoad() {
|
||||
// Ignore if a load is already pending
|
||||
if (m_loadDebounce->isActive()) return;
|
||||
|
||||
m_loadRetries = 0;
|
||||
m_loadDebounce->start();
|
||||
}
|
||||
|
||||
void SettingsFile::onLoadDebounced() {
|
||||
const auto isFinalTry = m_loadRetries >= k_maxLoadRetries;
|
||||
|
||||
if (load(isFinalTry) == LoadResult::ParseError && !isFinalTry) {
|
||||
// Likely a partial write, retry after another debounce
|
||||
++m_loadRetries;
|
||||
qCDebug(
|
||||
lcSettingsFile,
|
||||
"Retrying load of %s (%d/%d)",
|
||||
qUtf8Printable(m_path),
|
||||
m_loadRetries,
|
||||
k_maxLoadRetries);
|
||||
m_loadDebounce->start();
|
||||
}
|
||||
}
|
||||
|
||||
SettingsFile::LoadResult SettingsFile::load(bool reportErrors) {
|
||||
QFile file(m_path);
|
||||
|
||||
if (!file.exists()) {
|
||||
if (m_lastData) {
|
||||
m_lastData = std::nullopt;
|
||||
emit changed();
|
||||
return LoadResult::Changed;
|
||||
}
|
||||
return LoadResult::Unchanged;
|
||||
}
|
||||
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(
|
||||
lcSettingsFile,
|
||||
"Failed to open %s for reading: %s",
|
||||
qUtf8Printable(m_path),
|
||||
qUtf8Printable(file.errorString()));
|
||||
emit readFailed(
|
||||
QStringLiteral("Failed to open: %1").arg(file.errorString()));
|
||||
return LoadResult::Error;
|
||||
}
|
||||
|
||||
const auto data = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonParseError error;
|
||||
const auto doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
if (reportErrors) {
|
||||
qCWarning(
|
||||
lcSettingsFile,
|
||||
"Failed to parse %s as JSON: %s",
|
||||
qUtf8Printable(m_path),
|
||||
qUtf8Printable(error.errorString()));
|
||||
emit readFailed(
|
||||
QStringLiteral("Failed to parse: %1").arg(error.errorString()));
|
||||
} else {
|
||||
qCDebug(
|
||||
lcSettingsFile,
|
||||
"Failed to parse %s as JSON: %s",
|
||||
qUtf8Printable(m_path),
|
||||
qUtf8Printable(error.errorString()));
|
||||
}
|
||||
return LoadResult::ParseError;
|
||||
}
|
||||
|
||||
const auto json = doc.isObject() ? QJsonValue(doc.object())
|
||||
: QJsonValue(doc.array());
|
||||
|
||||
if (json == m_lastData) return LoadResult::Unchanged;
|
||||
|
||||
// Clear pending write, stuff loaded from file should take precedence
|
||||
m_pendingWrite = std::nullopt;
|
||||
|
||||
m_lastData = json;
|
||||
emit changed();
|
||||
|
||||
qCDebug(lcSettingsFile) << "Read JSON from" << m_path;
|
||||
qCDebug(lcSettingsFile) << " " << json;
|
||||
|
||||
return LoadResult::Changed;
|
||||
}
|
||||
|
||||
void SettingsFile::save() {
|
||||
if (!m_pendingWrite) return;
|
||||
|
||||
if (m_saveDebounce->isActive()) {
|
||||
// Queue save for debounce end
|
||||
QObject::connect(
|
||||
m_saveDebounce,
|
||||
&QTimer::timeout,
|
||||
this,
|
||||
&SettingsFile::save,
|
||||
// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) ConnectionType is OR-able by design
|
||||
static_cast<Qt::ConnectionType>(
|
||||
Qt::UniqueConnection | Qt::SingleShotConnection));
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce regardless of whether the save actually succeeded
|
||||
m_saveDebounce->start();
|
||||
|
||||
const auto json = m_pendingWrite.value();
|
||||
const auto data = (json.isObject() ? QJsonDocument(json.toObject())
|
||||
: QJsonDocument(json.toArray()))
|
||||
.toJson();
|
||||
m_pendingWrite = std::nullopt;
|
||||
|
||||
const auto dir = QFileInfo(m_path).absolutePath();
|
||||
|
||||
if (!QDir().mkpath(dir)) {
|
||||
qCWarning(lcSettingsFile) << "Failed to create dir" << dir;
|
||||
emit writeFailed(QStringLiteral("Failed to create parent directory"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Write atomically
|
||||
QSaveFile file(m_path);
|
||||
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qCWarning(
|
||||
lcSettingsFile,
|
||||
"Failed to open %s for writing: %s",
|
||||
qUtf8Printable(m_path),
|
||||
qUtf8Printable(file.errorString()));
|
||||
emit writeFailed(
|
||||
QStringLiteral("Failed to open: %1").arg(file.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
file.write(data);
|
||||
|
||||
if (!file.commit()) {
|
||||
qCWarning(
|
||||
lcSettingsFile,
|
||||
"Failed to write %s: %s",
|
||||
qUtf8Printable(m_path),
|
||||
qUtf8Printable(file.errorString()));
|
||||
emit writeFailed(
|
||||
QStringLiteral("Failed to write: %1").arg(file.errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastData = json;
|
||||
qCDebug(lcSettingsFile) << "Saved" << m_path;
|
||||
|
||||
initWatcher(); // The file may have just been created, or replaced by the rename
|
||||
}
|
||||
|
||||
} // namespace ZShell::settings
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user