fix: define declared method + insert new bar entries to config file list + rename Hyprsunset -> Nightlight
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -205,6 +205,10 @@ qreal Gpu::memoryTotal() const {
|
||||
return m_memoryTotal;
|
||||
}
|
||||
|
||||
bool Gpu::detecting() const {
|
||||
return m_detecting;
|
||||
}
|
||||
|
||||
void Gpu::setType(GpuType value) {
|
||||
if (value == m_type) return;
|
||||
|
||||
|
||||
@@ -3,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
|
||||
|
||||
@@ -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
|
||||
|
||||
+260
-211
@@ -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<ExpectedType>& expected, const QJsonValue& value) {
|
||||
return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} };
|
||||
DecodeResult mismatch(
|
||||
const QList<ExpectedType>& expected, const QJsonValue& value) {
|
||||
return {
|
||||
.value = QVariant(),
|
||||
.error = Diagnostic::mismatch(expected, value),
|
||||
.indexPath = {}};
|
||||
}
|
||||
|
||||
template <typename Container> ValueCodec* makeListCodec(const QMetaType& type) {
|
||||
const auto* elementCodec = ValueCodec::codecFor(QMetaType::fromType<typename Container::value_type>());
|
||||
return elementCodec ? new ListCodec<Container>(type, elementCodec) : nullptr;
|
||||
const auto* elementCodec = ValueCodec::codecFor(
|
||||
QMetaType::fromType<typename Container::value_type>());
|
||||
return elementCodec ? new ListCodec<Container>(type, elementCodec)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
// Unions are keyed by their alternatives in order, since order decides which one wins
|
||||
QString unionKey(const QList<QMetaType>& types) {
|
||||
QStringList ids;
|
||||
ids.reserve(types.size());
|
||||
for (const auto& type : types)
|
||||
ids << QString::number(type.id());
|
||||
return ids.join(u","_s);
|
||||
QStringList ids;
|
||||
ids.reserve(types.size());
|
||||
for (const auto& type : types)
|
||||
ids << QString::number(type.id());
|
||||
return ids.join(u","_s);
|
||||
}
|
||||
|
||||
QList<ExpectedType> expectedTypesOf(const QList<const ValueCodec*>& alternatives) {
|
||||
QList<ExpectedType> types;
|
||||
types.reserve(alternatives.size());
|
||||
for (const auto* codec : alternatives)
|
||||
types << codec->expected();
|
||||
return types;
|
||||
QList<ExpectedType> expectedTypesOf(
|
||||
const QList<const ValueCodec*>& alternatives) {
|
||||
QList<ExpectedType> types;
|
||||
types.reserve(alternatives.size());
|
||||
for (const auto* codec : alternatives)
|
||||
types << codec->expected();
|
||||
return types;
|
||||
}
|
||||
|
||||
using ListFactory = ValueCodec* (*)(const QMetaType&);
|
||||
|
||||
const QHash<int, ListFactory>& listFactories() {
|
||||
static const QHash<int, ListFactory> k_factories{
|
||||
{ QMetaType::fromType<QStringList>().id(), &makeListCodec<QStringList> },
|
||||
{ QMetaType::fromType<QList<qreal>>().id(), &makeListCodec<QList<qreal>> },
|
||||
};
|
||||
return k_factories;
|
||||
static const QHash<int, ListFactory> k_factories{
|
||||
{QMetaType::fromType<QStringList>().id(), &makeListCodec<QStringList>},
|
||||
{QMetaType::fromType<QList<qreal>>().id(),
|
||||
&makeListCodec<QList<qreal>>},
|
||||
};
|
||||
return k_factories;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ValueCodec::ValueCodec(const QMetaType& type, ExpectedType expected)
|
||||
: m_type(type)
|
||||
, m_expected(expected) {}
|
||||
: 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<int, ValueCodec*> s_registry;
|
||||
// Cache for codecs, keyed by type id
|
||||
static QHash<int, ValueCodec*> s_registry;
|
||||
|
||||
// Cached lookup
|
||||
if (const auto it = s_registry.constFind(type.id()); it != s_registry.constEnd())
|
||||
return *it;
|
||||
// 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<QMetaType>& types) {
|
||||
// Cache for union codecs, keyed by their alternatives
|
||||
static QHash<QString, ValueCodec*> s_registry;
|
||||
// Cache for union codecs, keyed by their alternatives
|
||||
static QHash<QString, ValueCodec*> s_registry;
|
||||
|
||||
if (types.size() < 2) {
|
||||
qCCritical(lcSettings, "A union needs at least two types, got %lld", types.size());
|
||||
return nullptr;
|
||||
}
|
||||
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<const ValueCodec*> alternatives;
|
||||
alternatives.reserve(types.size());
|
||||
QList<const ValueCodec*> alternatives;
|
||||
alternatives.reserve(types.size());
|
||||
|
||||
for (const auto& type : types) {
|
||||
const auto* codec = codecFor(type);
|
||||
if (!codec) {
|
||||
qCCritical(lcSettings, "No codec found for type %s, cannot build union", type.name());
|
||||
return nullptr;
|
||||
}
|
||||
alternatives << codec;
|
||||
}
|
||||
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<int>::min();
|
||||
constexpr auto k_max = std::numeric_limits<int>::max();
|
||||
if (num < static_cast<double>(k_min) || num > static_cast<double>(k_max)) {
|
||||
const auto message = QStringLiteral("Integer %1 is out of range, expected between %2 and %3").arg(QString::number(num, 'f', 0), QString::number(k_min), QString::number(k_max));
|
||||
return error(DiagnosticType::InvalidValue, message);
|
||||
}
|
||||
constexpr auto k_min = std::numeric_limits<int>::min();
|
||||
constexpr auto k_max = std::numeric_limits<int>::max();
|
||||
if (num < static_cast<double>(k_min) || num > static_cast<double>(k_max)) {
|
||||
const auto message =
|
||||
QStringLiteral(
|
||||
"Integer %1 is out of range, expected between %2 and %3")
|
||||
.arg(
|
||||
QString::number(num, 'f', 0),
|
||||
QString::number(k_min),
|
||||
QString::number(k_max));
|
||||
return error(DiagnosticType::InvalidValue, message);
|
||||
}
|
||||
|
||||
return { .value = static_cast<int>(num), .error = std::nullopt, .indexPath = {} };
|
||||
return {
|
||||
.value = static_cast<int>(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<qreal>(value.toDouble()), .error = std::nullopt, .indexPath = {} };
|
||||
return {
|
||||
.value = QVariant::fromValue<qreal>(value.toDouble()),
|
||||
.error = std::nullopt,
|
||||
.indexPath = {}};
|
||||
}
|
||||
|
||||
QJsonValue StringCodec::encode(const QVariant& value) const {
|
||||
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<int>(), &raw, m_type, decoded.data())) {
|
||||
qCCritical(lcSettings, "Failed to convert value %d to enum %s", raw, m_type.name());
|
||||
return error(DiagnosticType::InvalidValue,
|
||||
QStringLiteral("Could not convert %1 to %2").arg(key, QString::fromUtf8(m_type.name())));
|
||||
}
|
||||
const auto raw = m_metaEnum.value(i);
|
||||
QVariant decoded(m_type);
|
||||
if (!QMetaType::convert(
|
||||
QMetaType::fromType<int>(), &raw, m_type, decoded.data())) {
|
||||
qCCritical(
|
||||
lcSettings,
|
||||
"Failed to convert value %d to enum %s",
|
||||
raw,
|
||||
m_type.name());
|
||||
return error(
|
||||
DiagnosticType::InvalidValue,
|
||||
QStringLiteral("Could not convert %1 to %2")
|
||||
.arg(key, QString::fromUtf8(m_type.name())));
|
||||
}
|
||||
|
||||
return { .value = decoded, .error = std::nullopt, .indexPath = {} };
|
||||
}
|
||||
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 <typename Container>
|
||||
ListCodec<Container>::ListCodec(const QMetaType& type, const ValueCodec* elementCodec)
|
||||
: ValueCodec(type, ExpectedType::Array)
|
||||
, m_elementCodec(elementCodec) {}
|
||||
ListCodec<Container>::ListCodec(
|
||||
const QMetaType& type, const ValueCodec* elementCodec)
|
||||
: ValueCodec(type, ExpectedType::Array), m_elementCodec(elementCodec) {}
|
||||
|
||||
template <typename Container> QJsonValue ListCodec<Container>::encode(const QVariant& value) const {
|
||||
QJsonArray array;
|
||||
const auto list = value.value<Container>();
|
||||
for (const auto& element : list)
|
||||
array.append(m_elementCodec->encode(QVariant::fromValue(element)));
|
||||
return array;
|
||||
template <typename Container>
|
||||
QJsonValue ListCodec<Container>::encode(const QVariant& value) const {
|
||||
QJsonArray array;
|
||||
const auto list = value.value<Container>();
|
||||
for (const auto& element : list)
|
||||
array.append(m_elementCodec->encode(QVariant::fromValue(element)));
|
||||
return array;
|
||||
}
|
||||
|
||||
template <typename Container> DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const {
|
||||
if (!value.isArray())
|
||||
return mismatch(ExpectedType::Array, value);
|
||||
template <typename Container>
|
||||
DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const {
|
||||
if (!value.isArray()) return mismatch(ExpectedType::Array, value);
|
||||
|
||||
const auto array = value.toArray();
|
||||
Container list;
|
||||
list.reserve(array.size());
|
||||
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<Value>());
|
||||
}
|
||||
list.append(result.value.value<Value>());
|
||||
}
|
||||
|
||||
return { .value = QVariant::fromValue(list), .error = std::nullopt, .indexPath = {} };
|
||||
return {
|
||||
.value = QVariant::fromValue(list),
|
||||
.error = std::nullopt,
|
||||
.indexPath = {}};
|
||||
}
|
||||
|
||||
UnionCodec::UnionCodec(const QList<const ValueCodec*>& alternatives)
|
||||
: ValueCodec(QMetaType::fromType<QVariant>(), alternatives.first()->expected())
|
||||
, m_alternatives(alternatives)
|
||||
, m_expectedTypes(expectedTypesOf(alternatives)) {
|
||||
m_byType.reserve(alternatives.size());
|
||||
: ValueCodec(
|
||||
QMetaType::fromType<QVariant>(), alternatives.first()->expected())
|
||||
, m_alternatives(alternatives)
|
||||
, m_expectedTypes(expectedTypesOf(alternatives)) {
|
||||
m_byType.reserve(alternatives.size());
|
||||
|
||||
for (const auto* codec : alternatives)
|
||||
m_byType.insert(codec->type().id(), codec);
|
||||
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<DecodeResult> best;
|
||||
std::optional<DecodeResult> 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
|
||||
|
||||
@@ -13,43 +13,46 @@
|
||||
namespace ZShell::settings {
|
||||
|
||||
struct DecodeResult {
|
||||
QVariant value;
|
||||
std::optional<Diagnostic> error;
|
||||
QList<qsizetype> indexPath; // Index path of the failing element, outermost list first
|
||||
QVariant value;
|
||||
std::optional<Diagnostic> error;
|
||||
QList<qsizetype>
|
||||
indexPath; // Index path of the failing element, outermost list first
|
||||
};
|
||||
|
||||
class ValueCodec {
|
||||
public:
|
||||
explicit ValueCodec(const QMetaType& type, ExpectedType expected);
|
||||
virtual ~ValueCodec() = default;
|
||||
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<QMetaType>& types);
|
||||
// Returns the shared codec for a union of types, or nullptr if any of them is unsupported
|
||||
static ValueCodec* unionFor(const QList<QMetaType>& types);
|
||||
|
||||
[[nodiscard]] QMetaType type() const;
|
||||
[[nodiscard]] ExpectedType expected() const; // The JSON type this decodes from
|
||||
[[nodiscard]] virtual QJsonValue encode(const QVariant& value) const = 0;
|
||||
[[nodiscard]] virtual DecodeResult decode(const QJsonValue& value) const = 0;
|
||||
[[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 <typename Container> 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<const ValueCodec*>& alternatives);
|
||||
public:
|
||||
explicit UnionCodec(const QList<const ValueCodec*>& 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<const ValueCodec*> m_alternatives; // Tried in order, so the first to accept a value wins
|
||||
const QList<ExpectedType> m_expectedTypes; // Types of the alternatives, for diagnostics
|
||||
QHash<int, const ValueCodec*> m_byType; // Type id to alternative, for encoding
|
||||
private:
|
||||
const QList<const ValueCodec*>
|
||||
m_alternatives; // Tried in order, so the first to accept a value wins
|
||||
const QList<ExpectedType>
|
||||
m_expectedTypes; // Types of the alternatives, for diagnostics
|
||||
QHash<int, const ValueCodec*>
|
||||
m_byType; // Type id to alternative, for encoding
|
||||
};
|
||||
|
||||
} // namespace ZShell::settings
|
||||
|
||||
@@ -10,66 +10,72 @@ namespace ZShell::settings {
|
||||
|
||||
// Derived from Node and has ctor(path, fallback, parent)
|
||||
template <typename T>
|
||||
concept LayerType = std::derived_from<T, Node> && std::constructible_from<T, const QString&, T*, QObject*>;
|
||||
concept LayerType = std::derived_from<T, Node> &&
|
||||
std::constructible_from<T, const QString&, T*, QObject*>;
|
||||
|
||||
template <LayerType T> class LayerRegistry {
|
||||
public:
|
||||
explicit LayerRegistry(const QString& prefix, const QString& suffix, QObject* parent);
|
||||
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<QString, T*> m_layers;
|
||||
private:
|
||||
const QString m_prefix;
|
||||
const QString m_suffix;
|
||||
QObject* const m_parent;
|
||||
QHash<QString, T*> m_layers;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline QString stripTrailingSlashes(QStringView str) {
|
||||
while (str.endsWith(u'/'))
|
||||
str = str.left(str.length() - 1);
|
||||
return str.toString();
|
||||
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 <LayerType T>
|
||||
LayerRegistry<T>::LayerRegistry(const QString& prefix, const QString& suffix, QObject* parent)
|
||||
: m_prefix(detail::stripTrailingSlashes(prefix))
|
||||
, m_suffix(detail::stripLeadingSlashes(suffix))
|
||||
, m_parent(parent) {}
|
||||
LayerRegistry<T>::LayerRegistry(
|
||||
const QString& prefix, const QString& suffix, QObject* parent)
|
||||
: m_prefix(detail::stripTrailingSlashes(prefix))
|
||||
, m_suffix(detail::stripLeadingSlashes(suffix))
|
||||
, m_parent(parent) {}
|
||||
|
||||
template <LayerType T> QString LayerRegistry<T>::pathFor(const QString& name) const {
|
||||
return m_prefix + u'/' + name + u'/' + m_suffix;
|
||||
template <LayerType T>
|
||||
QString LayerRegistry<T>::pathFor(const QString& name) const {
|
||||
return m_prefix + u'/' + name + u'/' + m_suffix;
|
||||
}
|
||||
|
||||
template <LayerType T> QString LayerRegistry<T>::nameFor(T* layer) const {
|
||||
return m_layers.key(layer);
|
||||
return m_layers.key(layer);
|
||||
}
|
||||
|
||||
template <LayerType T> T* LayerRegistry<T>::get(const QString& name, T* fallback, bool* created) {
|
||||
if (auto* const layer = m_layers.value(name)) {
|
||||
if (created)
|
||||
*created = false;
|
||||
return layer;
|
||||
}
|
||||
template <LayerType T>
|
||||
T* LayerRegistry<T>::get(const QString& name, T* fallback, bool* created) {
|
||||
if (auto* const layer = m_layers.value(name)) {
|
||||
if (created) *created = false;
|
||||
return layer;
|
||||
}
|
||||
|
||||
if (created)
|
||||
*created = true;
|
||||
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
|
||||
|
||||
@@ -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<QList<QVariantMap>>();
|
||||
return value.typeId() == QMetaType::QVariantList ||
|
||||
value.typeId() == qMetaTypeId<QList<QVariantMap>>();
|
||||
}
|
||||
|
||||
QList<QVariantMap> toMaps(const QVariant& value) {
|
||||
if (value.typeId() == qMetaTypeId<QList<QVariantMap>>())
|
||||
return value.value<QList<QVariantMap>>();
|
||||
if (value.typeId() == qMetaTypeId<QList<QVariantMap>>())
|
||||
return value.value<QList<QVariantMap>>();
|
||||
|
||||
const auto list = value.toList();
|
||||
QList<QVariantMap> maps;
|
||||
maps.reserve(list.count());
|
||||
const auto list = value.toList();
|
||||
QList<QVariantMap> maps;
|
||||
maps.reserve(list.count());
|
||||
|
||||
for (const auto& el : list) {
|
||||
if (el.typeId() != QMetaType::QVariantMap) {
|
||||
qCCritical(lcSettings, "Expected a map in list properties, got %s.", el.typeName());
|
||||
continue;
|
||||
}
|
||||
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<Node*>();
|
||||
auto* const child = node->value(key).value<Node*>();
|
||||
|
||||
// Nested lists take their elements straight from props
|
||||
if (auto* const list = qobject_cast<ListNode*>(child)) {
|
||||
if (!isVariantListType(val)) {
|
||||
qCCritical(lcSettings, "Expected a list for node key %s on %s, got %s.", qUtf8Printable(key),
|
||||
qUtf8Printable(node->path()), val.typeName());
|
||||
continue;
|
||||
}
|
||||
// Nested lists take their elements straight from props
|
||||
if (auto* const list = qobject_cast<ListNode*>(child)) {
|
||||
if (!isVariantListType(val)) {
|
||||
qCCritical(
|
||||
lcSettings,
|
||||
"Expected a list for node key %s on %s, got %s.",
|
||||
qUtf8Printable(key),
|
||||
qUtf8Printable(node->path()),
|
||||
val.typeName());
|
||||
continue;
|
||||
}
|
||||
|
||||
list->setValue(valuesKey(), val);
|
||||
continue;
|
||||
}
|
||||
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<Diagnostic>* diagnostics) {
|
||||
if (key != valuesKey()) {
|
||||
qCCritical(lcSettings,
|
||||
"Attempted to write %s on list node %s. List nodes only have a 'values' key, something is wrong.",
|
||||
qUtf8Printable(key), qUtf8Printable(path()));
|
||||
return false;
|
||||
}
|
||||
bool ListNode::setValue(
|
||||
const QString& key, const QVariant& value, QList<Diagnostic>* diagnostics) {
|
||||
if (key != valuesKey()) {
|
||||
qCCritical(
|
||||
lcSettings,
|
||||
"Attempted to write %s on list node %s. List nodes only have a "
|
||||
"'values' key, something is wrong.",
|
||||
qUtf8Printable(key),
|
||||
qUtf8Printable(path()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto len = m_elements.count();
|
||||
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<QList<Node*>>()) {
|
||||
// On reset to fallback
|
||||
const auto nodes = value.value<QList<Node*>>();
|
||||
for (auto* const node : nodes)
|
||||
insertNode(m_elements.count(), node);
|
||||
} else if (isVariantListType(value)) {
|
||||
// From defaults or QML
|
||||
const auto list = toMaps(value);
|
||||
for (qsizetype i = 0; i < list.count(); ++i)
|
||||
insertNode(i, list.at(i), fallbackFor(i));
|
||||
} else if (value.typeId() == QMetaType::QJsonArray) {
|
||||
// From syncJson
|
||||
const auto array = value.toJsonArray();
|
||||
for (const auto& json : array)
|
||||
insertNode(m_elements.count(), json.toObject(), *diagnostics);
|
||||
} else {
|
||||
qCCritical(lcSettings, "Unexpected type %s for list node %s, something is wrong.", value.typeName(),
|
||||
qUtf8Printable(path()));
|
||||
return false;
|
||||
}
|
||||
if (value.typeId() == qMetaTypeId<QList<Node*>>()) {
|
||||
// On reset to fallback
|
||||
const auto nodes = value.value<QList<Node*>>();
|
||||
for (auto* const node : nodes)
|
||||
insertNode(m_elements.count(), node);
|
||||
} else if (isVariantListType(value)) {
|
||||
// From defaults or QML
|
||||
const auto list = toMaps(value);
|
||||
for (qsizetype i = 0; i < list.count(); ++i)
|
||||
insertNode(i, list.at(i), fallbackFor(i));
|
||||
} else if (value.typeId() == QMetaType::QJsonArray) {
|
||||
// From syncJson
|
||||
const auto array = value.toJsonArray();
|
||||
for (const auto& json : array)
|
||||
insertNode(m_elements.count(), json.toObject(), *diagnostics);
|
||||
insertMissingDefaults();
|
||||
} else {
|
||||
qCCritical(
|
||||
lcSettings,
|
||||
"Unexpected type %s for list node %s, something is wrong.",
|
||||
value.typeName(),
|
||||
qUtf8Printable(path()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!recordWrite(valuesKey(), true))
|
||||
return false;
|
||||
if (!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<QString, QList<Node*>> unmatched;
|
||||
for (auto* const element : std::as_const(m_elements))
|
||||
unmatched[element->value(idKey).toString()] << element;
|
||||
|
||||
Node* anchor = nullptr;
|
||||
for (const auto& props : defaultValue()) {
|
||||
const auto id = props.value(idKey).toString();
|
||||
if (id.isEmpty()) continue;
|
||||
|
||||
auto& candidates = unmatched[id];
|
||||
if (!candidates.isEmpty()) {
|
||||
anchor = candidates.takeFirst();
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto index = anchor ? m_elements.indexOf(anchor) + 1 : 0;
|
||||
anchor = insertNode(index, props, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void ListNode::resetToDefaults() {
|
||||
if (isNested()) {
|
||||
qCCritical(lcSettings, "List node %s has a parent list node, resetToDefaults should never be called on it.",
|
||||
qUtf8Printable(path()));
|
||||
return;
|
||||
}
|
||||
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<Diagnostic>& diagnostics) {
|
||||
if (!json.isArray()) {
|
||||
const auto d = Diagnostic::mismatch(ExpectedType::Array, json, path());
|
||||
qCWarning(lcSettings, "Error decoding option %s: %s", qUtf8Printable(d.option),
|
||||
qUtf8Printable(d.message));
|
||||
diagnostics << d;
|
||||
return false;
|
||||
}
|
||||
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<Node*>(child)));
|
||||
return QString::number(m_elements.indexOf(const_cast<Node*>(child)));
|
||||
}
|
||||
|
||||
Node* ListNode::elementAt(qsizetype index) const {
|
||||
if (!validIndex(index))
|
||||
return nullptr;
|
||||
return m_elements.at(index);
|
||||
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<QVariantMap> ListNode::defaultValue() const {
|
||||
const auto* desc = getDescriptor();
|
||||
if (!desc)
|
||||
return {};
|
||||
return desc->defaultValue().value<QList<QVariantMap>>();
|
||||
const auto* desc = getDescriptor();
|
||||
if (!desc) return {};
|
||||
return desc->defaultValue().value<QList<QVariantMap>>();
|
||||
}
|
||||
|
||||
bool ListNode::isNested() const {
|
||||
return qobject_cast<ListNode*>(parentNode());
|
||||
return qobject_cast<ListNode*>(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<ListNode*>(fallbackNode());
|
||||
if (!fallback || !fallback->validIndex(index))
|
||||
return nullptr;
|
||||
const auto* fallback = static_cast<ListNode*>(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<Diagnostic>& diagnostics) {
|
||||
// Write origin will be file/file reset, which is correct.
|
||||
// This function should only be called from the JSON path of setValue.
|
||||
auto* const node = createElement(nullptr); // Nodes synced from JSON get no fallback as they are overrides
|
||||
m_elements.insert(index, node);
|
||||
Node* ListNode::insertNode(
|
||||
qsizetype index, const QJsonObject& json, QList<Diagnostic>& diagnostics) {
|
||||
// Write origin will be file/file reset, which is correct.
|
||||
// This function should only be called from the JSON path of setValue.
|
||||
auto* const node = createElement(
|
||||
nullptr); // Nodes synced from JSON get no fallback as they are overrides
|
||||
m_elements.insert(index, node);
|
||||
|
||||
node->syncJson(json, diagnostics);
|
||||
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
|
||||
|
||||
@@ -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<qsizetype>;
|
||||
using MoveChanges = QList<QPair<qsizetype, qsizetype>>;
|
||||
using NodeChanges = QList<qsizetype>;
|
||||
using MoveChanges = QList<QPair<qsizetype, qsizetype>>;
|
||||
|
||||
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<Diagnostic>* 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<Diagnostic>* diagnostics);
|
||||
void resetToDefaults() override;
|
||||
|
||||
[[nodiscard]] QJsonValue toJson(bool sparse = true) const override;
|
||||
bool syncJson(const QJsonValue& json, QList<Diagnostic>& diagnostics) override;
|
||||
[[nodiscard]] QJsonValue toJson(bool sparse = true) const override;
|
||||
bool syncJson(
|
||||
const QJsonValue& json, QList<Diagnostic>& diagnostics) override;
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
void elementsChanged(const NodeChanges& added, const NodeChanges& removed, const MoveChanges& moved);
|
||||
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<Node*> m_elements;
|
||||
private:
|
||||
QList<Node*> m_elements;
|
||||
|
||||
[[nodiscard]] QList<QVariantMap> defaultValue() const;
|
||||
[[nodiscard]] bool isNested() const;
|
||||
[[nodiscard]] QList<QVariantMap> defaultValue() const;
|
||||
[[nodiscard]] bool isNested() const;
|
||||
|
||||
// Returns true if the mutation should be skipped, overlays cannot mutate global lists
|
||||
[[nodiscard]] bool rejectGlobalMutation() const;
|
||||
// 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<Diagnostic>& 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<Diagnostic>& diagnostics);
|
||||
|
||||
void onFallbackListNotify(
|
||||
const NodeChanges& added,
|
||||
const NodeChanges& removed,
|
||||
const MoveChanges& moved);
|
||||
};
|
||||
|
||||
} // namespace ZShell::settings
|
||||
|
||||
+191
-163
@@ -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<Node*>(parent());
|
||||
return qobject_cast<Node*>(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<Node*>(Qt::FindDirectChildrenOnly);
|
||||
for (auto* const child : childNodes)
|
||||
child->detachFallback();
|
||||
const auto childNodes = findChildren<Node*>(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<QString>& Node::overrides() const {
|
||||
return m_overrides;
|
||||
return m_overrides;
|
||||
}
|
||||
|
||||
bool Node::hasContent() const {
|
||||
return !m_overrides.isEmpty() || m_quarantine ||
|
||||
std::ranges::any_of(findChildren<Node*>(Qt::FindDirectChildrenOnly), &Node::hasContent);
|
||||
return !m_overrides.isEmpty() || m_quarantine ||
|
||||
std::ranges::any_of(
|
||||
findChildren<Node*>(Qt::FindDirectChildrenOnly),
|
||||
&Node::hasContent);
|
||||
}
|
||||
|
||||
QVariant Node::value(const QString& key) const {
|
||||
const auto* desc = schema().get(key);
|
||||
if (!desc) {
|
||||
qCCritical(
|
||||
lcSettings, "Attempted to read an unknown key %s, something is wrong.", qUtf8Printable(pathFor(key)));
|
||||
return {};
|
||||
}
|
||||
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<Node*>()->resetToDefaults();
|
||||
else if (!desc.globalOnly() || !m_fallbackNode) // Skip resetting global options on overlays
|
||||
setValue(desc.key, m_fallbackNode ? m_fallbackNode->value(desc.key) : desc.defaultValue());
|
||||
}
|
||||
// No write scope, callers should create the scope
|
||||
for (const auto& desc : schema().descriptors()) {
|
||||
if (desc.isNode)
|
||||
value(desc.key).value<Node*>()->resetToDefaults();
|
||||
else if (
|
||||
!desc.globalOnly() ||
|
||||
!m_fallbackNode) // Skip resetting global options on overlays
|
||||
setValue(
|
||||
desc.key,
|
||||
m_fallbackNode ? m_fallbackNode->value(desc.key)
|
||||
: desc.defaultValue());
|
||||
}
|
||||
}
|
||||
|
||||
const Quarantine* Node::quarantine() const {
|
||||
return m_quarantine.get();
|
||||
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<Diagnostic>& diagnostics, const QString& path) {
|
||||
qCWarning(lcSettings, "Global option definition %s found in overlay file, ignoring.", qUtf8Printable(path));
|
||||
diagnostics << Diagnostic{
|
||||
.type = DiagnosticType::GlobalOption,
|
||||
.option = path,
|
||||
.message = QStringLiteral("Global options should not be defined in overlay files"),
|
||||
};
|
||||
qCWarning(
|
||||
lcSettings,
|
||||
"Global option definition %s found in overlay file, ignoring.",
|
||||
qUtf8Printable(path));
|
||||
diagnostics << Diagnostic{
|
||||
.type = DiagnosticType::GlobalOption,
|
||||
.option = path,
|
||||
.message = QStringLiteral(
|
||||
"Global options should not be defined in overlay files"),
|
||||
};
|
||||
}
|
||||
|
||||
bool Node::rejectGlobalSync(QList<Diagnostic>& diagnostics) const {
|
||||
if (!m_globalOnly || !m_fallbackNode)
|
||||
return false;
|
||||
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<Node*>())
|
||||
return desc.key;
|
||||
}
|
||||
for (const auto& desc : schema().descriptors()) {
|
||||
if (desc.isNode && child == value(desc.key).value<Node*>())
|
||||
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
|
||||
|
||||
@@ -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<QString>& overrides() const;
|
||||
[[nodiscard]] bool hasContent() const; // Recursive
|
||||
[[nodiscard]] Q_INVOKABLE bool isGlobalOnly() const;
|
||||
[[nodiscard]] bool isOverride(const QString& key) const;
|
||||
[[nodiscard]] const QSet<QString>& overrides() const;
|
||||
[[nodiscard]] bool hasContent() const; // Recursive
|
||||
|
||||
[[nodiscard]] virtual const Schema& schema() const = 0;
|
||||
[[nodiscard]] virtual 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<Diagnostic>& 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<Diagnostic>& 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<Quarantine> 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<Quarantine> m_quarantine;
|
||||
const bool m_globalOnly; // Own flag or inherited from the parent node
|
||||
|
||||
void warnGlobalRead(const QString& key) const;
|
||||
// Returns true if the write should be skipped afterwards, the value is not one of the allowed types
|
||||
[[nodiscard]] bool rejectInvalidWrite(const QString& key, const QVariant& value) const;
|
||||
template <typename T> [[nodiscard]] bool rejectInvalidWrite(const QString& key, const T& value) const;
|
||||
// Returns true if the write should be skipped afterwards, overlays cannot write global options
|
||||
bool rejectGlobalWrite(const QString& key);
|
||||
static void warnGlobalSync(QList<Diagnostic>& diagnostics, const QString& path);
|
||||
bool rejectGlobalSync(QList<Diagnostic>& diagnostics) const; // Returns true if the sync should be rejected
|
||||
// Returns true if the notify signal should be emitted
|
||||
virtual bool recordWrite(const QString& key, bool changed);
|
||||
void warnGlobalRead(const QString& key) const;
|
||||
// Returns true if the write should be skipped afterwards, the value is not one of the allowed types
|
||||
[[nodiscard]] bool rejectInvalidWrite(
|
||||
const QString& key, const QVariant& value) const;
|
||||
template <typename T>
|
||||
[[nodiscard]] bool rejectInvalidWrite(
|
||||
const QString& key, const T& value) const;
|
||||
// Returns true if the write should be skipped afterwards, overlays cannot write global options
|
||||
bool rejectGlobalWrite(const QString& key);
|
||||
static void warnGlobalSync(
|
||||
QList<Diagnostic>& diagnostics, const QString& path);
|
||||
bool rejectGlobalSync(QList<Diagnostic>& diagnostics)
|
||||
const; // Returns true if the sync should be rejected
|
||||
// Returns true if the notify signal should be emitted
|
||||
virtual bool recordWrite(const QString& key, bool changed);
|
||||
|
||||
[[nodiscard]] bool removeQuarantined(const QString& key);
|
||||
[[nodiscard]] ChangeBatcher* batcher() const;
|
||||
[[nodiscard]] 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 <typename C, typename T>
|
||||
[[nodiscard]] T fallbackValue(T C::* member, std::type_identity_t<T> defaultValue) const;
|
||||
template <typename C, typename T>
|
||||
[[nodiscard]] T fallbackValue(
|
||||
T C::* member, std::type_identity_t<T> defaultValue) const;
|
||||
|
||||
private:
|
||||
QSet<QString> m_overrides; // Overridden keys from file/qml writes
|
||||
Node* const m_rootNode;
|
||||
Node* m_fallbackNode; // No fallback node either means global tree or inside overridden list
|
||||
private:
|
||||
QSet<QString> m_overrides; // Overridden keys from file/qml writes
|
||||
Node* const m_rootNode;
|
||||
Node* m_fallbackNode; // No fallback node either means global tree or inside overridden list
|
||||
|
||||
// For root node use only
|
||||
WriteOrigin m_writeOrigin;
|
||||
bool m_internalRead;
|
||||
ChangeBatcher* const m_batcher;
|
||||
// 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 <typename T> bool Node::rejectInvalidWrite(const QString& key, const T& value) const {
|
||||
Q_UNUSED(key)
|
||||
Q_UNUSED(value)
|
||||
return false; // Only QVariant unions can be given the wrong type
|
||||
template <typename T>
|
||||
bool Node::rejectInvalidWrite(const QString& key, const T& value) const {
|
||||
Q_UNUSED(key)
|
||||
Q_UNUSED(value)
|
||||
return false; // Only QVariant unions can be given the wrong type
|
||||
}
|
||||
|
||||
template <typename C, typename T> T Node::fallbackValue(T C::* member, std::type_identity_t<T> defaultValue) const {
|
||||
const auto* fallback = static_cast<const C*>(m_fallbackNode);
|
||||
return fallback ? fallback->*member : defaultValue;
|
||||
template <typename C, typename T>
|
||||
T Node::fallbackValue(
|
||||
T C::* member, std::type_identity_t<T> defaultValue) const {
|
||||
const auto* fallback = static_cast<const C*>(m_fallbackNode);
|
||||
return fallback ? fallback->*member : defaultValue;
|
||||
}
|
||||
|
||||
} // namespace ZShell::settings
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<QString, QJsonValue> m_quarantine;
|
||||
private:
|
||||
QHash<QString, QJsonValue> m_quarantine;
|
||||
};
|
||||
|
||||
} // namespace ZShell::settings
|
||||
|
||||
@@ -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<Diagnostic> 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
|
||||
|
||||
@@ -7,29 +7,32 @@
|
||||
namespace ZShell::settings {
|
||||
|
||||
class RootNode : public ObjectNode {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QList<ZShell::settings::Diagnostic> diagnostics READ diagnostics NOTIFY diagnosticsChanged)
|
||||
Q_PROPERTY(
|
||||
QList<ZShell::settings::Diagnostic> diagnostics READ diagnostics NOTIFY
|
||||
diagnosticsChanged)
|
||||
|
||||
public:
|
||||
explicit RootNode(const QString& path, RootNode* fallback, QObject* parent = nullptr);
|
||||
public:
|
||||
explicit RootNode(
|
||||
const QString& path, RootNode* fallback, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QList<Diagnostic> diagnostics() const;
|
||||
[[nodiscard]] QList<Diagnostic> diagnostics() const;
|
||||
|
||||
void load();
|
||||
void load();
|
||||
|
||||
signals:
|
||||
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<Diagnostic> m_diagnostics;
|
||||
private:
|
||||
SettingsFile* const m_file;
|
||||
QList<Diagnostic> m_diagnostics;
|
||||
|
||||
void reloadFromFile();
|
||||
void saveToFile();
|
||||
void reloadFromFile();
|
||||
void saveToFile();
|
||||
};
|
||||
|
||||
} // namespace ZShell::settings
|
||||
|
||||
@@ -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<const QMetaObject*, QHash<QString, Annotation>>& annotationCache() {
|
||||
static QHash<const QMetaObject*, QHash<QString, Annotation>> s_cache;
|
||||
return s_cache;
|
||||
static QHash<const QMetaObject*, QHash<QString, Annotation>> s_cache;
|
||||
return s_cache;
|
||||
}
|
||||
|
||||
// Union options carry their alternatives in the annotation, everything else goes by property type
|
||||
const ValueCodec* resolveCodec(Descriptor& desc) {
|
||||
auto& allowed = desc.annotation.allowedTypes;
|
||||
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<Descriptor>& 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
|
||||
|
||||
@@ -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<QMetaType> allowedTypes = {}; // For QVariant properties
|
||||
QVariant defaultValue;
|
||||
bool globalOnly = false;
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init) callers will get missing field init warnings without
|
||||
QList<QMetaType> allowedTypes = {}; // For QVariant properties
|
||||
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||
QString identityKey = {};
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Return trivially copyable types (e.g. bool) by value and others (e.g. QVariant) by const ref
|
||||
template <typename T>
|
||||
using AnnotationReturn = std::conditional_t<std::is_trivially_copyable_v<T> && sizeof(T) <= sizeof(void*), T, const T&>;
|
||||
using AnnotationReturn = std::conditional_t<
|
||||
std::is_trivially_copyable_v<T> && sizeof(T) <= sizeof(void*),
|
||||
T,
|
||||
const T&>;
|
||||
|
||||
} // namespace detail
|
||||
|
||||
#define ANNOTATION(Type, name) \
|
||||
Q_PROPERTY(Type name READ name) \
|
||||
\
|
||||
public: \
|
||||
[[nodiscard]] detail::AnnotationReturn<Type> name() const { \
|
||||
return annotation.name; \
|
||||
} \
|
||||
\
|
||||
private:
|
||||
#define ANNOTATION(Type, name) \
|
||||
Q_PROPERTY(Type name READ name) \
|
||||
\
|
||||
public: \
|
||||
[[nodiscard]] detail::AnnotationReturn<Type> name() const { \
|
||||
return annotation.name; \
|
||||
} \
|
||||
\
|
||||
private:
|
||||
|
||||
struct Descriptor {
|
||||
Q_GADGET
|
||||
QML_VALUE_TYPE(descriptor)
|
||||
Q_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<Descriptor>& descriptors() const;
|
||||
[[nodiscard]] const Descriptor* get(const QString& key) const;
|
||||
[[nodiscard]] const QList<Descriptor>& descriptors() const;
|
||||
[[nodiscard]] const Descriptor* get(const QString& key) const;
|
||||
|
||||
private:
|
||||
explicit Schema() = default;
|
||||
private:
|
||||
explicit Schema() = default;
|
||||
|
||||
QList<Descriptor> m_descriptors;
|
||||
QHash<QString, qsizetype> m_keyToIndex;
|
||||
QList<Descriptor> m_descriptors;
|
||||
QHash<QString, qsizetype> m_keyToIndex;
|
||||
};
|
||||
|
||||
} // namespace ZShell::settings
|
||||
|
||||
@@ -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<QJsonValue> 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::ConnectionType>(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::ConnectionType>(
|
||||
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
|
||||
|
||||
@@ -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<QJsonValue> read() const;
|
||||
void write(const QJsonValue& json);
|
||||
[[nodiscard]] std::optional<QJsonValue> 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<QJsonValue> m_lastData;
|
||||
std::optional<QJsonValue> m_pendingWrite;
|
||||
private:
|
||||
QString m_path;
|
||||
QFileSystemWatcher* m_watcher;
|
||||
QTimer* m_saveDebounce;
|
||||
QTimer* m_loadDebounce;
|
||||
int m_loadRetries;
|
||||
std::optional<QJsonValue> m_lastData;
|
||||
std::optional<QJsonValue> m_pendingWrite;
|
||||
|
||||
void onFileChanged();
|
||||
void onDirChanged();
|
||||
void onFileChanged();
|
||||
void onDirChanged();
|
||||
|
||||
void initWatcher();
|
||||
void scheduleLoad();
|
||||
void onLoadDebounced();
|
||||
void save();
|
||||
void initWatcher();
|
||||
void scheduleLoad();
|
||||
void onLoadDebounced();
|
||||
void save();
|
||||
};
|
||||
|
||||
} // namespace ZShell::settings
|
||||
|
||||
Reference in New Issue
Block a user