summaryrefslogtreecommitdiff
path: root/plugin/src/Caelestia/Models/filesystemmodel.cpp
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-13 14:38:44 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-13 14:38:44 +1000
commit306cfc06ed38a2f86616c1f2fe64de45321f21a6 (patch)
treea27c79d9c4d01c2dadeeae74c844875ab7ab4eed /plugin/src/Caelestia/Models/filesystemmodel.cpp
parentpopouts/tray: better interaction (diff)
downloadcaelestia-shell-306cfc06ed38a2f86616c1f2fe64de45321f21a6.tar.gz
caelestia-shell-306cfc06ed38a2f86616c1f2fe64de45321f21a6.tar.bz2
caelestia-shell-306cfc06ed38a2f86616c1f2fe64de45321f21a6.zip
plugin: refactor into modules
Diffstat (limited to 'plugin/src/Caelestia/Models/filesystemmodel.cpp')
-rw-r--r--plugin/src/Caelestia/Models/filesystemmodel.cpp430
1 files changed, 430 insertions, 0 deletions
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