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" role: "id"
DelegateChoice { DelegateChoice {
roleValue: "hyprsunset" roleValue: "nightlight"
delegate: EntryWrapper { delegate: EntryWrapper {
HyprsunsetWidget { NightlightWidget {
horizontal: root.horizontal horizontal: root.horizontal
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
@@ -210,8 +210,7 @@ GridLayout {
roleValue: "network" roleValue: "network"
delegate: EntryWrapper { delegate: EntryWrapper {
NetworkWidget { NetworkWidget {}
}
} }
} }
@@ -8,15 +8,15 @@ import qs.Services
WidgetBase { WidgetBase {
id: root id: root
property bool tempEnabled: Hyprsunset.enabled property bool tempEnabled: Nightlight.enabled
color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.full radius: Tokens.rounding.full
StateLayer { StateLayer {
onClicked: { onClicked: {
Hyprsunset.toggleNightLight(); Nightlight.toggleNightLight();
Hyprsunset.manualToggle = true; Nightlight.manualToggle = true;
} }
} }
+3 -3
View File
@@ -158,9 +158,9 @@ PageBase {
} }
onAccepted: { onAccepted: {
Config.general.color.scheduleHyprsunsetStart = startCurrent; Config.display.nightlight.scheduleStart = startCurrent;
Config.general.color.scheduleHyprsunsetEnd = endCurrent; Config.display.nightlight.scheduleEnd = endCurrent;
Hyprsunset.checkStartup(); Nightlight.checkStartup();
} }
} }
} }
+3 -2
View File
@@ -64,14 +64,15 @@ class Bar : public settings::ObjectNode {
LIST_ENTRY(spacer, true), LIST_ENTRY(spacer, true),
LIST_ENTRY(activeWindow, true), LIST_ENTRY(activeWindow, true),
LIST_ENTRY(spacer, true), LIST_ENTRY(spacer, true),
LIST_ENTRY(hyprsunset, true), LIST_ENTRY(nightlight, true),
LIST_ENTRY(tray, true), LIST_ENTRY(tray, true),
LIST_ENTRY(statusIcons, true), LIST_ENTRY(statusIcons, true),
LIST_ENTRY(network, false), LIST_ENTRY(network, false),
LIST_ENTRY(upower, true), LIST_ENTRY(upower, true),
LIST_ENTRY(clock, true), LIST_ENTRY(clock, true),
LIST_ENTRY(notifBell, true), LIST_ENTRY(notifBell, true),
})) }),
.identityKey = QStringLiteral("id"))
}; };
} // namespace ZShell::config } // namespace ZShell::config
+4
View File
@@ -205,6 +205,10 @@ qreal Gpu::memoryTotal() const {
return m_memoryTotal; return m_memoryTotal;
} }
bool Gpu::detecting() const {
return m_detecting;
}
void Gpu::setType(GpuType value) { void Gpu::setType(GpuType value) {
if (value == m_type) return; if (value == m_type) return;
+7 -10
View File
@@ -3,23 +3,20 @@
namespace ZShell::settings { namespace ZShell::settings {
ChangeBatcher::ChangeBatcher(QObject* parent) ChangeBatcher::ChangeBatcher(QObject* parent)
: QObject(parent) : QObject(parent), m_dirty(false) {}
, m_dirty(false) {}
void ChangeBatcher::dirty() { void ChangeBatcher::dirty() {
if (m_dirty) if (m_dirty) return;
return;
m_dirty = true; m_dirty = true;
QMetaObject::invokeMethod(this, &ChangeBatcher::flush, Qt::QueuedConnection); QMetaObject::invokeMethod(this, &ChangeBatcher::flush, Qt::QueuedConnection);
} }
void ChangeBatcher::flush() { void ChangeBatcher::flush() {
if (!m_dirty) if (!m_dirty) return;
return;
m_dirty = false; m_dirty = false;
emit dirtied(); emit dirtied();
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+9 -9
View File
@@ -5,20 +5,20 @@
namespace ZShell::settings { namespace ZShell::settings {
class ChangeBatcher : public QObject { class ChangeBatcher : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit ChangeBatcher(QObject* parent = nullptr); explicit ChangeBatcher(QObject* parent = nullptr);
void dirty(); void dirty();
signals: signals:
void dirtied(); void dirtied();
private: private:
bool m_dirty; bool m_dirty;
void flush(); void flush();
}; };
} // namespace ZShell::settings } // namespace ZShell::settings
+260 -211
View File
@@ -16,352 +16,401 @@ namespace {
// We don't know the option here so it isn't set, it should be set by the caller // 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) { DecodeResult error(DiagnosticType::Type type, const QString& message) {
Diagnostic error; Diagnostic error;
error.type = type; error.type = type;
error.message = message; error.message = message;
return { .value = QVariant(), .error = error, .indexPath = {} }; return {.value = QVariant(), .error = error, .indexPath = {}};
} }
DecodeResult mismatch(ExpectedType expected, const QJsonValue& value) { 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) { DecodeResult mismatch(
return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} }; const QList<ExpectedType>& expected, const QJsonValue& value) {
return {
.value = QVariant(),
.error = Diagnostic::mismatch(expected, value),
.indexPath = {}};
} }
template <typename Container> ValueCodec* makeListCodec(const QMetaType& type) { template <typename Container> ValueCodec* makeListCodec(const QMetaType& type) {
const auto* elementCodec = ValueCodec::codecFor(QMetaType::fromType<typename Container::value_type>()); const auto* elementCodec = ValueCodec::codecFor(
return elementCodec ? new ListCodec<Container>(type, elementCodec) : nullptr; 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 // Unions are keyed by their alternatives in order, since order decides which one wins
QString unionKey(const QList<QMetaType>& types) { QString unionKey(const QList<QMetaType>& types) {
QStringList ids; QStringList ids;
ids.reserve(types.size()); ids.reserve(types.size());
for (const auto& type : types) for (const auto& type : types)
ids << QString::number(type.id()); ids << QString::number(type.id());
return ids.join(u","_s); return ids.join(u","_s);
} }
QList<ExpectedType> expectedTypesOf(const QList<const ValueCodec*>& alternatives) { QList<ExpectedType> expectedTypesOf(
QList<ExpectedType> types; const QList<const ValueCodec*>& alternatives) {
types.reserve(alternatives.size()); QList<ExpectedType> types;
for (const auto* codec : alternatives) types.reserve(alternatives.size());
types << codec->expected(); for (const auto* codec : alternatives)
return types; types << codec->expected();
return types;
} }
using ListFactory = ValueCodec* (*)(const QMetaType&); using ListFactory = ValueCodec* (*)(const QMetaType&);
const QHash<int, ListFactory>& listFactories() { const QHash<int, ListFactory>& listFactories() {
static const QHash<int, ListFactory> k_factories{ static const QHash<int, ListFactory> k_factories{
{ QMetaType::fromType<QStringList>().id(), &makeListCodec<QStringList> }, {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; };
return k_factories;
} }
} // namespace } // namespace
ValueCodec::ValueCodec(const QMetaType& type, ExpectedType expected) ValueCodec::ValueCodec(const QMetaType& type, ExpectedType expected)
: m_type(type) : m_type(type), m_expected(expected) {}
, m_expected(expected) {}
QMetaType ValueCodec::type() const { QMetaType ValueCodec::type() const {
return m_type; return m_type;
} }
ExpectedType ValueCodec::expected() const { ExpectedType ValueCodec::expected() const {
return m_expected; return m_expected;
} }
ValueCodec* ValueCodec::codecFor(const QMetaType& type) { ValueCodec* ValueCodec::codecFor(const QMetaType& type) {
// Cache for codecs, keyed by type id // Cache for codecs, keyed by type id
static QHash<int, ValueCodec*> s_registry; static QHash<int, ValueCodec*> s_registry;
// Cached lookup // Cached lookup
if (const auto it = s_registry.constFind(type.id()); it != s_registry.constEnd()) if (const auto it = s_registry.constFind(type.id());
return *it; it != s_registry.constEnd())
return *it;
ValueCodec* codec = nullptr; ValueCodec* codec = nullptr;
switch (type.id()) { switch (type.id()) {
case QMetaType::Bool: case QMetaType::Bool:
codec = new BoolCodec(type); codec = new BoolCodec(type);
break; break;
case QMetaType::Int: case QMetaType::Int:
codec = new IntCodec(type); codec = new IntCodec(type);
break; break;
case QMetaType::Double: case QMetaType::Double:
codec = new RealCodec(type); codec = new RealCodec(type);
break; break;
case QMetaType::QString: case QMetaType::QString:
codec = new StringCodec(type); codec = new StringCodec(type);
break; break;
case QMetaType::QVariantList: case QMetaType::QVariantList:
codec = new VariantListCodec(type); codec = new VariantListCodec(type);
break; break;
case QMetaType::QVariantMap: case QMetaType::QVariantMap:
codec = new VariantMapCodec(type); codec = new VariantMapCodec(type);
break; break;
default: default:
if (const auto factory = listFactories().constFind(type.id()); factory != listFactories().constEnd()) if (const auto factory = listFactories().constFind(type.id());
codec = (*factory)(type); factory != listFactories().constEnd())
else if (util::isSupportedEnum(type)) codec = (*factory)(type);
codec = new EnumCodec(type, util::metaEnumFor(type)); else if (util::isSupportedEnum(type))
break; codec = new EnumCodec(type, util::metaEnumFor(type));
} break;
}
// Cache codec // Cache codec
if (codec) if (codec) s_registry.insert(type.id(), codec);
s_registry.insert(type.id(), codec);
return codec; return codec;
} }
ValueCodec* ValueCodec::unionFor(const QList<QMetaType>& types) { ValueCodec* ValueCodec::unionFor(const QList<QMetaType>& types) {
// Cache for union codecs, keyed by their alternatives // Cache for union codecs, keyed by their alternatives
static QHash<QString, ValueCodec*> s_registry; static QHash<QString, ValueCodec*> s_registry;
if (types.size() < 2) { if (types.size() < 2) {
qCCritical(lcSettings, "A union needs at least two types, got %lld", types.size()); qCCritical(
return nullptr; lcSettings,
} "A union needs at least two types, got %lld",
types.size());
return nullptr;
}
const auto key = unionKey(types); const auto key = unionKey(types);
// Cached lookup // Cached lookup
if (const auto it = s_registry.constFind(key); it != s_registry.constEnd()) if (const auto it = s_registry.constFind(key); it != s_registry.constEnd())
return *it; return *it;
QList<const ValueCodec*> alternatives; QList<const ValueCodec*> alternatives;
alternatives.reserve(types.size()); alternatives.reserve(types.size());
for (const auto& type : types) { for (const auto& type : types) {
const auto* codec = codecFor(type); const auto* codec = codecFor(type);
if (!codec) { if (!codec) {
qCCritical(lcSettings, "No codec found for type %s, cannot build union", type.name()); qCCritical(
return nullptr; lcSettings,
} "No codec found for type %s, cannot build union",
alternatives << codec; type.name());
} return nullptr;
}
alternatives << codec;
}
auto* const codec = new UnionCodec(alternatives); auto* const codec = new UnionCodec(alternatives);
s_registry.insert(key, codec); s_registry.insert(key, codec);
return codec; return codec;
} }
QJsonValue BoolCodec::encode(const QVariant& value) const { QJsonValue BoolCodec::encode(const QVariant& value) const {
return value.toBool(); return value.toBool();
} }
DecodeResult BoolCodec::decode(const QJsonValue& value) const { DecodeResult BoolCodec::decode(const QJsonValue& value) const {
// 1 and "true" are not booleans // 1 and "true" are not booleans
if (!value.isBool()) if (!value.isBool()) return mismatch(ExpectedType::Bool, value);
return mismatch(ExpectedType::Bool, value);
return { .value = value.toBool(), .error = std::nullopt, .indexPath = {} }; return {.value = value.toBool(), .error = std::nullopt, .indexPath = {}};
} }
QJsonValue IntCodec::encode(const QVariant& value) const { QJsonValue IntCodec::encode(const QVariant& value) const {
return value.toInt(); return value.toInt();
} }
DecodeResult IntCodec::decode(const QJsonValue& value) const { DecodeResult IntCodec::decode(const QJsonValue& value) const {
if (!value.isDouble()) if (!value.isDouble()) return mismatch(ExpectedType::Int, value);
return mismatch(ExpectedType::Int, value);
const auto num = value.toDouble(); const auto num = value.toDouble();
double integral; double integral;
if (std::fpclassify(std::modf(num, &integral)) != FP_ZERO) if (std::fpclassify(std::modf(num, &integral)) != FP_ZERO)
return error( 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_min = std::numeric_limits<int>::min();
constexpr auto k_max = std::numeric_limits<int>::max(); constexpr auto k_max = std::numeric_limits<int>::max();
if (num < static_cast<double>(k_min) || num > static_cast<double>(k_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 =
return error(DiagnosticType::InvalidValue, 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 { QJsonValue RealCodec::encode(const QVariant& value) const {
return value.toDouble(); return value.toDouble();
} }
DecodeResult RealCodec::decode(const QJsonValue& value) const { DecodeResult RealCodec::decode(const QJsonValue& value) const {
if (!value.isDouble()) if (!value.isDouble()) return mismatch(ExpectedType::Real, value);
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 { QJsonValue StringCodec::encode(const QVariant& value) const {
return value.toString(); return value.toString();
} }
DecodeResult StringCodec::decode(const QJsonValue& value) const { DecodeResult StringCodec::decode(const QJsonValue& value) const {
if (!value.isString()) if (!value.isString()) return mismatch(ExpectedType::String, value);
return mismatch(ExpectedType::String, value);
return { .value = value.toString(), .error = std::nullopt, .indexPath = {} }; return {.value = value.toString(), .error = std::nullopt, .indexPath = {}};
} }
QJsonValue VariantListCodec::encode(const QVariant& value) const { QJsonValue VariantListCodec::encode(const QVariant& value) const {
return QJsonArray::fromVariantList(value.toList()); return QJsonArray::fromVariantList(value.toList());
} }
DecodeResult VariantListCodec::decode(const QJsonValue& value) const { DecodeResult VariantListCodec::decode(const QJsonValue& value) const {
if (!value.isArray()) if (!value.isArray()) return mismatch(ExpectedType::Array, value);
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 { QJsonValue VariantMapCodec::encode(const QVariant& value) const {
return QJsonObject::fromVariantMap(value.toMap()); return QJsonObject::fromVariantMap(value.toMap());
} }
DecodeResult VariantMapCodec::decode(const QJsonValue& value) const { DecodeResult VariantMapCodec::decode(const QJsonValue& value) const {
if (!value.isObject()) if (!value.isObject()) return mismatch(ExpectedType::Object, value);
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) EnumCodec::EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum)
: ValueCodec(type, ExpectedType::String) : ValueCodec(type, ExpectedType::String), m_metaEnum(metaEnum) {}
, m_metaEnum(metaEnum) {}
QJsonValue EnumCodec::encode(const QVariant& value) const { QJsonValue EnumCodec::encode(const QVariant& value) const {
const auto* key = util::enumKeyFor(m_metaEnum, value); const auto* key = util::enumKeyFor(m_metaEnum, value);
if (!key) { if (!key) {
qCWarning( qCWarning(
lcSettings, "Cannot encode value %lld of enum %s, no such enumerator", value.toLongLong(), m_type.name()); lcSettings,
return {}; "Cannot encode value %lld of enum %s, no such enumerator",
} value.toLongLong(),
m_type.name());
return {};
}
return QString::fromUtf8(key); return QString::fromUtf8(key);
} }
DecodeResult EnumCodec::decode(const QJsonValue& value) const { DecodeResult EnumCodec::decode(const QJsonValue& value) const {
if (!value.isString()) if (!value.isString()) return mismatch(ExpectedType::String, value);
return mismatch(ExpectedType::String, value);
const auto key = value.toString(); const auto key = value.toString();
for (int i = 0; i < m_metaEnum.keyCount(); ++i) { 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(
continue; QString::fromUtf8(m_metaEnum.key(i)),
key,
Qt::CaseInsensitive) != 0)
continue;
const auto raw = m_metaEnum.value(i); const auto raw = m_metaEnum.value(i);
QVariant decoded(m_type); QVariant decoded(m_type);
if (!QMetaType::convert(QMetaType::fromType<int>(), &raw, m_type, decoded.data())) { if (!QMetaType::convert(
qCCritical(lcSettings, "Failed to convert value %d to enum %s", raw, m_type.name()); QMetaType::fromType<int>(), &raw, m_type, decoded.data())) {
return error(DiagnosticType::InvalidValue, qCCritical(
QStringLiteral("Could not convert %1 to %2").arg(key, QString::fromUtf8(m_type.name()))); 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 = {} }; return {.value = decoded, .error = std::nullopt, .indexPath = {}};
} }
QStringList options; QStringList options;
options.reserve(m_metaEnum.keyCount()); options.reserve(m_metaEnum.keyCount());
for (int i = 0; i < m_metaEnum.keyCount(); ++i) for (int i = 0; i < m_metaEnum.keyCount(); ++i)
options << QString::fromUtf8(m_metaEnum.key(i)); options << QString::fromUtf8(m_metaEnum.key(i));
return error(DiagnosticType::InvalidValue, return error(
QStringLiteral("Invalid enum value %1. Expected one of %2").arg(key, options.join(u", "_s))); DiagnosticType::InvalidValue,
QStringLiteral("Invalid enum value %1. Expected one of %2")
.arg(key, options.join(u", "_s)));
} }
template <typename Container> template <typename Container>
ListCodec<Container>::ListCodec(const QMetaType& type, const ValueCodec* elementCodec) ListCodec<Container>::ListCodec(
: ValueCodec(type, ExpectedType::Array) const QMetaType& type, const ValueCodec* elementCodec)
, m_elementCodec(elementCodec) {} : ValueCodec(type, ExpectedType::Array), m_elementCodec(elementCodec) {}
template <typename Container> QJsonValue ListCodec<Container>::encode(const QVariant& value) const { template <typename Container>
QJsonArray array; QJsonValue ListCodec<Container>::encode(const QVariant& value) const {
const auto list = value.value<Container>(); QJsonArray array;
for (const auto& element : list) const auto list = value.value<Container>();
array.append(m_elementCodec->encode(QVariant::fromValue(element))); for (const auto& element : list)
return array; array.append(m_elementCodec->encode(QVariant::fromValue(element)));
return array;
} }
template <typename Container> DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const { template <typename Container>
if (!value.isArray()) DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const {
return mismatch(ExpectedType::Array, value); if (!value.isArray()) return mismatch(ExpectedType::Array, value);
const auto array = value.toArray(); const auto array = value.toArray();
Container list; Container list;
list.reserve(array.size()); list.reserve(array.size());
for (qsizetype i = 0; i < array.size(); ++i) { for (qsizetype i = 0; i < array.size(); ++i) {
auto result = m_elementCodec->decode(array.at(i)); auto result = m_elementCodec->decode(array.at(i));
// Reject the entire list if any element is invalid // Reject the entire list if any element is invalid
if (result.error) { if (result.error) {
result.indexPath.prepend(i); result.indexPath.prepend(i);
return result; return result;
} }
list.append(result.value.value<Value>()); 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) UnionCodec::UnionCodec(const QList<const ValueCodec*>& alternatives)
: ValueCodec(QMetaType::fromType<QVariant>(), alternatives.first()->expected()) : ValueCodec(
, m_alternatives(alternatives) QMetaType::fromType<QVariant>(), alternatives.first()->expected())
, m_expectedTypes(expectedTypesOf(alternatives)) { , m_alternatives(alternatives)
m_byType.reserve(alternatives.size()); , m_expectedTypes(expectedTypesOf(alternatives)) {
m_byType.reserve(alternatives.size());
for (const auto* codec : alternatives) for (const auto* codec : alternatives)
m_byType.insert(codec->type().id(), codec); m_byType.insert(codec->type().id(), codec);
} }
QJsonValue UnionCodec::encode(const QVariant& value) const { QJsonValue UnionCodec::encode(const QVariant& value) const {
// An unset union is simply absent from the file // An unset union is simply absent from the file
if (!value.isValid()) if (!value.isValid()) return QJsonValue::Undefined;
return QJsonValue::Undefined;
const auto* codec = m_byType.value(value.metaType().id()); const auto* codec = m_byType.value(value.metaType().id());
if (!codec) { if (!codec) {
qCWarning(lcSettings, "Cannot encode value of type %s, it is not one of the allowed types", value.typeName()); qCWarning(
return QJsonValue::Undefined; lcSettings,
} "Cannot encode value of type %s, it is not one of the allowed "
"types",
value.typeName());
return QJsonValue::Undefined;
}
return codec->encode(value); return codec->encode(value);
} }
DecodeResult UnionCodec::decode(const QJsonValue& value) const { DecodeResult UnionCodec::decode(const QJsonValue& value) const {
std::optional<DecodeResult> best; std::optional<DecodeResult> best;
for (const auto* codec : m_alternatives) { for (const auto* codec : m_alternatives) {
auto result = codec->decode(value); auto result = codec->decode(value);
// The first alternative to accept the value wins // The first alternative to accept the value wins
if (!result.error) if (!result.error) return result;
return result;
// Non-nested type mismatch just means try another alternative // Non-nested type mismatch just means try another alternative
if (result.indexPath.isEmpty() && result.error->type == DiagnosticType::TypeMismatch) if (result.indexPath.isEmpty() &&
continue; result.error->type == DiagnosticType::TypeMismatch)
continue;
// Report the deepest error that isn't a type mismatch // Report the deepest error that isn't a type mismatch
if (!best || result.indexPath.size() > best->indexPath.size()) if (!best || result.indexPath.size() > best->indexPath.size())
best = std::move(result); best = std::move(result);
} }
if (best) if (best) return *best;
return *best;
// Nothing matched the shape, so report every alternative // Nothing matched the shape, so report every alternative
return mismatch(m_expectedTypes, value); return mismatch(m_expectedTypes, value);
} }
// Instantiated for types as needed // Instantiated for types as needed
+54 -48
View File
@@ -13,43 +13,46 @@
namespace ZShell::settings { namespace ZShell::settings {
struct DecodeResult { struct DecodeResult {
QVariant value; QVariant value;
std::optional<Diagnostic> error; 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 { class ValueCodec {
public: public:
explicit ValueCodec(const QMetaType& type, ExpectedType expected); explicit ValueCodec(const QMetaType& type, ExpectedType expected);
virtual ~ValueCodec() = default; virtual ~ValueCodec() = default;
// Returns the shared codec for a type, or nullptr if the type is unsupported // Returns the shared codec for a type, or nullptr if the type is unsupported
static ValueCodec* codecFor(const QMetaType& type); static ValueCodec* codecFor(const QMetaType& type);
// Returns the shared codec for a union of types, or nullptr if any of them is unsupported // Returns the shared codec for a union of types, or nullptr if any of them is unsupported
static ValueCodec* unionFor(const QList<QMetaType>& types); static ValueCodec* unionFor(const QList<QMetaType>& types);
[[nodiscard]] QMetaType type() const; [[nodiscard]] QMetaType type() const;
[[nodiscard]] ExpectedType expected() const; // The JSON type this decodes from [[nodiscard]] ExpectedType expected()
[[nodiscard]] virtual QJsonValue encode(const QVariant& value) const = 0; const; // The JSON type this decodes from
[[nodiscard]] virtual DecodeResult decode(const QJsonValue& value) const = 0; [[nodiscard]] virtual QJsonValue encode(const QVariant& value) const = 0;
[[nodiscard]] virtual DecodeResult decode(const QJsonValue& value) const = 0;
protected: protected:
const QMetaType m_type; const QMetaType m_type;
const ExpectedType m_expected; const ExpectedType m_expected;
Q_DISABLE_COPY_MOVE(ValueCodec) Q_DISABLE_COPY_MOVE(ValueCodec)
}; };
#define CODEC(Type, Expected) \ #define CODEC(Type, Expected) \
class Type##Codec : public ValueCodec { \ class Type##Codec : public ValueCodec { \
public: \ public: \
explicit Type##Codec(const QMetaType& type) \ explicit Type##Codec(const QMetaType& type) \
: ValueCodec(type, ExpectedType::Expected) {} \ : ValueCodec(type, ExpectedType::Expected) {} \
\ \
[[nodiscard]] QJsonValue encode(const QVariant& value) const override; \ [[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) CODEC(Bool, Bool)
CODEC(Int, Int) CODEC(Int, Int)
@@ -61,41 +64,44 @@ CODEC(VariantMap, Object)
#undef CODEC #undef CODEC
class EnumCodec : public ValueCodec { class EnumCodec : public ValueCodec {
public: public:
explicit EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum); explicit EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override; [[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override; [[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private: private:
const QMetaEnum m_metaEnum; const QMetaEnum m_metaEnum;
}; };
template <typename Container> class ListCodec : public ValueCodec { template <typename Container> class ListCodec : public ValueCodec {
using Value = Container::value_type; using Value = Container::value_type;
public: public:
explicit ListCodec(const QMetaType& type, const ValueCodec* elementCodec); explicit ListCodec(const QMetaType& type, const ValueCodec* elementCodec);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override; [[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override; [[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private: private:
const ValueCodec* m_elementCodec; const ValueCodec* m_elementCodec;
}; };
// Decodes any one of several types, for options that accept more than one shape // Decodes any one of several types, for options that accept more than one shape
class UnionCodec : public ValueCodec { class UnionCodec : public ValueCodec {
public: public:
explicit UnionCodec(const QList<const ValueCodec*>& alternatives); explicit UnionCodec(const QList<const ValueCodec*>& alternatives);
[[nodiscard]] QJsonValue encode(const QVariant& value) const override; [[nodiscard]] QJsonValue encode(const QVariant& value) const override;
[[nodiscard]] DecodeResult decode(const QJsonValue& value) const override; [[nodiscard]] DecodeResult decode(const QJsonValue& value) const override;
private: private:
const QList<const ValueCodec*> m_alternatives; // Tried in order, so the first to accept a value wins const QList<const ValueCodec*>
const QList<ExpectedType> m_expectedTypes; // Types of the alternatives, for diagnostics m_alternatives; // Tried in order, so the first to accept a value wins
QHash<int, const ValueCodec*> m_byType; // Type id to alternative, for encoding 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 } // namespace ZShell::settings
+41 -35
View File
@@ -10,66 +10,72 @@ namespace ZShell::settings {
// Derived from Node and has ctor(path, fallback, parent) // Derived from Node and has ctor(path, fallback, parent)
template <typename T> 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 { template <LayerType T> class LayerRegistry {
public: 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 pathFor(const QString& name) const;
[[nodiscard]] QString nameFor(T* layer) 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: private:
const QString m_prefix; const QString m_prefix;
const QString m_suffix; const QString m_suffix;
QObject* const m_parent; QObject* const m_parent;
QHash<QString, T*> m_layers; QHash<QString, T*> m_layers;
}; };
namespace detail { namespace detail {
inline QString stripTrailingSlashes(QStringView str) { inline QString stripTrailingSlashes(QStringView str) {
while (str.endsWith(u'/')) while (str.endsWith(u'/'))
str = str.left(str.length() - 1); str = str.left(str.length() - 1);
return str.toString(); return str.toString();
} }
inline QString stripLeadingSlashes(QStringView str) { inline QString stripLeadingSlashes(QStringView str) {
while (str.startsWith(u'/')) while (str.startsWith(u'/'))
str = str.mid(1); str = str.mid(1);
return str.toString(); return str.toString();
} }
} // namespace detail } // namespace detail
template <LayerType T> template <LayerType T>
LayerRegistry<T>::LayerRegistry(const QString& prefix, const QString& suffix, QObject* parent) LayerRegistry<T>::LayerRegistry(
: m_prefix(detail::stripTrailingSlashes(prefix)) const QString& prefix, const QString& suffix, QObject* parent)
, m_suffix(detail::stripLeadingSlashes(suffix)) : m_prefix(detail::stripTrailingSlashes(prefix))
, m_parent(parent) {} , m_suffix(detail::stripLeadingSlashes(suffix))
, m_parent(parent) {}
template <LayerType T> QString LayerRegistry<T>::pathFor(const QString& name) const { template <LayerType T>
return m_prefix + u'/' + name + u'/' + m_suffix; 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 { template <LayerType T> QString LayerRegistry<T>::nameFor(T* layer) const {
return m_layers.key(layer); return m_layers.key(layer);
} }
template <LayerType T> T* LayerRegistry<T>::get(const QString& name, T* fallback, bool* created) { template <LayerType T>
if (auto* const layer = m_layers.value(name)) { T* LayerRegistry<T>::get(const QString& name, T* fallback, bool* created) {
if (created) if (auto* const layer = m_layers.value(name)) {
*created = false; if (created) *created = false;
return layer; return layer;
} }
if (created) if (created) *created = true;
*created = true;
auto* const layer = new T(pathFor(name), fallback, m_parent); auto* const layer = new T(pathFor(name), fallback, m_parent);
m_layers.insert(name, layer); m_layers.insert(name, layer);
return layer; return layer;
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+360 -270
View File
@@ -11,435 +11,525 @@ using Qt::StringLiterals::operator""_s;
namespace { namespace {
QString valuesKey() { QString valuesKey() {
return u"values"_s; return u"values"_s;
} }
void deleteNode(Node* node) { void deleteNode(Node* node) {
node->detachFallback(); node->detachFallback();
node->setParent(nullptr); node->setParent(nullptr);
node->deleteLater(); node->deleteLater();
} }
bool isVariantListType(const QVariant& value) { 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) { QList<QVariantMap> toMaps(const QVariant& value) {
if (value.typeId() == qMetaTypeId<QList<QVariantMap>>()) if (value.typeId() == qMetaTypeId<QList<QVariantMap>>())
return value.value<QList<QVariantMap>>(); return value.value<QList<QVariantMap>>();
const auto list = value.toList(); const auto list = value.toList();
QList<QVariantMap> maps; QList<QVariantMap> maps;
maps.reserve(list.count()); maps.reserve(list.count());
for (const auto& el : list) { for (const auto& el : list) {
if (el.typeId() != QMetaType::QVariantMap) { if (el.typeId() != QMetaType::QVariantMap) {
qCCritical(lcSettings, "Expected a map in list properties, got %s.", el.typeName()); qCCritical(
continue; lcSettings,
} "Expected a map in list properties, got %s.",
el.typeName());
continue;
}
maps << el.toMap(); maps << el.toMap();
} }
return maps; return maps;
} }
void syncNodeRecursive(Node* node, const QVariantMap& props) { void syncNodeRecursive(Node* node, const QVariantMap& props) {
const auto& schema = node->schema(); const auto& schema = node->schema();
for (const auto& [key, val] : props.asKeyValueRange()) { for (const auto& [key, val] : props.asKeyValueRange()) {
const auto* desc = schema.get(key); const auto* desc = schema.get(key);
if (!desc) { if (!desc) {
qCCritical(lcSettings, "Unknown key %s in props for %s, something is wrong.", qUtf8Printable(key), qCCritical(
qUtf8Printable(node->path())); lcSettings,
continue; "Unknown key %s in props for %s, something is wrong.",
} qUtf8Printable(key),
qUtf8Printable(node->path()));
continue;
}
// `setValue` warns on type mismatches itself // `setValue` warns on type mismatches itself
if (!desc->isNode) { if (!desc->isNode) {
node->setValue(key, val); node->setValue(key, val);
continue; continue;
} }
auto* const child = node->value(key).value<Node*>(); auto* const child = node->value(key).value<Node*>();
// Nested lists take their elements straight from props // Nested lists take their elements straight from props
if (auto* const list = qobject_cast<ListNode*>(child)) { if (auto* const list = qobject_cast<ListNode*>(child)) {
if (!isVariantListType(val)) { if (!isVariantListType(val)) {
qCCritical(lcSettings, "Expected a list for node key %s on %s, got %s.", qUtf8Printable(key), qCCritical(
qUtf8Printable(node->path()), val.typeName()); lcSettings,
continue; "Expected a list for node key %s on %s, got %s.",
} qUtf8Printable(key),
qUtf8Printable(node->path()),
val.typeName());
continue;
}
list->setValue(valuesKey(), val); list->setValue(valuesKey(), val);
continue; continue;
} }
if (val.typeId() != QMetaType::QVariantMap) { if (val.typeId() != QMetaType::QVariantMap) {
qCCritical(lcSettings, "Expected a map for node key %s on %s, got %s.", qUtf8Printable(key), qCCritical(
qUtf8Printable(node->path()), val.typeName()); lcSettings,
continue; "Expected a map for node key %s on %s, got %s.",
} qUtf8Printable(key),
qUtf8Printable(node->path()),
val.typeName());
continue;
}
syncNodeRecursive(child, val.toMap()); syncNodeRecursive(child, val.toMap());
} }
} }
} // namespace } // namespace
ListNode::ListNode(ListNode* fallback, QObject* parent, bool globalOnly) ListNode::ListNode(ListNode* fallback, QObject* parent, bool globalOnly)
: Node(fallback, parent, globalOnly) { : Node(fallback, parent, globalOnly) {
if (fallback) { if (fallback) {
// Disconnect generic fallback notify, lists use a custom one // Disconnect generic fallback notify, lists use a custom one
QObject::disconnect(fallback, &ListNode::optionChanged, this, nullptr); QObject::disconnect(fallback, &ListNode::optionChanged, this, nullptr);
if (!m_globalOnly) if (!m_globalOnly)
QObject::connect(fallback, &ListNode::elementsChanged, this, &ListNode::onFallbackListNotify); QObject::connect(
} fallback,
&ListNode::elementsChanged,
this,
&ListNode::onFallbackListNotify);
}
} }
qsizetype ListNode::count() const { qsizetype ListNode::count() const {
return m_elements.count(); return m_elements.count();
} }
QVariantList ListNode::values() const { QVariantList ListNode::values() const {
QVariantList list; QVariantList list;
list.reserve(m_elements.count()); list.reserve(m_elements.count());
for (auto* const element : m_elements) for (auto* const element : m_elements)
list << QVariant::fromValue(element); list << QVariant::fromValue(element);
return list; return list;
} }
void ListNode::remove(qsizetype index) { void ListNode::remove(qsizetype index) {
if (rejectGlobalMutation()) if (rejectGlobalMutation()) return;
return;
const WriteScope scope(this, WriteOrigin::Qml); const WriteScope scope(this, WriteOrigin::Qml);
if (!validIndex(index)) { if (!validIndex(index)) {
qCWarning(lcSettings, "Attempted to remove invalid index %lld from list node %s, ignoring.", index, qCWarning(
qUtf8Printable(path())); lcSettings,
"Attempted to remove invalid index %lld from list node %s, "
"ignoring.",
index,
qUtf8Printable(path()));
recordWrite(valuesKey(), false); recordWrite(valuesKey(), false);
return; return;
} }
deleteNode(m_elements.takeAt(index)); deleteNode(m_elements.takeAt(index));
recordWrite(valuesKey(), true); recordWrite(valuesKey(), true);
emit countChanged(); emit countChanged();
emit elementsChanged({}, { index }, {}); emit elementsChanged({}, {index}, {});
} }
void ListNode::move(qsizetype from, qsizetype to) { void ListNode::move(qsizetype from, qsizetype to) {
if (rejectGlobalMutation()) if (rejectGlobalMutation()) return;
return;
const WriteScope scope(this, WriteOrigin::Qml); const WriteScope scope(this, WriteOrigin::Qml);
const auto fromInvalid = !validIndex(from); const auto fromInvalid = !validIndex(from);
if (fromInvalid || !validIndex(to)) { if (fromInvalid || !validIndex(to)) {
if (fromInvalid) if (fromInvalid)
qCWarning(lcSettings, "Attempted to move invalid index %lld to index %lld on list node %s, ignoring.", from, qCWarning(
to, qUtf8Printable(path())); lcSettings,
else "Attempted to move invalid index %lld to index %lld on list "
qCWarning(lcSettings, "Attempted to move index %lld to invalid index %lld on list node %s, ignoring.", from, "node %s, ignoring.",
to, qUtf8Printable(path())); 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); recordWrite(valuesKey(), false);
return; return;
} }
m_elements.move(from, to); m_elements.move(from, to);
recordWrite(valuesKey(), true); recordWrite(valuesKey(), true);
emit elementsChanged({}, {}, { { from, to } }); emit elementsChanged({}, {}, {{from, to}});
} }
void ListNode::clear() { void ListNode::clear() {
if (rejectGlobalMutation()) if (rejectGlobalMutation()) return;
return;
const WriteScope scope(this, WriteOrigin::Qml); const WriteScope scope(this, WriteOrigin::Qml);
if (m_elements.isEmpty()) { if (m_elements.isEmpty()) {
recordWrite(valuesKey(), false); recordWrite(valuesKey(), false);
return; return;
} }
const auto len = m_elements.count(); const auto len = m_elements.count();
for (auto* const element : std::as_const(m_elements)) for (auto* const element : std::as_const(m_elements))
deleteNode(element); deleteNode(element);
m_elements.clear(); m_elements.clear();
recordWrite(valuesKey(), true); recordWrite(valuesKey(), true);
emit countChanged(); emit countChanged();
NodeChanges removed(len); NodeChanges removed(len);
std::iota(removed.begin(), removed.end(), 0); std::iota(removed.begin(), removed.end(), 0);
emit elementsChanged({}, removed, {}); emit elementsChanged({}, removed, {});
} }
QString ListNode::pathFor(const QString& key) const { QString ListNode::pathFor(const QString& key) const {
return elementPath(path(), key); return elementPath(path(), key);
} }
const Schema& ListNode::schema() const { const Schema& ListNode::schema() const {
// Offset +1 so count is not registered (allow read only cause values is read only) // 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(
return k_schema; &staticMetaObject, Node::staticMetaObject.propertyCount() + 1, true);
return k_schema;
} }
QVariant ListNode::value(const QString& key) const { QVariant ListNode::value(const QString& key) const {
if (key != valuesKey()) { if (key != valuesKey()) {
qCCritical(lcSettings, qCCritical(
"Attempted to read %s on list node %s. List nodes only have a 'values' key, something is wrong.", lcSettings,
qUtf8Printable(key), qUtf8Printable(path())); "Attempted to read %s on list node %s. List nodes only have a "
return {}; "'values' key, something is wrong.",
} qUtf8Printable(key),
qUtf8Printable(path()));
return {};
}
return QVariant::fromValue(m_elements); return QVariant::fromValue(m_elements);
} }
bool ListNode::setValue(const QString& key, const QVariant& value) { bool ListNode::setValue(const QString& key, const QVariant& value) {
return setValue(key, value, nullptr); return setValue(key, value, nullptr);
} }
bool ListNode::setValue(const QString& key, const QVariant& value, QList<Diagnostic>* diagnostics) { bool ListNode::setValue(
if (key != valuesKey()) { const QString& key, const QVariant& value, QList<Diagnostic>* diagnostics) {
qCCritical(lcSettings, if (key != valuesKey()) {
"Attempted to write %s on list node %s. List nodes only have a 'values' key, something is wrong.", qCCritical(
qUtf8Printable(key), qUtf8Printable(path())); lcSettings,
return false; "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(); const auto len = m_elements.count();
for (auto* const element : std::as_const(m_elements)) for (auto* const element : std::as_const(m_elements))
deleteNode(element); deleteNode(element);
m_elements.clear(); m_elements.clear();
if (value.typeId() == qMetaTypeId<QList<Node*>>()) { if (value.typeId() == qMetaTypeId<QList<Node*>>()) {
// On reset to fallback // On reset to fallback
const auto nodes = value.value<QList<Node*>>(); const auto nodes = value.value<QList<Node*>>();
for (auto* const node : nodes) for (auto* const node : nodes)
insertNode(m_elements.count(), node); insertNode(m_elements.count(), node);
} else if (isVariantListType(value)) { } else if (isVariantListType(value)) {
// From defaults or QML // From defaults or QML
const auto list = toMaps(value); const auto list = toMaps(value);
for (qsizetype i = 0; i < list.count(); ++i) for (qsizetype i = 0; i < list.count(); ++i)
insertNode(i, list.at(i), fallbackFor(i)); insertNode(i, list.at(i), fallbackFor(i));
} else if (value.typeId() == QMetaType::QJsonArray) { } else if (value.typeId() == QMetaType::QJsonArray) {
// From syncJson // From syncJson
const auto array = value.toJsonArray(); const auto array = value.toJsonArray();
for (const auto& json : array) for (const auto& json : array)
insertNode(m_elements.count(), json.toObject(), *diagnostics); insertNode(m_elements.count(), json.toObject(), *diagnostics);
} else { insertMissingDefaults();
qCCritical(lcSettings, "Unexpected type %s for list node %s, something is wrong.", value.typeName(), } else {
qUtf8Printable(path())); qCCritical(
return false; lcSettings,
} "Unexpected type %s for list node %s, something is wrong.",
value.typeName(),
qUtf8Printable(path()));
return false;
}
if (!recordWrite(valuesKey(), true)) if (!recordWrite(valuesKey(), true)) return false;
return false;
if (len != m_elements.count()) if (len != m_elements.count()) emit countChanged();
emit countChanged();
NodeChanges added(m_elements.count()); NodeChanges added(m_elements.count());
NodeChanges removed(len); NodeChanges removed(len);
std::iota(added.begin(), added.end(), 0); std::iota(added.begin(), added.end(), 0);
std::iota(removed.begin(), removed.end(), 0); std::iota(removed.begin(), removed.end(), 0);
emit elementsChanged(added, removed, {}); emit elementsChanged(added, removed, {});
return true; 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() { void ListNode::resetToDefaults() {
if (isNested()) { if (isNested()) {
qCCritical(lcSettings, "List node %s has a parent list node, resetToDefaults should never be called on it.", qCCritical(
qUtf8Printable(path())); lcSettings,
return; "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 // Don't reset global only list nodes on overlays
if (fallbackNode() && m_globalOnly) if (fallbackNode() && m_globalOnly) return;
return;
const WriteScope scope(this, WriteOrigin::FileReset); 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 { QJsonValue ListNode::toJson(bool sparse) const {
Q_UNUSED(sparse) // Lists can't be sparse Q_UNUSED(sparse) // Lists can't be sparse
QJsonArray array; QJsonArray array;
for (auto* const element : m_elements) for (auto* const element : m_elements)
array << element->toJson(false); array << element->toJson(false);
return array; return array;
} }
bool ListNode::syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) { bool ListNode::syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) {
if (!json.isArray()) { if (!json.isArray()) {
const auto d = Diagnostic::mismatch(ExpectedType::Array, json, path()); const auto d = Diagnostic::mismatch(ExpectedType::Array, json, path());
qCWarning(lcSettings, "Error decoding option %s: %s", qUtf8Printable(d.option), qCWarning(
qUtf8Printable(d.message)); lcSettings,
diagnostics << d; "Error decoding option %s: %s",
return false; qUtf8Printable(d.option),
} qUtf8Printable(d.message));
diagnostics << d;
return false;
}
// Refuse syncs to global only list nodes on overlays // Refuse syncs to global only list nodes on overlays
if (rejectGlobalSync(diagnostics)) if (rejectGlobalSync(diagnostics)) return false;
return false;
const WriteScope scope(this, WriteOrigin::File); const WriteScope scope(this, WriteOrigin::File);
setValue(valuesKey(), json.toArray(), &diagnostics); setValue(valuesKey(), json.toArray(), &diagnostics);
return true; return true;
} }
bool ListNode::recordWrite(const QString& key, bool changed) { bool ListNode::recordWrite(const QString& key, bool changed) {
const auto wasOverride = isOverride(key); const auto wasOverride = isOverride(key);
const auto notify = Node::recordWrite(key, changed); const auto notify = Node::recordWrite(key, changed);
// Elements inside overridden lists have no fallbacks, so detach here // Elements inside overridden lists have no fallbacks, so detach here
if (!wasOverride && isOverride(key)) if (!wasOverride && isOverride(key))
for (auto* const element : std::as_const(m_elements)) for (auto* const element : std::as_const(m_elements))
element->detachFallback(); element->detachFallback();
return notify; return notify;
} }
QString ListNode::keyOf(const Node* child) const { QString ListNode::keyOf(const Node* child) const {
return QString::number(m_elements.indexOf(const_cast<Node*>(child))); return QString::number(m_elements.indexOf(const_cast<Node*>(child)));
} }
Node* ListNode::elementAt(qsizetype index) const { Node* ListNode::elementAt(qsizetype index) const {
if (!validIndex(index)) if (!validIndex(index)) return nullptr;
return nullptr; return m_elements.at(index);
return m_elements.at(index);
} }
Node* ListNode::insertElement(const QVariantMap& props, qsizetype index) { Node* ListNode::insertElement(const QVariantMap& props, qsizetype index) {
if (rejectGlobalMutation()) if (rejectGlobalMutation()) return nullptr;
return nullptr;
const WriteScope scope(this, WriteOrigin::Qml); const WriteScope scope(this, WriteOrigin::Qml);
if (!validIndex(index)) // Invalid index means append if (!validIndex(index)) // Invalid index means append
index = m_elements.count(); index = m_elements.count();
auto* const element = insertNode(index, props, nullptr); auto* const element = insertNode(index, props, nullptr);
recordWrite(valuesKey(), true); recordWrite(valuesKey(), true);
emit countChanged(); emit countChanged();
emit elementsChanged({ index }, {}, {}); emit elementsChanged({index}, {}, {});
return element; return element;
} }
QList<QVariantMap> ListNode::defaultValue() const { QList<QVariantMap> ListNode::defaultValue() const {
const auto* desc = getDescriptor(); const auto* desc = getDescriptor();
if (!desc) if (!desc) return {};
return {}; return desc->defaultValue().value<QList<QVariantMap>>();
return desc->defaultValue().value<QList<QVariantMap>>();
} }
bool ListNode::isNested() const { bool ListNode::isNested() const {
return qobject_cast<ListNode*>(parentNode()); return qobject_cast<ListNode*>(parentNode());
} }
bool ListNode::rejectGlobalMutation() const { bool ListNode::rejectGlobalMutation() const {
if (!m_globalOnly || !fallbackNode()) if (!m_globalOnly || !fallbackNode()) return false;
return false;
qCWarning(lcSettings, qCWarning(
"Attempted to mutate global list %s from an overlay layer, ignoring. " lcSettings,
"This should not be used, mutate global lists from the global layer instead.", "Attempted to mutate global list %s from an overlay layer, ignoring. "
qUtf8Printable(path())); "This should not be used, mutate global lists from the global layer "
"instead.",
qUtf8Printable(path()));
return true; return true;
} }
bool ListNode::validIndex(qsizetype index) const { bool ListNode::validIndex(qsizetype index) const {
return index >= 0 && index < m_elements.count(); return index >= 0 && index < m_elements.count();
} }
const Descriptor* ListNode::getDescriptor() const { const Descriptor* ListNode::getDescriptor() const {
if (!parentNode()) { // List nodes should not be root nodes if (!parentNode()) { // List nodes should not be root nodes
qCCritical(lcSettings, "List node %s has no parent, something is wrong.", qUtf8Printable(path())); qCCritical(
return nullptr; lcSettings,
} "List node %s has no parent, something is wrong.",
qUtf8Printable(path()));
return nullptr;
}
const auto* desc = parentNode()->schema().get(key()); const auto* desc = parentNode()->schema().get(key());
if (!desc) { if (!desc) {
qCCritical(lcSettings, "List node %s is not in parent schema, something is wrong.", qUtf8Printable(path())); qCCritical(
return nullptr; lcSettings,
} "List node %s is not in parent schema, something is wrong.",
qUtf8Printable(path()));
return nullptr;
}
return desc; return desc;
} }
Node* ListNode::fallbackFor(qsizetype index) const { Node* ListNode::fallbackFor(qsizetype index) const {
if (isOverride(valuesKey())) if (isOverride(valuesKey())) return nullptr;
return nullptr;
const auto* fallback = static_cast<ListNode*>(fallbackNode()); const auto* fallback = static_cast<ListNode*>(fallbackNode());
if (!fallback || !fallback->validIndex(index)) if (!fallback || !fallback->validIndex(index)) return nullptr;
return nullptr;
return fallback->m_elements.at(index); return fallback->m_elements.at(index);
} }
Node* ListNode::insertNode(qsizetype index, const QVariantMap& props, Node* fallback) { Node* ListNode::insertNode(
auto* const node = createElement(fallback); qsizetype index, const QVariantMap& props, Node* fallback) {
m_elements.insert(index, node); auto* const node = createElement(fallback);
m_elements.insert(index, node);
const WriteScope scope(node, WriteOrigin::Init); const WriteScope scope(node, WriteOrigin::Init);
syncNodeRecursive(node, props); syncNodeRecursive(node, props);
return node; return node;
} }
Node* ListNode::insertNode(qsizetype index, Node* node) { Node* ListNode::insertNode(qsizetype index, Node* node) {
auto* const copy = createElement(node); auto* const copy = createElement(node);
m_elements.insert(index, copy); m_elements.insert(index, copy);
return copy; return copy;
} }
Node* ListNode::insertNode(qsizetype index, const QJsonObject& json, QList<Diagnostic>& diagnostics) { Node* ListNode::insertNode(
// Write origin will be file/file reset, which is correct. qsizetype index, const QJsonObject& json, QList<Diagnostic>& diagnostics) {
// This function should only be called from the JSON path of setValue. // Write origin will be file/file reset, which is correct.
auto* const node = createElement(nullptr); // Nodes synced from JSON get no fallback as they are overrides // This function should only be called from the JSON path of setValue.
m_elements.insert(index, node); 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); node->syncJson(json, diagnostics);
return node; return node;
} }
void ListNode::onFallbackListNotify(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved) { void ListNode::onFallbackListNotify(
if (isOverride(valuesKey())) const NodeChanges& added,
return; const NodeChanges& removed,
const MoveChanges& moved) {
if (isOverride(valuesKey())) return;
const WriteScope scope(this, WriteOrigin::Layer); const WriteScope scope(this, WriteOrigin::Layer);
for (const auto index : removed | std::views::reverse) for (const auto index : removed | std::views::reverse)
deleteNode(m_elements.takeAt(index)); deleteNode(m_elements.takeAt(index));
for (const auto index : added) for (const auto index : added)
insertNode(index, fallbackFor(index)); insertNode(index, fallbackFor(index));
for (const auto& move : moved) for (const auto& move : moved)
m_elements.move(move.first, move.second); m_elements.move(move.first, move.second);
if (!recordWrite(valuesKey(), true)) if (!recordWrite(valuesKey(), true)) return;
return;
if (!added.isEmpty() || !removed.isEmpty()) if (!added.isEmpty() || !removed.isEmpty()) emit countChanged();
emit countChanged(); emit elementsChanged(added, removed, moved);
emit elementsChanged(added, removed, moved);
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+60 -43
View File
@@ -5,65 +5,82 @@
namespace ZShell::settings { namespace ZShell::settings {
class ListNode : public Node { class ListNode : public Node {
Q_OBJECT Q_OBJECT
QML_ANONYMOUS QML_ANONYMOUS
Q_PROPERTY(qsizetype count READ count NOTIFY countChanged) Q_PROPERTY(qsizetype count READ count NOTIFY countChanged)
Q_PROPERTY(QVariantList values READ values NOTIFY elementsChanged) Q_PROPERTY(QVariantList values READ values NOTIFY elementsChanged)
using NodeChanges = QList<qsizetype>; using NodeChanges = QList<qsizetype>;
using MoveChanges = QList<QPair<qsizetype, qsizetype>>; using MoveChanges = QList<QPair<qsizetype, qsizetype>>;
public: 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]] qsizetype count() const;
[[nodiscard]] QVariantList values() const; [[nodiscard]] QVariantList values() const;
Q_INVOKABLE void remove(qsizetype index); Q_INVOKABLE void remove(qsizetype index);
Q_INVOKABLE void move(qsizetype from, qsizetype to); Q_INVOKABLE void move(qsizetype from, qsizetype to);
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
[[nodiscard]] QString pathFor(const QString& key) const override; [[nodiscard]] QString pathFor(const QString& key) const override;
[[nodiscard]] const Schema& schema() const override; [[nodiscard]] const Schema& schema() const override;
[[nodiscard]] QVariant value(const QString& key) 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) override;
bool setValue(const QString& key, const QVariant& value, QList<Diagnostic>* diagnostics); bool setValue(
void resetToDefaults() override; const QString& key,
const QVariant& value,
QList<Diagnostic>* diagnostics);
void resetToDefaults() override;
[[nodiscard]] QJsonValue toJson(bool sparse = true) const 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: signals:
void countChanged(); 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: protected:
bool recordWrite(const QString& key, bool changed) override; bool recordWrite(const QString& key, bool changed) override;
[[nodiscard]] QString keyOf(const Node* child) const override; [[nodiscard]] QString keyOf(const Node* child) const override;
[[nodiscard]] Node* elementAt(qsizetype index) const; [[nodiscard]] Node* elementAt(qsizetype index) const;
[[nodiscard]] Node* insertElement(const QVariantMap& props, qsizetype index = -1); [[nodiscard]] Node* insertElement(
[[nodiscard]] virtual Node* createElement(Node* fallback) = 0; const QVariantMap& props, qsizetype index = -1);
[[nodiscard]] virtual Node* createElement(Node* fallback) = 0;
private: private:
QList<Node*> m_elements; QList<Node*> m_elements;
[[nodiscard]] QList<QVariantMap> defaultValue() const; [[nodiscard]] QList<QVariantMap> defaultValue() const;
[[nodiscard]] bool isNested() const; [[nodiscard]] bool isNested() const;
// Returns true if the mutation should be skipped, overlays cannot mutate global lists // Returns true if the mutation should be skipped, overlays cannot mutate global lists
[[nodiscard]] bool rejectGlobalMutation() const; [[nodiscard]] bool rejectGlobalMutation() const;
[[nodiscard]] bool validIndex(qsizetype index) const; [[nodiscard]] bool validIndex(qsizetype index) const;
[[nodiscard]] const Descriptor* getDescriptor() const; [[nodiscard]] const Descriptor* getDescriptor() const;
[[nodiscard]] Node* fallbackFor(qsizetype index) const; [[nodiscard]] Node* fallbackFor(qsizetype index) const;
Node* insertNode(qsizetype index, const QVariantMap& props, Node* fallback); void insertMissingDefaults();
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); 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 } // namespace ZShell::settings
+191 -163
View File
@@ -6,275 +6,303 @@ namespace ZShell::settings {
using Qt::StringLiterals::operator""_s; using Qt::StringLiterals::operator""_s;
Node::Node(Node* fallback, QObject* parent, bool globalOnly) Node::Node(Node* fallback, QObject* parent, bool globalOnly)
: QObject(parent) : QObject(parent)
, m_globalOnly(globalOnly || (parentNode() && parentNode()->m_globalOnly)) , m_globalOnly(globalOnly || (parentNode() && parentNode()->m_globalOnly))
, m_rootNode(parentNode() ? parentNode()->rootNode() : this) , m_rootNode(parentNode() ? parentNode()->rootNode() : this)
, m_fallbackNode(fallback) , m_fallbackNode(fallback)
, m_writeOrigin(WriteOrigin::Qml) , m_writeOrigin(WriteOrigin::Qml)
, m_internalRead(false) , m_internalRead(false)
, m_batcher(m_rootNode == this ? new ChangeBatcher(this) : nullptr) { , m_batcher(m_rootNode == this ? new ChangeBatcher(this) : nullptr) {
if (fallback && !m_globalOnly) if (fallback && !m_globalOnly)
QObject::connect(fallback, &Node::optionChanged, this, &Node::onFallbackNotify); QObject::connect(
fallback, &Node::optionChanged, this, &Node::onFallbackNotify);
} }
QString Node::key() const { QString Node::key() const {
return parentNode() ? parentNode()->keyOf(this) : QString(); return parentNode() ? parentNode()->keyOf(this) : QString();
} }
QString Node::path() const { QString Node::path() const {
return parentNode() ? parentNode()->pathFor(key()) : key(); return parentNode() ? parentNode()->pathFor(key()) : key();
} }
QString Node::pathFor(const QString& key) const { QString Node::pathFor(const QString& key) const {
const auto p = path(); const auto p = path();
return p.isEmpty() ? key : p + u'.' + key; return p.isEmpty() ? key : p + u'.' + key;
} }
QString Node::elementPath(const QString& path, const QString& index) { QString Node::elementPath(const QString& path, const QString& index) {
return path + u'[' + index + u']'; return path + u'[' + index + u']';
} }
Node* Node::parentNode() const { Node* Node::parentNode() const {
return qobject_cast<Node*>(parent()); return qobject_cast<Node*>(parent());
} }
Node* Node::rootNode() const { Node* Node::rootNode() const {
return m_rootNode; return m_rootNode;
} }
Node* Node::fallbackNode() const { Node* Node::fallbackNode() const {
return m_fallbackNode; return m_fallbackNode;
} }
void Node::detachFallback() { void Node::detachFallback() {
if (m_fallbackNode) { if (m_fallbackNode) {
QObject::disconnect(m_fallbackNode, nullptr, this, nullptr); QObject::disconnect(m_fallbackNode, nullptr, this, nullptr);
m_fallbackNode = nullptr; m_fallbackNode = nullptr;
} }
const auto childNodes = findChildren<Node*>(Qt::FindDirectChildrenOnly); const auto childNodes = findChildren<Node*>(Qt::FindDirectChildrenOnly);
for (auto* const child : childNodes) for (auto* const child : childNodes)
child->detachFallback(); child->detachFallback();
} }
bool Node::isGlobalOnly() const { bool Node::isGlobalOnly() const {
return m_globalOnly; return m_globalOnly;
} }
bool Node::isOverride(const QString& key) const { bool Node::isOverride(const QString& key) const {
return m_overrides.contains(key); return m_overrides.contains(key);
} }
const QSet<QString>& Node::overrides() const { const QSet<QString>& Node::overrides() const {
return m_overrides; return m_overrides;
} }
bool Node::hasContent() const { bool Node::hasContent() const {
return !m_overrides.isEmpty() || m_quarantine || 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 { QVariant Node::value(const QString& key) const {
const auto* desc = schema().get(key); const auto* desc = schema().get(key);
if (!desc) { if (!desc) {
qCCritical( qCCritical(
lcSettings, "Attempted to read an unknown key %s, something is wrong.", qUtf8Printable(pathFor(key))); lcSettings,
return {}; "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 // Generated getters warn on global reads, this silences them as the warning is only for QML reads
const InternalRead guard(m_rootNode); const InternalRead guard(m_rootNode);
return metaObject()->property(desc->metaIndex).read(this); return metaObject()->property(desc->metaIndex).read(this);
} }
bool Node::setValue(const QString& key, const QVariant& value) { bool Node::setValue(const QString& key, const QVariant& value) {
const auto* desc = schema().get(key); const auto* desc = schema().get(key);
if (!desc) { if (!desc) {
qCCritical(lcSettings, "Attempted to set an unknown key %s, something is wrong.", qUtf8Printable(pathFor(key))); qCCritical(
return false; lcSettings,
} "Attempted to set an unknown key %s, something is wrong.",
qUtf8Printable(pathFor(key)));
return false;
}
if (desc->isNode) { if (desc->isNode) {
qCCritical(lcSettings, "Attempted to set node %s directly, something is wrong.", qUtf8Printable(pathFor(key))); qCCritical(
return false; 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 // Type mismatch, conversion should happen before this function is called
if (rejectInvalidWrite(key, value)) if (rejectInvalidWrite(key, value)) return false;
return false;
return metaObject()->property(desc->metaIndex).write(this, value); return metaObject()->property(desc->metaIndex).write(this, value);
} }
void Node::resetToDefaults() { void Node::resetToDefaults() {
// Don't reset global only nodes on overlays, the whole subtree belongs to the global layer // Don't reset global only nodes on overlays, the whole subtree belongs to the global layer
if (m_globalOnly && m_fallbackNode) if (m_globalOnly && m_fallbackNode) return;
return;
m_quarantine.reset(); // Reset quarantine as well m_quarantine.reset(); // Reset quarantine as well
// No write scope, callers should create the scope // No write scope, callers should create the scope
for (const auto& desc : schema().descriptors()) { for (const auto& desc : schema().descriptors()) {
if (desc.isNode) if (desc.isNode)
value(desc.key).value<Node*>()->resetToDefaults(); value(desc.key).value<Node*>()->resetToDefaults();
else if (!desc.globalOnly() || !m_fallbackNode) // Skip resetting global options on overlays else if (
setValue(desc.key, m_fallbackNode ? m_fallbackNode->value(desc.key) : desc.defaultValue()); !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 { const Quarantine* Node::quarantine() const {
return m_quarantine.get(); return m_quarantine.get();
} }
bool Node::rejectInvalidWrite(const QString& key, const QVariant& value) const { bool Node::rejectInvalidWrite(const QString& key, const QVariant& value) const {
const auto* desc = schema().get(key); const auto* desc = schema().get(key);
if (!desc || desc->accepts(value.metaType())) if (!desc || desc->accepts(value.metaType())) return false;
return false;
qCWarning(lcSettings, "Type mismatch for %s, expected %s got %s", qUtf8Printable(pathFor(key)), qCWarning(
qUtf8Printable(desc->typeString()), value.metaType().name()); lcSettings,
return true; "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 { void Node::warnGlobalRead(const QString& key) const {
if (!m_fallbackNode || m_rootNode->m_internalRead) if (!m_fallbackNode || m_rootNode->m_internalRead) return;
return;
qCWarning(lcSettings, qCWarning(
"Global option %s was read from an overlay layer. " lcSettings,
"This should not be used, read global options from the global layer instead.", "Global option %s was read from an overlay layer. "
qUtf8Printable(pathFor(key))); "This should not be used, read global options from the global layer "
"instead.",
qUtf8Printable(pathFor(key)));
} }
bool Node::rejectGlobalWrite(const QString& key) { bool Node::rejectGlobalWrite(const QString& key) {
const auto* desc = schema().get(key); const auto* desc = schema().get(key);
if (!desc) { if (!desc) {
qCCritical(lcSettings, "Attempted to check a write for an unknown key %s, something is seriously wrong...", qCCritical(
qUtf8Printable(pathFor(key))); lcSettings,
return false; "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 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) if ((!m_globalOnly && !desc->globalOnly()) || !fromUser || !m_fallbackNode)
return false; return false;
if (origin == WriteOrigin::QmlReset) if (origin == WriteOrigin::QmlReset)
qCWarning(lcSettings, qCWarning(
"Attempted to reset global option %s from an overlay layer, ignoring. " lcSettings,
"This should not be used, reset global options from the global layer instead.", "Attempted to reset global option %s from an overlay layer, "
qUtf8Printable(pathFor(key))); "ignoring. "
else "This should not be used, reset global options from the global "
qCWarning(lcSettings, "layer instead.",
"Attempted to write global option %s from an overlay layer, ignoring. " qUtf8Printable(pathFor(key)));
"This should not be used, write global options from the global layer instead.", else
qUtf8Printable(pathFor(key))); 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; return true;
} }
void Node::warnGlobalSync(QList<Diagnostic>& diagnostics, const QString& path) { void Node::warnGlobalSync(QList<Diagnostic>& diagnostics, const QString& path) {
qCWarning(lcSettings, "Global option definition %s found in overlay file, ignoring.", qUtf8Printable(path)); qCWarning(
diagnostics << Diagnostic{ lcSettings,
.type = DiagnosticType::GlobalOption, "Global option definition %s found in overlay file, ignoring.",
.option = path, qUtf8Printable(path));
.message = QStringLiteral("Global options should not be defined in overlay files"), 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 { bool Node::rejectGlobalSync(QList<Diagnostic>& diagnostics) const {
if (!m_globalOnly || !m_fallbackNode) if (!m_globalOnly || !m_fallbackNode) return false;
return false;
warnGlobalSync(diagnostics, path()); warnGlobalSync(diagnostics, path());
return true; return true;
} }
bool Node::recordWrite(const QString& key, bool changed) { bool Node::recordWrite(const QString& key, bool changed) {
const auto* desc = schema().get(key); const auto* desc = schema().get(key);
if (!desc) { if (!desc) {
qCCritical(lcSettings, "Attempted to record a write for an unknown key %s, something is seriously wrong...", qCCritical(
qUtf8Printable(pathFor(key))); lcSettings,
return false; "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 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; bool dirty = changed;
switch (origin) { switch (origin) {
// Init does not notify or write to file // Init does not notify or write to file
case WriteOrigin::Init: case WriteOrigin::Init:
return false; return false;
// File and qml both count as overrides // File and qml both count as overrides
case WriteOrigin::File: case WriteOrigin::File:
case WriteOrigin::Qml: case WriteOrigin::Qml:
dirty |= !m_overrides.contains(key); dirty |= !m_overrides.contains(key);
m_overrides << key; m_overrides << key;
break; break;
// Layer is not an override, it is a sync with the fallback value // Layer is not an override, it is a sync with the fallback value
case WriteOrigin::Layer: case WriteOrigin::Layer:
break; break;
// Both resets clear the override // Both resets clear the override
case WriteOrigin::FileReset: case WriteOrigin::FileReset:
case WriteOrigin::QmlReset: case WriteOrigin::QmlReset:
dirty |= m_overrides.remove(key); dirty |= m_overrides.remove(key);
break; break;
} }
// User writes/reset override quarantine // User writes/reset override quarantine
if (fromUser) if (fromUser) dirty |= removeQuarantined(key);
dirty |= removeQuarantined(key);
// Both qml and reset write to the file (only write if dirty) // Both qml and reset write to the file (only write if dirty)
if (fromUser && dirty) if (fromUser && dirty) m_rootNode->m_batcher->dirty();
m_rootNode->m_batcher->dirty();
if (changed) if (changed) emit optionChanged(key);
emit optionChanged(key);
return changed; return changed;
} }
bool Node::removeQuarantined(const QString& key) { bool Node::removeQuarantined(const QString& key) {
if (!m_quarantine) if (!m_quarantine) return false;
return false;
const auto removed = m_quarantine->remove(key); const auto removed = m_quarantine->remove(key);
if (m_quarantine->isEmpty()) if (m_quarantine->isEmpty()) m_quarantine.reset();
m_quarantine.reset();
return removed; return removed;
} }
ChangeBatcher* Node::batcher() const { ChangeBatcher* Node::batcher() const {
return m_rootNode->m_batcher; return m_rootNode->m_batcher;
} }
QString Node::keyOf(const Node* child) const { QString Node::keyOf(const Node* child) const {
for (const auto& desc : schema().descriptors()) { for (const auto& desc : schema().descriptors()) {
if (desc.isNode && child == value(desc.key).value<Node*>()) if (desc.isNode && child == value(desc.key).value<Node*>())
return desc.key; return desc.key;
} }
return {}; return {};
} }
void Node::onFallbackNotify(const QString& key) { void Node::onFallbackNotify(const QString& key) {
if (m_overrides.contains(key)) if (m_overrides.contains(key)) return;
return;
// Don't mirror global options onto overlays // Don't mirror global options onto overlays
const auto* desc = schema().get(key); const auto* desc = schema().get(key);
if (desc && desc->globalOnly()) if (desc && desc->globalOnly()) return;
return;
const WriteScope scope(this, WriteOrigin::Layer); const WriteScope scope(this, WriteOrigin::Layer);
setValue(key, m_fallbackNode->value(key)); setValue(key, m_fallbackNode->value(key));
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+79 -63
View File
@@ -12,89 +12,105 @@
namespace ZShell::settings { namespace ZShell::settings {
class Node : public QObject { class Node : public QObject {
Q_OBJECT Q_OBJECT
public: public:
// Global only nodes are inherited, anything inside one is also global only // 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 key() const; // The key of this in the parent node
[[nodiscard]] QString path() const; [[nodiscard]] QString path() const;
[[nodiscard]] virtual QString pathFor(const QString& key) const; [[nodiscard]] virtual QString pathFor(const QString& key) const;
[[nodiscard]] static QString elementPath(const QString& path, const QString& index); [[nodiscard]] static QString elementPath(
[[nodiscard]] Node* parentNode() const; const QString& path, const QString& index);
[[nodiscard]] Node* rootNode() const; [[nodiscard]] Node* parentNode() const;
[[nodiscard]] Node* fallbackNode() const; [[nodiscard]] Node* rootNode() const;
void detachFallback(); // Recursive, unlinks this and its children from the fallback layer [[nodiscard]] Node* fallbackNode() const;
void
detachFallback(); // Recursive, unlinks this and its children from the fallback layer
[[nodiscard]] Q_INVOKABLE bool isGlobalOnly() const; [[nodiscard]] Q_INVOKABLE bool isGlobalOnly() const;
[[nodiscard]] bool isOverride(const QString& key) const; [[nodiscard]] bool isOverride(const QString& key) const;
[[nodiscard]] const QSet<QString>& overrides() const; [[nodiscard]] const QSet<QString>& overrides() const;
[[nodiscard]] bool hasContent() const; // Recursive [[nodiscard]] bool hasContent() const; // Recursive
[[nodiscard]] virtual const Schema& schema() const = 0; [[nodiscard]] virtual const Schema& schema() const = 0;
[[nodiscard]] virtual QVariant value(const QString& key) const; [[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 bool setValue(
virtual void resetToDefaults(); // Recursive, resets to fallbacks then defaults if not overridden 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; [[nodiscard]] virtual QJsonValue toJson(bool sparse = true) const = 0;
// Returns false if the entire node was rejected // Returns false if the entire node was rejected
virtual bool syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) = 0; virtual bool syncJson(
[[nodiscard]] const Quarantine* quarantine() const; const QJsonValue& json, QList<Diagnostic>& diagnostics) = 0;
[[nodiscard]] const Quarantine* quarantine() const;
signals: signals:
void optionChanged(const QString& key); void optionChanged(const QString& key);
protected: protected:
// Null means empty, otherwise it has content // Null means empty, otherwise it has content
std::unique_ptr<Quarantine> m_quarantine; std::unique_ptr<Quarantine> m_quarantine;
const bool m_globalOnly; // Own flag or inherited from the parent node const bool m_globalOnly; // Own flag or inherited from the parent node
void warnGlobalRead(const QString& key) const; void warnGlobalRead(const QString& key) const;
// Returns true if the write should be skipped afterwards, the value is not one of the allowed types // 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; [[nodiscard]] bool rejectInvalidWrite(
template <typename T> [[nodiscard]] bool rejectInvalidWrite(const QString& key, const T& value) const; const QString& key, const QVariant& value) const;
// Returns true if the write should be skipped afterwards, overlays cannot write global options template <typename T>
bool rejectGlobalWrite(const QString& key); [[nodiscard]] bool rejectInvalidWrite(
static void warnGlobalSync(QList<Diagnostic>& diagnostics, const QString& path); const QString& key, const T& value) const;
bool rejectGlobalSync(QList<Diagnostic>& diagnostics) const; // Returns true if the sync should be rejected // Returns true if the write should be skipped afterwards, overlays cannot write global options
// Returns true if the notify signal should be emitted bool rejectGlobalWrite(const QString& key);
virtual bool recordWrite(const QString& key, bool changed); 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]] bool removeQuarantined(const QString& key);
[[nodiscard]] ChangeBatcher* batcher() const; [[nodiscard]] ChangeBatcher* batcher() const;
[[nodiscard]] virtual QString keyOf(const Node* child) const; [[nodiscard]] virtual QString keyOf(const Node* child) const;
template <typename C, typename T> 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: private:
QSet<QString> m_overrides; // Overridden keys from file/qml writes QSet<QString> m_overrides; // Overridden keys from file/qml writes
Node* const m_rootNode; Node* const m_rootNode;
Node* m_fallbackNode; // No fallback node either means global tree or inside overridden list Node* m_fallbackNode; // No fallback node either means global tree or inside overridden list
// For root node use only // For root node use only
WriteOrigin m_writeOrigin; WriteOrigin m_writeOrigin;
bool m_internalRead; bool m_internalRead;
ChangeBatcher* const m_batcher; ChangeBatcher* const m_batcher;
void onFallbackNotify(const QString& key); void onFallbackNotify(const QString& key);
friend class WriteScope; friend class WriteScope;
friend class InternalRead; friend class InternalRead;
}; };
template <typename T> bool Node::rejectInvalidWrite(const QString& key, const T& value) const { template <typename T>
Q_UNUSED(key) bool Node::rejectInvalidWrite(const QString& key, const T& value) const {
Q_UNUSED(value) Q_UNUSED(key)
return false; // Only QVariant unions can be given the wrong type 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>
const auto* fallback = static_cast<const C*>(m_fallbackNode); T Node::fallbackValue(
return fallback ? fallback->*member : defaultValue; 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 } // namespace ZShell::settings
+8 -8
View File
@@ -5,23 +5,23 @@
namespace ZShell::settings { namespace ZShell::settings {
void ObjectQuarantine::insert(const QString& key, const QJsonValue& value) { void ObjectQuarantine::insert(const QString& key, const QJsonValue& value) {
m_quarantine.insert(key, value); m_quarantine.insert(key, value);
} }
bool ObjectQuarantine::remove(const QString& key) { bool ObjectQuarantine::remove(const QString& key) {
return m_quarantine.remove(key); return m_quarantine.remove(key);
} }
bool ObjectQuarantine::isEmpty() const { bool ObjectQuarantine::isEmpty() const {
return m_quarantine.isEmpty(); return m_quarantine.isEmpty();
} }
QJsonValue ObjectQuarantine::apply(const QJsonValue& json) const { QJsonValue ObjectQuarantine::apply(const QJsonValue& json) const {
auto result = json.toObject(); auto result = json.toObject();
for (const auto& [key, value] : m_quarantine.asKeyValueRange()) for (const auto& [key, value] : m_quarantine.asKeyValueRange())
if (!result.contains(key)) // Don't clobber existing values if (!result.contains(key)) // Don't clobber existing values
result.insert(key, value); result.insert(key, value);
return result; return result;
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+15 -15
View File
@@ -7,27 +7,27 @@
namespace ZShell::settings { namespace ZShell::settings {
class Quarantine { class Quarantine {
public: public:
explicit Quarantine() = default; explicit Quarantine() = default;
virtual ~Quarantine() = default; virtual ~Quarantine() = default;
virtual void insert(const QString& key, const QJsonValue& value) = 0; virtual void insert(const QString& key, const QJsonValue& value) = 0;
[[nodiscard]] virtual bool remove(const QString& key) = 0; [[nodiscard]] virtual bool remove(const QString& key) = 0;
[[nodiscard]] virtual bool isEmpty() const = 0; [[nodiscard]] virtual bool isEmpty() const = 0;
[[nodiscard]] virtual QJsonValue apply(const QJsonValue& json) const = 0; [[nodiscard]] virtual QJsonValue apply(const QJsonValue& json) const = 0;
Q_DISABLE_COPY_MOVE(Quarantine) Q_DISABLE_COPY_MOVE(Quarantine)
}; };
class ObjectQuarantine : public Quarantine { class ObjectQuarantine : public Quarantine {
public: public:
void insert(const QString& key, const QJsonValue& value) override; void insert(const QString& key, const QJsonValue& value) override;
[[nodiscard]] bool remove(const QString& key) override; [[nodiscard]] bool remove(const QString& key) override;
[[nodiscard]] bool isEmpty() const override; [[nodiscard]] bool isEmpty() const override;
[[nodiscard]] QJsonValue apply(const QJsonValue& json) const override; [[nodiscard]] QJsonValue apply(const QJsonValue& json) const override;
private: private:
QHash<QString, QJsonValue> m_quarantine; QHash<QString, QJsonValue> m_quarantine;
}; };
} // namespace ZShell::settings } // namespace ZShell::settings
+22 -21
View File
@@ -5,42 +5,43 @@
namespace ZShell::settings { namespace ZShell::settings {
RootNode::RootNode(const QString& path, RootNode* fallback, QObject* parent) RootNode::RootNode(const QString& path, RootNode* fallback, QObject* parent)
: ObjectNode(fallback, parent) : ObjectNode(fallback, parent), m_file(new SettingsFile(path, this)) {
, m_file(new SettingsFile(path, this)) { QObject::connect(
QObject::connect(batcher(), &ChangeBatcher::dirtied, this, &RootNode::saveToFile); batcher(), &ChangeBatcher::dirtied, this, &RootNode::saveToFile);
QObject::connect(m_file, &SettingsFile::changed, this, &RootNode::reloadFromFile); QObject::connect(
QObject::connect(m_file, &SettingsFile::readFailed, this, [this](const QString& error) { m_file, &SettingsFile::changed, this, &RootNode::reloadFromFile);
emit treeLoadFailed(this, error); QObject::connect(
}); m_file, &SettingsFile::readFailed, this, [this](const QString& error) {
QObject::connect(m_file, &SettingsFile::writeFailed, this, [this](const QString& error) { emit treeLoadFailed(this, error);
emit treeSaveFailed(this, error); });
}); QObject::connect(
m_file, &SettingsFile::writeFailed, this, [this](const QString& error) {
emit treeSaveFailed(this, error);
});
} }
void RootNode::load() { void RootNode::load() {
if (m_file->load() != SettingsFile::LoadResult::Changed) if (m_file->load() != SettingsFile::LoadResult::Changed) reloadFromFile();
reloadFromFile();
} }
QList<Diagnostic> RootNode::diagnostics() const { QList<Diagnostic> RootNode::diagnostics() const {
return m_diagnostics; return m_diagnostics;
} }
void RootNode::reloadFromFile() { void RootNode::reloadFromFile() {
const auto oldDiagnostics = m_diagnostics; const auto oldDiagnostics = m_diagnostics;
m_diagnostics.clear(); m_diagnostics.clear();
const auto data = m_file->read(); const auto data = m_file->read();
syncJson(data ? data.value() : QJsonObject(), m_diagnostics); syncJson(data ? data.value() : QJsonObject(), m_diagnostics);
if (m_diagnostics != oldDiagnostics) if (m_diagnostics != oldDiagnostics) emit diagnosticsChanged();
emit diagnosticsChanged();
emit treeLoaded(this); emit treeLoaded(this);
} }
void RootNode::saveToFile() { void RootNode::saveToFile() {
m_file->write(toJson()); m_file->write(toJson());
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+19 -16
View File
@@ -7,29 +7,32 @@
namespace ZShell::settings { namespace ZShell::settings {
class RootNode : public ObjectNode { class RootNode : public ObjectNode {
Q_OBJECT 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: 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; [[nodiscard]] QList<Diagnostic> diagnostics() const;
void load(); void load();
signals: signals:
void diagnosticsChanged(); void diagnosticsChanged();
void treeLoaded(RootNode* self); void treeLoaded(RootNode* self);
void treeLoadFailed(RootNode* self, const QString& error); void treeLoadFailed(RootNode* self, const QString& error);
void treeSaveFailed(RootNode* self, const QString& error); void treeSaveFailed(RootNode* self, const QString& error);
private: private:
SettingsFile* const m_file; SettingsFile* const m_file;
QList<Diagnostic> m_diagnostics; QList<Diagnostic> m_diagnostics;
void reloadFromFile(); void reloadFromFile();
void saveToFile(); void saveToFile();
}; };
} // namespace ZShell::settings } // namespace ZShell::settings
+74 -70
View File
@@ -16,118 +16,122 @@ namespace {
// Descriptor annotations are stored here by Schema::annotate, which is called in the CONFIG_XXX macros. // Descriptor annotations are stored here by Schema::annotate, which is called in the CONFIG_XXX macros.
// Schema::build then takes them from here. // Schema::build then takes them from here.
QHash<const QMetaObject*, QHash<QString, Annotation>>& annotationCache() { QHash<const QMetaObject*, QHash<QString, Annotation>>& annotationCache() {
static QHash<const QMetaObject*, QHash<QString, Annotation>> s_cache; static QHash<const QMetaObject*, QHash<QString, Annotation>> s_cache;
return s_cache; return s_cache;
} }
// Union options carry their alternatives in the annotation, everything else goes by property type // Union options carry their alternatives in the annotation, everything else goes by property type
const ValueCodec* resolveCodec(Descriptor& desc) { const ValueCodec* resolveCodec(Descriptor& desc) {
auto& allowed = desc.annotation.allowedTypes; auto& allowed = desc.annotation.allowedTypes;
if (allowed.isEmpty()) if (allowed.isEmpty()) return ValueCodec::codecFor(desc.type);
return ValueCodec::codecFor(desc.type);
if (desc.type.id() != QMetaType::QVariant) { if (desc.type.id() != QMetaType::QVariant) {
qCCritical(lcSchema, "Allowed types are only valid for QVariant properties, ignoring them for %s", qCCritical(
qUtf8Printable(desc.key)); lcSchema,
allowed.clear(); "Allowed types are only valid for QVariant properties, ignoring "
return ValueCodec::codecFor(desc.type); "them for %s",
} qUtf8Printable(desc.key));
allowed.clear();
return ValueCodec::codecFor(desc.type);
}
if (const auto* codec = ValueCodec::unionFor(allowed)) if (const auto* codec = ValueCodec::unionFor(allowed)) return codec;
return codec;
allowed.clear(); allowed.clear();
return nullptr; return nullptr;
} }
bool isNodeType(const QMetaType& type) { bool isNodeType(const QMetaType& type) {
if (!type.flags().testFlag(QMetaType::PointerToQObject)) if (!type.flags().testFlag(QMetaType::PointerToQObject)) return false;
return false; const auto* meta = type.metaObject();
const auto* meta = type.metaObject(); return meta && meta->inherits(&Node::staticMetaObject);
return meta && meta->inherits(&Node::staticMetaObject);
} }
} // namespace } // namespace
QString Descriptor::typeString() const { QString Descriptor::typeString() const {
if (annotation.allowedTypes.isEmpty()) if (annotation.allowedTypes.isEmpty())
return QString::fromUtf8(type.name()); return QString::fromUtf8(type.name());
QStringList names; QStringList names;
names.reserve(annotation.allowedTypes.size()); names.reserve(annotation.allowedTypes.size());
for (const auto& allowed : annotation.allowedTypes) for (const auto& allowed : annotation.allowedTypes)
names << QString::fromUtf8(allowed.name()); names << QString::fromUtf8(allowed.name());
return names.join(QStringLiteral(" | ")); return names.join(QStringLiteral(" | "));
} }
bool Descriptor::accepts(const QMetaType& valueType) const { bool Descriptor::accepts(const QMetaType& valueType) const {
// Unset is a valid state for QVariant options, they are simply absent from the file // Unset is a valid state for QVariant options, they are simply absent from the file
if (type.id() == QMetaType::QVariant && !valueType.isValid()) if (type.id() == QMetaType::QVariant && !valueType.isValid()) return true;
return true;
if (annotation.allowedTypes.isEmpty()) if (annotation.allowedTypes.isEmpty()) return type == valueType;
return type == valueType;
return annotation.allowedTypes.contains(valueType); return annotation.allowedTypes.contains(valueType);
} }
Schema Schema::build(const QMetaObject* meta, int baseOffset, bool includeReadOnly) { Schema Schema::build(
Schema schema; const QMetaObject* meta, int baseOffset, bool includeReadOnly) {
schema.m_descriptors.reserve(meta->propertyCount() - baseOffset); Schema schema;
schema.m_descriptors.reserve(meta->propertyCount() - baseOffset);
// Descriptors are taken from cache since this should be called exactly once per class // Descriptors are taken from cache since this should be called exactly once per class
const auto annotations = annotationCache().take(meta); const auto annotations = annotationCache().take(meta);
qCDebug(lcSchema) << "Building schema for" << meta->className(); qCDebug(lcSchema) << "Building schema for" << meta->className();
for (int i = baseOffset; i < meta->propertyCount(); ++i) { for (int i = baseOffset; i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i); const auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name()); const auto key = QString::fromUtf8(prop.name());
const auto isNode = isNodeType(prop.metaType()); const auto isNode = isNodeType(prop.metaType());
// Skip read only properties (unless includeReadOnly) // Skip read only properties (unless includeReadOnly)
if (!isNode && !includeReadOnly && !prop.isWritable()) { if (!isNode && !includeReadOnly && !prop.isWritable()) {
qCDebug(lcSchema) << " Skipping computed property" << key; qCDebug(lcSchema) << " Skipping computed property" << key;
continue; continue;
} }
qCDebug(lcSchema) << " Adding property" << key; qCDebug(lcSchema) << " Adding property" << key;
Descriptor desc{ Descriptor desc{
.key = key, .key = key,
.type = prop.metaType(), .type = prop.metaType(),
.metaIndex = i, .metaIndex = i,
.isNode = isNode, .isNode = isNode,
.annotation = annotations.value(key), .annotation = annotations.value(key),
}; };
if (!isNode) { if (!isNode) {
desc.codec = resolveCodec(desc); desc.codec = resolveCodec(desc);
if (!desc.codec) if (!desc.codec)
qCCritical(lcSchema, "No codec for %s of type %s, it will not be loaded or saved", qUtf8Printable(key), qCCritical(
desc.type.name()); 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_descriptors.append(std::move(desc));
schema.m_keyToIndex.insert(key, schema.m_descriptors.size() - 1); schema.m_keyToIndex.insert(key, schema.m_descriptors.size() - 1);
} }
return schema; return schema;
} }
void Schema::annotate(const QMetaObject* meta, const QString& key, Annotation annotation) { void Schema::annotate(
annotationCache()[meta].insert(key, std::move(annotation)); const QMetaObject* meta, const QString& key, Annotation annotation) {
annotationCache()[meta].insert(key, std::move(annotation));
} }
const QList<Descriptor>& Schema::descriptors() const { const QList<Descriptor>& Schema::descriptors() const {
return m_descriptors; return m_descriptors;
} }
const Descriptor* Schema::get(const QString& key) const { const Descriptor* Schema::get(const QString& key) const {
const auto it = m_keyToIndex.find(key); const auto it = m_keyToIndex.find(key);
return it != m_keyToIndex.end() ? &m_descriptors[it.value()] : nullptr; return it != m_keyToIndex.end() ? &m_descriptors[it.value()] : nullptr;
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+48 -40
View File
@@ -13,69 +13,77 @@ namespace ZShell::settings {
class ValueCodec; class ValueCodec;
struct Annotation { struct Annotation {
QVariant defaultValue; QVariant defaultValue;
bool globalOnly = false; bool globalOnly = false;
// NOLINTNEXTLINE(readability-redundant-member-init) callers will get missing field init warnings without // NOLINTNEXTLINE(readability-redundant-member-init) callers will get missing field init warnings without
QList<QMetaType> allowedTypes = {}; // For QVariant properties QList<QMetaType> allowedTypes = {}; // For QVariant properties
// NOLINTNEXTLINE(readability-redundant-member-init)
QString identityKey = {};
}; };
namespace detail { namespace detail {
// Return trivially copyable types (e.g. bool) by value and others (e.g. QVariant) by const ref // Return trivially copyable types (e.g. bool) by value and others (e.g. QVariant) by const ref
template <typename T> 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 } // namespace detail
#define ANNOTATION(Type, name) \ #define ANNOTATION(Type, name) \
Q_PROPERTY(Type name READ name) \ Q_PROPERTY(Type name READ name) \
\ \
public: \ public: \
[[nodiscard]] detail::AnnotationReturn<Type> name() const { \ [[nodiscard]] detail::AnnotationReturn<Type> name() const { \
return annotation.name; \ return annotation.name; \
} \ } \
\ \
private: private:
struct Descriptor { struct Descriptor {
Q_GADGET Q_GADGET
QML_VALUE_TYPE(descriptor) QML_VALUE_TYPE(descriptor)
Q_PROPERTY(QString key MEMBER key) Q_PROPERTY(QString key MEMBER key)
Q_PROPERTY(QString type READ typeString) Q_PROPERTY(QString type READ typeString)
Q_PROPERTY(int metaIndex MEMBER metaIndex) Q_PROPERTY(int metaIndex MEMBER metaIndex)
Q_PROPERTY(bool isNode MEMBER isNode) Q_PROPERTY(bool isNode MEMBER isNode)
ANNOTATION(QVariant, defaultValue) ANNOTATION(QVariant, defaultValue)
ANNOTATION(bool, globalOnly) ANNOTATION(bool, globalOnly)
ANNOTATION(QString, identityKey)
public: public:
QString key; QString key;
QMetaType type; QMetaType type;
int metaIndex; int metaIndex;
bool isNode; bool isNode;
Annotation annotation; Annotation annotation;
const ValueCodec* codec = nullptr; // Null for nodes and unsupported types const ValueCodec* codec = nullptr; // Null for nodes and unsupported types
[[nodiscard]] QString typeString() const; [[nodiscard]] QString typeString() const;
[[nodiscard]] bool accepts(const QMetaType& valueType) const; [[nodiscard]] bool accepts(const QMetaType& valueType) const;
}; };
#undef ANNOTATION #undef ANNOTATION
class Schema { class Schema {
public: public:
static Schema build(const QMetaObject* meta, int baseOffset, bool includeReadOnly = false); static Schema build(
static void annotate(const QMetaObject* meta, const QString& key, Annotation annotation); 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 QList<Descriptor>& descriptors() const;
[[nodiscard]] const Descriptor* get(const QString& key) const; [[nodiscard]] const Descriptor* get(const QString& key) const;
private: private:
explicit Schema() = default; explicit Schema() = default;
QList<Descriptor> m_descriptors; QList<Descriptor> m_descriptors;
QHash<QString, qsizetype> m_keyToIndex; QHash<QString, qsizetype> m_keyToIndex;
}; };
} // namespace ZShell::settings } // namespace ZShell::settings
+157 -120
View File
@@ -30,184 +30,221 @@ constexpr int k_maxLoadRetries = 3;
} // namespace } // namespace
SettingsFile::SettingsFile(QString path, QObject* parent) SettingsFile::SettingsFile(QString path, QObject* parent)
: QObject(parent) : QObject(parent)
, m_path(std::move(path)) , m_path(std::move(path))
, m_watcher(new QFileSystemWatcher(this)) , m_watcher(new QFileSystemWatcher(this))
, m_saveDebounce(new QTimer(this)) , m_saveDebounce(new QTimer(this))
, m_loadDebounce(new QTimer(this)) , m_loadDebounce(new QTimer(this))
, m_loadRetries(0) { , m_loadRetries(0) {
m_saveDebounce->setSingleShot(true); m_saveDebounce->setSingleShot(true);
m_saveDebounce->setInterval(500); // Save at most once every 500ms m_saveDebounce->setInterval(500); // Save at most once every 500ms
m_loadDebounce->setSingleShot(true); m_loadDebounce->setSingleShot(true);
m_loadDebounce->setInterval(50); // Coalesce watcher events within 50ms 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(
QObject::connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &SettingsFile::onDirChanged); m_watcher,
&QFileSystemWatcher::fileChanged,
this,
&SettingsFile::onFileChanged);
QObject::connect(
m_watcher,
&QFileSystemWatcher::directoryChanged,
this,
&SettingsFile::onDirChanged);
initWatcher(); initWatcher();
} }
std::optional<QJsonValue> SettingsFile::read() const { std::optional<QJsonValue> SettingsFile::read() const {
return m_lastData; return m_lastData;
} }
void SettingsFile::write(const QJsonValue& json) { void SettingsFile::write(const QJsonValue& json) {
m_pendingWrite = json; m_pendingWrite = json;
save(); save();
} }
void SettingsFile::onFileChanged() { void SettingsFile::onFileChanged() {
initWatcher(); // Re-add path in case the file was replaced initWatcher(); // Re-add path in case the file was replaced
scheduleLoad(); scheduleLoad();
} }
void SettingsFile::onDirChanged() { void SettingsFile::onDirChanged() {
// Ignore if the file is already watched // Ignore if the file is already watched
if (m_watcher->files().contains(m_path)) if (m_watcher->files().contains(m_path)) return;
return;
initWatcher(); initWatcher();
if (QFile::exists(m_path)) if (QFile::exists(m_path)) scheduleLoad();
scheduleLoad();
} }
void SettingsFile::initWatcher() { void SettingsFile::initWatcher() {
const QFileInfo info(m_path); const QFileInfo info(m_path);
const auto dir = info.absolutePath(); const auto dir = info.absolutePath();
if (QDir(dir).exists() && !m_watcher->directories().contains(dir)) if (QDir(dir).exists() && !m_watcher->directories().contains(dir))
m_watcher->addPath(dir); m_watcher->addPath(dir);
if (info.exists() && !m_watcher->files().contains(m_path)) if (info.exists() && !m_watcher->files().contains(m_path))
m_watcher->addPath(m_path); m_watcher->addPath(m_path);
} }
void SettingsFile::scheduleLoad() { void SettingsFile::scheduleLoad() {
// Ignore if a load is already pending // Ignore if a load is already pending
if (m_loadDebounce->isActive()) if (m_loadDebounce->isActive()) return;
return;
m_loadRetries = 0; m_loadRetries = 0;
m_loadDebounce->start(); m_loadDebounce->start();
} }
void SettingsFile::onLoadDebounced() { void SettingsFile::onLoadDebounced() {
const auto isFinalTry = m_loadRetries >= k_maxLoadRetries; const auto isFinalTry = m_loadRetries >= k_maxLoadRetries;
if (load(isFinalTry) == LoadResult::ParseError && !isFinalTry) { if (load(isFinalTry) == LoadResult::ParseError && !isFinalTry) {
// Likely a partial write, retry after another debounce // Likely a partial write, retry after another debounce
++m_loadRetries; ++m_loadRetries;
qCDebug(lcSettingsFile, "Retrying load of %s (%d/%d)", qUtf8Printable(m_path), m_loadRetries, k_maxLoadRetries); qCDebug(
m_loadDebounce->start(); lcSettingsFile,
} "Retrying load of %s (%d/%d)",
qUtf8Printable(m_path),
m_loadRetries,
k_maxLoadRetries);
m_loadDebounce->start();
}
} }
SettingsFile::LoadResult SettingsFile::load(bool reportErrors) { SettingsFile::LoadResult SettingsFile::load(bool reportErrors) {
QFile file(m_path); QFile file(m_path);
if (!file.exists()) { if (!file.exists()) {
if (m_lastData) { if (m_lastData) {
m_lastData = std::nullopt; m_lastData = std::nullopt;
emit changed(); emit changed();
return LoadResult::Changed; return LoadResult::Changed;
} }
return LoadResult::Unchanged; return LoadResult::Unchanged;
} }
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcSettingsFile, "Failed to open %s for reading: %s", qUtf8Printable(m_path), qCWarning(
qUtf8Printable(file.errorString())); lcSettingsFile,
emit readFailed(QStringLiteral("Failed to open: %1").arg(file.errorString())); "Failed to open %s for reading: %s",
return LoadResult::Error; qUtf8Printable(m_path),
} qUtf8Printable(file.errorString()));
emit readFailed(
QStringLiteral("Failed to open: %1").arg(file.errorString()));
return LoadResult::Error;
}
const auto data = file.readAll(); const auto data = file.readAll();
file.close(); file.close();
QJsonParseError error; QJsonParseError error;
const auto doc = QJsonDocument::fromJson(data, &error); const auto doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
if (reportErrors) { if (reportErrors) {
qCWarning(lcSettingsFile, "Failed to parse %s as JSON: %s", qUtf8Printable(m_path), qCWarning(
qUtf8Printable(error.errorString())); lcSettingsFile,
emit readFailed(QStringLiteral("Failed to parse: %1").arg(error.errorString())); "Failed to parse %s as JSON: %s",
} else { qUtf8Printable(m_path),
qCDebug(lcSettingsFile, "Failed to parse %s as JSON: %s", qUtf8Printable(m_path), qUtf8Printable(error.errorString()));
qUtf8Printable(error.errorString())); emit readFailed(
} QStringLiteral("Failed to parse: %1").arg(error.errorString()));
return LoadResult::ParseError; } 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()); const auto json = doc.isObject() ? QJsonValue(doc.object())
: QJsonValue(doc.array());
if (json == m_lastData) if (json == m_lastData) return LoadResult::Unchanged;
return LoadResult::Unchanged;
// Clear pending write, stuff loaded from file should take precedence // Clear pending write, stuff loaded from file should take precedence
m_pendingWrite = std::nullopt; m_pendingWrite = std::nullopt;
m_lastData = json; m_lastData = json;
emit changed(); emit changed();
qCDebug(lcSettingsFile) << "Read JSON from" << m_path; qCDebug(lcSettingsFile) << "Read JSON from" << m_path;
qCDebug(lcSettingsFile) << " " << json; qCDebug(lcSettingsFile) << " " << json;
return LoadResult::Changed; return LoadResult::Changed;
} }
void SettingsFile::save() { void SettingsFile::save() {
if (!m_pendingWrite) if (!m_pendingWrite) return;
return;
if (m_saveDebounce->isActive()) { if (m_saveDebounce->isActive()) {
// Queue save for debounce end // Queue save for debounce end
QObject::connect(m_saveDebounce, &QTimer::timeout, this, &SettingsFile::save, QObject::connect(
// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) ConnectionType is OR-able by design m_saveDebounce,
static_cast<Qt::ConnectionType>(Qt::UniqueConnection | Qt::SingleShotConnection)); &QTimer::timeout,
return; 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 // Debounce regardless of whether the save actually succeeded
m_saveDebounce->start(); m_saveDebounce->start();
const auto json = m_pendingWrite.value(); 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())
m_pendingWrite = std::nullopt; : QJsonDocument(json.toArray()))
.toJson();
m_pendingWrite = std::nullopt;
const auto dir = QFileInfo(m_path).absolutePath(); const auto dir = QFileInfo(m_path).absolutePath();
if (!QDir().mkpath(dir)) { if (!QDir().mkpath(dir)) {
qCWarning(lcSettingsFile) << "Failed to create dir" << dir; qCWarning(lcSettingsFile) << "Failed to create dir" << dir;
emit writeFailed(QStringLiteral("Failed to create parent directory")); emit writeFailed(QStringLiteral("Failed to create parent directory"));
return; return;
} }
// Write atomically // Write atomically
QSaveFile file(m_path); QSaveFile file(m_path);
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly)) {
qCWarning(lcSettingsFile, "Failed to open %s for writing: %s", qUtf8Printable(m_path), qCWarning(
qUtf8Printable(file.errorString())); lcSettingsFile,
emit writeFailed(QStringLiteral("Failed to open: %1").arg(file.errorString())); "Failed to open %s for writing: %s",
return; qUtf8Printable(m_path),
} qUtf8Printable(file.errorString()));
emit writeFailed(
QStringLiteral("Failed to open: %1").arg(file.errorString()));
return;
}
file.write(data); file.write(data);
if (!file.commit()) { if (!file.commit()) {
qCWarning(lcSettingsFile, "Failed to write %s: %s", qUtf8Printable(m_path), qUtf8Printable(file.errorString())); qCWarning(
emit writeFailed(QStringLiteral("Failed to write: %1").arg(file.errorString())); lcSettingsFile,
return; "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; m_lastData = json;
qCDebug(lcSettingsFile) << "Saved" << m_path; qCDebug(lcSettingsFile) << "Saved" << m_path;
initWatcher(); // The file may have just been created, or replaced by the rename initWatcher(); // The file may have just been created, or replaced by the rename
} }
} // namespace ZShell::settings } // namespace ZShell::settings
+25 -30
View File
@@ -8,44 +8,39 @@
namespace ZShell::settings { namespace ZShell::settings {
class SettingsFile : public QObject { class SettingsFile : public QObject {
Q_OBJECT Q_OBJECT
public: public:
enum class LoadResult : quint8 { enum class LoadResult : quint8 { Unchanged, Changed, ParseError, Error };
Unchanged,
Changed,
ParseError,
Error
};
explicit SettingsFile(QString path, QObject* parent = nullptr); explicit SettingsFile(QString path, QObject* parent = nullptr);
[[nodiscard]] std::optional<QJsonValue> read() const; [[nodiscard]] std::optional<QJsonValue> read() const;
void write(const QJsonValue& json); void write(const QJsonValue& json);
LoadResult load(bool reportErrors = true); LoadResult load(bool reportErrors = true);
signals: signals:
void changed(); // Data changed, not file watcher event void changed(); // Data changed, not file watcher event
void readFailed(const QString& error); void readFailed(const QString& error);
void writeFailed(const QString& error); void writeFailed(const QString& error);
private: private:
QString m_path; QString m_path;
QFileSystemWatcher* m_watcher; QFileSystemWatcher* m_watcher;
QTimer* m_saveDebounce; QTimer* m_saveDebounce;
QTimer* m_loadDebounce; QTimer* m_loadDebounce;
int m_loadRetries; int m_loadRetries;
std::optional<QJsonValue> m_lastData; std::optional<QJsonValue> m_lastData;
std::optional<QJsonValue> m_pendingWrite; std::optional<QJsonValue> m_pendingWrite;
void onFileChanged(); void onFileChanged();
void onDirChanged(); void onDirChanged();
void initWatcher(); void initWatcher();
void scheduleLoad(); void scheduleLoad();
void onLoadDebounced(); void onLoadDebounced();
void save(); void save();
}; };
} // namespace ZShell::settings } // namespace ZShell::settings