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
 | 
						datastream.cpp
 | 
				
			||||||
	process.cpp
 | 
						process.cpp
 | 
				
			||||||
	fileview.cpp
 | 
						fileview.cpp
 | 
				
			||||||
 | 
						jsonadapter.cpp
 | 
				
			||||||
	ipccomm.cpp
 | 
						ipccomm.cpp
 | 
				
			||||||
	ipc.cpp
 | 
						ipc.cpp
 | 
				
			||||||
	ipchandler.cpp
 | 
						ipchandler.cpp
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -289,6 +289,12 @@ void FileViewWriter::write(
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FileView::~FileView() {
 | 
				
			||||||
 | 
						if (this->mAdapter) {
 | 
				
			||||||
 | 
							this->mAdapter->setFileView(nullptr);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::loadAsync(bool doStringConversion) {
 | 
					void FileView::loadAsync(bool doStringConversion) {
 | 
				
			||||||
	// Writes update via operationFinished, making a read both invalid and outdated.
 | 
						// Writes update via operationFinished, making a read both invalid and outdated.
 | 
				
			||||||
	if (!this->liveOperation || this->pathInFlight != this->targetPath) {
 | 
						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
 | 
					} // namespace qs::io
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@
 | 
				
			||||||
#include <qqmlparserstatus.h>
 | 
					#include <qqmlparserstatus.h>
 | 
				
			||||||
#include <qrunnable.h>
 | 
					#include <qrunnable.h>
 | 
				
			||||||
#include <qstringview.h>
 | 
					#include <qstringview.h>
 | 
				
			||||||
 | 
					#include <qtclasshelpermacros.h>
 | 
				
			||||||
#include <qtmetamacros.h>
 | 
					#include <qtmetamacros.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "../core/doc.hpp"
 | 
					#include "../core/doc.hpp"
 | 
				
			||||||
| 
						 | 
					@ -140,11 +141,13 @@ public:
 | 
				
			||||||
	bool doAtomicWrite;
 | 
						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,
 | 
					/// A reader for small to medium files that don't need seeking/cursor access,
 | 
				
			||||||
/// suitable for most text files.
 | 
					/// suitable for most text files.
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// #### Example: Reading a JSON
 | 
					/// #### Example: Reading a JSON as text
 | 
				
			||||||
/// ```qml
 | 
					/// ```qml
 | 
				
			||||||
/// FileView {
 | 
					/// FileView {
 | 
				
			||||||
///   id: jsonFile
 | 
					///   id: jsonFile
 | 
				
			||||||
| 
						 | 
					@ -156,6 +159,8 @@ public:
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// readonly property var jsonData: JSON.parse(jsonFile.text())
 | 
					/// 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 {
 | 
					class FileView: public QObject {
 | 
				
			||||||
	Q_OBJECT;
 | 
						Q_OBJECT;
 | 
				
			||||||
	// clang-format off
 | 
						// clang-format off
 | 
				
			||||||
| 
						 | 
					@ -215,6 +220,14 @@ class FileView: public QObject {
 | 
				
			||||||
	/// > }
 | 
						/// > }
 | 
				
			||||||
	/// > ```
 | 
						/// > ```
 | 
				
			||||||
	Q_PROPERTY(bool watchChanges READ default WRITE default NOTIFY watchChangesChanged BINDABLE bindableWatchChanges);
 | 
						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 __path READ path WRITE setPath NOTIFY pathChanged);
 | 
				
			||||||
	QSDOC_HIDE Q_PROPERTY(QString __text READ text NOTIFY internalTextChanged);
 | 
						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 __blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged);
 | 
				
			||||||
	QSDOC_HIDE Q_PROPERTY(bool __printErrors READ default WRITE default NOTIFY printErrorsChanged BINDABLE bindablePrintErrors);
 | 
						QSDOC_HIDE Q_PROPERTY(bool __printErrors READ default WRITE default NOTIFY printErrorsChanged BINDABLE bindablePrintErrors);
 | 
				
			||||||
	// clang-format on
 | 
						// clang-format on
 | 
				
			||||||
 | 
						Q_CLASSINFO("DefaultProperty", "adapter");
 | 
				
			||||||
	QML_NAMED_ELEMENT(FileViewInternal);
 | 
						QML_NAMED_ELEMENT(FileViewInternal);
 | 
				
			||||||
	QSDOC_NAMED_ELEMENT(FileView);
 | 
						QSDOC_NAMED_ELEMENT(FileView);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public:
 | 
					public:
 | 
				
			||||||
	explicit FileView(QObject* parent = nullptr): QObject(parent) {}
 | 
						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.
 | 
						/// 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.
 | 
						/// 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.
 | 
						/// It acts the same as changing @@path to a new file, except loading the same file.
 | 
				
			||||||
	Q_INVOKABLE void reload();
 | 
						Q_INVOKABLE void reload();
 | 
				
			||||||
 | 
						/// Write the content of the current @@adapter to the selected file.
 | 
				
			||||||
 | 
						Q_INVOKABLE void writeAdapter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	[[nodiscard]] QString path() const;
 | 
						[[nodiscard]] QString path() const;
 | 
				
			||||||
	void setPath(const QString& path);
 | 
						void setPath(const QString& path);
 | 
				
			||||||
| 
						 | 
					@ -312,6 +330,9 @@ public:
 | 
				
			||||||
	[[nodiscard]] QBindable<bool> bindablePrintErrors() { return &this->bPrintErrors; }
 | 
						[[nodiscard]] QBindable<bool> bindablePrintErrors() { return &this->bPrintErrors; }
 | 
				
			||||||
	[[nodiscard]] QBindable<bool> bindableWatchChanges() { return &this->bWatchChanges; }
 | 
						[[nodiscard]] QBindable<bool> bindableWatchChanges() { return &this->bWatchChanges; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] FileViewAdapter* adapter() const;
 | 
				
			||||||
 | 
						void setAdapter(FileViewAdapter* adapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
signals:
 | 
					signals:
 | 
				
			||||||
	/// Emitted if the file was loaded successfully.
 | 
						/// Emitted if the file was loaded successfully.
 | 
				
			||||||
	void loaded();
 | 
						void loaded();
 | 
				
			||||||
| 
						 | 
					@ -323,6 +344,8 @@ signals:
 | 
				
			||||||
	void saveFailed(qs::io::FileViewError::Enum error);
 | 
						void saveFailed(qs::io::FileViewError::Enum error);
 | 
				
			||||||
	/// Emitted if the file changes on disk and @@watchChanges is true.
 | 
						/// Emitted if the file changes on disk and @@watchChanges is true.
 | 
				
			||||||
	void fileChanged();
 | 
						void fileChanged();
 | 
				
			||||||
 | 
						/// Emitted when the active @@adapter$'s data is changed.
 | 
				
			||||||
 | 
						void adapterUpdated();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	void pathChanged();
 | 
						void pathChanged();
 | 
				
			||||||
	QSDOC_HIDE void internalTextChanged();
 | 
						QSDOC_HIDE void internalTextChanged();
 | 
				
			||||||
| 
						 | 
					@ -337,9 +360,11 @@ signals:
 | 
				
			||||||
	void atomicWritesChanged();
 | 
						void atomicWritesChanged();
 | 
				
			||||||
	void printErrorsChanged();
 | 
						void printErrorsChanged();
 | 
				
			||||||
	void watchChangesChanged();
 | 
						void watchChangesChanged();
 | 
				
			||||||
 | 
						void adapterChanged();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private slots:
 | 
					private slots:
 | 
				
			||||||
	void operationFinished();
 | 
						void operationFinished();
 | 
				
			||||||
 | 
						void onAdapterDestroyed();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private:
 | 
					private:
 | 
				
			||||||
	void loadAsync(bool doStringConversion);
 | 
						void loadAsync(bool doStringConversion);
 | 
				
			||||||
| 
						 | 
					@ -373,6 +398,7 @@ private:
 | 
				
			||||||
	bool mBlockLoading = false;
 | 
						bool mBlockLoading = false;
 | 
				
			||||||
	bool mBlockAllReads = false;
 | 
						bool mBlockAllReads = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FileViewAdapter* mAdapter = nullptr;
 | 
				
			||||||
	QFileSystemWatcher* watcher = nullptr;
 | 
						QFileSystemWatcher* watcher = nullptr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	GuardedEmitter<&FileView::internalTextChanged> textChangedEmitter;
 | 
						GuardedEmitter<&FileView::internalTextChanged> textChangedEmitter;
 | 
				
			||||||
| 
						 | 
					@ -406,4 +432,26 @@ public:
 | 
				
			||||||
	void setBlockAllReads(bool blockAllReads);
 | 
						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
 | 
					} // 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",
 | 
						"socket.hpp",
 | 
				
			||||||
	"process.hpp",
 | 
						"process.hpp",
 | 
				
			||||||
	"fileview.hpp",
 | 
						"fileview.hpp",
 | 
				
			||||||
 | 
						"jsonadapter.hpp",
 | 
				
			||||||
	"ipchandler.hpp",
 | 
						"ipchandler.hpp",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
-----
 | 
					-----
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue