#include "filesystemmodel.hpp" #include #include #include #include #include #include #include #include #include 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; }