fix: define declared method + insert new bar entries to config file list + rename Hyprsunset -> Nightlight

This commit is contained in:
2026-09-30 14:26:56 +02:00
parent 133d453f6f
commit 10d763cfe5
23 changed files with 1445 additions and 1184 deletions
+3 -4
View File
@@ -89,10 +89,10 @@ GridLayout {
role: "id"
DelegateChoice {
roleValue: "hyprsunset"
roleValue: "nightlight"
delegate: EntryWrapper {
HyprsunsetWidget {
NightlightWidget {
horizontal: root.horizontal
visible: !root.fullscreen || root.isHovered
}
@@ -210,8 +210,7 @@ GridLayout {
roleValue: "network"
delegate: EntryWrapper {
NetworkWidget {
}
NetworkWidget {}
}
}
@@ -8,15 +8,15 @@ import qs.Services
WidgetBase {
id: root
property bool tempEnabled: Hyprsunset.enabled
property bool tempEnabled: Nightlight.enabled
color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
StateLayer {
onClicked: {
Hyprsunset.toggleNightLight();
Hyprsunset.manualToggle = true;
Nightlight.toggleNightLight();
Nightlight.manualToggle = true;
}
}
+3 -3
View File
@@ -158,9 +158,9 @@ PageBase {
}
onAccepted: {
Config.general.color.scheduleHyprsunsetStart = startCurrent;
Config.general.color.scheduleHyprsunsetEnd = endCurrent;
Hyprsunset.checkStartup();
Config.display.nightlight.scheduleStart = startCurrent;
Config.display.nightlight.scheduleEnd = endCurrent;
Nightlight.checkStartup();
}
}
}
+3 -2
View File
@@ -64,14 +64,15 @@ class Bar : public settings::ObjectNode {
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),
}))
}),
.identityKey = QStringLiteral("id"))
};
} // namespace ZShell::config
+4
View File
@@ -205,6 +205,10 @@ qreal Gpu::memoryTotal() const {
return m_memoryTotal;
}
bool Gpu::detecting() const {
return m_detecting;
}
void Gpu::setType(GpuType value) {
if (value == m_type) return;
+3 -6
View File
@@ -3,20 +3,17 @@
namespace ZShell::settings {
ChangeBatcher::ChangeBatcher(QObject* parent)
: QObject(parent)
, m_dirty(false) {}
: QObject(parent), m_dirty(false) {}
void ChangeBatcher::dirty() {
if (m_dirty)
return;
if (m_dirty) return;
m_dirty = true;
QMetaObject::invokeMethod(this, &ChangeBatcher::flush, Qt::QueuedConnection);
}
void ChangeBatcher::flush() {
if (!m_dirty)
return;
if (!m_dirty) return;
m_dirty = false;
emit dirtied();
+111 -62
View File
@@ -23,16 +23,25 @@ DecodeResult error(DiagnosticType::Type type, const QString& message) {
}
DecodeResult mismatch(ExpectedType expected, const QJsonValue& value) {
return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} };
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 = {} };
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;
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
@@ -44,7 +53,8 @@ QString unionKey(const QList<QMetaType>& types) {
return ids.join(u","_s);
}
QList<ExpectedType> expectedTypesOf(const QList<const ValueCodec*>& alternatives) {
QList<ExpectedType> expectedTypesOf(
const QList<const ValueCodec*>& alternatives) {
QList<ExpectedType> types;
types.reserve(alternatives.size());
for (const auto* codec : alternatives)
@@ -57,7 +67,8 @@ 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>> },
{QMetaType::fromType<QList<qreal>>().id(),
&makeListCodec<QList<qreal>>},
};
return k_factories;
}
@@ -65,8 +76,7 @@ const QHash<int, ListFactory>& listFactories() {
} // namespace
ValueCodec::ValueCodec(const QMetaType& type, ExpectedType expected)
: m_type(type)
, m_expected(expected) {}
: m_type(type), m_expected(expected) {}
QMetaType ValueCodec::type() const {
return m_type;
@@ -81,7 +91,8 @@ ValueCodec* ValueCodec::codecFor(const QMetaType& type) {
static QHash<int, ValueCodec*> s_registry;
// Cached lookup
if (const auto it = s_registry.constFind(type.id()); it != s_registry.constEnd())
if (const auto it = s_registry.constFind(type.id());
it != s_registry.constEnd())
return *it;
ValueCodec* codec = nullptr;
@@ -105,7 +116,8 @@ ValueCodec* ValueCodec::codecFor(const QMetaType& type) {
codec = new VariantMapCodec(type);
break;
default:
if (const auto factory = listFactories().constFind(type.id()); factory != listFactories().constEnd())
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));
@@ -113,8 +125,7 @@ ValueCodec* ValueCodec::codecFor(const QMetaType& type) {
}
// Cache codec
if (codec)
s_registry.insert(type.id(), codec);
if (codec) s_registry.insert(type.id(), codec);
return codec;
}
@@ -124,7 +135,10 @@ ValueCodec* ValueCodec::unionFor(const QList<QMetaType>& types) {
static QHash<QString, ValueCodec*> s_registry;
if (types.size() < 2) {
qCCritical(lcSettings, "A union needs at least two types, got %lld", types.size());
qCCritical(
lcSettings,
"A union needs at least two types, got %lld",
types.size());
return nullptr;
}
@@ -140,7 +154,10 @@ ValueCodec* ValueCodec::unionFor(const QList<QMetaType>& types) {
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());
qCCritical(
lcSettings,
"No codec found for type %s, cannot build union",
type.name());
return nullptr;
}
alternatives << codec;
@@ -158,8 +175,7 @@ QJsonValue BoolCodec::encode(const QVariant& value) const {
DecodeResult BoolCodec::decode(const QJsonValue& value) const {
// 1 and "true" are not booleans
if (!value.isBool())
return mismatch(ExpectedType::Bool, value);
if (!value.isBool()) return mismatch(ExpectedType::Bool, value);
return {.value = value.toBool(), .error = std::nullopt, .indexPath = {}};
}
@@ -169,24 +185,32 @@ QJsonValue IntCodec::encode(const QVariant& value) const {
}
DecodeResult IntCodec::decode(const QJsonValue& value) const {
if (!value.isDouble())
return mismatch(ExpectedType::Int, value);
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)));
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));
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 = {} };
return {
.value = static_cast<int>(num), .error = std::nullopt, .indexPath = {}};
}
QJsonValue RealCodec::encode(const QVariant& value) const {
@@ -194,10 +218,12 @@ QJsonValue RealCodec::encode(const QVariant& value) const {
}
DecodeResult RealCodec::decode(const QJsonValue& value) const {
if (!value.isDouble())
return mismatch(ExpectedType::Real, value);
if (!value.isDouble()) return mismatch(ExpectedType::Real, value);
return { .value = QVariant::fromValue<qreal>(value.toDouble()), .error = std::nullopt, .indexPath = {} };
return {
.value = QVariant::fromValue<qreal>(value.toDouble()),
.error = std::nullopt,
.indexPath = {}};
}
QJsonValue StringCodec::encode(const QVariant& value) const {
@@ -205,8 +231,7 @@ QJsonValue StringCodec::encode(const QVariant& value) const {
}
DecodeResult StringCodec::decode(const QJsonValue& value) const {
if (!value.isString())
return mismatch(ExpectedType::String, value);
if (!value.isString()) return mismatch(ExpectedType::String, value);
return {.value = value.toString(), .error = std::nullopt, .indexPath = {}};
}
@@ -216,10 +241,12 @@ QJsonValue VariantListCodec::encode(const QVariant& value) const {
}
DecodeResult VariantListCodec::decode(const QJsonValue& value) const {
if (!value.isArray())
return mismatch(ExpectedType::Array, value);
if (!value.isArray()) return mismatch(ExpectedType::Array, value);
return { .value = value.toArray().toVariantList(), .error = std::nullopt, .indexPath = {} };
return {
.value = value.toArray().toVariantList(),
.error = std::nullopt,
.indexPath = {}};
}
QJsonValue VariantMapCodec::encode(const QVariant& value) const {
@@ -227,21 +254,25 @@ QJsonValue VariantMapCodec::encode(const QVariant& value) const {
}
DecodeResult VariantMapCodec::decode(const QJsonValue& value) const {
if (!value.isObject())
return mismatch(ExpectedType::Object, value);
if (!value.isObject()) return mismatch(ExpectedType::Object, value);
return { .value = value.toObject().toVariantMap(), .error = std::nullopt, .indexPath = {} };
return {
.value = value.toObject().toVariantMap(),
.error = std::nullopt,
.indexPath = {}};
}
EnumCodec::EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum)
: ValueCodec(type, ExpectedType::String)
, m_metaEnum(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());
lcSettings,
"Cannot encode value %lld of enum %s, no such enumerator",
value.toLongLong(),
m_type.name());
return {};
}
@@ -249,21 +280,30 @@ QJsonValue EnumCodec::encode(const QVariant& value) const {
}
DecodeResult EnumCodec::decode(const QJsonValue& value) const {
if (!value.isString())
return mismatch(ExpectedType::String, value);
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)
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())));
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 = {}};
@@ -274,16 +314,19 @@ DecodeResult EnumCodec::decode(const QJsonValue& value) const {
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)));
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) {}
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 {
template <typename Container>
QJsonValue ListCodec<Container>::encode(const QVariant& value) const {
QJsonArray array;
const auto list = value.value<Container>();
for (const auto& element : list)
@@ -291,9 +334,9 @@ template <typename Container> QJsonValue ListCodec<Container>::encode(const QVar
return array;
}
template <typename Container> DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const {
if (!value.isArray())
return mismatch(ExpectedType::Array, value);
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;
@@ -311,11 +354,15 @@ template <typename Container> DecodeResult ListCodec<Container>::decode(const QJ
list.append(result.value.value<Value>());
}
return { .value = QVariant::fromValue(list), .error = std::nullopt, .indexPath = {} };
return {
.value = QVariant::fromValue(list),
.error = std::nullopt,
.indexPath = {}};
}
UnionCodec::UnionCodec(const QList<const ValueCodec*>& alternatives)
: ValueCodec(QMetaType::fromType<QVariant>(), alternatives.first()->expected())
: ValueCodec(
QMetaType::fromType<QVariant>(), alternatives.first()->expected())
, m_alternatives(alternatives)
, m_expectedTypes(expectedTypesOf(alternatives)) {
m_byType.reserve(alternatives.size());
@@ -326,12 +373,15 @@ UnionCodec::UnionCodec(const QList<const ValueCodec*>& alternatives)
QJsonValue UnionCodec::encode(const QVariant& value) const {
// An unset union is simply absent from the file
if (!value.isValid())
return QJsonValue::Undefined;
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());
qCWarning(
lcSettings,
"Cannot encode value of type %s, it is not one of the allowed "
"types",
value.typeName());
return QJsonValue::Undefined;
}
@@ -345,11 +395,11 @@ DecodeResult UnionCodec::decode(const QJsonValue& value) const {
auto result = codec->decode(value);
// The first alternative to accept the value wins
if (!result.error)
return result;
if (!result.error) return result;
// Non-nested type mismatch just means try another alternative
if (result.indexPath.isEmpty() && result.error->type == DiagnosticType::TypeMismatch)
if (result.indexPath.isEmpty() &&
result.error->type == DiagnosticType::TypeMismatch)
continue;
// Report the deepest error that isn't a type mismatch
@@ -357,8 +407,7 @@ DecodeResult UnionCodec::decode(const QJsonValue& value) const {
best = std::move(result);
}
if (best)
return *best;
if (best) return *best;
// Nothing matched the shape, so report every alternative
return mismatch(m_expectedTypes, value);
+12 -6
View File
@@ -15,7 +15,8 @@ namespace ZShell::settings {
struct DecodeResult {
QVariant value;
std::optional<Diagnostic> error;
QList<qsizetype> indexPath; // Index path of the failing element, outermost list first
QList<qsizetype>
indexPath; // Index path of the failing element, outermost list first
};
class ValueCodec {
@@ -30,7 +31,8 @@ public:
static ValueCodec* unionFor(const QList<QMetaType>& types);
[[nodiscard]] QMetaType type() const;
[[nodiscard]] ExpectedType expected() const; // The JSON type this decodes from
[[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;
@@ -48,7 +50,8 @@ protected:
: ValueCodec(type, ExpectedType::Expected) {} \
\
[[nodiscard]] QJsonValue encode(const QVariant& value) const override; \
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override; \
[[nodiscard]] DecodeResult decode( \
const QJsonValue& value) const override; \
};
CODEC(Bool, Bool)
@@ -93,9 +96,12 @@ public:
[[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
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
+16 -10
View File
@@ -10,15 +10,20 @@ 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*>;
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);
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
[[nodiscard]] T* get(
const QString& name,
T* fallback,
bool* created = nullptr); // Created on demand
private:
const QString m_prefix;
@@ -44,12 +49,14 @@ inline QString stripLeadingSlashes(QStringView str) {
} // namespace detail
template <LayerType T>
LayerRegistry<T>::LayerRegistry(const QString& prefix, const QString& suffix, QObject* parent)
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 {
template <LayerType T>
QString LayerRegistry<T>::pathFor(const QString& name) const {
return m_prefix + u'/' + name + u'/' + m_suffix;
}
@@ -57,15 +64,14 @@ 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) {
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;
if (created) *created = false;
return layer;
}
if (created)
*created = true;
if (created) *created = true;
auto* const layer = new T(pathFor(name), fallback, m_parent);
m_layers.insert(name, layer);
+155 -65
View File
@@ -21,7 +21,8 @@ void deleteNode(Node* node) {
}
bool isVariantListType(const QVariant& value) {
return value.typeId() == QMetaType::QVariantList || value.typeId() == qMetaTypeId<QList<QVariantMap>>();
return value.typeId() == QMetaType::QVariantList ||
value.typeId() == qMetaTypeId<QList<QVariantMap>>();
}
QList<QVariantMap> toMaps(const QVariant& value) {
@@ -34,7 +35,10 @@ QList<QVariantMap> toMaps(const QVariant& value) {
for (const auto& el : list) {
if (el.typeId() != QMetaType::QVariantMap) {
qCCritical(lcSettings, "Expected a map in list properties, got %s.", el.typeName());
qCCritical(
lcSettings,
"Expected a map in list properties, got %s.",
el.typeName());
continue;
}
@@ -51,7 +55,10 @@ void syncNodeRecursive(Node* node, const QVariantMap& props) {
const auto* desc = schema.get(key);
if (!desc) {
qCCritical(lcSettings, "Unknown key %s in props for %s, something is wrong.", qUtf8Printable(key),
qCCritical(
lcSettings,
"Unknown key %s in props for %s, something is wrong.",
qUtf8Printable(key),
qUtf8Printable(node->path()));
continue;
}
@@ -67,8 +74,12 @@ void syncNodeRecursive(Node* node, const QVariantMap& props) {
// 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());
qCCritical(
lcSettings,
"Expected a list for node key %s on %s, got %s.",
qUtf8Printable(key),
qUtf8Printable(node->path()),
val.typeName());
continue;
}
@@ -77,8 +88,12 @@ void syncNodeRecursive(Node* node, const QVariantMap& props) {
}
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());
qCCritical(
lcSettings,
"Expected a map for node key %s on %s, got %s.",
qUtf8Printable(key),
qUtf8Printable(node->path()),
val.typeName());
continue;
}
@@ -95,7 +110,11 @@ ListNode::ListNode(ListNode* fallback, QObject* parent, bool globalOnly)
QObject::disconnect(fallback, &ListNode::optionChanged, this, nullptr);
if (!m_globalOnly)
QObject::connect(fallback, &ListNode::elementsChanged, this, &ListNode::onFallbackListNotify);
QObject::connect(
fallback,
&ListNode::elementsChanged,
this,
&ListNode::onFallbackListNotify);
}
}
@@ -112,13 +131,16 @@ QVariantList ListNode::values() const {
}
void ListNode::remove(qsizetype index) {
if (rejectGlobalMutation())
return;
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,
qCWarning(
lcSettings,
"Attempted to remove invalid index %lld from list node %s, "
"ignoring.",
index,
qUtf8Printable(path()));
recordWrite(valuesKey(), false);
@@ -133,19 +155,28 @@ void ListNode::remove(qsizetype index) {
}
void ListNode::move(qsizetype from, qsizetype to) {
if (rejectGlobalMutation())
return;
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()));
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()));
qCWarning(
lcSettings,
"Attempted to move index %lld to invalid index %lld on list "
"node %s, ignoring.",
from,
to,
qUtf8Printable(path()));
recordWrite(valuesKey(), false);
return;
@@ -158,8 +189,7 @@ void ListNode::move(qsizetype from, qsizetype to) {
}
void ListNode::clear() {
if (rejectGlobalMutation())
return;
if (rejectGlobalMutation()) return;
const WriteScope scope(this, WriteOrigin::Qml);
@@ -188,15 +218,19 @@ QString ListNode::pathFor(const QString& key) const {
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);
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()));
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 {};
}
@@ -207,11 +241,15 @@ 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) {
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()));
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;
}
@@ -236,17 +274,19 @@ bool ListNode::setValue(const QString& key, const QVariant& value, QList<Diagnos
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(),
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 (!recordWrite(valuesKey(), true)) return false;
if (len != m_elements.count())
emit countChanged();
if (len != m_elements.count()) emit countChanged();
NodeChanges added(m_elements.count());
NodeChanges removed(len);
@@ -257,19 +297,62 @@ bool ListNode::setValue(const QString& key, const QVariant& value, QList<Diagnos
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.",
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;
if (fallbackNode() && m_globalOnly) return;
const WriteScope scope(this, WriteOrigin::FileReset);
setValue(valuesKey(), fallbackNode() ? fallbackNode()->value(valuesKey()) : QVariant::fromValue(defaultValue()));
setValue(
valuesKey(),
fallbackNode() ? fallbackNode()->value(valuesKey())
: QVariant::fromValue(defaultValue()));
}
QJsonValue ListNode::toJson(bool sparse) const {
@@ -284,15 +367,17 @@ QJsonValue ListNode::toJson(bool sparse) const {
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),
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;
if (rejectGlobalSync(diagnostics)) return false;
const WriteScope scope(this, WriteOrigin::File);
setValue(valuesKey(), json.toArray(), &diagnostics);
@@ -317,14 +402,12 @@ QString ListNode::keyOf(const Node* child) const {
}
Node* ListNode::elementAt(qsizetype index) const {
if (!validIndex(index))
return nullptr;
if (!validIndex(index)) return nullptr;
return m_elements.at(index);
}
Node* ListNode::insertElement(const QVariantMap& props, qsizetype index) {
if (rejectGlobalMutation())
return nullptr;
if (rejectGlobalMutation()) return nullptr;
const WriteScope scope(this, WriteOrigin::Qml);
@@ -342,8 +425,7 @@ Node* ListNode::insertElement(const QVariantMap& props, qsizetype index) {
QList<QVariantMap> ListNode::defaultValue() const {
const auto* desc = getDescriptor();
if (!desc)
return {};
if (!desc) return {};
return desc->defaultValue().value<QList<QVariantMap>>();
}
@@ -352,12 +434,13 @@ bool ListNode::isNested() const {
}
bool ListNode::rejectGlobalMutation() const {
if (!m_globalOnly || !fallbackNode())
return false;
if (!m_globalOnly || !fallbackNode()) return false;
qCWarning(lcSettings,
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.",
"This should not be used, mutate global lists from the global layer "
"instead.",
qUtf8Printable(path()));
return true;
@@ -369,14 +452,20 @@ bool ListNode::validIndex(qsizetype index) const {
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()));
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()));
qCCritical(
lcSettings,
"List node %s is not in parent schema, something is wrong.",
qUtf8Printable(path()));
return nullptr;
}
@@ -384,17 +473,16 @@ const Descriptor* ListNode::getDescriptor() const {
}
Node* ListNode::fallbackFor(qsizetype index) const {
if (isOverride(valuesKey()))
return nullptr;
if (isOverride(valuesKey())) return nullptr;
const auto* fallback = static_cast<ListNode*>(fallbackNode());
if (!fallback || !fallback->validIndex(index))
return nullptr;
if (!fallback || !fallback->validIndex(index)) return nullptr;
return fallback->m_elements.at(index);
}
Node* ListNode::insertNode(qsizetype index, const QVariantMap& props, Node* fallback) {
Node* ListNode::insertNode(
qsizetype index, const QVariantMap& props, Node* fallback) {
auto* const node = createElement(fallback);
m_elements.insert(index, node);
@@ -410,10 +498,12 @@ Node* ListNode::insertNode(qsizetype index, Node* node) {
return copy;
}
Node* ListNode::insertNode(qsizetype index, const QJsonObject& json, QList<Diagnostic>& diagnostics) {
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
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);
@@ -421,9 +511,11 @@ Node* ListNode::insertNode(qsizetype index, const QJsonObject& json, QList<Diagn
return node;
}
void ListNode::onFallbackListNotify(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved) {
if (isOverride(valuesKey()))
return;
void ListNode::onFallbackListNotify(
const NodeChanges& added,
const NodeChanges& removed,
const MoveChanges& moved) {
if (isOverride(valuesKey())) return;
const WriteScope scope(this, WriteOrigin::Layer);
@@ -434,11 +526,9 @@ void ListNode::onFallbackListNotify(const NodeChanges& added, const NodeChanges&
for (const auto& move : moved)
m_elements.move(move.first, move.second);
if (!recordWrite(valuesKey(), true))
return;
if (!recordWrite(valuesKey(), true)) return;
if (!added.isEmpty() || !removed.isEmpty())
emit countChanged();
if (!added.isEmpty() || !removed.isEmpty()) emit countChanged();
emit elementsChanged(added, removed, moved);
}
+24 -7
View File
@@ -15,7 +15,8 @@ class ListNode : public Node {
using MoveChanges = QList<QPair<qsizetype, qsizetype>>;
public:
explicit ListNode(ListNode* fallback, QObject* parent = nullptr, bool globalOnly = false);
explicit ListNode(
ListNode* fallback, QObject* parent = nullptr, bool globalOnly = false);
[[nodiscard]] qsizetype count() const;
[[nodiscard]] QVariantList values() const;
@@ -28,22 +29,30 @@ public:
[[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);
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;
bool syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) override;
signals:
void countChanged();
void elementsChanged(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved);
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]] Node* insertElement(
const QVariantMap& props, qsizetype index = -1);
[[nodiscard]] virtual Node* createElement(Node* fallback) = 0;
private:
@@ -59,11 +68,19 @@ private:
[[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);
Node* insertNode(
qsizetype index,
const QJsonObject& json,
QList<Diagnostic>& diagnostics);
void onFallbackListNotify(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved);
void onFallbackListNotify(
const NodeChanges& added,
const NodeChanges& removed,
const MoveChanges& moved);
};
} // namespace ZShell::settings
+75 -47
View File
@@ -14,7 +14,8 @@ Node::Node(Node* fallback, QObject* parent, bool globalOnly)
, m_internalRead(false)
, m_batcher(m_rootNode == this ? new ChangeBatcher(this) : nullptr) {
if (fallback && !m_globalOnly)
QObject::connect(fallback, &Node::optionChanged, this, &Node::onFallbackNotify);
QObject::connect(
fallback, &Node::optionChanged, this, &Node::onFallbackNotify);
}
QString Node::key() const {
@@ -71,14 +72,18 @@ const QSet<QString>& Node::overrides() const {
bool Node::hasContent() const {
return !m_overrides.isEmpty() || m_quarantine ||
std::ranges::any_of(findChildren<Node*>(Qt::FindDirectChildrenOnly), &Node::hasContent);
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)));
lcSettings,
"Attempted to read an unknown key %s, something is wrong.",
qUtf8Printable(pathFor(key)));
return {};
}
@@ -90,26 +95,30 @@ QVariant Node::value(const QString& key) const {
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)));
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)));
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;
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;
if (m_globalOnly && m_fallbackNode) return;
m_quarantine.reset(); // Reset quarantine as well
@@ -117,8 +126,13 @@ void Node::resetToDefaults() {
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());
else if (
!desc.globalOnly() ||
!m_fallbackNode) // Skip resetting global options on overlays
setValue(
desc.key,
m_fallbackNode ? m_fallbackNode->value(desc.key)
: desc.defaultValue());
}
}
@@ -128,64 +142,81 @@ const Quarantine* Node::quarantine() const {
bool Node::rejectInvalidWrite(const QString& key, const QVariant& value) const {
const auto* desc = schema().get(key);
if (!desc || desc->accepts(value.metaType()))
return false;
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());
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;
if (!m_fallbackNode || m_rootNode->m_internalRead) return;
qCWarning(lcSettings,
qCWarning(
lcSettings,
"Global option %s was read from an overlay layer. "
"This should not be used, read global options from the global layer instead.",
"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...",
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;
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.",
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.",
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));
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"),
.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;
if (!m_globalOnly || !m_fallbackNode) return false;
warnGlobalSync(diagnostics, path());
return true;
@@ -194,13 +225,17 @@ bool Node::rejectGlobalSync(QList<Diagnostic>& diagnostics) const {
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...",
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;
const auto fromUser = origin == WriteOrigin::Qml ||
origin == WriteOrigin::QmlReset;
bool dirty = changed;
switch (origin) {
@@ -227,26 +262,21 @@ bool Node::recordWrite(const QString& key, bool changed) {
}
// User writes/reset override quarantine
if (fromUser)
dirty |= removeQuarantined(key);
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 (fromUser && dirty) m_rootNode->m_batcher->dirty();
if (changed)
emit optionChanged(key);
if (changed) emit optionChanged(key);
return changed;
}
bool Node::removeQuarantined(const QString& key) {
if (!m_quarantine)
return false;
if (!m_quarantine) return false;
const auto removed = m_quarantine->remove(key);
if (m_quarantine->isEmpty())
m_quarantine.reset();
if (m_quarantine->isEmpty()) m_quarantine.reset();
return removed;
}
@@ -265,13 +295,11 @@ QString Node::keyOf(const Node* child) const {
}
void Node::onFallbackNotify(const QString& key) {
if (m_overrides.contains(key))
return;
if (m_overrides.contains(key)) return;
// Don't mirror global options onto overlays
const auto* desc = schema().get(key);
if (desc && desc->globalOnly())
return;
if (desc && desc->globalOnly()) return;
const WriteScope scope(this, WriteOrigin::Layer);
setValue(key, m_fallbackNode->value(key));
+29 -13
View File
@@ -16,16 +16,19 @@ class Node : public QObject {
public:
// Global only nodes are inherited, anything inside one is also global only
explicit Node(Node* fallback, QObject* parent = nullptr, bool globalOnly = false);
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]] 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
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;
@@ -35,12 +38,16 @@ public:
[[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
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;
virtual bool syncJson(
const QJsonValue& json, QList<Diagnostic>& diagnostics) = 0;
[[nodiscard]] const Quarantine* quarantine() const;
signals:
@@ -53,12 +60,17 @@ protected:
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;
[[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
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);
@@ -68,7 +80,8 @@ protected:
[[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;
[[nodiscard]] T fallbackValue(
T C::* member, std::type_identity_t<T> defaultValue) const;
private:
QSet<QString> m_overrides; // Overridden keys from file/qml writes
@@ -86,13 +99,16 @@ private:
friend class InternalRead;
};
template <typename T> bool Node::rejectInvalidWrite(const QString& key, const T& value) const {
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 {
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;
}
+11 -10
View File
@@ -5,21 +5,23 @@
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) {
: 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) {
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();
if (m_file->load() != SettingsFile::LoadResult::Changed) reloadFromFile();
}
QList<Diagnostic> RootNode::diagnostics() const {
@@ -33,8 +35,7 @@ void RootNode::reloadFromFile() {
const auto data = m_file->read();
syncJson(data ? data.value() : QJsonObject(), m_diagnostics);
if (m_diagnostics != oldDiagnostics)
emit diagnosticsChanged();
if (m_diagnostics != oldDiagnostics) emit diagnosticsChanged();
emit treeLoaded(this);
}
+5 -2
View File
@@ -9,10 +9,13 @@ namespace ZShell::settings {
class RootNode : public ObjectNode {
Q_OBJECT
Q_PROPERTY(QList<ZShell::settings::Diagnostic> diagnostics READ diagnostics NOTIFY diagnosticsChanged)
Q_PROPERTY(
QList<ZShell::settings::Diagnostic> diagnostics READ diagnostics NOTIFY
diagnosticsChanged)
public:
explicit RootNode(const QString& path, RootNode* fallback, QObject* parent = nullptr);
explicit RootNode(
const QString& path, RootNode* fallback, QObject* parent = nullptr);
[[nodiscard]] QList<Diagnostic> diagnostics() const;
+18 -14
View File
@@ -24,26 +24,26 @@ QHash<const QMetaObject*, QHash<QString, Annotation>>& annotationCache() {
const ValueCodec* resolveCodec(Descriptor& desc) {
auto& allowed = desc.annotation.allowedTypes;
if (allowed.isEmpty())
return ValueCodec::codecFor(desc.type);
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",
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;
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;
if (!type.flags().testFlag(QMetaType::PointerToQObject)) return false;
const auto* meta = type.metaObject();
return meta && meta->inherits(&Node::staticMetaObject);
}
@@ -64,16 +64,15 @@ QString Descriptor::typeString() const {
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 (type.id() == QMetaType::QVariant && !valueType.isValid()) return true;
if (annotation.allowedTypes.isEmpty())
return type == valueType;
if (annotation.allowedTypes.isEmpty()) return type == valueType;
return annotation.allowedTypes.contains(valueType);
}
Schema Schema::build(const QMetaObject* meta, int baseOffset, bool includeReadOnly) {
Schema Schema::build(
const QMetaObject* meta, int baseOffset, bool includeReadOnly) {
Schema schema;
schema.m_descriptors.reserve(meta->propertyCount() - baseOffset);
@@ -106,7 +105,11 @@ Schema Schema::build(const QMetaObject* meta, int baseOffset, bool includeReadOn
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),
qCCritical(
lcSchema,
"No codec for %s of type %s, it will not be loaded or "
"saved",
qUtf8Printable(key),
desc.type.name());
}
@@ -117,7 +120,8 @@ Schema Schema::build(const QMetaObject* meta, int baseOffset, bool includeReadOn
return schema;
}
void Schema::annotate(const QMetaObject* meta, const QString& key, Annotation annotation) {
void Schema::annotate(
const QMetaObject* meta, const QString& key, Annotation annotation) {
annotationCache()[meta].insert(key, std::move(annotation));
}
+11 -3
View File
@@ -17,13 +17,18 @@ struct Annotation {
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&>;
using AnnotationReturn = std::conditional_t<
std::is_trivially_copyable_v<T> && sizeof(T) <= sizeof(void*),
T,
const T&>;
} // namespace detail
@@ -48,6 +53,7 @@ struct Descriptor {
ANNOTATION(QVariant, defaultValue)
ANNOTATION(bool, globalOnly)
ANNOTATION(QString, identityKey)
public:
QString key;
@@ -65,8 +71,10 @@ public:
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);
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;
+64 -27
View File
@@ -41,10 +41,19 @@ SettingsFile::SettingsFile(QString path, QObject* parent)
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_loadDebounce, &QTimer::timeout, this, &SettingsFile::onLoadDebounced);
QObject::connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &SettingsFile::onFileChanged);
QObject::connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &SettingsFile::onDirChanged);
QObject::connect(
m_watcher,
&QFileSystemWatcher::fileChanged,
this,
&SettingsFile::onFileChanged);
QObject::connect(
m_watcher,
&QFileSystemWatcher::directoryChanged,
this,
&SettingsFile::onDirChanged);
initWatcher();
}
@@ -65,13 +74,11 @@ void SettingsFile::onFileChanged() {
void SettingsFile::onDirChanged() {
// Ignore if the file is already watched
if (m_watcher->files().contains(m_path))
return;
if (m_watcher->files().contains(m_path)) return;
initWatcher();
if (QFile::exists(m_path))
scheduleLoad();
if (QFile::exists(m_path)) scheduleLoad();
}
void SettingsFile::initWatcher() {
@@ -87,8 +94,7 @@ void SettingsFile::initWatcher() {
void SettingsFile::scheduleLoad() {
// Ignore if a load is already pending
if (m_loadDebounce->isActive())
return;
if (m_loadDebounce->isActive()) return;
m_loadRetries = 0;
m_loadDebounce->start();
@@ -100,7 +106,12 @@ void SettingsFile::onLoadDebounced() {
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);
qCDebug(
lcSettingsFile,
"Retrying load of %s (%d/%d)",
qUtf8Printable(m_path),
m_loadRetries,
k_maxLoadRetries);
m_loadDebounce->start();
}
}
@@ -118,9 +129,13 @@ SettingsFile::LoadResult SettingsFile::load(bool reportErrors) {
}
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcSettingsFile, "Failed to open %s for reading: %s", qUtf8Printable(m_path),
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()));
emit readFailed(
QStringLiteral("Failed to open: %1").arg(file.errorString()));
return LoadResult::Error;
}
@@ -132,20 +147,27 @@ SettingsFile::LoadResult SettingsFile::load(bool reportErrors) {
if (error.error != QJsonParseError::NoError) {
if (reportErrors) {
qCWarning(lcSettingsFile, "Failed to parse %s as JSON: %s", qUtf8Printable(m_path),
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()));
emit readFailed(
QStringLiteral("Failed to parse: %1").arg(error.errorString()));
} else {
qCDebug(lcSettingsFile, "Failed to parse %s as JSON: %s", qUtf8Printable(m_path),
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());
const auto json = doc.isObject() ? QJsonValue(doc.object())
: QJsonValue(doc.array());
if (json == m_lastData)
return LoadResult::Unchanged;
if (json == m_lastData) return LoadResult::Unchanged;
// Clear pending write, stuff loaded from file should take precedence
m_pendingWrite = std::nullopt;
@@ -160,14 +182,18 @@ SettingsFile::LoadResult SettingsFile::load(bool reportErrors) {
}
void SettingsFile::save() {
if (!m_pendingWrite)
return;
if (!m_pendingWrite) return;
if (m_saveDebounce->isActive()) {
// Queue save for debounce end
QObject::connect(m_saveDebounce, &QTimer::timeout, this, &SettingsFile::save,
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));
static_cast<Qt::ConnectionType>(
Qt::UniqueConnection | Qt::SingleShotConnection));
return;
}
@@ -175,7 +201,9 @@ void SettingsFile::save() {
m_saveDebounce->start();
const auto json = m_pendingWrite.value();
const auto data = (json.isObject() ? QJsonDocument(json.toObject()) : QJsonDocument(json.toArray())).toJson();
const auto data = (json.isObject() ? QJsonDocument(json.toObject())
: QJsonDocument(json.toArray()))
.toJson();
m_pendingWrite = std::nullopt;
const auto dir = QFileInfo(m_path).absolutePath();
@@ -190,17 +218,26 @@ void SettingsFile::save() {
QSaveFile file(m_path);
if (!file.open(QIODevice::WriteOnly)) {
qCWarning(lcSettingsFile, "Failed to open %s for writing: %s", qUtf8Printable(m_path),
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()));
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()));
qCWarning(
lcSettingsFile,
"Failed to write %s: %s",
qUtf8Printable(m_path),
qUtf8Printable(file.errorString()));
emit writeFailed(
QStringLiteral("Failed to write: %1").arg(file.errorString()));
return;
}
+1 -6
View File
@@ -11,12 +11,7 @@ class SettingsFile : public QObject {
Q_OBJECT
public:
enum class LoadResult : quint8 {
Unchanged,
Changed,
ParseError,
Error
};
enum class LoadResult : quint8 { Unchanged, Changed, ParseError, Error };
explicit SettingsFile(QString path, QObject* parent = nullptr);