forked from quickshell/quickshell
		
	io/fileview: add adapter support and JsonAdapter
This commit is contained in:
		
							parent
							
								
									cb69c2d016
								
							
						
					
					
						commit
						fee4942771
					
				
					 6 changed files with 487 additions and 2 deletions
				
			
		| 
						 | 
				
			
			@ -2,6 +2,7 @@ qt_add_library(quickshell-io STATIC
 | 
			
		|||
	datastream.cpp
 | 
			
		||||
	process.cpp
 | 
			
		||||
	fileview.cpp
 | 
			
		||||
	jsonadapter.cpp
 | 
			
		||||
	ipccomm.cpp
 | 
			
		||||
	ipc.cpp
 | 
			
		||||
	ipchandler.cpp
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -289,6 +289,12 @@ void FileViewWriter::write(
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FileView::~FileView() {
 | 
			
		||||
	if (this->mAdapter) {
 | 
			
		||||
		this->mAdapter->setFileView(nullptr);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::loadAsync(bool doStringConversion) {
 | 
			
		||||
	// Writes update via operationFinished, making a read both invalid and outdated.
 | 
			
		||||
	if (!this->liveOperation || this->pathInFlight != this->targetPath) {
 | 
			
		||||
| 
						 | 
				
			
			@ -636,4 +642,55 @@ void FileView::setBlockAllReads(bool blockAllReads) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FileViewAdapter* FileView::adapter() const { return this->mAdapter; }
 | 
			
		||||
 | 
			
		||||
void FileView::setAdapter(FileViewAdapter* adapter) {
 | 
			
		||||
	if (adapter == this->mAdapter) return;
 | 
			
		||||
 | 
			
		||||
	if (this->mAdapter) {
 | 
			
		||||
		this->mAdapter->setFileView(nullptr);
 | 
			
		||||
		QObject::disconnect(this->mAdapter, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mAdapter = adapter;
 | 
			
		||||
 | 
			
		||||
	if (adapter) {
 | 
			
		||||
		this->mAdapter->setFileView(this);
 | 
			
		||||
		QObject::connect(adapter, &FileViewAdapter::adapterUpdated, this, &FileView::adapterUpdated);
 | 
			
		||||
		QObject::connect(adapter, &QObject::destroyed, this, &FileView::onAdapterDestroyed);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	emit this->adapterChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::writeAdapter() {
 | 
			
		||||
	if (!this->mAdapter) {
 | 
			
		||||
		qmlWarning(this) << "Cannot call writeAdapter without an adapter.";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->setData(this->mAdapter->serializeAdapter());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::onAdapterDestroyed() { this->mAdapter = nullptr; }
 | 
			
		||||
 | 
			
		||||
void FileViewAdapter::setFileView(FileView* fileView) {
 | 
			
		||||
	if (fileView == this->mFileView) return;
 | 
			
		||||
 | 
			
		||||
	if (this->mFileView) {
 | 
			
		||||
		QObject::disconnect(this->mFileView, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mFileView = fileView;
 | 
			
		||||
 | 
			
		||||
	if (fileView) {
 | 
			
		||||
		QObject::connect(fileView, &FileView::dataChanged, this, &FileViewAdapter::onDataChanged);
 | 
			
		||||
		this->setFileView(fileView);
 | 
			
		||||
	} else {
 | 
			
		||||
		this->setFileView(nullptr);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileViewAdapter::onDataChanged() { this->deserializeAdapter(this->mFileView->data()); }
 | 
			
		||||
 | 
			
		||||
} // namespace qs::io
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@
 | 
			
		|||
#include <qqmlparserstatus.h>
 | 
			
		||||
#include <qrunnable.h>
 | 
			
		||||
#include <qstringview.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
 | 
			
		||||
#include "../core/doc.hpp"
 | 
			
		||||
| 
						 | 
				
			
			@ -140,11 +141,13 @@ public:
 | 
			
		|||
	bool doAtomicWrite;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! Simplified reader for small files.
 | 
			
		||||
class FileViewAdapter;
 | 
			
		||||
 | 
			
		||||
///! Simple accessor for small files.
 | 
			
		||||
/// A reader for small to medium files that don't need seeking/cursor access,
 | 
			
		||||
/// suitable for most text files.
 | 
			
		||||
///
 | 
			
		||||
/// #### Example: Reading a JSON
 | 
			
		||||
/// #### Example: Reading a JSON as text
 | 
			
		||||
/// ```qml
 | 
			
		||||
/// FileView {
 | 
			
		||||
///   id: jsonFile
 | 
			
		||||
| 
						 | 
				
			
			@ -156,6 +159,8 @@ public:
 | 
			
		|||
///
 | 
			
		||||
/// readonly property var jsonData: JSON.parse(jsonFile.text())
 | 
			
		||||
/// ```
 | 
			
		||||
///
 | 
			
		||||
/// Also see @@JsonAdapter for an alternative way to handle reading and writing JSON files.
 | 
			
		||||
class FileView: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	// clang-format off
 | 
			
		||||
| 
						 | 
				
			
			@ -215,6 +220,14 @@ class FileView: public QObject {
 | 
			
		|||
	/// > }
 | 
			
		||||
	/// > ```
 | 
			
		||||
	Q_PROPERTY(bool watchChanges READ default WRITE default NOTIFY watchChangesChanged BINDABLE bindableWatchChanges);
 | 
			
		||||
	/// In addition to directly reading/writing the file as text, *adapters* can be used to
 | 
			
		||||
	/// expose a file's content in new ways.
 | 
			
		||||
	///
 | 
			
		||||
	/// An adapter will automatically be given the loaded file's content.
 | 
			
		||||
	/// Its state may be saved with @@writeAdapter().
 | 
			
		||||
	///
 | 
			
		||||
	/// Currently the only adapter is @@JsonAdapter.
 | 
			
		||||
	Q_PROPERTY(FileViewAdapter* adapter READ adapter WRITE setAdapter NOTIFY adapterChanged);
 | 
			
		||||
 | 
			
		||||
	QSDOC_HIDE Q_PROPERTY(QString __path READ path WRITE setPath NOTIFY pathChanged);
 | 
			
		||||
	QSDOC_HIDE Q_PROPERTY(QString __text READ text NOTIFY internalTextChanged);
 | 
			
		||||
| 
						 | 
				
			
			@ -230,11 +243,14 @@ class FileView: public QObject {
 | 
			
		|||
	QSDOC_HIDE Q_PROPERTY(bool __blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged);
 | 
			
		||||
	QSDOC_HIDE Q_PROPERTY(bool __printErrors READ default WRITE default NOTIFY printErrorsChanged BINDABLE bindablePrintErrors);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	Q_CLASSINFO("DefaultProperty", "adapter");
 | 
			
		||||
	QML_NAMED_ELEMENT(FileViewInternal);
 | 
			
		||||
	QSDOC_NAMED_ELEMENT(FileView);
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit FileView(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
	~FileView() override;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(FileView);
 | 
			
		||||
 | 
			
		||||
	/// Returns the data of the file specified by @@path as text.
 | 
			
		||||
	///
 | 
			
		||||
| 
						 | 
				
			
			@ -280,6 +296,8 @@ public:
 | 
			
		|||
	/// This will not block if @@blockLoading is set, only if @@blockAllReads is true.
 | 
			
		||||
	/// It acts the same as changing @@path to a new file, except loading the same file.
 | 
			
		||||
	Q_INVOKABLE void reload();
 | 
			
		||||
	/// Write the content of the current @@adapter to the selected file.
 | 
			
		||||
	Q_INVOKABLE void writeAdapter();
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QString path() const;
 | 
			
		||||
	void setPath(const QString& path);
 | 
			
		||||
| 
						 | 
				
			
			@ -312,6 +330,9 @@ public:
 | 
			
		|||
	[[nodiscard]] QBindable<bool> bindablePrintErrors() { return &this->bPrintErrors; }
 | 
			
		||||
	[[nodiscard]] QBindable<bool> bindableWatchChanges() { return &this->bWatchChanges; }
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] FileViewAdapter* adapter() const;
 | 
			
		||||
	void setAdapter(FileViewAdapter* adapter);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	/// Emitted if the file was loaded successfully.
 | 
			
		||||
	void loaded();
 | 
			
		||||
| 
						 | 
				
			
			@ -323,6 +344,8 @@ signals:
 | 
			
		|||
	void saveFailed(qs::io::FileViewError::Enum error);
 | 
			
		||||
	/// Emitted if the file changes on disk and @@watchChanges is true.
 | 
			
		||||
	void fileChanged();
 | 
			
		||||
	/// Emitted when the active @@adapter$'s data is changed.
 | 
			
		||||
	void adapterUpdated();
 | 
			
		||||
 | 
			
		||||
	void pathChanged();
 | 
			
		||||
	QSDOC_HIDE void internalTextChanged();
 | 
			
		||||
| 
						 | 
				
			
			@ -337,9 +360,11 @@ signals:
 | 
			
		|||
	void atomicWritesChanged();
 | 
			
		||||
	void printErrorsChanged();
 | 
			
		||||
	void watchChangesChanged();
 | 
			
		||||
	void adapterChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void operationFinished();
 | 
			
		||||
	void onAdapterDestroyed();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void loadAsync(bool doStringConversion);
 | 
			
		||||
| 
						 | 
				
			
			@ -373,6 +398,7 @@ private:
 | 
			
		|||
	bool mBlockLoading = false;
 | 
			
		||||
	bool mBlockAllReads = false;
 | 
			
		||||
 | 
			
		||||
	FileViewAdapter* mAdapter = nullptr;
 | 
			
		||||
	QFileSystemWatcher* watcher = nullptr;
 | 
			
		||||
 | 
			
		||||
	GuardedEmitter<&FileView::internalTextChanged> textChangedEmitter;
 | 
			
		||||
| 
						 | 
				
			
			@ -406,4 +432,26 @@ public:
 | 
			
		|||
	void setBlockAllReads(bool blockAllReads);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// See @@FileView.adapter.
 | 
			
		||||
class FileViewAdapter: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	QML_UNCREATABLE("");
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	void setFileView(FileView* fileView);
 | 
			
		||||
	virtual void deserializeAdapter(const QByteArray& data) = 0;
 | 
			
		||||
	[[nodiscard]] virtual QByteArray serializeAdapter() = 0;
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	/// This signal is fired when data in the adapter changes, and triggers @@FileView.adapterUpdated(s).
 | 
			
		||||
	void adapterUpdated();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onDataChanged();
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
	FileView* mFileView = nullptr;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::io
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										262
									
								
								src/io/jsonadapter.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								src/io/jsonadapter.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,262 @@
 | 
			
		|||
#include "jsonadapter.hpp"
 | 
			
		||||
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qjsonarray.h>
 | 
			
		||||
#include <qjsondocument.h>
 | 
			
		||||
#include <qjsonobject.h>
 | 
			
		||||
#include <qjsonvalue.h>
 | 
			
		||||
#include <qjsvalue.h>
 | 
			
		||||
#include <qmetaobject.h>
 | 
			
		||||
#include <qnamespace.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qobjectdefs.h>
 | 
			
		||||
#include <qqml.h>
 | 
			
		||||
#include <qqmlengine.h>
 | 
			
		||||
#include <qqmlinfo.h>
 | 
			
		||||
#include <qqmllist.h>
 | 
			
		||||
#include <qstringview.h>
 | 
			
		||||
#include <qvariant.h>
 | 
			
		||||
 | 
			
		||||
namespace qs::io {
 | 
			
		||||
 | 
			
		||||
void JsonAdapter::componentComplete() { this->connectNotifiers(); }
 | 
			
		||||
 | 
			
		||||
void JsonAdapter::deserializeAdapter(const QByteArray& data) {
 | 
			
		||||
	if (data.isEmpty()) return;
 | 
			
		||||
 | 
			
		||||
	// Importing this makes CI builds fail for some reason.
 | 
			
		||||
	QJsonParseError error; // NOLINT (misc-include-cleaner)
 | 
			
		||||
	auto json = QJsonDocument::fromJson(data, &error);
 | 
			
		||||
 | 
			
		||||
	if (error.error != QJsonParseError::NoError) {
 | 
			
		||||
		qmlWarning(this) << "Failed to deserialize json: " << error.errorString();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!json.isObject()) {
 | 
			
		||||
		qmlWarning(this) << "Failed to deserialize json: not an object";
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->changesBlocked = true;
 | 
			
		||||
	this->oldCreatedObjects = this->createdObjects;
 | 
			
		||||
	this->createdObjects.clear();
 | 
			
		||||
 | 
			
		||||
	this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject);
 | 
			
		||||
 | 
			
		||||
	for (auto* object: oldCreatedObjects) {
 | 
			
		||||
		delete object; // FIXME: QMetaType::destroy?
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->oldCreatedObjects.clear();
 | 
			
		||||
	this->changesBlocked = false;
 | 
			
		||||
 | 
			
		||||
	this->connectNotifiers();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void JsonAdapter::connectNotifiers() {
 | 
			
		||||
	auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()");
 | 
			
		||||
	connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) {
 | 
			
		||||
	const auto* metaObject = obj->metaObject();
 | 
			
		||||
 | 
			
		||||
	for (auto i = base->propertyOffset(); i != metaObject->propertyCount(); i++) {
 | 
			
		||||
		const auto prop = metaObject->property(i);
 | 
			
		||||
 | 
			
		||||
		if (prop.isReadable() && prop.hasNotifySignal()) {
 | 
			
		||||
			QMetaObject::connect(obj, prop.notifySignalIndex(), this, notifySlot, Qt::UniqueConnection);
 | 
			
		||||
 | 
			
		||||
			auto val = prop.read(obj);
 | 
			
		||||
			if (val.canView<JsonObject*>()) {
 | 
			
		||||
				auto* pobj = prop.read(obj).view<JsonObject*>();
 | 
			
		||||
				if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
 | 
			
		||||
			} else if (val.canConvert<QQmlListProperty<JsonObject>>()) {
 | 
			
		||||
				auto listVal = val.value<QQmlListProperty<JsonObject>>();
 | 
			
		||||
 | 
			
		||||
				auto len = listVal.count(&listVal);
 | 
			
		||||
				for (auto i = 0; i != len; i++) {
 | 
			
		||||
					auto* pobj = listVal.at(&listVal, i);
 | 
			
		||||
 | 
			
		||||
					if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void JsonAdapter::onPropertyChanged() {
 | 
			
		||||
	if (this->changesBlocked) return;
 | 
			
		||||
 | 
			
		||||
	this->connectNotifiers();
 | 
			
		||||
	this->adapterUpdated();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QByteArray JsonAdapter::serializeAdapter() {
 | 
			
		||||
	return QJsonDocument(this->serializeRec(this, &JsonAdapter::staticMetaObject))
 | 
			
		||||
	    .toJson(QJsonDocument::Indented);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* base) const {
 | 
			
		||||
	QJsonObject json;
 | 
			
		||||
	const auto* metaObject = obj->metaObject();
 | 
			
		||||
 | 
			
		||||
	for (auto i = base->propertyOffset(); i != metaObject->propertyCount(); i++) {
 | 
			
		||||
		const auto prop = metaObject->property(i);
 | 
			
		||||
 | 
			
		||||
		if (prop.isReadable() && prop.hasNotifySignal()) {
 | 
			
		||||
			auto val = prop.read(obj);
 | 
			
		||||
			if (val.canView<JsonObject*>()) {
 | 
			
		||||
				auto* pobj = val.view<JsonObject*>();
 | 
			
		||||
 | 
			
		||||
				if (pobj) {
 | 
			
		||||
					json.insert(prop.name(), serializeRec(pobj, &JsonObject::staticMetaObject));
 | 
			
		||||
				} else {
 | 
			
		||||
					json.insert(prop.name(), QJsonValue::Null);
 | 
			
		||||
				}
 | 
			
		||||
			} else if (val.canConvert<QQmlListProperty<JsonObject>>()) {
 | 
			
		||||
				QJsonArray array;
 | 
			
		||||
				auto listVal = val.value<QQmlListProperty<JsonObject>>();
 | 
			
		||||
 | 
			
		||||
				auto len = listVal.count(&listVal);
 | 
			
		||||
				for (auto i = 0; i != len; i++) {
 | 
			
		||||
					auto* pobj = listVal.at(&listVal, i);
 | 
			
		||||
 | 
			
		||||
					if (pobj) {
 | 
			
		||||
						array.push_back(serializeRec(pobj, &JsonObject::staticMetaObject));
 | 
			
		||||
					} else {
 | 
			
		||||
						array.push_back(QJsonValue::Null);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				json.insert(prop.name(), array);
 | 
			
		||||
			} else if (val.canConvert<QJSValue>()) {
 | 
			
		||||
				auto variant = val.value<QJSValue>().toVariant();
 | 
			
		||||
				auto jv = QJsonValue::fromVariant(variant);
 | 
			
		||||
				json.insert(prop.name(), jv);
 | 
			
		||||
			} else {
 | 
			
		||||
				auto jv = QJsonValue::fromVariant(val);
 | 
			
		||||
				json.insert(prop.name(), jv);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return json;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QMetaObject* base) {
 | 
			
		||||
	const auto* metaObject = obj->metaObject();
 | 
			
		||||
 | 
			
		||||
	for (auto i = base->propertyOffset(); i != metaObject->propertyCount(); i++) {
 | 
			
		||||
		const auto prop = metaObject->property(i);
 | 
			
		||||
		if (json.contains(prop.name())) {
 | 
			
		||||
			auto jval = json.value(prop.name());
 | 
			
		||||
 | 
			
		||||
			if (prop.metaType() == QMetaType::fromType<QVariant>()) {
 | 
			
		||||
				auto variant = jval.toVariant();
 | 
			
		||||
				auto oldValue = prop.read(this).value<QJSValue>();
 | 
			
		||||
 | 
			
		||||
				// Calling prop.write with a new QJSValue will cause a property update
 | 
			
		||||
				// even if content is identical.
 | 
			
		||||
				if (jval.toVariant() != oldValue.toVariant()) {
 | 
			
		||||
					auto jsValue = qmlEngine(this)->fromVariant<QJSValue>(jval.toVariant());
 | 
			
		||||
					prop.write(this, QVariant::fromValue(jsValue));
 | 
			
		||||
				}
 | 
			
		||||
			} else if (QMetaType::canView(prop.metaType(), QMetaType::fromType<JsonObject*>())) {
 | 
			
		||||
				// FIXME: This doesn't support creating descendants of JsonObject, as QMetaType.metaObject()
 | 
			
		||||
				// returns null for QML types.
 | 
			
		||||
 | 
			
		||||
				if (jval.isObject()) {
 | 
			
		||||
					auto* currentValue = prop.read(obj).view<JsonObject*>();
 | 
			
		||||
					auto isNew = currentValue == nullptr;
 | 
			
		||||
 | 
			
		||||
					if (isNew) {
 | 
			
		||||
						// metaObject->metaType removes the pointer
 | 
			
		||||
						currentValue =
 | 
			
		||||
						    static_cast<JsonObject*>(prop.metaType().metaObject()->metaType().create());
 | 
			
		||||
 | 
			
		||||
						currentValue->setParent(this);
 | 
			
		||||
						this->createdObjects.push_back(currentValue);
 | 
			
		||||
					} else if (oldCreatedObjects.removeOne(currentValue)) {
 | 
			
		||||
						createdObjects.push_back(currentValue);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject);
 | 
			
		||||
 | 
			
		||||
					if (isNew) prop.write(obj, QVariant::fromValue(currentValue));
 | 
			
		||||
				} else if (jval.isNull()) {
 | 
			
		||||
					prop.write(obj, QVariant::fromValue(nullptr));
 | 
			
		||||
				} else {
 | 
			
		||||
					qmlWarning(this) << "Failed to deserialize property " << prop.name() << " as object. Got "
 | 
			
		||||
					                 << jval.toVariant().typeName();
 | 
			
		||||
				}
 | 
			
		||||
			} else if (QMetaType::canConvert(
 | 
			
		||||
			               prop.metaType(),
 | 
			
		||||
			               QMetaType::fromType<QQmlListProperty<JsonObject>>()
 | 
			
		||||
			           ))
 | 
			
		||||
			{
 | 
			
		||||
				auto pval = prop.read(this);
 | 
			
		||||
 | 
			
		||||
				if (pval.canConvert<QQmlListProperty<JsonObject>>()) {
 | 
			
		||||
					auto lp = pval.value<QQmlListProperty<JsonObject>>();
 | 
			
		||||
					auto array = jval.toArray();
 | 
			
		||||
					auto lpCount = lp.count(&lp);
 | 
			
		||||
 | 
			
		||||
					auto i = 0;
 | 
			
		||||
					for (; i != array.count(); i++) {
 | 
			
		||||
						JsonObject* currentValue = nullptr;
 | 
			
		||||
						auto isNew = i >= lpCount;
 | 
			
		||||
 | 
			
		||||
						const auto& jsonValue = array.at(i);
 | 
			
		||||
						if (jsonValue.isObject()) {
 | 
			
		||||
							if (isNew) {
 | 
			
		||||
								currentValue = lp.at(&lp, i);
 | 
			
		||||
								if (oldCreatedObjects.removeOne(currentValue)) {
 | 
			
		||||
									createdObjects.push_back(currentValue);
 | 
			
		||||
								}
 | 
			
		||||
							} else {
 | 
			
		||||
								// FIXME: should be the type inside the QQmlListProperty but how can we get that?
 | 
			
		||||
								currentValue = static_cast<JsonObject*>(QMetaType::fromType<JsonObject>().create());
 | 
			
		||||
								currentValue->setParent(this);
 | 
			
		||||
								this->createdObjects.push_back(currentValue);
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							this->deserializeRec(
 | 
			
		||||
							    jsonValue.toObject(),
 | 
			
		||||
							    currentValue,
 | 
			
		||||
							    &JsonObject::staticMetaObject
 | 
			
		||||
							);
 | 
			
		||||
						} else if (!jsonValue.isNull()) {
 | 
			
		||||
							qmlWarning(this) << "Failed to deserialize property" << prop.name()
 | 
			
		||||
							                 << ": Member of object array is not an object: "
 | 
			
		||||
							                 << jsonValue.toVariant().typeName();
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if (isNew) {
 | 
			
		||||
							lp.append(&lp, currentValue);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					for (; i < lpCount; i++) {
 | 
			
		||||
						lp.removeLast(&lp);
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					qmlWarning(this) << "Failed to deserialize property " << prop.name()
 | 
			
		||||
					                 << ": property is a list<JsonObject> but contains null.";
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				auto variant = jval.toVariant();
 | 
			
		||||
 | 
			
		||||
				if (variant.convert(prop.metaType())) {
 | 
			
		||||
					prop.write(obj, variant);
 | 
			
		||||
				} else {
 | 
			
		||||
					qmlWarning(this) << "Failed to deserialize property " << prop.name() << ": expected "
 | 
			
		||||
					                 << prop.metaType().name() << " but got " << jval.toVariant().typeName();
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::io
 | 
			
		||||
							
								
								
									
										116
									
								
								src/io/jsonadapter.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/io/jsonadapter.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,116 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qjsondocument.h>
 | 
			
		||||
#include <qjsonobject.h>
 | 
			
		||||
#include <qjsonvalue.h>
 | 
			
		||||
#include <qjsvalue.h>
 | 
			
		||||
#include <qlist.h>
 | 
			
		||||
#include <qobjectdefs.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qqmlparserstatus.h>
 | 
			
		||||
#include <qstringview.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
 | 
			
		||||
#include "fileview.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::io {
 | 
			
		||||
 | 
			
		||||
/// See @@JsonAdapter.
 | 
			
		||||
class JsonObject: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! FileView adapter for accessing JSON files.
 | 
			
		||||
/// JsonAdapter is a @@FileView adapter that exposes a JSON file as a set of QML
 | 
			
		||||
/// properties that can be read and written to.
 | 
			
		||||
///
 | 
			
		||||
/// Each property defined in a JsonAdapter corresponds to a key in the JSON file.
 | 
			
		||||
/// Supported property types are:
 | 
			
		||||
/// - Primitves (`int`, `bool`, `string`, `real`)
 | 
			
		||||
/// - Sub-object adapters (@@JsonObject$)
 | 
			
		||||
/// - JSON objects and arrays, as a `var` type
 | 
			
		||||
/// - Lists of any of the above (`list<string>` etc)
 | 
			
		||||
///
 | 
			
		||||
/// When the @@FileView$'s data is loaded, properties of a JsonAdapter or
 | 
			
		||||
/// sub-object adapter (@@JsonObject$) are updated if their values have changed.
 | 
			
		||||
///
 | 
			
		||||
/// When properties of a JsonAdapter or sub-object adapter are changed from QML,
 | 
			
		||||
/// @@FileView.adapterUpdated(s) is emitted, which may be used to save the file's new
 | 
			
		||||
/// state (see @@FileView.writeAdapter()$).
 | 
			
		||||
///
 | 
			
		||||
/// ### Example
 | 
			
		||||
/// ```qml
 | 
			
		||||
/// @@FileView {
 | 
			
		||||
///   path: "/path/to/file"
 | 
			
		||||
///
 | 
			
		||||
///   // when changes are made on disk, reload the file's content
 | 
			
		||||
///   watchChanges: true
 | 
			
		||||
///   onFileChanged: reload()
 | 
			
		||||
///
 | 
			
		||||
///   // when changes are made to properties in the adapter, save them
 | 
			
		||||
///   onAdapterUpdated: writeAdapter()
 | 
			
		||||
///
 | 
			
		||||
///   JsonAdapter {
 | 
			
		||||
///     property string myStringProperty: "default value"
 | 
			
		||||
///     onMyStringPropertyChanged: {
 | 
			
		||||
///       console.log("myStringProperty was changed via qml or on disk")
 | 
			
		||||
///     }
 | 
			
		||||
///
 | 
			
		||||
///     property list<string> stringList: [ "default", "value" ]
 | 
			
		||||
///
 | 
			
		||||
///     property JsonObject subObject: JsonObject {
 | 
			
		||||
///       property string subObjectProperty: "default value"
 | 
			
		||||
///       onSubObjectPropertyChanged: console.log("same as above")
 | 
			
		||||
///     }
 | 
			
		||||
///
 | 
			
		||||
///     // works the same way as subObject
 | 
			
		||||
///     property var inlineJson: { "a": "b" }
 | 
			
		||||
///   }
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
///
 | 
			
		||||
/// The above snippet produces the JSON document below:
 | 
			
		||||
/// ```json
 | 
			
		||||
/// {
 | 
			
		||||
///    "myStringProperty": "default value",
 | 
			
		||||
///    "stringList": [
 | 
			
		||||
///      "default",
 | 
			
		||||
///      "value"
 | 
			
		||||
///    ],
 | 
			
		||||
///    "subObject": {
 | 
			
		||||
///      "subObjectProperty": "default value"
 | 
			
		||||
///    },
 | 
			
		||||
///    "inlineJson": {
 | 
			
		||||
///      "a": "b"
 | 
			
		||||
///    }
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
class JsonAdapter
 | 
			
		||||
    : public FileViewAdapter
 | 
			
		||||
    , public QQmlParserStatus {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	void classBegin() override {}
 | 
			
		||||
	void componentComplete() override;
 | 
			
		||||
 | 
			
		||||
	void deserializeAdapter(const QByteArray& data) override;
 | 
			
		||||
	[[nodiscard]] QByteArray serializeAdapter() override;
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onPropertyChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void connectNotifiers();
 | 
			
		||||
	void connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base);
 | 
			
		||||
	void deserializeRec(const QJsonObject& json, QObject* obj, const QMetaObject* base);
 | 
			
		||||
	[[nodiscard]] QJsonObject serializeRec(const QObject* obj, const QMetaObject* base) const;
 | 
			
		||||
 | 
			
		||||
	bool changesBlocked = false;
 | 
			
		||||
	QList<JsonObject*> createdObjects;
 | 
			
		||||
	QList<JsonObject*> oldCreatedObjects;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::io
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ headers = [
 | 
			
		|||
	"socket.hpp",
 | 
			
		||||
	"process.hpp",
 | 
			
		||||
	"fileview.hpp",
 | 
			
		||||
	"jsonadapter.hpp",
 | 
			
		||||
	"ipchandler.hpp",
 | 
			
		||||
]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue