From 306cfc06ed38a2f86616c1f2fe64de45321f21a6 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:38:44 +1000 Subject: plugin: refactor into modules --- plugin/src/Caelestia/Models/filesystemmodel.cpp | 430 ++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 plugin/src/Caelestia/Models/filesystemmodel.cpp (limited to 'plugin/src/Caelestia/Models/filesystemmodel.cpp') 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 +#include +#include + +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(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 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 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(this); + connect(watcher, &QFutureWatcher::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, QSet>>& promise) { + const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; + + std::optional 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 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 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, QSet>>(this); + + connect(watcher, &QFutureWatcher, QSet>>::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& removedPaths, const QSet& addedPaths) { + QList 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 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 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 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(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(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(m_entries.indexOf(entry)); + } + if (!batchItems.isEmpty()) { + beginInsertRows(QModelIndex(), insertStart, static_cast(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 -- cgit v1.2.3-freya