forked from quickshell/quickshell
		
	io/fileview: add write support
FileView is now getting somewhat out of hand. The asynchronous parts especially need to be redone, but this will work for now.
This commit is contained in:
		
							parent
							
								
									2d05c7a89e
								
							
						
					
					
						commit
						70be74e80d
					
				
					 3 changed files with 556 additions and 139 deletions
				
			
		| 
						 | 
					@ -4,11 +4,13 @@ FileViewInternal {
 | 
				
			||||||
	property bool preload: this.__preload;
 | 
						property bool preload: this.__preload;
 | 
				
			||||||
	property bool blockLoading: this.__blockLoading;
 | 
						property bool blockLoading: this.__blockLoading;
 | 
				
			||||||
	property bool blockAllReads: this.__blockAllReads;
 | 
						property bool blockAllReads: this.__blockAllReads;
 | 
				
			||||||
 | 
						property bool printErrors: this.__printErrors;
 | 
				
			||||||
	property string path: this.__path;
 | 
						property string path: this.__path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onPreloadChanged: this.__preload = preload;
 | 
						onPreloadChanged: this.__preload = preload;
 | 
				
			||||||
	onBlockLoadingChanged: this.__blockLoading = this.blockLoading;
 | 
						onBlockLoadingChanged: this.__blockLoading = this.blockLoading;
 | 
				
			||||||
	onBlockAllReadsChanged: this.__blockAllReads = this.blockAllReads;
 | 
						onBlockAllReadsChanged: this.__blockAllReads = this.blockAllReads;
 | 
				
			||||||
 | 
						onPrintErrorsChanged: this.__printErrors = this.printErrors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Unfortunately path can't be kept as an empty string until the file loads
 | 
						// Unfortunately path can't be kept as an empty string until the file loads
 | 
				
			||||||
	// without using QQmlPropertyValueInterceptor which is private. If we lean fully
 | 
						// without using QQmlPropertyValueInterceptor which is private. If we lean fully
 | 
				
			||||||
| 
						 | 
					@ -16,6 +18,7 @@ FileViewInternal {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onPathChanged: {
 | 
						onPathChanged: {
 | 
				
			||||||
		if (!this.preload) this.__preload = false;
 | 
							if (!this.preload) this.__preload = false;
 | 
				
			||||||
 | 
							this.__printErrors = this.printErrors;
 | 
				
			||||||
		this.__path = this.path;
 | 
							this.__path = this.path;
 | 
				
			||||||
		if (this.preload) this.__preload = true;
 | 
							if (this.preload) this.__preload = true;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -30,16 +33,18 @@ FileViewInternal {
 | 
				
			||||||
		if (!this.preload) this.__preload = false;
 | 
							if (!this.preload) this.__preload = false;
 | 
				
			||||||
		this.__blockLoading = this.blockLoading;
 | 
							this.__blockLoading = this.blockLoading;
 | 
				
			||||||
		this.__blockAllReads = this.blockAllReads;
 | 
							this.__blockAllReads = this.blockAllReads;
 | 
				
			||||||
 | 
							this.__printErrors = this.printErrors;
 | 
				
			||||||
		this.__path = this.path;
 | 
							this.__path = this.path;
 | 
				
			||||||
		const text = this.__text;
 | 
							const text = this.__text;
 | 
				
			||||||
		if (this.preload) this.__preload = true;
 | 
							if (this.preload) this.__preload = true;
 | 
				
			||||||
		return text;
 | 
							return text;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function data(): string {
 | 
						function data(): var {
 | 
				
			||||||
		if (!this.preload) this.__preload = false;
 | 
							if (!this.preload) this.__preload = false;
 | 
				
			||||||
		this.__blockLoading = this.blockLoading;
 | 
							this.__blockLoading = this.blockLoading;
 | 
				
			||||||
		this.__blockAllReads = this.blockAllReads;
 | 
							this.__blockAllReads = this.blockAllReads;
 | 
				
			||||||
 | 
							this.__printErrors = this.printErrors;
 | 
				
			||||||
		this.__path = this.path;
 | 
							this.__path = this.path;
 | 
				
			||||||
		const data = this.__data;
 | 
							const data = this.__data;
 | 
				
			||||||
		if (this.preload) this.__preload = true;
 | 
							if (this.preload) this.__preload = true;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,9 @@
 | 
				
			||||||
#include <array>
 | 
					#include <array>
 | 
				
			||||||
#include <utility>
 | 
					#include <utility>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <qatomic.h>
 | 
				
			||||||
 | 
					#include <qdir.h>
 | 
				
			||||||
 | 
					#include <qfiledevice.h>
 | 
				
			||||||
#include <qfileinfo.h>
 | 
					#include <qfileinfo.h>
 | 
				
			||||||
#include <qlogging.h>
 | 
					#include <qlogging.h>
 | 
				
			||||||
#include <qloggingcategory.h>
 | 
					#include <qloggingcategory.h>
 | 
				
			||||||
| 
						 | 
					@ -9,6 +12,9 @@
 | 
				
			||||||
#include <qnamespace.h>
 | 
					#include <qnamespace.h>
 | 
				
			||||||
#include <qobject.h>
 | 
					#include <qobject.h>
 | 
				
			||||||
#include <qobjectdefs.h>
 | 
					#include <qobjectdefs.h>
 | 
				
			||||||
 | 
					#include <qqmlinfo.h>
 | 
				
			||||||
 | 
					#include <qsavefile.h>
 | 
				
			||||||
 | 
					#include <qscopedpointer.h>
 | 
				
			||||||
#include <qthreadpool.h>
 | 
					#include <qthreadpool.h>
 | 
				
			||||||
#include <qtmetamacros.h>
 | 
					#include <qtmetamacros.h>
 | 
				
			||||||
#include <qtypes.h>
 | 
					#include <qtypes.h>
 | 
				
			||||||
| 
						 | 
					@ -19,105 +25,271 @@ namespace qs::io {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg);
 | 
					Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FileViewReader::FileViewReader(QString path, bool doStringConversion)
 | 
					QString FileViewError::toString(FileViewError::Enum value) {
 | 
				
			||||||
    : doStringConversion(doStringConversion) {
 | 
						switch (value) {
 | 
				
			||||||
	this->state.path = std::move(path);
 | 
						case Success: return "Success";
 | 
				
			||||||
 | 
						case Unknown: return "An unknown error has occurred";
 | 
				
			||||||
 | 
						case FileNotFound: return "The specified file does not exist";
 | 
				
			||||||
 | 
						case PermissionDenied: return "Permission denied";
 | 
				
			||||||
 | 
						case NotAFile: return "The specified path was not a file";
 | 
				
			||||||
 | 
						default: return "Invalid error";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool FileViewData::operator==(const FileViewData& other) const {
 | 
				
			||||||
 | 
						if (this->data == other.data && !this->data.isEmpty()) return true;
 | 
				
			||||||
 | 
						if (this->text == other.text && !this->text.isEmpty()) return true;
 | 
				
			||||||
 | 
						return this->operator const QByteArray&() == other.operator const QByteArray&();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool FileViewData::isEmpty() const { return this->data.isEmpty() && this->text.isEmpty(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FileViewData::operator const QString&() const {
 | 
				
			||||||
 | 
						if (this->text.isEmpty() && !this->data.isEmpty()) {
 | 
				
			||||||
 | 
							this->text = QString::fromUtf8(this->data);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return this->text;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FileViewData::operator const QByteArray&() const {
 | 
				
			||||||
 | 
						if (this->data.isEmpty() && !this->text.isEmpty()) {
 | 
				
			||||||
 | 
							this->data = this->text.toUtf8();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return this->data;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FileViewOperation::FileViewOperation(FileView* owner): owner(owner) {
 | 
				
			||||||
	this->setAutoDelete(false);
 | 
						this->setAutoDelete(false);
 | 
				
			||||||
	this->blockMutex.lock();
 | 
						this->blockMutex.lock();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileViewReader::run() {
 | 
					void FileViewOperation::block() {
 | 
				
			||||||
	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
 | 
						// block until a lock can be acauired, then immediately drop it
 | 
				
			||||||
	auto unused = QMutexLocker(&this->blockMutex);
 | 
						auto unused = QMutexLocker(&this->blockMutex);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileViewReader::finished() {
 | 
					void FileViewOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileViewOperation::finishRun() {
 | 
				
			||||||
 | 
						this->blockMutex.unlock();
 | 
				
			||||||
 | 
						QMetaObject::invokeMethod(this, &FileViewOperation::finished, Qt::QueuedConnection);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileViewOperation::finished() {
 | 
				
			||||||
	emit this->done();
 | 
						emit this->done();
 | 
				
			||||||
 | 
						// Delete happens on the main thread, after done(), meaning no operation accesses
 | 
				
			||||||
 | 
						// will be a UAF.
 | 
				
			||||||
	delete this;
 | 
						delete this;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileViewReader::read(FileViewState& state, bool doStringConversion) {
 | 
					void FileViewReader::run() {
 | 
				
			||||||
	{
 | 
						if (!this->shouldCancel) {
 | 
				
			||||||
		qCDebug(logFileView) << "Reader started for" << state.path;
 | 
							FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		auto info = QFileInfo(state.path);
 | 
							if (this->shouldCancel.loadAcquire()) {
 | 
				
			||||||
		state.exists = info.exists();
 | 
								qCDebug(logFileView) << "Read" << this << "of" << state.path << "canceled for" << this->owner;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!state.exists) return;
 | 
						this->finishRun();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!info.isFile()) {
 | 
					void FileViewReader::read(
 | 
				
			||||||
			qCCritical(logFileView) << state.path << "is not a file.";
 | 
					    FileView* view,
 | 
				
			||||||
			goto error;
 | 
					    FileViewState& state,
 | 
				
			||||||
		} else if (!info.isReadable()) {
 | 
					    bool doStringConversion,
 | 
				
			||||||
			qCCritical(logFileView) << "No permission to read" << state.path;
 | 
					    const QAtomicInteger<bool>& shouldCancel
 | 
				
			||||||
			state.error = true;
 | 
					) {
 | 
				
			||||||
			goto error;
 | 
						qCDebug(logFileView) << "Reader started for" << state.path;
 | 
				
			||||||
		}
 | 
					
 | 
				
			||||||
 | 
						auto info = QFileInfo(state.path);
 | 
				
			||||||
		auto file = QFile(state.path);
 | 
						state.exists = info.exists();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!file.open(QFile::ReadOnly)) {
 | 
						if (!state.exists) {
 | 
				
			||||||
			qCCritical(logFileView) << "Failed to open" << state.path;
 | 
							if (state.printErrors) {
 | 
				
			||||||
			goto error;
 | 
								qmlWarning(view) << "Read of " << state.path << " failed: File does not exist.";
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		auto& data = state.data;
 | 
					 | 
				
			||||||
		if (file.size() != 0) {
 | 
					 | 
				
			||||||
			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;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			auto buf = std::array<char, 4096>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			while (true) {
 | 
					 | 
				
			||||||
				auto r = file.read(buf.data(), buf.size()); // NOLINT
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (r == -1) {
 | 
					 | 
				
			||||||
					qCCritical(logFileView) << "Failed to read" << state.path;
 | 
					 | 
				
			||||||
					goto error;
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					data.append(buf.data(), r);
 | 
					 | 
				
			||||||
					if (r == 0) break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (doStringConversion) {
 | 
					 | 
				
			||||||
			state.text = QString::fromUtf8(state.data);
 | 
					 | 
				
			||||||
			state.textDirty = false;
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			state.textDirty = true;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.error = FileViewError::FileNotFound;
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
error:
 | 
						if (!info.isFile()) {
 | 
				
			||||||
	state.error = true;
 | 
							if (state.printErrors) {
 | 
				
			||||||
 | 
								qmlWarning(view) << "Read of " << state.path << " failed: Not a file.";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.error = FileViewError::NotAFile;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						} else if (!info.isReadable()) {
 | 
				
			||||||
 | 
							if (state.printErrors) {
 | 
				
			||||||
 | 
								qmlWarning(view) << "Read of " << state.path << " failed: Permission denied.";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.error = FileViewError::PermissionDenied;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auto file = QFile(state.path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!file.open(QFile::ReadOnly)) {
 | 
				
			||||||
 | 
							qmlWarning(view) << "Read of " << state.path << " failed: Unknown failure when opening file.";
 | 
				
			||||||
 | 
							state.error = FileViewError::Unknown;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (file.size() != 0) {
 | 
				
			||||||
 | 
							auto data = QByteArray(file.size(), Qt::Uninitialized);
 | 
				
			||||||
 | 
							qint64 i = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							while (true) {
 | 
				
			||||||
 | 
								if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auto r = file.read(data.data() + i, data.length() - i); // NOLINT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (r == -1) {
 | 
				
			||||||
 | 
									qmlWarning(view) << "Read of " << state.path << " failed: read() failed.";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									state.error = FileViewError::Unknown;
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								} else if (r == 0) {
 | 
				
			||||||
 | 
									data.resize(i);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								i += r;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.data = data;
 | 
				
			||||||
 | 
						} else { // Mostly happens in /proc and friends, which have zero sized files with content.
 | 
				
			||||||
 | 
							QByteArray data;
 | 
				
			||||||
 | 
							auto buf = std::array<char, 4096>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							while (true) {
 | 
				
			||||||
 | 
								if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auto r = file.read(buf.data(), buf.size()); // NOLINT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (r == -1) {
 | 
				
			||||||
 | 
									qmlWarning(view) << "Read of " << state.path << " failed: read() failed.";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									state.error = FileViewError::Unknown;
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									data.append(buf.data(), r);
 | 
				
			||||||
 | 
									if (r == 0) break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.data = data;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (doStringConversion) {
 | 
				
			||||||
 | 
							state.data.operator const QString&();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileViewWriter::run() {
 | 
				
			||||||
 | 
						if (!this->shouldCancel.loadAcquire()) {
 | 
				
			||||||
 | 
							FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this->shouldCancel.loadAcquire()) {
 | 
				
			||||||
 | 
								qCDebug(logFileView) << "Write" << this << "of" << state.path << "canceled for"
 | 
				
			||||||
 | 
								                     << this->owner;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						this->finishRun();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileViewWriter::write(
 | 
				
			||||||
 | 
					    FileView* view,
 | 
				
			||||||
 | 
					    FileViewState& state,
 | 
				
			||||||
 | 
					    bool doAtomicWrite,
 | 
				
			||||||
 | 
					    const QAtomicInteger<bool>& shouldCancel
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						qCDebug(logFileView) << "Writer started for" << state.path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auto info = QFileInfo(state.path);
 | 
				
			||||||
 | 
						state.exists = info.exists();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!state.exists) {
 | 
				
			||||||
 | 
							auto dir = info.dir();
 | 
				
			||||||
 | 
							if (!dir.mkpath(".")) {
 | 
				
			||||||
 | 
								if (state.printErrors) {
 | 
				
			||||||
 | 
									qmlWarning(view) << "Write of " << state.path
 | 
				
			||||||
 | 
									                 << " failed: Could not create parent directories of file.";
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								state.error = FileViewError::PermissionDenied;
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if (!info.isWritable()) {
 | 
				
			||||||
 | 
							if (state.printErrors) {
 | 
				
			||||||
 | 
								qmlWarning(view) << "Write of " << state.path << " failed: Permission denied.";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							state.error = FileViewError::PermissionDenied;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						QScopedPointer<QFileDevice> file;
 | 
				
			||||||
 | 
						if (doAtomicWrite) {
 | 
				
			||||||
 | 
							file.reset(new QSaveFile(state.path));
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							file.reset(new QFile(state.path));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!file->open(QFile::WriteOnly)) {
 | 
				
			||||||
 | 
							qmlWarning(view) << "Write of " << state.path << " failed: Unknown error when opening file.";
 | 
				
			||||||
 | 
							state.error = FileViewError::Unknown;
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const QByteArray& data = state.data;
 | 
				
			||||||
 | 
						qint64 i = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						while (true) {
 | 
				
			||||||
 | 
							if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto r = file->write(data.data() + i, data.length() - i); // NOLINT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (r == -1) {
 | 
				
			||||||
 | 
								qmlWarning(view) << "Write of " << state.path << " failed: write() failed.";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								state.error = FileViewError::Unknown;
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								i += r;
 | 
				
			||||||
 | 
								if (i == data.length()) break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (shouldCancel.loadAcquire()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (doAtomicWrite) {
 | 
				
			||||||
 | 
							qDebug() << "Atomic commit";
 | 
				
			||||||
 | 
							if (!reinterpret_cast<QSaveFile*>(file.get())->commit()) { // NOLINT
 | 
				
			||||||
 | 
								qmlWarning(view) << "Write of " << state.path << " failed: Atomic commit failed.";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::loadAsync(bool doStringConversion) {
 | 
					void FileView::loadAsync(bool doStringConversion) {
 | 
				
			||||||
	if (!this->reader || this->pathInFlight != this->targetPath) {
 | 
						// Writes update via operationFinished, making a read both invalid and outdated.
 | 
				
			||||||
 | 
						if (!this->liveOperation || this->pathInFlight != this->targetPath) {
 | 
				
			||||||
		this->cancelAsync();
 | 
							this->cancelAsync();
 | 
				
			||||||
		this->pathInFlight = this->targetPath;
 | 
							this->pathInFlight = this->targetPath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -126,40 +298,92 @@ void FileView::loadAsync(bool doStringConversion) {
 | 
				
			||||||
			this->updateState(state);
 | 
								this->updateState(state);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			qCDebug(logFileView) << "Starting async load for" << this << "of" << this->targetPath;
 | 
								qCDebug(logFileView) << "Starting async load for" << this << "of" << this->targetPath;
 | 
				
			||||||
			this->reader = new FileViewReader(this->targetPath, doStringConversion);
 | 
								auto* reader = new FileViewReader(this, doStringConversion);
 | 
				
			||||||
			QObject::connect(this->reader, &FileViewReader::done, this, &FileView::readerFinished);
 | 
								reader->state.path = this->targetPath;
 | 
				
			||||||
			QThreadPool::globalInstance()->start(this->reader); // takes ownership
 | 
								reader->state.printErrors = this->bPrintErrors;
 | 
				
			||||||
 | 
								QObject::connect(reader, &FileViewOperation::done, this, &FileView::operationFinished);
 | 
				
			||||||
 | 
								QThreadPool::globalInstance()->start(reader); // takes ownership
 | 
				
			||||||
 | 
								this->liveOperation = reader;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::cancelAsync() {
 | 
					void FileView::saveAsync() {
 | 
				
			||||||
	if (this->reader) {
 | 
						if (this->targetPath.isEmpty()) {
 | 
				
			||||||
		qCDebug(logFileView) << "Disowning async read for" << this;
 | 
							qmlWarning(this) << "Cannot write file, as no path has been specified.";
 | 
				
			||||||
		QObject::disconnect(this->reader, nullptr, this, nullptr);
 | 
							this->writeData = FileViewData();
 | 
				
			||||||
		this->reader = nullptr;
 | 
						} else {
 | 
				
			||||||
 | 
							// cancel will blank the data if waiting
 | 
				
			||||||
 | 
							auto data = this->writeData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this->cancelAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							qCDebug(logFileView) << "Starting async save for" << this << "of" << this->targetPath;
 | 
				
			||||||
 | 
							auto* writer = new FileViewWriter(this, this->bAtomicWrites);
 | 
				
			||||||
 | 
							writer->state.path = this->targetPath;
 | 
				
			||||||
 | 
							writer->state.data = std::move(data);
 | 
				
			||||||
 | 
							writer->state.printErrors = this->bPrintErrors;
 | 
				
			||||||
 | 
							QObject::connect(writer, &FileViewOperation::done, this, &FileView::operationFinished);
 | 
				
			||||||
 | 
							QThreadPool::globalInstance()->start(writer); // takes ownership
 | 
				
			||||||
 | 
							this->liveOperation = writer;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::readerFinished() {
 | 
					void FileView::cancelAsync() {
 | 
				
			||||||
	if (this->sender() != this->reader) {
 | 
						if (!this->liveOperation) return;
 | 
				
			||||||
		qCWarning(logFileView) << "got read finished from dropped FileViewReader" << this->sender();
 | 
						this->liveOperation->tryCancel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (this->liveReader()) {
 | 
				
			||||||
 | 
							qCDebug(logFileView) << "Disowning async read for" << this;
 | 
				
			||||||
 | 
							QObject::disconnect(this->liveOperation, nullptr, this, nullptr);
 | 
				
			||||||
 | 
							this->liveOperation = nullptr;
 | 
				
			||||||
 | 
						} else if (this->liveWriter()) {
 | 
				
			||||||
 | 
							// We don't want to start a read or write operation in the middle of a write.
 | 
				
			||||||
 | 
							// This really shouldn't block but it isn't worth fixing for now.
 | 
				
			||||||
 | 
							qCDebug(logFileView) << "Blocking on write for" << this;
 | 
				
			||||||
 | 
							this->waitForJob();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileView::operationFinished() {
 | 
				
			||||||
 | 
						if (this->sender() != this->liveOperation) {
 | 
				
			||||||
 | 
							qCWarning(logFileView) << "got operation finished from dropped operation" << this->sender();
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	qCDebug(logFileView) << "Async load finished for" << this;
 | 
						qCDebug(logFileView) << "Async operation finished for" << this;
 | 
				
			||||||
	this->updateState(this->reader->state);
 | 
						this->writeData = FileViewData();
 | 
				
			||||||
	this->reader = nullptr;
 | 
						this->updateState(this->liveOperation->state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (this->liveReader()) {
 | 
				
			||||||
 | 
							if (this->state.error) emit this->loadFailed(this->state.error);
 | 
				
			||||||
 | 
							else emit this->loaded();
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if (this->state.error) emit this->saveFailed(this->state.error);
 | 
				
			||||||
 | 
							else emit this->saved();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						this->liveOperation = nullptr;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::reload() { this->updatePath(); }
 | 
					void FileView::reload() { this->updatePath(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool FileView::blockUntilLoaded() {
 | 
					bool FileView::waitForJob() {
 | 
				
			||||||
	if (this->reader != nullptr) {
 | 
						if (this->liveOperation != nullptr) {
 | 
				
			||||||
		QObject::disconnect(this->reader, nullptr, this, nullptr);
 | 
							QObject::disconnect(this->liveOperation, nullptr, this, nullptr);
 | 
				
			||||||
		this->reader->block();
 | 
							this->liveOperation->block();
 | 
				
			||||||
		this->updateState(this->reader->state);
 | 
							this->writeData = FileViewData();
 | 
				
			||||||
		this->reader = nullptr;
 | 
							this->updateState(this->liveOperation->state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this->liveReader()) {
 | 
				
			||||||
 | 
								if (this->state.error) emit this->loadFailed(this->state.error);
 | 
				
			||||||
 | 
								else emit this->loaded();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								if (this->state.error) emit this->saveFailed(this->state.error);
 | 
				
			||||||
 | 
								else emit this->saved();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this->liveOperation = nullptr;
 | 
				
			||||||
		return true;
 | 
							return true;
 | 
				
			||||||
	} else return false;
 | 
						} else return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -168,10 +392,34 @@ void FileView::loadSync() {
 | 
				
			||||||
	if (this->targetPath.isEmpty()) {
 | 
						if (this->targetPath.isEmpty()) {
 | 
				
			||||||
		auto state = FileViewState();
 | 
							auto state = FileViewState();
 | 
				
			||||||
		this->updateState(state);
 | 
							this->updateState(state);
 | 
				
			||||||
	} else if (!this->blockUntilLoaded()) {
 | 
						} else if (!this->waitForJob()) {
 | 
				
			||||||
		auto state = FileViewState(this->targetPath);
 | 
							auto state = FileViewState(this->targetPath);
 | 
				
			||||||
		FileViewReader::read(state, false);
 | 
							state.printErrors = this->bPrintErrors;
 | 
				
			||||||
 | 
							FileViewReader::read(this, state, false);
 | 
				
			||||||
		this->updateState(state);
 | 
							this->updateState(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this->state.error) emit this->loadFailed(this->state.error);
 | 
				
			||||||
 | 
							else emit this->loaded();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileView::saveSync() {
 | 
				
			||||||
 | 
						if (this->targetPath.isEmpty()) {
 | 
				
			||||||
 | 
							qmlWarning(this) << "Cannot write file, as no path has been specified.";
 | 
				
			||||||
 | 
							this->writeData = FileViewData();
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Both reads and writes will be outdated.
 | 
				
			||||||
 | 
							if (this->liveOperation) this->cancelAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto state = FileViewState(this->targetPath);
 | 
				
			||||||
 | 
							state.data = this->writeData;
 | 
				
			||||||
 | 
							state.printErrors = this->bPrintErrors;
 | 
				
			||||||
 | 
							FileViewWriter::write(this, state, this->bAtomicWrites);
 | 
				
			||||||
 | 
							this->writeData = FileViewData();
 | 
				
			||||||
 | 
							this->updateState(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this->state.error) emit this->saveFailed(this->state.error);
 | 
				
			||||||
 | 
							else emit this->saved();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -188,8 +436,6 @@ void FileView::updateState(FileViewState& newState) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (dataChanged) {
 | 
						if (dataChanged) {
 | 
				
			||||||
		this->state.data = newState.data;
 | 
							this->state.data = newState.data;
 | 
				
			||||||
		this->state.text = newState.text;
 | 
					 | 
				
			||||||
		this->state.textDirty = newState.textDirty;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	this->state.exists = newState.exists;
 | 
						this->state.exists = newState.exists;
 | 
				
			||||||
| 
						 | 
					@ -202,15 +448,6 @@ void FileView::updateState(FileViewState& newState) {
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (dataChanged) this->emitDataChanged();
 | 
						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; }
 | 
					QString FileView::path() const { return this->state.path; }
 | 
				
			||||||
| 
						 | 
					@ -218,6 +455,13 @@ QString FileView::path() const { return this->state.path; }
 | 
				
			||||||
void FileView::setPath(const QString& path) {
 | 
					void FileView::setPath(const QString& path) {
 | 
				
			||||||
	auto p = path.startsWith("file://") ? path.sliced(7) : path;
 | 
						auto p = path.startsWith("file://") ? path.sliced(7) : path;
 | 
				
			||||||
	if (p == this->targetPath) return;
 | 
						if (p == this->targetPath) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (this->liveWriter()) {
 | 
				
			||||||
 | 
							this->waitForJob();
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							this->cancelAsync();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	this->targetPath = p;
 | 
						this->targetPath = p;
 | 
				
			||||||
	this->updatePath();
 | 
						this->updatePath();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -231,21 +475,31 @@ void FileView::updatePath() {
 | 
				
			||||||
	} else if (this->mPreload) {
 | 
						} else if (this->mPreload) {
 | 
				
			||||||
		this->loadAsync(true);
 | 
							this->loadAsync(true);
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// loadAsync will do this already
 | 
					 | 
				
			||||||
		this->cancelAsync();
 | 
					 | 
				
			||||||
		this->emitDataChanged();
 | 
							this->emitDataChanged();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool FileView::shouldBlock() const {
 | 
					bool FileView::shouldBlockRead() const {
 | 
				
			||||||
	return this->mBlockAllReads || (this->mBlockLoading && !this->mLoadedOrAsync);
 | 
						return this->mBlockAllReads || (this->mBlockLoading && !this->mLoadedOrAsync);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FileViewReader* FileView::liveReader() const {
 | 
				
			||||||
 | 
						return dynamic_cast<FileViewReader*>(this->liveOperation);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FileViewWriter* FileView::liveWriter() const {
 | 
				
			||||||
 | 
						return dynamic_cast<FileViewWriter*>(this->liveOperation);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FileViewData& FileView::writeCmpData() const {
 | 
				
			||||||
 | 
						return this->writeData.isEmpty() ? this->state.data : this->writeData;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
QByteArray FileView::data() {
 | 
					QByteArray FileView::data() {
 | 
				
			||||||
	auto guard = this->dataChangedEmitter.block();
 | 
						auto guard = this->dataChangedEmitter.block();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!this->mPrepared) {
 | 
						if (!this->mPrepared) {
 | 
				
			||||||
		if (this->shouldBlock()) this->loadSync();
 | 
							if (this->shouldBlockRead()) this->loadSync();
 | 
				
			||||||
		else this->loadAsync(false);
 | 
							else this->loadAsync(false);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -256,12 +510,27 @@ QString FileView::text() {
 | 
				
			||||||
	auto guard = this->textChangedEmitter.block();
 | 
						auto guard = this->textChangedEmitter.block();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!this->mPrepared) {
 | 
						if (!this->mPrepared) {
 | 
				
			||||||
		if (this->shouldBlock()) this->loadSync();
 | 
							if (this->shouldBlockRead()) this->loadSync();
 | 
				
			||||||
		else this->loadAsync(true);
 | 
							else this->loadAsync(true);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	this->textConversion();
 | 
						return this->state.data;
 | 
				
			||||||
	return this->state.text;
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileView::setData(const QByteArray& data) {
 | 
				
			||||||
 | 
						if (this->writeCmpData().operator const QByteArray&() == data) return;
 | 
				
			||||||
 | 
						this->writeData = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (this->bBlockWrites) this->saveSync();
 | 
				
			||||||
 | 
						else this->saveAsync();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileView::setText(const QString& text) {
 | 
				
			||||||
 | 
						if (this->writeCmpData().operator const QString&() == text) return;
 | 
				
			||||||
 | 
						this->writeData = text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (this->bBlockWrites) this->saveSync();
 | 
				
			||||||
 | 
						else this->saveAsync();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::emitDataChanged() {
 | 
					void FileView::emitDataChanged() {
 | 
				
			||||||
| 
						 | 
					@ -291,12 +560,12 @@ void FileView::setPreload(bool preload) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::setBlockLoading(bool blockLoading) {
 | 
					void FileView::setBlockLoading(bool blockLoading) {
 | 
				
			||||||
	if (blockLoading != this->mBlockLoading) {
 | 
						if (blockLoading != this->mBlockLoading) {
 | 
				
			||||||
		auto wasBlocking = this->shouldBlock();
 | 
							auto wasBlocking = this->shouldBlockRead();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this->mBlockLoading = blockLoading;
 | 
							this->mBlockLoading = blockLoading;
 | 
				
			||||||
		emit this->blockLoadingChanged();
 | 
							emit this->blockLoadingChanged();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!wasBlocking && this->shouldBlock()) {
 | 
							if (!wasBlocking && this->shouldBlockRead()) {
 | 
				
			||||||
			this->emitDataChanged();
 | 
								this->emitDataChanged();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -304,12 +573,12 @@ void FileView::setBlockLoading(bool blockLoading) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileView::setBlockAllReads(bool blockAllReads) {
 | 
					void FileView::setBlockAllReads(bool blockAllReads) {
 | 
				
			||||||
	if (blockAllReads != this->mBlockAllReads) {
 | 
						if (blockAllReads != this->mBlockAllReads) {
 | 
				
			||||||
		auto wasBlocking = this->shouldBlock();
 | 
							auto wasBlocking = this->shouldBlockRead();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this->mBlockAllReads = blockAllReads;
 | 
							this->mBlockAllReads = blockAllReads;
 | 
				
			||||||
		emit this->blockAllReadsChanged();
 | 
							emit this->blockAllReadsChanged();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!wasBlocking && this->shouldBlock()) {
 | 
							if (!wasBlocking && this->shouldBlockRead()) {
 | 
				
			||||||
			this->emitDataChanged();
 | 
								this->emitDataChanged();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,17 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <utility>
 | 
					#include <utility>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <qatomic.h>
 | 
				
			||||||
 | 
					#include <qdebug.h>
 | 
				
			||||||
#include <qlogging.h>
 | 
					#include <qlogging.h>
 | 
				
			||||||
#include <qmutex.h>
 | 
					#include <qmutex.h>
 | 
				
			||||||
#include <qobject.h>
 | 
					#include <qobject.h>
 | 
				
			||||||
 | 
					#include <qpointer.h>
 | 
				
			||||||
#include <qproperty.h>
 | 
					#include <qproperty.h>
 | 
				
			||||||
#include <qqmlintegration.h>
 | 
					#include <qqmlintegration.h>
 | 
				
			||||||
#include <qqmlparserstatus.h>
 | 
					#include <qqmlparserstatus.h>
 | 
				
			||||||
#include <qrunnable.h>
 | 
					#include <qrunnable.h>
 | 
				
			||||||
 | 
					#include <qstringview.h>
 | 
				
			||||||
#include <qtmetamacros.h>
 | 
					#include <qtmetamacros.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "../core/doc.hpp"
 | 
					#include "../core/doc.hpp"
 | 
				
			||||||
| 
						 | 
					@ -16,34 +20,74 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace qs::io {
 | 
					namespace qs::io {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FileViewError: public QObject {
 | 
				
			||||||
 | 
						Q_OBJECT;
 | 
				
			||||||
 | 
						QML_ELEMENT;
 | 
				
			||||||
 | 
						QML_SINGLETON;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						enum Enum : quint8 {
 | 
				
			||||||
 | 
							/// No error occured.
 | 
				
			||||||
 | 
							Success = 0,
 | 
				
			||||||
 | 
							/// An unknown error occured. Check the logs for details.
 | 
				
			||||||
 | 
							Unknown = 1,
 | 
				
			||||||
 | 
							/// The file to read does not exist.
 | 
				
			||||||
 | 
							FileNotFound = 2,
 | 
				
			||||||
 | 
							/// Permission to read/write the file was not granted, or permission
 | 
				
			||||||
 | 
							/// to create parent directories was not granted when writing the file.
 | 
				
			||||||
 | 
							PermissionDenied = 3,
 | 
				
			||||||
 | 
							/// The specified path to read/write exists and was not a file.
 | 
				
			||||||
 | 
							NotAFile = 4,
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						Q_ENUM(Enum);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Q_INVOKABLE static QString toString(qs::io::FileViewError::Enum value);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct FileViewData {
 | 
				
			||||||
 | 
						FileViewData() = default;
 | 
				
			||||||
 | 
						FileViewData(QString text): text(std::move(text)) {}
 | 
				
			||||||
 | 
						FileViewData(QByteArray data): data(std::move(data)) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] bool operator==(const FileViewData& other) const;
 | 
				
			||||||
 | 
						[[nodiscard]] bool isEmpty() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						operator const QString&() const;
 | 
				
			||||||
 | 
						operator const QByteArray&() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private:
 | 
				
			||||||
 | 
						mutable QString text;
 | 
				
			||||||
 | 
						mutable QByteArray data;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct FileViewState {
 | 
					struct FileViewState {
 | 
				
			||||||
	FileViewState() = default;
 | 
						FileViewState() = default;
 | 
				
			||||||
	explicit FileViewState(QString path): path(std::move(path)) {}
 | 
						explicit FileViewState(QString path): path(std::move(path)) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	QString path;
 | 
						QString path;
 | 
				
			||||||
	QString text;
 | 
						FileViewData data;
 | 
				
			||||||
	QByteArray data;
 | 
					 | 
				
			||||||
	bool textDirty = false;
 | 
					 | 
				
			||||||
	bool exists = false;
 | 
						bool exists = false;
 | 
				
			||||||
	bool error = false;
 | 
						bool printErrors = true;
 | 
				
			||||||
 | 
						FileViewError::Enum error = FileViewError::Success;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FileView;
 | 
					class FileView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FileViewReader
 | 
					class FileViewOperation
 | 
				
			||||||
    : public QObject
 | 
					    : public QObject
 | 
				
			||||||
    , public QRunnable {
 | 
					    , public QRunnable {
 | 
				
			||||||
	Q_OBJECT;
 | 
						Q_OBJECT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public:
 | 
					public:
 | 
				
			||||||
	explicit FileViewReader(QString path, bool doStringConversion);
 | 
						explicit FileViewOperation(FileView* owner);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	void run() override;
 | 
					 | 
				
			||||||
	void block();
 | 
						void block();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	FileViewState state;
 | 
						// Attempt to cancel the operation, which may or may not be possible.
 | 
				
			||||||
 | 
						// If possible, block() returns sooner.
 | 
				
			||||||
 | 
						void tryCancel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static void read(FileViewState& state, bool doStringConversion);
 | 
						FileViewState state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
signals:
 | 
					signals:
 | 
				
			||||||
	void done();
 | 
						void done();
 | 
				
			||||||
| 
						 | 
					@ -51,9 +95,48 @@ signals:
 | 
				
			||||||
private slots:
 | 
					private slots:
 | 
				
			||||||
	void finished();
 | 
						void finished();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private:
 | 
					protected:
 | 
				
			||||||
	bool doStringConversion;
 | 
					 | 
				
			||||||
	QMutex blockMutex;
 | 
						QMutex blockMutex;
 | 
				
			||||||
 | 
						QPointer<FileView> owner;
 | 
				
			||||||
 | 
						QAtomicInteger<bool> shouldCancel = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void finishRun();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FileViewReader: public FileViewOperation {
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						explicit FileViewReader(FileView* owner, bool doStringConversion)
 | 
				
			||||||
 | 
						    : FileViewOperation(owner)
 | 
				
			||||||
 | 
						    , doStringConversion(doStringConversion) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void run() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static void read(
 | 
				
			||||||
 | 
						    FileView* view,
 | 
				
			||||||
 | 
						    FileViewState& state,
 | 
				
			||||||
 | 
						    bool doStringConversion,
 | 
				
			||||||
 | 
						    const QAtomicInteger<bool>& shouldCancel = false
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						bool doStringConversion;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FileViewWriter: public FileViewOperation {
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						explicit FileViewWriter(FileView* owner, bool doAtomicWrite)
 | 
				
			||||||
 | 
						    : FileViewOperation(owner)
 | 
				
			||||||
 | 
						    , doAtomicWrite(doAtomicWrite) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void run() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static void write(
 | 
				
			||||||
 | 
						    FileView* view,
 | 
				
			||||||
 | 
						    FileViewState& state,
 | 
				
			||||||
 | 
						    bool doAtomicWrite,
 | 
				
			||||||
 | 
						    const QAtomicInteger<bool>& shouldCancel = false
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						bool doAtomicWrite;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
///! Simplified reader for small files.
 | 
					///! Simplified reader for small files.
 | 
				
			||||||
| 
						 | 
					@ -104,6 +187,21 @@ class FileView: public QObject {
 | 
				
			||||||
	/// > [!WARNING] We cannot think of a valid use case for this.
 | 
						/// > [!WARNING] We cannot think of a valid use case for this.
 | 
				
			||||||
	/// > You almost definitely want @@blockLoading.
 | 
						/// > You almost definitely want @@blockLoading.
 | 
				
			||||||
	QSDOC_PROPERTY_OVERRIDE(bool blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged);
 | 
						QSDOC_PROPERTY_OVERRIDE(bool blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged);
 | 
				
			||||||
 | 
						/// If true (default false), all calls to @@setText or @@setData will block the
 | 
				
			||||||
 | 
						/// UI thread until the write succeeds or fails.
 | 
				
			||||||
 | 
						///
 | 
				
			||||||
 | 
						/// > [!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.
 | 
				
			||||||
 | 
						Q_PROPERTY(bool blockWrites READ default WRITE default NOTIFY blockWritesChanged BINDABLE bindableBlockWrites);
 | 
				
			||||||
 | 
						/// If true (default), all calls to @@setText or @@setData will be performed atomically,
 | 
				
			||||||
 | 
						/// meaning if the write fails for any reason, the file will not be modified.
 | 
				
			||||||
 | 
						///
 | 
				
			||||||
 | 
						/// > [!NOTE] This works by creating another file with the desired content, and renaming
 | 
				
			||||||
 | 
						/// > it over the existing file if successful.
 | 
				
			||||||
 | 
						Q_PROPERTY(bool atomicWrites READ default WRITE default NOTIFY blockWritesChanged BINDABLE bindableAtomicWrites);
 | 
				
			||||||
 | 
						/// If true (default), read or write errors will be printed to the quickshell logs.
 | 
				
			||||||
 | 
						/// If false, all known errors will not be printed.
 | 
				
			||||||
 | 
						QSDOC_PROPERTY_OVERRIDE(bool printErrors READ default WRITE default NOTIFY printErrorsChanged);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	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);
 | 
				
			||||||
| 
						 | 
					@ -117,6 +215,7 @@ class FileView: public QObject {
 | 
				
			||||||
	Q_PROPERTY(bool loaded READ isLoadedOrAsync NOTIFY loadedOrAsyncChanged);
 | 
						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 __blockLoading READ blockLoading WRITE setBlockLoading NOTIFY blockLoadingChanged);
 | 
				
			||||||
	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);
 | 
				
			||||||
	// clang-format on
 | 
						// clang-format on
 | 
				
			||||||
	QML_NAMED_ELEMENT(FileViewInternal);
 | 
						QML_NAMED_ELEMENT(FileViewInternal);
 | 
				
			||||||
	QSDOC_NAMED_ELEMENT(FileView);
 | 
						QSDOC_NAMED_ELEMENT(FileView);
 | 
				
			||||||
| 
						 | 
					@ -162,7 +261,7 @@ public:
 | 
				
			||||||
	/// Block all operations until the currently running load completes.
 | 
						/// Block all operations until the currently running load completes.
 | 
				
			||||||
	///
 | 
						///
 | 
				
			||||||
	/// > [!WARNING] See @@blockLoading for an explanation and warning about blocking.
 | 
						/// > [!WARNING] See @@blockLoading for an explanation and warning about blocking.
 | 
				
			||||||
	Q_INVOKABLE bool blockUntilLoaded();
 | 
						Q_INVOKABLE bool waitForJob();
 | 
				
			||||||
	/// Unload the loaded file and reload it, usually in response to changes.
 | 
						/// 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.
 | 
						/// This will not block if @@blockLoading is set, only if @@blockAllReads is true.
 | 
				
			||||||
| 
						 | 
					@ -175,9 +274,39 @@ public:
 | 
				
			||||||
	[[nodiscard]] QByteArray data();
 | 
						[[nodiscard]] QByteArray data();
 | 
				
			||||||
	[[nodiscard]] QString text();
 | 
						[[nodiscard]] QString text();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// These generally should not be called prior to component completion, making it safe not to force
 | 
				
			||||||
 | 
						// property resolution.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// Sets the content of the file specified by @@path as an [ArrayBuffer].
 | 
				
			||||||
 | 
						///
 | 
				
			||||||
 | 
						/// @@atomicWrites and @@blockWrites affect the behavior of this function.
 | 
				
			||||||
 | 
						///
 | 
				
			||||||
 | 
						/// @@saved() or @@saveFailed() will be emitted on completion.
 | 
				
			||||||
 | 
						Q_INVOKABLE void setData(const QByteArray& data);
 | 
				
			||||||
 | 
						/// Sets the content of the file specified by @@path as text.
 | 
				
			||||||
 | 
						///
 | 
				
			||||||
 | 
						/// @@atomicWrites and @@blockWrites affect the behavior of this function.
 | 
				
			||||||
 | 
						///
 | 
				
			||||||
 | 
						/// @@saved() or @@saveFailed() will be emitted on completion.
 | 
				
			||||||
 | 
						///
 | 
				
			||||||
 | 
						/// [ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
 | 
				
			||||||
 | 
						Q_INVOKABLE void setText(const QString& text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Const bindables functions silently do nothing on setValue.
 | 
				
			||||||
 | 
						[[nodiscard]] QBindable<bool> bindableBlockWrites() { return &this->bBlockWrites; }
 | 
				
			||||||
 | 
						[[nodiscard]] QBindable<bool> bindableAtomicWrites() { return &this->bAtomicWrites; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[[nodiscard]] QBindable<bool> bindablePrintErrors() { return &this->bPrintErrors; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
signals:
 | 
					signals:
 | 
				
			||||||
	///! Fires if the file failed to load. A warning will be printed in the log.
 | 
						/// Emitted if the file was loaded successfully.
 | 
				
			||||||
	void loadFailed();
 | 
						void loaded();
 | 
				
			||||||
 | 
						/// Emitted if the file failed to load.
 | 
				
			||||||
 | 
						void loadFailed(qs::io::FileViewError::Enum error);
 | 
				
			||||||
 | 
						/// Emitted if the file was saved successfully.
 | 
				
			||||||
 | 
						void saved();
 | 
				
			||||||
 | 
						/// Emitted if the file failed to save.
 | 
				
			||||||
 | 
						void saveFailed(qs::io::FileViewError::Enum error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	void pathChanged();
 | 
						void pathChanged();
 | 
				
			||||||
	QSDOC_HIDE void internalTextChanged();
 | 
						QSDOC_HIDE void internalTextChanged();
 | 
				
			||||||
| 
						 | 
					@ -188,22 +317,30 @@ signals:
 | 
				
			||||||
	void loadedOrAsyncChanged();
 | 
						void loadedOrAsyncChanged();
 | 
				
			||||||
	void blockLoadingChanged();
 | 
						void blockLoadingChanged();
 | 
				
			||||||
	void blockAllReadsChanged();
 | 
						void blockAllReadsChanged();
 | 
				
			||||||
 | 
						void blockWritesChanged();
 | 
				
			||||||
 | 
						void atomicWritesChanged();
 | 
				
			||||||
 | 
						void printErrorsChanged();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private slots:
 | 
					private slots:
 | 
				
			||||||
	void readerFinished();
 | 
						void operationFinished();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private:
 | 
					private:
 | 
				
			||||||
	void loadAsync(bool doStringConversion);
 | 
						void loadAsync(bool doStringConversion);
 | 
				
			||||||
 | 
						void saveAsync();
 | 
				
			||||||
	void cancelAsync();
 | 
						void cancelAsync();
 | 
				
			||||||
	void loadSync();
 | 
						void loadSync();
 | 
				
			||||||
 | 
						void saveSync();
 | 
				
			||||||
	void updateState(FileViewState& newState);
 | 
						void updateState(FileViewState& newState);
 | 
				
			||||||
	void textConversion();
 | 
					 | 
				
			||||||
	void updatePath();
 | 
						void updatePath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	[[nodiscard]] bool shouldBlock() const;
 | 
						[[nodiscard]] bool shouldBlockRead() const;
 | 
				
			||||||
 | 
						[[nodiscard]] FileViewReader* liveReader() const;
 | 
				
			||||||
 | 
						[[nodiscard]] FileViewWriter* liveWriter() const;
 | 
				
			||||||
 | 
						[[nodiscard]] const FileViewData& writeCmpData() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	FileViewState state;
 | 
						FileViewState state;
 | 
				
			||||||
	FileViewReader* reader = nullptr;
 | 
						FileViewData writeData;
 | 
				
			||||||
 | 
						FileViewOperation* liveOperation = nullptr;
 | 
				
			||||||
	QString pathInFlight;
 | 
						QString pathInFlight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	QString targetPath;
 | 
						QString targetPath;
 | 
				
			||||||
| 
						 | 
					@ -233,6 +370,12 @@ public:
 | 
				
			||||||
	DECLARE_MEMBER_WITH_GET(FileView, blockLoading, mBlockLoading, blockLoadingChanged);
 | 
						DECLARE_MEMBER_WITH_GET(FileView, blockLoading, mBlockLoading, blockLoadingChanged);
 | 
				
			||||||
	DECLARE_MEMBER_WITH_GET(FileView, blockAllReads, mBlockAllReads, blockAllReadsChanged);
 | 
						DECLARE_MEMBER_WITH_GET(FileView, blockAllReads, mBlockAllReads, blockAllReadsChanged);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// clang-format off
 | 
				
			||||||
 | 
						Q_OBJECT_BINDABLE_PROPERTY(FileView, bool, bBlockWrites, &FileView::blockWritesChanged);
 | 
				
			||||||
 | 
						Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(FileView, bool, bAtomicWrites, true, &FileView::atomicWritesChanged);
 | 
				
			||||||
 | 
						Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(FileView, bool, bPrintErrors, true, &FileView::printErrorsChanged);
 | 
				
			||||||
 | 
						// clang-format on
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	void setPreload(bool preload);
 | 
						void setPreload(bool preload);
 | 
				
			||||||
	void setBlockLoading(bool blockLoading);
 | 
						void setBlockLoading(bool blockLoading);
 | 
				
			||||||
	void setBlockAllReads(bool blockAllReads);
 | 
						void setBlockAllReads(bool blockAllReads);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue