summaryrefslogtreecommitdiff
path: root/plugin/src/Caelestia/Models
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/src/Caelestia/Models')
-rw-r--r--plugin/src/Caelestia/Models/CMakeLists.txt8
-rw-r--r--plugin/src/Caelestia/Models/filesystemmodel.cpp430
-rw-r--r--plugin/src/Caelestia/Models/filesystemmodel.hpp131
3 files changed, 569 insertions, 0 deletions
diff --git a/plugin/src/Caelestia/Models/CMakeLists.txt b/plugin/src/Caelestia/Models/CMakeLists.txt
new file mode 100644
index 0000000..640e29e
--- /dev/null
+++ b/plugin/src/Caelestia/Models/CMakeLists.txt
@@ -0,0 +1,8 @@
+qml_module(caelestia-models
+ URI Caelestia.Models
+ SOURCES
+ filesystemmodel.hpp filesystemmodel.cpp
+ LIBRARIES
+ Qt::Gui
+ Qt::Concurrent
+)
diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp
new file mode 100644
index 0000000..54807b5
--- /dev/null
+++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp
@@ -0,0 +1,430 @@
+#include "filesystemmodel.hpp"
+
+#include <qdiriterator.h>
+#include <qfuturewatcher.h>
+#include <qtconcurrentrun.h>
+
+namespace caelestia {
+
+FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)
+ : QObject(parent)
+ , m_fileInfo(QFileInfo(path))
+ , m_path(path)
+ , m_relativePath(relativePath)
+ , m_isImageInitialised(false)
+ , m_mimeTypeInitialised(false) {}
+
+QString FileSystemEntry::path() const {
+ return m_path;
+};
+
+QString FileSystemEntry::relativePath() const {
+ return m_relativePath;
+};
+
+QString FileSystemEntry::name() const {
+ return m_fileInfo.fileName();
+};
+
+QString FileSystemEntry::parentDir() const {
+ return m_fileInfo.absolutePath();
+};
+
+QString FileSystemEntry::suffix() const {
+ return m_fileInfo.completeSuffix();
+};
+
+qint64 FileSystemEntry::size() const {
+ return m_fileInfo.size();
+};
+
+bool FileSystemEntry::isDir() const {
+ return m_fileInfo.isDir();
+};
+
+bool FileSystemEntry::isImage() const {
+ if (!m_isImageInitialised) {
+ QImageReader reader(m_path);
+ m_isImage = reader.canRead();
+ m_isImageInitialised = true;
+ }
+ return m_isImage;
+}
+
+QString FileSystemEntry::mimeType() const {
+ if (!m_mimeTypeInitialised) {
+ const QMimeDatabase db;
+ m_mimeType = db.mimeTypeForFile(m_path).name();
+ m_mimeTypeInitialised = true;
+ }
+ return m_mimeType;
+}
+
+FileSystemModel::FileSystemModel(QObject* parent)
+ : QAbstractListModel(parent)
+ , m_recursive(false)
+ , m_watchChanges(true)
+ , m_showHidden(false)
+ , m_filter(NoFilter) {
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);
+}
+
+int FileSystemModel::rowCount(const QModelIndex& parent) const {
+ if (parent != QModelIndex()) {
+ return 0;
+ }
+ return static_cast<int>(m_entries.size());
+}
+
+QVariant FileSystemModel::data(const QModelIndex& index, int role) const {
+ if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {
+ return QVariant();
+ }
+ return QVariant::fromValue(m_entries.at(index.row()));
+}
+
+QHash<int, QByteArray> FileSystemModel::roleNames() const {
+ return { { Qt::UserRole, "modelData" } };
+}
+
+QString FileSystemModel::path() const {
+ return m_path;
+}
+
+void FileSystemModel::setPath(const QString& path) {
+ if (m_path == path) {
+ return;
+ }
+
+ m_path = path;
+ emit pathChanged();
+
+ m_dir.setPath(m_path);
+ update();
+}
+
+bool FileSystemModel::recursive() const {
+ return m_recursive;
+}
+
+void FileSystemModel::setRecursive(bool recursive) {
+ if (m_recursive == recursive) {
+ return;
+ }
+
+ m_recursive = recursive;
+ emit recursiveChanged();
+
+ update();
+}
+
+bool FileSystemModel::watchChanges() const {
+ return m_watchChanges;
+}
+
+void FileSystemModel::setWatchChanges(bool watchChanges) {
+ if (m_watchChanges == watchChanges) {
+ return;
+ }
+
+ m_watchChanges = watchChanges;
+ emit watchChangesChanged();
+
+ update();
+}
+
+bool FileSystemModel::showHidden() const {
+ return m_showHidden;
+}
+
+void FileSystemModel::setShowHidden(bool showHidden) {
+ if (m_showHidden == showHidden) {
+ return;
+ }
+
+ m_showHidden = showHidden;
+ emit showHiddenChanged();
+
+ update();
+}
+
+FileSystemModel::Filter FileSystemModel::filter() const {
+ return m_filter;
+}
+
+void FileSystemModel::setFilter(Filter filter) {
+ if (m_filter == filter) {
+ return;
+ }
+
+ m_filter = filter;
+ emit filterChanged();
+
+ update();
+}
+
+QList<FileSystemEntry*> FileSystemModel::entries() const {
+ return m_entries;
+}
+
+void FileSystemModel::watchDirIfRecursive(const QString& path) {
+ if (m_recursive && m_watchChanges) {
+ const auto currentDir = m_dir;
+ const bool showHidden = m_showHidden;
+ const auto future = QtConcurrent::run([showHidden, path]() {
+ QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
+ if (showHidden) {
+ filters |= QDir::Hidden;
+ }
+
+ QDirIterator iter(path, filters, QDirIterator::Subdirectories);
+ QStringList dirs;
+ while (iter.hasNext()) {
+ dirs << iter.next();
+ }
+ return dirs;
+ });
+ const auto watcher = new QFutureWatcher<QStringList>(this);
+ connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() {
+ const auto paths = watcher->result();
+ if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
+ // Ignore if dir or showHidden has changed
+ m_watcher.addPaths(paths);
+ }
+ watcher->deleteLater();
+ });
+ watcher->setFuture(future);
+ }
+}
+
+void FileSystemModel::update() {
+ updateWatcher();
+ updateEntries();
+}
+
+void FileSystemModel::updateWatcher() {
+ if (!m_watcher.directories().isEmpty()) {
+ m_watcher.removePaths(m_watcher.directories());
+ }
+
+ if (!m_watchChanges || m_path.isEmpty()) {
+ return;
+ }
+
+ m_watcher.addPath(m_path);
+ watchDirIfRecursive(m_path);
+}
+
+void FileSystemModel::updateEntries() {
+ if (m_path.isEmpty()) {
+ if (!m_entries.isEmpty()) {
+ beginResetModel();
+ qDeleteAll(m_entries);
+ m_entries.clear();
+ emit entriesChanged();
+ endResetModel();
+ }
+
+ return;
+ }
+
+ for (auto& future : m_futures) {
+ future.cancel();
+ }
+ m_futures.clear();
+
+ updateEntriesForDir(m_path);
+}
+
+void FileSystemModel::updateEntriesForDir(const QString& dir) {
+ const bool recursive = m_recursive;
+ const bool showHidden = m_showHidden;
+ const auto filter = m_filter;
+ const auto oldEntries = m_entries;
+ const auto baseDir = m_dir;
+
+ const auto future = QtConcurrent::run([dir, recursive, showHidden, filter, oldEntries, baseDir](
+ QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
+ const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
+
+ std::optional<QDirIterator> iter;
+
+ if (filter == Images) {
+ QStringList nameFilters;
+ for (const auto& format : QImageReader::supportedImageFormats()) {
+ nameFilters << "*." + format;
+ }
+
+ QDir::Filters filters = QDir::Files;
+ if (showHidden) {
+ filters |= QDir::Hidden;
+ }
+
+ iter.emplace(dir, nameFilters, filters, flags);
+ } else {
+ QDir::Filters filters;
+
+ if (filter == Files) {
+ filters = QDir::Files;
+ } else if (filter == Dirs) {
+ filters = QDir::Dirs | QDir::NoDotAndDotDot;
+ } else {
+ filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
+ }
+
+ if (showHidden) {
+ filters |= QDir::Hidden;
+ }
+
+ iter.emplace(dir, filters, flags);
+ }
+
+ QSet<QString> newPaths;
+ while (iter->hasNext()) {
+ if (promise.isCanceled()) {
+ return;
+ }
+
+ QString path = iter->next();
+
+ if (filter == Images) {
+ QImageReader reader(path);
+ if (!reader.canRead()) {
+ continue;
+ }
+ }
+
+ newPaths.insert(path);
+ }
+
+ QSet<QString> oldPaths;
+ for (const auto& entry : oldEntries) {
+ oldPaths.insert(entry->path());
+ }
+
+ if (promise.isCanceled() || newPaths == oldPaths) {
+ return;
+ }
+
+ promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));
+ });
+
+ if (m_futures.contains(dir)) {
+ m_futures[dir].cancel();
+ }
+ m_futures.insert(dir, future);
+
+ const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this);
+
+ connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() {
+ m_futures.remove(dir);
+
+ if (!watcher->future().isResultReadyAt(0)) {
+ watcher->deleteLater();
+ return;
+ }
+
+ const auto result = watcher->result();
+ applyChanges(result.first, result.second);
+
+ watcher->deleteLater();
+ });
+
+ watcher->setFuture(future);
+}
+
+void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
+ QList<int> removedIndices;
+ for (int i = 0; i < m_entries.size(); ++i) {
+ if (removedPaths.contains(m_entries[i]->path())) {
+ removedIndices << i;
+ }
+ }
+ std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
+
+ int start = -1;
+ int end = -1;
+ for (int idx : removedIndices) {
+ if (start == -1) {
+ start = idx;
+ end = idx;
+ } else if (idx == end - 1) {
+ end = idx;
+ } else {
+ beginRemoveRows(QModelIndex(), end, start);
+ for (int i = start; i >= end; --i) {
+ emit removed(m_entries[i]->path());
+ delete m_entries.takeAt(i);
+ }
+ endRemoveRows();
+
+ start = idx;
+ end = idx;
+ }
+ }
+ if (start != -1) {
+ beginRemoveRows(QModelIndex(), end, start);
+ for (int i = start; i >= end; --i) {
+ emit removed(m_entries[i]->path());
+ delete m_entries.takeAt(i);
+ }
+ endRemoveRows();
+ }
+
+ QList<FileSystemEntry*> newEntries;
+ for (const auto& path : addedPaths) {
+ newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
+ }
+ std::sort(newEntries.begin(), newEntries.end(), &FileSystemModel::compareEntries);
+
+ int insertStart = -1;
+ int prevRow = -1;
+ QList<FileSystemEntry*> batchItems;
+ for (const auto& entry : newEntries) {
+ const auto it = std::lower_bound(m_entries.begin(), m_entries.end(), entry, &FileSystemModel::compareEntries);
+ int row = static_cast<int>(it - m_entries.begin());
+
+ if (insertStart == -1) {
+ insertStart = row;
+ prevRow = row;
+ batchItems.clear();
+ batchItems << entry;
+ } else if (row == prevRow + 1) {
+ prevRow = row;
+ batchItems << entry;
+ } else {
+ beginInsertRows(QModelIndex(), insertStart, static_cast<int>(insertStart + batchItems.size() - 1));
+ for (int i = 0; i < batchItems.size(); ++i) {
+ m_entries.insert(insertStart + i, batchItems[i]);
+ emit added(batchItems[i]);
+ }
+ endInsertRows();
+
+ insertStart = row;
+ prevRow = row;
+ batchItems.clear();
+ batchItems << entry;
+ }
+ prevRow = static_cast<int>(m_entries.indexOf(entry));
+ }
+ if (!batchItems.isEmpty()) {
+ beginInsertRows(QModelIndex(), insertStart, static_cast<int>(insertStart + batchItems.size() - 1));
+ for (int i = 0; i < batchItems.size(); ++i) {
+ m_entries.insert(insertStart + i, batchItems[i]);
+ emit added(batchItems[i]);
+ }
+ endInsertRows();
+ }
+
+ emit entriesChanged();
+}
+
+bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) {
+ if (a->isDir() != b->isDir()) {
+ return a->isDir();
+ }
+ return a->relativePath().localeAwareCompare(b->relativePath()) < 0;
+}
+
+} // namespace caelestia
diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp
new file mode 100644
index 0000000..4ea5f0a
--- /dev/null
+++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp
@@ -0,0 +1,131 @@
+#pragma once
+
+#include <qabstractitemmodel.h>
+#include <qdir.h>
+#include <qfilesystemwatcher.h>
+#include <qfuture.h>
+#include <qimagereader.h>
+#include <qmimedatabase.h>
+#include <qobject.h>
+#include <qqmlintegration.h>
+
+namespace caelestia {
+
+class FileSystemEntry : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
+
+ Q_PROPERTY(QString path READ path CONSTANT)
+ Q_PROPERTY(QString relativePath READ relativePath CONSTANT)
+ Q_PROPERTY(QString name READ name CONSTANT)
+ Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
+ Q_PROPERTY(QString suffix READ suffix CONSTANT)
+ Q_PROPERTY(qint64 size READ size CONSTANT)
+ Q_PROPERTY(bool isDir READ isDir CONSTANT)
+ Q_PROPERTY(bool isImage READ isImage CONSTANT)
+ Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
+
+public:
+ explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
+
+ [[nodiscard]] QString path() const;
+ [[nodiscard]] QString relativePath() const;
+ [[nodiscard]] QString name() const;
+ [[nodiscard]] QString parentDir() const;
+ [[nodiscard]] QString suffix() const;
+ [[nodiscard]] qint64 size() const;
+ [[nodiscard]] bool isDir() const;
+ [[nodiscard]] bool isImage() const;
+ [[nodiscard]] QString mimeType() const;
+
+private:
+ const QFileInfo m_fileInfo;
+
+ const QString m_path;
+ const QString m_relativePath;
+
+ mutable bool m_isImage;
+ mutable bool m_isImageInitialised;
+
+ mutable QString m_mimeType;
+ mutable bool m_mimeTypeInitialised;
+};
+
+class FileSystemModel : public QAbstractListModel {
+ Q_OBJECT
+ QML_ELEMENT
+
+ Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
+ Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
+ Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
+ Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
+ Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
+
+ Q_PROPERTY(QList<FileSystemEntry*> entries READ entries NOTIFY entriesChanged)
+
+public:
+ enum Filter {
+ NoFilter,
+ Images,
+ Files,
+ Dirs
+ };
+ Q_ENUM(Filter)
+
+ explicit FileSystemModel(QObject* parent = nullptr);
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ [[nodiscard]] QString path() const;
+ void setPath(const QString& path);
+
+ [[nodiscard]] bool recursive() const;
+ void setRecursive(bool recursive);
+
+ [[nodiscard]] bool watchChanges() const;
+ void setWatchChanges(bool watchChanges);
+
+ [[nodiscard]] bool showHidden() const;
+ void setShowHidden(bool showHidden);
+
+ [[nodiscard]] Filter filter() const;
+ void setFilter(Filter filter);
+
+ [[nodiscard]] QList<FileSystemEntry*> entries() const;
+
+signals:
+ void pathChanged();
+ void recursiveChanged();
+ void watchChangesChanged();
+ void showHiddenChanged();
+ void filterChanged();
+ void entriesChanged();
+
+ void added(const FileSystemEntry* entry);
+ void removed(const QString& path);
+
+private:
+ QDir m_dir;
+ QFileSystemWatcher m_watcher;
+ QList<FileSystemEntry*> m_entries;
+ QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures;
+
+ QString m_path;
+ bool m_recursive;
+ bool m_watchChanges;
+ bool m_showHidden;
+ Filter m_filter;
+
+ void watchDirIfRecursive(const QString& path);
+ void update();
+ void updateWatcher();
+ void updateEntries();
+ void updateEntriesForDir(const QString& dir);
+ void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
+ [[nodiscard]] static bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b);
+};
+
+} // namespace caelestia