diff --git a/Components/Menu.qml b/Components/Menu.qml index 449e6de..b8eaad1 100644 --- a/Components/Menu.qml +++ b/Components/Menu.qml @@ -126,7 +126,7 @@ MouseArea { CustomRect { id: item - readonly property bool active: modelData === root.active + readonly property bool active: modelData === root?.active required property int index required property MenuItem modelData @@ -163,19 +163,19 @@ MouseArea { MaterialIcon { Layout.alignment: Qt.AlignVCenter color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant - text: item.modelData.icon + text: item.modelData?.icon ?? "" } CustomText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface - text: item.modelData.text + text: item.modelData?.text ?? "" } Loader { Layout.alignment: Qt.AlignVCenter - active: item.modelData.trailingIcon.length > 0 + active: item.modelData?.trailingIcon.length > 0 asynchronous: true visible: active diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 12d02ba..6f70a2c 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -302,8 +302,6 @@ Item { root.visibilities.sidebar = false; root.panels.popouts.hasCurrent = false; root.visibilities.launcher = false; - } else { - Config.save(); } } diff --git a/Helpers/TaskbarApps.qml b/Helpers/TaskbarApps.qml index 829ed15..5e2cf59 100644 --- a/Helpers/TaskbarApps.qml +++ b/Helpers/TaskbarApps.qml @@ -73,7 +73,6 @@ Singleton { Config.dock.pinnedApps = pinnedApps; root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id))); - Config.saveNoToast(); } function isPinned(appId) { diff --git a/Modules/Launcher/Services/SchemeVariants.qml b/Modules/Launcher/Services/SchemeVariants.qml index 27ba36d..08a754f 100644 --- a/Modules/Launcher/Services/SchemeVariants.qml +++ b/Modules/Launcher/Services/SchemeVariants.qml @@ -82,7 +82,6 @@ Searcher { list.visibilities.launcher = false; Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]); Config.colors.schemeType = variant; - Config.save(); } } } diff --git a/Modules/Settings/Pages/ScreenshotPage.qml b/Modules/Settings/Pages/ScreenshotPage.qml index 28f6180..2a08a2d 100644 --- a/Modules/Settings/Pages/ScreenshotPage.qml +++ b/Modules/Settings/Pages/ScreenshotPage.qml @@ -70,10 +70,7 @@ PageBase { } ] - onSelected: item => { - Config.screenshot.mode = item.value; - Config.save(); - } + onSelected: item => Config.screenshot.mode = item.value } SectionHeader { diff --git a/Modules/Settings/Pages/WallpaperPage.qml b/Modules/Settings/Pages/WallpaperPage.qml index 52e688c..6f3e14a 100644 --- a/Modules/Settings/Pages/WallpaperPage.qml +++ b/Modules/Settings/Pages/WallpaperPage.qml @@ -104,7 +104,6 @@ PageBase { onApplySettings: (start, end) => { Config.general.color.scheduleDarkStart = start; Config.general.color.scheduleDarkEnd = end; - Config.save(); ModeScheduler.checkStartup(); PopupManager.requestClose(); } @@ -146,7 +145,6 @@ PageBase { onApplySettings: (start, end) => { Config.general.color.scheduleHyprsunsetStart = start; Config.general.color.scheduleHyprsunsetEnd = end; - Config.save(); Hyprsunset.checkStartup(); PopupManager.requestClose(); } diff --git a/Plugins/ZShell/Config/config.cpp b/Plugins/ZShell/Config/config.cpp index ea2c307..268b4c1 100644 --- a/Plugins/ZShell/Config/config.cpp +++ b/Plugins/ZShell/Config/config.cpp @@ -95,15 +95,31 @@ QString Config::filePath() const { void Config::loadSync() { m_loading = true; + QFile f(filePath()); + QJsonObject before; + + bool existed = false; + if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); - if (doc.isObject()) loadFromJson(QJsonValue(doc.object())); + if (doc.isObject()) { + before = doc.object(); + existed = true; + } else { + qInfo() << "Config: existing config at" << filePath() + << "is empty or not a valid JSON object - using defaults"; + } } else { qInfo() << "Config: no existing config at" << filePath() << "- using defaults"; } + + loadFromJson(QJsonValue(before)); m_loading = false; + + const auto after = toJson().toObject(); + if (!existed || after != before) saveNow(); } void Config::updateWatch() { @@ -148,27 +164,21 @@ void Config::loadAsync() { if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); + const bool valid = doc.isObject(); + const QJsonObject before = valid ? doc.object() : QJsonObject(); - if (doc.isObject()) { - QMetaObject::invokeMethod( - this, - [this, doc]() { - loadFromJson(QJsonValue(doc.object())); - m_loading = false; + QMetaObject::invokeMethod( + this, + [this, valid, before]() { + loadFromJson(QJsonValue(before)); + m_loading = false; - if (m_reloadPending) m_reloadTimer.start(); - }, - Qt::QueuedConnection); - } else { - QMetaObject::invokeMethod( - this, - [this]() { - m_loading = false; + const auto after = toJson().toObject(); + if (!valid || after != before) saveNow(); - if (m_reloadPending) m_reloadTimer.start(); - }, - Qt::QueuedConnection); - } + if (m_reloadPending) m_reloadTimer.start(); + }, + Qt::QueuedConnection); } else { qInfo() << "Config: failed to reload from" << filePath() << "- using in-memory values"; diff --git a/Plugins/ZShell/Config/configlist.cpp b/Plugins/ZShell/Config/configlist.cpp index ef60bbd..4f75cda 100644 --- a/Plugins/ZShell/Config/configlist.cpp +++ b/Plugins/ZShell/Config/configlist.cpp @@ -79,13 +79,14 @@ void ConfigList::loadFromJson(const QJsonValue& json) { if (!json.isArray()) { qCWarning( lcConfig, - "Option '%s' must be a list, ignoring", + "Option '%s' must be a list, resetting to default", qUtf8Printable(propertyPath())); - m_rejectedJson = json; + + resetToDefaults(); + materializeDefaults(); return; } - m_rejectedJson = QJsonValue::Undefined; populate(json.toArray()); m_loaded = true; } @@ -93,12 +94,11 @@ void ConfigList::loadFromJson(const QJsonValue& json) { QJsonValue ConfigList::toJson() const { if (m_loaded) return elementsToJson(); - return m_rejectedJson; + return QJsonValue::Undefined; } void ConfigList::clearLoadedKeys() { m_loaded = false; - m_rejectedJson = QJsonValue::Undefined; } QStringList ConfigList::unknownKeys() const { @@ -113,6 +113,15 @@ QStringList ConfigList::unknownKeys() const { return keys; } +void ConfigList::materializeDefaults() { + for (auto* const item : m_items) + item->materializeDefaults(); + + if (m_global) return; + + m_loaded = true; +} + void ConfigList::resyncFromGlobal() { syncValuesFromGlobal(); } diff --git a/Plugins/ZShell/Config/configlist.hpp b/Plugins/ZShell/Config/configlist.hpp index 3ee24ba..5787526 100644 --- a/Plugins/ZShell/Config/configlist.hpp +++ b/Plugins/ZShell/Config/configlist.hpp @@ -33,6 +33,7 @@ class ConfigList : public ConfigNode { [[nodiscard]] QJsonValue toJson() const override; void clearLoadedKeys() override; [[nodiscard]] QStringList unknownKeys() const override; + void materializeDefaults() override; void resyncFromGlobal() override; signals: @@ -71,7 +72,6 @@ class ConfigList : public ConfigNode { QJsonArray m_defaults; QList m_items; bool m_loaded = false; - QJsonValue m_rejectedJson = QJsonValue::Undefined; }; } // namespace ZShell::config @@ -103,7 +103,6 @@ class ConfigList : public ConfigNode { } \ }; - #define CONFIG_LIST(Type, name, ...) \ Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \ \ @@ -115,6 +114,8 @@ class ConfigList : public ConfigNode { private: \ Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__)); +#define LIST_ENTRY(id, enabled) \ + vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}}) namespace ZShell::config { @@ -136,6 +137,3 @@ class ListEntry : public ConfigObject { CONFIG_LIST_TYPE(ListEntry, EntryList) } // namespace ZShell::config - -#define LIST_ENTRY(id, enabled) \ - vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}}) diff --git a/Plugins/ZShell/Config/confignode.hpp b/Plugins/ZShell/Config/confignode.hpp index 4316f34..d4e4389 100644 --- a/Plugins/ZShell/Config/confignode.hpp +++ b/Plugins/ZShell/Config/confignode.hpp @@ -26,6 +26,8 @@ class ConfigNode : public QObject { [[nodiscard]] virtual QStringList unknownKeys() const = 0; [[nodiscard]] virtual QList childNodes() const; + virtual void materializeDefaults() = 0; + void syncFromGlobal(ConfigNode* global); virtual void resyncFromGlobal() = 0; diff --git a/Plugins/ZShell/Config/configobject.cpp b/Plugins/ZShell/Config/configobject.cpp index 4fb0056..9ee0517 100644 --- a/Plugins/ZShell/Config/configobject.cpp +++ b/Plugins/ZShell/Config/configobject.cpp @@ -7,6 +7,51 @@ namespace ZShell::config { +namespace { + +bool isStringArray(const QJsonArray& arr) { + for (const auto& v : arr) { + if (!v.isString()) return false; + } + return true; +} + +bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) { + switch (val.type()) { + case QJsonValue::Bool: + return type.id() == QMetaType::Bool; + + case QJsonValue::Double: + switch (type.id()) { + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::LongLong: + case QMetaType::ULongLong: + case QMetaType::Double: + case QMetaType::Float: + return true; + default: + return false; + } + + case QJsonValue::String: + return type.id() == QMetaType::QString; + + case QJsonValue::Array: + if (type.id() == QMetaType::QStringList) + return isStringArray(val.toArray()); + return type.id() == QMetaType::QVariantList; + + case QJsonValue::Object: + case QJsonValue::Null: + case QJsonValue::Undefined: + default: + return false; + } +} + +} // namespace + ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {} void ConfigObject::loadFromJson(const QJsonValue& json) { @@ -14,6 +59,7 @@ void ConfigObject::loadFromJson(const QJsonValue& json) { const auto* meta = metaObject(); QSet known; + QSet invalid; for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) { auto prop = meta->property(i); @@ -34,6 +80,15 @@ void ConfigObject::loadFromJson(const QJsonValue& json) { if (!prop.isWritable()) continue; + if (!jsonValueMatchesType(jsonVal, prop.metaType())) { + qWarning() << "Config: type mismatch for" << key << "in" + << meta->className() << "- expected" + << prop.metaType().name() << "got value" << jsonVal + << "- resetting to default"; + invalid.insert(key); + continue; + } + if (prop.metaType().id() == QMetaType::QStringList) { QStringList list; const auto jsonArr = jsonVal.toArray(); @@ -56,10 +111,12 @@ void ConfigObject::loadFromJson(const QJsonValue& json) { m_extras = {}; for (auto it = obj.begin(); it != obj.end(); ++it) { - if (!known.contains(it.key())) { + if (!known.contains(it.key()) || invalid.contains(it.key())) { m_extras.insert(it.key(), it.value()); } } + + materializeDefaults(); } QJsonValue ConfigObject::toJson() const { @@ -104,9 +161,6 @@ QJsonValue ConfigObject::toJson() const { obj.insert(key, QJsonValue::fromVariant(value)); } - for (auto it = m_extras.begin(); it != m_extras.end(); ++it) - obj.insert(it.key(), it.value()); - if (obj.isEmpty()) return QJsonValue::Undefined; return obj; @@ -159,6 +213,26 @@ QList ConfigObject::childNodes() const { return nodes; } +void ConfigObject::materializeDefaults() { + const auto* meta = metaObject(); + + for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) { + const auto prop = meta->property(i); + const auto key = QString::fromUtf8(prop.name()); + + if (auto* const node = prop.read(this).value()) { + node->materializeDefaults(); + continue; + } + + if (!prop.isWritable()) continue; + + if (m_global) continue; + + m_loadedKeys.insert(key); + } +} + void ConfigObject::syncValuesFromGlobal() { const auto* meta = metaObject(); diff --git a/Plugins/ZShell/Config/configobject.hpp b/Plugins/ZShell/Config/configobject.hpp index 4814e6f..7de39ed 100644 --- a/Plugins/ZShell/Config/configobject.hpp +++ b/Plugins/ZShell/Config/configobject.hpp @@ -62,6 +62,7 @@ class ConfigObject : public ConfigNode { void clearLoadedKeys() override; [[nodiscard]] QStringList unknownKeys() const override; [[nodiscard]] QList childNodes() const override; + void materializeDefaults() override; void resyncFromGlobal() override; [[nodiscard]] virtual QStringList identityKeys() const; diff --git a/Services/Colors.qml b/Services/Colors.qml index f11b78b..bff79f1 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -113,7 +113,6 @@ Singleton { function setMode(mode: string): void { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]); Config.general.color.mode = mode; - Config.save(); } function swapRG(c: color): color {