diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 3080075e..924d55b5 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -280,7 +281,6 @@ void FileViewWriter::write( if (shouldCancel.loadAcquire()) return; if (doAtomicWrite) { - qDebug() << "Atomic commit"; if (!reinterpret_cast(file.get())->commit()) { // NOLINT qmlWarning(view) << "Write of " << state.path << " failed: Atomic commit failed."; } @@ -477,6 +477,49 @@ void FileView::updatePath() { } else { this->emitDataChanged(); } + + this->updateWatchedFiles(); +} + +void FileView::updateWatchedFiles() { + delete this->watcher; + + if (!this->targetPath.isEmpty() && this->bWatchChanges) { + qCDebug(logFileView) << "Creating watcher for" << this << "at" << this->targetPath; + this->watcher = new QFileSystemWatcher(this); + this->watcher->addPath(this->targetPath); + this->watcher->addPath(QDir(this->targetPath).dirName()); + + QObject::connect( + this->watcher, + &QFileSystemWatcher::fileChanged, + this, + &FileView::onWatchedFileChanged + ); + + QObject::connect( + this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &FileView::onWatchedDirectoryChanged + ); + } +} + +void FileView::onWatchedFileChanged() { + if (!this->watcher->files().contains(this->targetPath)) { + this->watcher->addPath(this->targetPath); + } + + emit this->fileChanged(); +} + +void FileView::onWatchedDirectoryChanged() { + if (!this->watcher->files().contains(this->targetPath) && QFileInfo(this->targetPath).exists()) { + // the file was just created + this->watcher->addPath(this->targetPath); + emit this->fileChanged(); + } } bool FileView::shouldBlockRead() const { diff --git a/src/io/fileview.hpp b/src/io/fileview.hpp index b2b768ef..01d31fa9 100644 --- a/src/io/fileview.hpp +++ b/src/io/fileview.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -187,13 +188,13 @@ class FileView: public QObject { /// > [!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); - /// If true (default false), all calls to @@setText or @@setData will block the + /// 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, + /// 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 @@ -202,6 +203,18 @@ class FileView: public QObject { /// 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); + /// If true (defaule false), @@fileChanged() will be called whenever the content of the file + /// changes on disk, including when @@setText() or @@setData() are used. + /// + /// > [!NOTE] You can reload the file's content whenever it changes on disk like so: + /// > ```qml + /// > FileView { + /// > // ... + /// > watchChanges: true + /// > onFileChanged: this.reload() + /// > } + /// > ``` + Q_PROPERTY(bool watchChanges READ default WRITE default NOTIFY watchChangesChanged BINDABLE bindableWatchChanges); QSDOC_HIDE Q_PROPERTY(QString __path READ path WRITE setPath NOTIFY pathChanged); QSDOC_HIDE Q_PROPERTY(QString __text READ text NOTIFY internalTextChanged); @@ -297,6 +310,7 @@ public: [[nodiscard]] QBindable bindableAtomicWrites() { return &this->bAtomicWrites; } [[nodiscard]] QBindable bindablePrintErrors() { return &this->bPrintErrors; } + [[nodiscard]] QBindable bindableWatchChanges() { return &this->bWatchChanges; } signals: /// Emitted if the file was loaded successfully. @@ -307,6 +321,8 @@ signals: void saved(); /// Emitted if the file failed to save. void saveFailed(qs::io::FileViewError::Enum error); + /// Emitted if the file changes on disk and @@watchChanges is true. + void fileChanged(); void pathChanged(); QSDOC_HIDE void internalTextChanged(); @@ -320,6 +336,7 @@ signals: void blockWritesChanged(); void atomicWritesChanged(); void printErrorsChanged(); + void watchChangesChanged(); private slots: void operationFinished(); @@ -332,6 +349,9 @@ private: void saveSync(); void updateState(FileViewState& newState); void updatePath(); + void updateWatchedFiles(); + void onWatchedFileChanged(); + void onWatchedDirectoryChanged(); [[nodiscard]] bool shouldBlockRead() const; [[nodiscard]] FileViewReader* liveReader() const; @@ -353,6 +373,8 @@ private: bool mBlockLoading = false; bool mBlockAllReads = false; + QFileSystemWatcher* watcher = nullptr; + GuardedEmitter<&FileView::internalTextChanged> textChangedEmitter; GuardedEmitter<&FileView::internalDataChanged> dataChangedEmitter; void emitDataChanged(); @@ -374,8 +396,11 @@ public: 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); + Q_OBJECT_BINDABLE_PROPERTY(FileView, bool, bWatchChanges, &FileView::watchChangesChanged); // clang-format on + QS_BINDING_SUBSCRIBE_METHOD(FileView, bWatchChanges, updateWatchedFiles, onValueChanged); + void setPreload(bool preload); void setBlockLoading(bool blockLoading); void setBlockAllReads(bool blockAllReads);