#include "Config.hpp" #include #include #include #include #include #include #include Config::Config(QObject* parent) : ConfigSection(parent) { m_appearance = new Appearance(this); m_background = new Background(this); m_bar = new Bar(this); m_clipboard = new Clipboard(this); m_colors = new Colors(this); m_dashboard = new Dashboard(this); m_dock = new Dock(this); m_general = new General(this); m_launcher = new Launcher(this); m_lock = new Lock(this); m_notifs = new Notifs(this); m_osd = new Osd(this); m_overview = new Overview(this); m_screenshot = new Screenshot(this); m_services = new Services(this); m_sidebar = new Sidebar(this); m_utilities = new Utilities(this); wireSignals(); // forwards every child's changed() up to our own changed() connect(this, &ConfigSection::changed, this, &Config::scheduleSave); m_saveTimer.setSingleShot(true); m_saveTimer.setInterval( 300); // debounce: coalesce rapid-fire edits (e.g. dragging a slider) connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync); loadSync(); m_firstLoadDone = true; } Config* Config::create(QQmlEngine*, QJSEngine*) { // The QML engine takes ownership of singletons created this way. return new Config(); } QString Config::filePath() const { const QString dir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/zshell"); QDir().mkpath(dir); return dir + QStringLiteral("/config.json"); } void Config::loadSync() { m_loading = true; // suppress scheduleSave() while properties are set from disk QFile f(filePath()); if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); if (doc.isObject()) fromJson(doc.object()); } else { qInfo() << "Config: no existing config at" << filePath() << "- using defaults"; } m_loading = false; } void Config::loadAsync() { m_loading = true; auto future = QtConcurrent::run([this]() { QFile f(filePath()); if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); if (doc.isObject()) { QMetaObject::invokeMethod(this, [this, doc]() { fromJson(doc.object()); m_loading = false; }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod(this, [this]() { m_loading = false; }, Qt::QueuedConnection); } } else { qInfo() << "Config: failed to reload from" << filePath() << "- using in-memory values"; QMetaObject::invokeMethod(this, [this]() { m_loading = false; }, Qt::QueuedConnection); } }); m_loadFuture = future; } void Config::load() { if (!m_firstLoadDone) { loadSync(); } else { loadAsync(); } } void Config::reloadAsync() { loadAsync(); } void Config::scheduleSave() { if (m_loading) return; m_saveTimer.start(); // restarts the window if it's already running } void Config::flushAsync() { auto future = QtConcurrent::run([this]() { const QByteArray data = QJsonDocument(toJson()).toJson(QJsonDocument::Indented); writeAtomically(data); }); } void Config::saveNow() { m_saveTimer.stop(); flushAsync(); } void Config::writeAtomically(const QByteArray& data) { // QSaveFile writes to a temp file next to the target and atomically // renames it into place on commit() - a crash or power loss mid-write // can never leave config.json half-written. QSaveFile f(filePath()); if (!f.open(QIODevice::WriteOnly)) { qWarning() << "Config: failed to open for atomic write:" << f.errorString(); return; } f.write(data); if (!f.commit()) qWarning() << "Config: commit failed:" << f.errorString(); }