forked from quickshell/quickshell
		
	io/fileview: add FileView
This commit is contained in:
		
							parent
							
								
									3a1eec0ed5
								
							
						
					
					
						commit
						85be3861ce
					
				
					 6 changed files with 635 additions and 1 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
#include <type_traits>
 | 
			
		||||
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
 | 
			
		||||
// NOLINTBEGIN
 | 
			
		||||
#define DROP_EMIT(object, func)                                                                    \
 | 
			
		||||
	DropEmitter(object, static_cast<void (*)(typeof(object))>([](typeof(object) o) { o->func(); }))
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +74,8 @@ private:
 | 
			
		|||
	DECLARE_MEMBER_GET(name);                                                                        \
 | 
			
		||||
	DECLARE_MEMBER_SET(name, setter)
 | 
			
		||||
 | 
			
		||||
#define DECLARE_MEMBER_SETONLY(class, name, setter, member, signal) DECLARE_MEMBER(cl
 | 
			
		||||
 | 
			
		||||
#define DECLARE_MEMBER_FULL(class, name, setter, member, signal)                                   \
 | 
			
		||||
	DECLARE_MEMBER(class, name, member, signal);                                                     \
 | 
			
		||||
	DECLARE_MEMBER_GETSET(name, setter)
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +127,8 @@ private:
 | 
			
		|||
#define DEFINE_MEMBER_GETSET(Class, name, setter)                                                  \
 | 
			
		||||
	DEFINE_MEMBER_GET(Class, name)                                                                   \
 | 
			
		||||
	DEFINE_MEMBER_SET(Class, name, setter)
 | 
			
		||||
 | 
			
		||||
#define MEMBER_EMIT(name) std::remove_reference_t<decltype(*this)>::M_##name::emitter(this)
 | 
			
		||||
// NOLINTEND
 | 
			
		||||
 | 
			
		||||
template <typename T>
 | 
			
		||||
| 
						 | 
				
			
			@ -154,6 +160,12 @@ public:
 | 
			
		|||
		} else {
 | 
			
		||||
			if (MemberMetadata::get(obj) == value) return DropEmitter();
 | 
			
		||||
			obj->*member = value;
 | 
			
		||||
			return MemberMetadata::emitter(obj);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static Ret emitter(Class* obj) {
 | 
			
		||||
		if constexpr (signal != nullptr) {
 | 
			
		||||
			return DropEmitter(obj, &MemberMetadata::emitForObject);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -170,3 +182,32 @@ public:
 | 
			
		|||
	using Ref = const Type&;
 | 
			
		||||
	using Ret = std::conditional_t<hasSignal, DropEmitter, void>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class GuardedEmitBlocker {
 | 
			
		||||
public:
 | 
			
		||||
	explicit GuardedEmitBlocker(bool* var): var(var) { *this->var = true; }
 | 
			
		||||
	~GuardedEmitBlocker() { *this->var = false; }
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(GuardedEmitBlocker);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	bool* var;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template <auto signal>
 | 
			
		||||
class GuardedEmitter {
 | 
			
		||||
	using Traits = MemberPointerTraits<decltype(signal)>;
 | 
			
		||||
	using Class = Traits::Class;
 | 
			
		||||
 | 
			
		||||
	bool blocked = false;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	GuardedEmitter() = default;
 | 
			
		||||
	~GuardedEmitter() = default;
 | 
			
		||||
	Q_DISABLE_COPY_MOVE(GuardedEmitter);
 | 
			
		||||
 | 
			
		||||
	void call(Class* obj) {
 | 
			
		||||
		if (!this->blocked) (obj->*signal)();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
qt_add_library(quickshell-io STATIC
 | 
			
		||||
	datastream.cpp
 | 
			
		||||
	process.cpp
 | 
			
		||||
	fileview.cpp
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
add_library(quickshell-io-init OBJECT init.cpp)
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +10,12 @@ if (SOCKETS)
 | 
			
		|||
	target_sources(quickshell-io PRIVATE socket.cpp)
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
qt_add_qml_module(quickshell-io URI Quickshell.Io VERSION 0.1)
 | 
			
		||||
qt_add_qml_module(quickshell-io
 | 
			
		||||
	URI Quickshell.Io
 | 
			
		||||
	VERSION 0.1
 | 
			
		||||
	QML_FILES
 | 
			
		||||
		FileView.qml
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
target_link_libraries(quickshell-io PRIVATE ${QT_DEPS})
 | 
			
		||||
target_link_libraries(quickshell-io-init PRIVATE ${QT_DEPS})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										48
									
								
								src/io/FileView.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/io/FileView.qml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
import Quickshell.Io
 | 
			
		||||
 | 
			
		||||
FileViewInternal {
 | 
			
		||||
	property bool preload: this.__preload;
 | 
			
		||||
	property bool blockLoading: this.__blockLoading;
 | 
			
		||||
	property bool blockAllReads: this.__blockAllReads;
 | 
			
		||||
	property string path: this.__path;
 | 
			
		||||
 | 
			
		||||
	onPreloadChanged: this.__preload = preload;
 | 
			
		||||
	onBlockLoadingChanged: this.__blockLoading = this.blockLoading;
 | 
			
		||||
	onBlockAllReadsChanged: this.__blockAllReads = this.blockAllReads;
 | 
			
		||||
 | 
			
		||||
	// Unfortunately path can't be kept as an empty string until the file loads
 | 
			
		||||
	// without using QQmlPropertyValueInterceptor which is private. If we lean fully
 | 
			
		||||
	// into using private code in the future, there will be no reason not to do it here.
 | 
			
		||||
 | 
			
		||||
	onPathChanged: {
 | 
			
		||||
		if (!this.preload) this.__preload = false;
 | 
			
		||||
		this.__path = this.path;
 | 
			
		||||
		if (this.preload) this.__preload = true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// The C++ side can't force bindings to be resolved in a specific order so
 | 
			
		||||
	// its done here. Functions are used to avoid the eager loading aspect of properties.
 | 
			
		||||
 | 
			
		||||
	// Preload is set as it is below to avoid starting an async read from a preload
 | 
			
		||||
	// if the user wants an initial blocking read.
 | 
			
		||||
 | 
			
		||||
	function text(): string {
 | 
			
		||||
		if (!this.preload) this.__preload = false;
 | 
			
		||||
		this.__blockLoading = this.blockLoading;
 | 
			
		||||
		this.__blockAllReads = this.blockAllReads;
 | 
			
		||||
		this.__path = this.path;
 | 
			
		||||
		const text = this.__text;
 | 
			
		||||
		if (this.preload) this.__preload = true;
 | 
			
		||||
		return text;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function data(): string {
 | 
			
		||||
		if (!this.preload) this.__preload = false;
 | 
			
		||||
		this.__blockLoading = this.blockLoading;
 | 
			
		||||
		this.__blockAllReads = this.blockAllReads;
 | 
			
		||||
		this.__path = this.path;
 | 
			
		||||
		const data = this.__data;
 | 
			
		||||
		if (this.preload) this.__preload = true;
 | 
			
		||||
		return data;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										302
									
								
								src/io/fileview.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								src/io/fileview.cpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,302 @@
 | 
			
		|||
#include "fileview.hpp"
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include <qfileinfo.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qmutex.h>
 | 
			
		||||
#include <qnamespace.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qobjectdefs.h>
 | 
			
		||||
#include <qthreadpool.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "../core/util.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::io {
 | 
			
		||||
 | 
			
		||||
Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg);
 | 
			
		||||
 | 
			
		||||
FileViewReader::FileViewReader(QString path, bool doStringConversion)
 | 
			
		||||
    : doStringConversion(doStringConversion) {
 | 
			
		||||
	this->state.path = std::move(path);
 | 
			
		||||
	this->setAutoDelete(false);
 | 
			
		||||
	this->blockMutex.lock();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileViewReader::run() {
 | 
			
		||||
	FileViewReader::read(this->state, this->doStringConversion);
 | 
			
		||||
 | 
			
		||||
	this->blockMutex.unlock();
 | 
			
		||||
	QMetaObject::invokeMethod(this, &FileViewReader::finished, Qt::QueuedConnection);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileViewReader::block() {
 | 
			
		||||
	// block until a lock can be acauired, then immediately drop it
 | 
			
		||||
	auto unused = QMutexLocker(&this->blockMutex);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileViewReader::finished() {
 | 
			
		||||
	emit this->done();
 | 
			
		||||
	delete this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileViewReader::read(FileViewState& state, bool doStringConversion) {
 | 
			
		||||
	{
 | 
			
		||||
		qCDebug(logFileView) << "Reader started for" << state.path;
 | 
			
		||||
 | 
			
		||||
		auto info = QFileInfo(state.path);
 | 
			
		||||
		state.exists = info.exists();
 | 
			
		||||
 | 
			
		||||
		if (!state.exists) return;
 | 
			
		||||
 | 
			
		||||
		if (!info.isFile()) {
 | 
			
		||||
			qCCritical(logFileView) << state.path << "is not a file.";
 | 
			
		||||
			goto error;
 | 
			
		||||
		} else if (!info.isReadable()) {
 | 
			
		||||
			qCCritical(logFileView) << "No permission to read" << state.path;
 | 
			
		||||
			state.error = true;
 | 
			
		||||
			goto error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto file = QFile(state.path);
 | 
			
		||||
 | 
			
		||||
		if (!file.open(QFile::ReadOnly)) {
 | 
			
		||||
			qCCritical(logFileView) << "Failed to open" << state.path;
 | 
			
		||||
			goto error;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto& data = state.data;
 | 
			
		||||
		data = QByteArray(file.size(), Qt::Uninitialized);
 | 
			
		||||
 | 
			
		||||
		qint64 i = 0;
 | 
			
		||||
 | 
			
		||||
		while (true) {
 | 
			
		||||
			auto r = file.read(data.data() + i, data.length() - i); // NOLINT
 | 
			
		||||
 | 
			
		||||
			if (r == -1) {
 | 
			
		||||
				qCCritical(logFileView) << "Failed to read" << state.path;
 | 
			
		||||
				goto error;
 | 
			
		||||
			} else if (r == 0) {
 | 
			
		||||
				data.resize(i);
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			i += r;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (doStringConversion) {
 | 
			
		||||
			state.text = QString::fromUtf8(state.data);
 | 
			
		||||
			state.textDirty = false;
 | 
			
		||||
		} else {
 | 
			
		||||
			state.textDirty = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
error:
 | 
			
		||||
	state.error = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::loadAsync(bool doStringConversion) {
 | 
			
		||||
	if (!this->reader || this->pathInFlight != this->targetPath) {
 | 
			
		||||
		this->cancelAsync();
 | 
			
		||||
		this->pathInFlight = this->targetPath;
 | 
			
		||||
 | 
			
		||||
		if (this->targetPath.isEmpty()) {
 | 
			
		||||
			auto state = FileViewState();
 | 
			
		||||
			this->updateState(state);
 | 
			
		||||
		} else {
 | 
			
		||||
			qCDebug(logFileView) << "Starting async load for" << this << "of" << this->targetPath;
 | 
			
		||||
			this->reader = new FileViewReader(this->targetPath, doStringConversion);
 | 
			
		||||
			QObject::connect(this->reader, &FileViewReader::done, this, &FileView::readerFinished);
 | 
			
		||||
			QThreadPool::globalInstance()->start(this->reader); // takes ownership
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::cancelAsync() {
 | 
			
		||||
	if (this->reader) {
 | 
			
		||||
		qCDebug(logFileView) << "Disowning async read for" << this;
 | 
			
		||||
		QObject::disconnect(this->reader, nullptr, this, nullptr);
 | 
			
		||||
		this->reader = nullptr;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::readerFinished() {
 | 
			
		||||
	if (this->sender() != this->reader) {
 | 
			
		||||
		qCWarning(logFileView) << "got read finished from dropped FileViewReader" << this->sender();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	qCDebug(logFileView) << "Async load finished for" << this;
 | 
			
		||||
	this->updateState(this->reader->state);
 | 
			
		||||
	this->reader = nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::reload() { this->updatePath(); }
 | 
			
		||||
 | 
			
		||||
bool FileView::blockUntilLoaded() {
 | 
			
		||||
	if (this->reader != nullptr) {
 | 
			
		||||
		QObject::disconnect(this->reader, nullptr, this, nullptr);
 | 
			
		||||
		this->reader->block();
 | 
			
		||||
		this->updateState(this->reader->state);
 | 
			
		||||
		this->reader = nullptr;
 | 
			
		||||
		return true;
 | 
			
		||||
	} else return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::loadSync() {
 | 
			
		||||
	if (this->targetPath.isEmpty()) {
 | 
			
		||||
		auto state = FileViewState();
 | 
			
		||||
		this->updateState(state);
 | 
			
		||||
	} else if (!this->blockUntilLoaded()) {
 | 
			
		||||
		auto state = FileViewState {.path = this->targetPath};
 | 
			
		||||
		FileViewReader::read(state, false);
 | 
			
		||||
		this->updateState(state);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::updateState(FileViewState& newState) {
 | 
			
		||||
	DEFINE_DROP_EMIT_IF(newState.path != this->state.path, this, pathChanged);
 | 
			
		||||
	// assume if the path was changed the data also changed
 | 
			
		||||
	auto dataChanged = pathChanged || newState.data != this->state.data;
 | 
			
		||||
	// DEFINE_DROP_EMIT_IF(newState.exists != this->state.exists, this, existsChanged);
 | 
			
		||||
 | 
			
		||||
	this->mPrepared = true;
 | 
			
		||||
	auto loadedChanged = this->setLoadedOrAsync(!newState.path.isEmpty() && newState.exists);
 | 
			
		||||
 | 
			
		||||
	this->state.path = std::move(newState.path);
 | 
			
		||||
 | 
			
		||||
	if (dataChanged) {
 | 
			
		||||
		this->state.data = newState.data;
 | 
			
		||||
		this->state.text = newState.text;
 | 
			
		||||
		this->state.textDirty = newState.textDirty;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->state.exists = newState.exists;
 | 
			
		||||
	this->state.error = newState.error;
 | 
			
		||||
 | 
			
		||||
	DropEmitter::call(
 | 
			
		||||
	    pathChanged,
 | 
			
		||||
	    // existsChanged,
 | 
			
		||||
	    loadedChanged
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	if (dataChanged) this->emitDataChanged();
 | 
			
		||||
 | 
			
		||||
	if (this->state.error) emit this->loadFailed();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::textConversion() {
 | 
			
		||||
	if (this->state.textDirty) {
 | 
			
		||||
		this->state.text = QString::fromUtf8(this->state.data);
 | 
			
		||||
		this->state.textDirty = false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QString FileView::path() const { return this->state.path; }
 | 
			
		||||
 | 
			
		||||
void FileView::setPath(const QString& path) {
 | 
			
		||||
	auto p = path.startsWith("file://") ? path.sliced(7) : path;
 | 
			
		||||
	if (p == this->targetPath) return;
 | 
			
		||||
	this->targetPath = p;
 | 
			
		||||
	this->updatePath();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::updatePath() {
 | 
			
		||||
	this->mPrepared = false;
 | 
			
		||||
 | 
			
		||||
	if (this->targetPath.isEmpty()) {
 | 
			
		||||
		auto state = FileViewState();
 | 
			
		||||
		this->updateState(state);
 | 
			
		||||
	} else if (this->mPreload) {
 | 
			
		||||
		this->loadAsync(true);
 | 
			
		||||
	} else {
 | 
			
		||||
		// loadAsync will do this already
 | 
			
		||||
		this->cancelAsync();
 | 
			
		||||
		this->emitDataChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool FileView::shouldBlock() const {
 | 
			
		||||
	return this->mBlockAllReads || (this->mBlockLoading && !this->mLoadedOrAsync);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QByteArray FileView::data() {
 | 
			
		||||
	auto guard = this->dataChangedEmitter.block();
 | 
			
		||||
 | 
			
		||||
	if (!this->mPrepared) {
 | 
			
		||||
		if (this->shouldBlock()) this->loadSync();
 | 
			
		||||
		else this->loadAsync(false);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return this->state.data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QString FileView::text() {
 | 
			
		||||
	auto guard = this->textChangedEmitter.block();
 | 
			
		||||
 | 
			
		||||
	if (!this->mPrepared) {
 | 
			
		||||
		if (this->shouldBlock()) this->loadSync();
 | 
			
		||||
		else this->loadAsync(true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->textConversion();
 | 
			
		||||
	return this->state.text;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::emitDataChanged() {
 | 
			
		||||
	this->dataChangedEmitter.call(this);
 | 
			
		||||
	this->textChangedEmitter.call(this);
 | 
			
		||||
	emit this->dataChanged();
 | 
			
		||||
	emit this->textChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DEFINE_MEMBER_GETSET(FileView, isLoadedOrAsync, setLoadedOrAsync);
 | 
			
		||||
DEFINE_MEMBER_GET(FileView, shouldPreload);
 | 
			
		||||
DEFINE_MEMBER_GET(FileView, blockLoading);
 | 
			
		||||
DEFINE_MEMBER_GET(FileView, blockAllReads);
 | 
			
		||||
 | 
			
		||||
void FileView::setPreload(bool preload) {
 | 
			
		||||
	if (preload != this->mPreload) {
 | 
			
		||||
		this->mPreload = preload;
 | 
			
		||||
		emit this->preloadChanged();
 | 
			
		||||
 | 
			
		||||
		if (preload) this->emitDataChanged();
 | 
			
		||||
 | 
			
		||||
		if (!this->mPrepared && this->mPreload) {
 | 
			
		||||
			this->loadAsync(false);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::setBlockLoading(bool blockLoading) {
 | 
			
		||||
	if (blockLoading != this->mBlockLoading) {
 | 
			
		||||
		auto wasBlocking = this->shouldBlock();
 | 
			
		||||
 | 
			
		||||
		this->mBlockLoading = blockLoading;
 | 
			
		||||
		emit this->blockLoadingChanged();
 | 
			
		||||
 | 
			
		||||
		if (!wasBlocking && this->shouldBlock()) {
 | 
			
		||||
			this->emitDataChanged();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FileView::setBlockAllReads(bool blockAllReads) {
 | 
			
		||||
	if (blockAllReads != this->mBlockAllReads) {
 | 
			
		||||
		auto wasBlocking = this->shouldBlock();
 | 
			
		||||
 | 
			
		||||
		this->mBlockAllReads = blockAllReads;
 | 
			
		||||
		emit this->blockAllReadsChanged();
 | 
			
		||||
 | 
			
		||||
		if (!wasBlocking && this->shouldBlock()) {
 | 
			
		||||
			this->emitDataChanged();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::io
 | 
			
		||||
							
								
								
									
										236
									
								
								src/io/fileview.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								src/io/fileview.hpp
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,236 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qmutex.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qproperty.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qqmlparserstatus.h>
 | 
			
		||||
#include <qrunnable.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
 | 
			
		||||
#include "../core/doc.hpp"
 | 
			
		||||
#include "../core/util.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::io {
 | 
			
		||||
 | 
			
		||||
struct FileViewState {
 | 
			
		||||
	QString path;
 | 
			
		||||
	QString text;
 | 
			
		||||
	QByteArray data;
 | 
			
		||||
	bool textDirty = false;
 | 
			
		||||
	bool exists = false;
 | 
			
		||||
	bool error = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FileView;
 | 
			
		||||
 | 
			
		||||
class FileViewReader
 | 
			
		||||
    : public QObject
 | 
			
		||||
    , public QRunnable {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit FileViewReader(QString path, bool doStringConversion);
 | 
			
		||||
 | 
			
		||||
	void run() override;
 | 
			
		||||
	void block();
 | 
			
		||||
 | 
			
		||||
	FileViewState state;
 | 
			
		||||
 | 
			
		||||
	static void read(FileViewState& state, bool doStringConversion);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void done();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void finished();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	bool doStringConversion;
 | 
			
		||||
	QMutex blockMutex;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///! Simplified reader 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
 | 
			
		||||
/// ```qml
 | 
			
		||||
/// FileView {
 | 
			
		||||
///   id: jsonFile
 | 
			
		||||
///   path: Qt.resolvedUrl("./your.json")
 | 
			
		||||
///   // Forces the file to be loaded by the time we call JSON.parse().
 | 
			
		||||
///   // see blockLoading's property documentation for details.
 | 
			
		||||
///   blockLoading: true
 | 
			
		||||
/// }
 | 
			
		||||
///
 | 
			
		||||
/// readonly property var jsonData: JSON.parse(jsonFile.text())
 | 
			
		||||
/// ```
 | 
			
		||||
class FileView: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	/// The path to the file that should be read, or an empty string to unload the file.
 | 
			
		||||
	QSDOC_PROPERTY_OVERRIDE(QString path READ path WRITE setPath NOTIFY pathChanged);
 | 
			
		||||
	/// If the file should be loaded in the background immediately when set. Defaults to true.
 | 
			
		||||
	///
 | 
			
		||||
	/// This may either increase or decrease the amount of time it takes to load the file
 | 
			
		||||
	/// depending on how large the file is, how fast its storage is, and how you access its data.
 | 
			
		||||
	QSDOC_PROPERTY_OVERRIDE(bool preload READ shouldPreload WRITE setPreload NOTIFY preloadChanged);
 | 
			
		||||
	/// If @@text() and @@data() should block all operations until the file is loaded. Defaults to false.
 | 
			
		||||
	///
 | 
			
		||||
	/// If the file is already loaded, no blocking will occur.
 | 
			
		||||
	/// If a file was loaded, and @@path was changed to a new file, no blocking will occur.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!WARNING] Blocking operations should be used carefully to avoid stutters and other performance
 | 
			
		||||
	/// > degradations. Blocking means that your interface **WILL NOT FUNCTION** during the call.
 | 
			
		||||
	/// >
 | 
			
		||||
	/// > **We recommend you use a blocking load ONLY for files loaded before the windows of your shell
 | 
			
		||||
	/// > are loaded, which happens after `Component.onCompleted` runs for the root component of your shell.**
 | 
			
		||||
	/// >
 | 
			
		||||
	/// > The most reasonable use case would be to load things like configuration files that the program
 | 
			
		||||
	/// > must have available.
 | 
			
		||||
	QSDOC_PROPERTY_OVERRIDE(bool blockLoading READ blockLoading WRITE setBlockLoading NOTIFY blockLoadingChanged);
 | 
			
		||||
	/// If @@text() and @@data() should block all operations while a file loads. Defaults to false.
 | 
			
		||||
	///
 | 
			
		||||
	/// This is nearly identical to @@blockLoading, but will additionally block when
 | 
			
		||||
	/// a file is loaded and @@path changes.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!WARNING] We cannot think of a valid use case for this.
 | 
			
		||||
	/// > You almost definitely want @@blockLoading.
 | 
			
		||||
	QSDOC_PROPERTY_OVERRIDE(bool blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged);
 | 
			
		||||
 | 
			
		||||
	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(QByteArray __data READ data NOTIFY internalDataChanged);
 | 
			
		||||
	QSDOC_HIDE Q_PROPERTY(bool __preload READ shouldPreload WRITE setPreload NOTIFY preloadChanged);
 | 
			
		||||
	/// If a file is currently loaded, which may or may not be the one currently specified by @@path.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!INFO] If a file is loaded, @@path is changed, and a new file is loaded,
 | 
			
		||||
	/// > this property will stay true the whole time.
 | 
			
		||||
	/// > If @@path is set to an empty string to unload the file it will become false.
 | 
			
		||||
	Q_PROPERTY(bool loaded READ isLoadedOrAsync NOTIFY loadedOrAsyncChanged);
 | 
			
		||||
	QSDOC_HIDE Q_PROPERTY(bool __blockLoading READ blockLoading WRITE setBlockLoading NOTIFY blockLoadingChanged);
 | 
			
		||||
	QSDOC_HIDE Q_PROPERTY(bool __blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	QML_NAMED_ELEMENT(FileViewInternal);
 | 
			
		||||
	QSDOC_NAMED_ELEMENT(FileView);
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit FileView(QObject* parent = nullptr): QObject(parent) {}
 | 
			
		||||
 | 
			
		||||
	/// Returns the data of the file specified by @@path as text.
 | 
			
		||||
	///
 | 
			
		||||
	/// If @@blockAllReads is true, all changes to @@path will cause the program to block
 | 
			
		||||
	/// when this function is called.
 | 
			
		||||
	///
 | 
			
		||||
	/// If @@blockLoading is true, reading this property before the file has been loaded
 | 
			
		||||
	/// will block, but changing @@path or calling @@reload() will return the old data
 | 
			
		||||
	/// until the load completes.
 | 
			
		||||
	///
 | 
			
		||||
	/// If neither is true, an empty string will be returned if no file is loaded,
 | 
			
		||||
	/// otherwise it will behave as in the case above.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!INFO] Due to technical limitations, @@text() could not be a property,
 | 
			
		||||
	/// > however you can treat it like a property, it will trigger property updates
 | 
			
		||||
	/// > as a property would, and the signal `textChanged()` is present.
 | 
			
		||||
	//@ Q_INVOKABLE QString text();
 | 
			
		||||
	/// Returns the data of the file specified by @@path as an [ArrayBuffer].
 | 
			
		||||
	///
 | 
			
		||||
	/// If @@blockAllReads is true, all changes to @@path will cause the program to block
 | 
			
		||||
	/// when this function is called.
 | 
			
		||||
	///
 | 
			
		||||
	/// If @@blockLoading is true, reading this property before the file has been loaded
 | 
			
		||||
	/// will block, but changing @@path or calling @@reload() will return the old data
 | 
			
		||||
	/// until the load completes.
 | 
			
		||||
	///
 | 
			
		||||
	/// If neither is true, an empty buffer will be returned if no file is loaded,
 | 
			
		||||
	/// otherwise it will behave as in the case above.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!INFO] Due to technical limitations, @@data() could not be a property,
 | 
			
		||||
	/// > however you can treat it like a property, it will trigger property updates
 | 
			
		||||
	/// > as a property would, and the signal `dataChanged()` is present.
 | 
			
		||||
	///
 | 
			
		||||
	/// [ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
 | 
			
		||||
	//@ Q_INVOKABLE QByteArray data();
 | 
			
		||||
 | 
			
		||||
	/// Block all operations until the currently running load completes.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!WARNING] See @@blockLoading for an explanation and warning about blocking.
 | 
			
		||||
	Q_INVOKABLE bool blockUntilLoaded();
 | 
			
		||||
	/// Unload the loaded file and reload it, usually in response to changes.
 | 
			
		||||
	///
 | 
			
		||||
	/// 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();
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QString path() const;
 | 
			
		||||
	void setPath(const QString& path);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QByteArray data();
 | 
			
		||||
	[[nodiscard]] QString text();
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	///! Fires if the file failed to load. A warning will be printed in the log.
 | 
			
		||||
	void loadFailed();
 | 
			
		||||
 | 
			
		||||
	void pathChanged();
 | 
			
		||||
	QSDOC_HIDE void internalTextChanged();
 | 
			
		||||
	QSDOC_HIDE void internalDataChanged();
 | 
			
		||||
	QSDOC_HIDE void textChanged();
 | 
			
		||||
	QSDOC_HIDE void dataChanged();
 | 
			
		||||
	void preloadChanged();
 | 
			
		||||
	void loadedOrAsyncChanged();
 | 
			
		||||
	void blockLoadingChanged();
 | 
			
		||||
	void blockAllReadsChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void readerFinished();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	void loadAsync(bool doStringConversion);
 | 
			
		||||
	void cancelAsync();
 | 
			
		||||
	void loadSync();
 | 
			
		||||
	void updateState(FileViewState& newState);
 | 
			
		||||
	void textConversion();
 | 
			
		||||
	void updatePath();
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] bool shouldBlock() const;
 | 
			
		||||
 | 
			
		||||
	FileViewState state;
 | 
			
		||||
	FileViewReader* reader = nullptr;
 | 
			
		||||
	QString pathInFlight;
 | 
			
		||||
 | 
			
		||||
	QString targetPath;
 | 
			
		||||
	bool mAsyncUpdate = true;
 | 
			
		||||
	bool mWritable = false;
 | 
			
		||||
	bool mCreate = false;
 | 
			
		||||
	bool mPreload = true;
 | 
			
		||||
	bool mPrepared = false;
 | 
			
		||||
	bool mLoadedOrAsync = false;
 | 
			
		||||
	bool mBlockLoading = false;
 | 
			
		||||
	bool mBlockAllReads = false;
 | 
			
		||||
 | 
			
		||||
	GuardedEmitter<&FileView::internalTextChanged> textChangedEmitter;
 | 
			
		||||
	GuardedEmitter<&FileView::internalDataChanged> dataChangedEmitter;
 | 
			
		||||
	void emitDataChanged();
 | 
			
		||||
 | 
			
		||||
	DECLARE_PRIVATE_MEMBER(
 | 
			
		||||
	    FileView,
 | 
			
		||||
	    isLoadedOrAsync,
 | 
			
		||||
	    setLoadedOrAsync,
 | 
			
		||||
	    mLoadedOrAsync,
 | 
			
		||||
	    loadedOrAsyncChanged
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	DECLARE_MEMBER_WITH_GET(FileView, shouldPreload, mPreload, preloadChanged);
 | 
			
		||||
	DECLARE_MEMBER_WITH_GET(FileView, blockLoading, mBlockLoading, blockLoadingChanged);
 | 
			
		||||
	DECLARE_MEMBER_WITH_GET(FileView, blockAllReads, mBlockAllReads, blockAllReadsChanged);
 | 
			
		||||
 | 
			
		||||
	void setPreload(bool preload);
 | 
			
		||||
	void setBlockLoading(bool blockLoading);
 | 
			
		||||
	void setBlockAllReads(bool blockAllReads);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::io
 | 
			
		||||
| 
						 | 
				
			
			@ -4,5 +4,6 @@ headers = [
 | 
			
		|||
	"datastream.hpp",
 | 
			
		||||
	"socket.hpp",
 | 
			
		||||
	"process.hpp",
 | 
			
		||||
	"fileview.hpp",
 | 
			
		||||
]
 | 
			
		||||
-----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue