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