Skip to content

Commit

Permalink
Project: Moved the directory scanning to its own thread
Browse files Browse the repository at this point in the history
This way it does not block the UI, which is needed since the scanning
can take a bit for large directories.
  • Loading branch information
bjorn committed Dec 4, 2019
1 parent dcf378b commit e7ab8b3
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 53 deletions.
2 changes: 2 additions & 0 deletions src/tiled/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,8 @@ bool MainWindow::openFile(const QString &fileName, FileFormat *fileFormat)

void MainWindow::openLastFiles()
{
mProjectDock->openLastProject();

mSettings.beginGroup(QLatin1String("recentFiles"));

QStringList lastOpenFiles = mSettings.value(
Expand Down
20 changes: 12 additions & 8 deletions src/tiled/projectdock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,6 @@ ProjectDock::ProjectDock(QWidget *parent)
layout->setMargin(0);
layout->setSpacing(0);

// Reopen last used project
const auto prefs = Preferences::instance();
const auto settings = prefs->settings();
const auto lastProjectFileName = settings->value(QLatin1String(LAST_PROJECT_KEY)).toString();
if (prefs->openLastFilesOnStartup() && !lastProjectFileName.isEmpty())
openProjectFile(lastProjectFileName);

layout->addWidget(mProjectView);

setWidget(widget);
Expand All @@ -98,6 +91,17 @@ ProjectDock::ProjectDock(QWidget *parent)
});
}

void ProjectDock::openLastProject()
{
mProjectView->model()->updateNameFilters();

const auto prefs = Preferences::instance();
const auto settings = prefs->settings();
const auto lastProjectFileName = settings->value(QLatin1String(LAST_PROJECT_KEY)).toString();
if (prefs->openLastFilesOnStartup() && !lastProjectFileName.isEmpty())
openProjectFile(lastProjectFileName);
}

void ProjectDock::openProject()
{
const QString projectFilesFilter = tr("Tiled Projects (*.tiled-project)");
Expand Down Expand Up @@ -229,7 +233,7 @@ ProjectView::ProjectView(QWidget *parent)
setDragEnabled(true);
setDefaultDropAction(Qt::MoveAction);

setModel(new ProjectModel(Project()));
setModel(new ProjectModel(this));

connect(this, &QAbstractItemView::activated,
this, &ProjectView::onActivated);
Expand Down
1 change: 1 addition & 0 deletions src/tiled/projectdock.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ProjectDock final : public QDockWidget

QString projectFileName() const;

void openLastProject();
void openProject();
void openProjectFile(const QString &fileName);
void saveProjectAs();
Expand Down
172 changes: 130 additions & 42 deletions src/tiled/projectmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,48 +33,69 @@

namespace Tiled {

class FolderScanner
class FolderScanner : public QObject
{
Q_OBJECT

public:
FolderScanner(const QString &folder, const QStringList &nameFilters)
: mFolder(folder)
, mNameFilters(nameFilters)
{}
void setNameFilters(const QStringList &nameFilters);
void scanFolder(const QString &folder);

std::unique_ptr<FolderEntry> scan();
signals:
void scanFinished(FolderEntry *entry);

private:
void scan(FolderEntry &folder);

const QString mFolder;
const QStringList mNameFilters;
void scan(FolderEntry &folder, QSet<QString> &visitedFolders) const;

QSet<QString> mVisitedFolders;
QStringList mNameFilters;
};


ProjectModel::ProjectModel(Project project, QObject *parent)
ProjectModel::ProjectModel(QObject *parent)
: QAbstractItemModel(parent)
, mProject(std::move(project))
, mScanner(new FolderScanner)
{
mScanner->moveToThread(&mScanningThread);
connect(&mScanningThread, &QThread::finished, mScanner, &QObject::deleteLater);

mFileIconProvider.setOptions(QFileIconProvider::DontUseCustomDirectoryIcons);

updateNameFilters();
mUpdateNameFiltersTimer.setInterval(100);
mUpdateNameFiltersTimer.setSingleShot(true);
connect(&mUpdateNameFiltersTimer, &QTimer::timeout,
this, &ProjectModel::updateNameFilters);
connect(&mUpdateNameFiltersTimer, &QTimer::timeout, this, &ProjectModel::updateNameFilters);

connect(PluginManager::instance(), &PluginManager::objectAdded,
this, &ProjectModel::pluginObjectAddedOrRemoved);
connect(PluginManager::instance(), &PluginManager::objectRemoved,
this, &ProjectModel::pluginObjectAddedOrRemoved);

connect(this, &ProjectModel::nameFiltersChanged, mScanner, &FolderScanner::setNameFilters);
connect(this, &ProjectModel::scanFolder, mScanner, &FolderScanner::scanFolder);
connect(mScanner, &FolderScanner::scanFinished, this, &ProjectModel::folderScanned);

mScanningThread.start();
}

ProjectModel::~ProjectModel()
{
mFoldersPendingScan.clear();
mScanningThread.requestInterruption();
mScanningThread.quit();
mScanningThread.wait();
}

void ProjectModel::setProject(Project project)
{
beginResetModel();

mProject = std::move(project);
scanFolders();
mFolders.clear();
for (const QString &folder : mProject.folders()) {
mFolders.push_back(std::make_unique<FolderEntry>(folder));
scheduleFolderScan(folder);
}

endResetModel();
}

Expand All @@ -85,7 +106,8 @@ void ProjectModel::addFolder(const QString &folder)
beginInsertRows(QModelIndex(), row, row);

mProject.addFolder(folder);
mFolders.push_back(FolderScanner(folder, mNameFilters).scan());
mFolders.push_back(std::make_unique<FolderEntry>(folder));
scheduleFolderScan(folder);

endInsertRows();
}
Expand All @@ -100,9 +122,8 @@ void ProjectModel::removeFolder(int row)

void ProjectModel::refreshFolders()
{
beginResetModel();
scanFolders();
endResetModel();
for (const auto &folder : mFolders)
scheduleFolderScan(folder->filePath);
}

QString ProjectModel::filePath(const QModelIndex &index) const
Expand All @@ -121,7 +142,7 @@ QModelIndex ProjectModel::index(int row, int column, const QModelIndex &parent)
if (row < int(entry->entries.size()))
return createIndex(row, column, entry->entries.at(row).get());
} else {
if (row < int(mProject.folders().size()))
if (row < int(mFolders.size()))
return createIndex(row, column, mFolders.at(row).get());
}

Expand All @@ -137,9 +158,9 @@ QModelIndex ProjectModel::parent(const QModelIndex &index) const
int ProjectModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid())
return mProject.folders().size();
return mFolders.size();

FolderEntry *entry = static_cast<FolderEntry*>(parent.internalPointer());
FolderEntry *entry = entryForIndex(parent);
return entry->entries.size();
}

Expand All @@ -155,8 +176,14 @@ QVariant ProjectModel::data(const QModelIndex &index, int role) const

FolderEntry *entry = entryForIndex(index);
switch (role) {
case Qt::DisplayRole:
return QFileInfo(entry->filePath).fileName();
case Qt::DisplayRole: {
QString name = QFileInfo(entry->filePath).fileName();
if (!entry->parent && (mScanningFolder == entry->filePath || mFoldersPendingScan.contains(entry->filePath))) {
name.append(QLatin1Char(' '));
name.append(tr("(Refreshing)"));
}
return name;
}
case Qt::DecorationRole:
return mFileIconProvider.icon(QFileInfo(entry->filePath));
case Qt::ToolTipRole:
Expand Down Expand Up @@ -213,7 +240,7 @@ QModelIndex ProjectModel::indexForEntry(FolderEntry *entry) const

const auto &container = entry->parent ? entry->parent->entries : mFolders;
const auto it = std::find_if(container.begin(), container.end(),
[entry] (auto &value) { return value.get() == entry; });
[entry] (const std::unique_ptr<FolderEntry> &value) { return value.get() == entry; });

Q_ASSERT(it != container.end());
return createIndex(std::distance(container.begin(), it), 0, entry);
Expand All @@ -228,6 +255,8 @@ void ProjectModel::pluginObjectAddedOrRemoved(QObject *object)

void ProjectModel::updateNameFilters()
{
mUpdateNameFiltersTimer.stop();

QStringList nameFilters;

const auto fileFormats = PluginManager::objects<FileFormat>();
Expand All @@ -241,33 +270,90 @@ void ProjectModel::updateNameFilters()

if (mNameFilters != nameFilters) {
mNameFilters = nameFilters;
emit nameFiltersChanged(nameFilters);
refreshFolders();
}
}

void ProjectModel::scanFolders()
void ProjectModel::scheduleFolderScan(const QString &folder)
{
// TODO: This process should run in a thread (potentially one job for each folder)
mFolders.clear();
if (mScanningFolder.isEmpty()) {
mScanningFolder = folder;
emit scanFolder(mScanningFolder);
} else if (!mFoldersPendingScan.contains(folder)) {
mFoldersPendingScan.append(folder);
}

for (const QString &folder : mProject.folders())
mFolders.push_back(FolderScanner(folder, mNameFilters).scan());
emit dataChanged(index(0, 0),
index(mFolders.size() - 1, 0), { Qt::DisplayRole });
}

void ProjectModel::folderScanned(FolderEntry *resultPointer)
{
const std::unique_ptr<FolderEntry> result { resultPointer };

Q_ASSERT(!result->parent);

const auto it = std::find_if(mFolders.begin(), mFolders.end(),
[&] (const std::unique_ptr<FolderEntry> &value) { return value->filePath == result->filePath; });

// The folder may have been removed in the meantime
if (it == mFolders.end())
return;

// There appears to be no way to reset a subset of the model, so signal the
// removal of all previous rows and re-adding of the new rows instead.

const std::unique_ptr<FolderEntry> &entry = *it;
const QModelIndex index = indexForEntry(entry.get());

beginRemoveRows(index, 0, entry->entries.size() - 1);
entry->entries.clear();
endRemoveRows();

beginInsertRows(index, 0, result->entries.size() - 1);
entry->entries.swap(result->entries);

// Fix up parent pointers
for (auto &childEntry: entry->entries)
childEntry->parent = entry.get();

endInsertRows();

if (!mFoldersPendingScan.isEmpty()) {
mScanningFolder = mFoldersPendingScan.takeFirst();
emit scanFolder(mScanningFolder);
} else {
mScanningFolder.clear();
}

emit dataChanged(index, index, { Qt::DisplayRole });
}

///////////////////////////////////////////////////////////////////////////////

std::unique_ptr<FolderEntry> FolderScanner::scan()
void FolderScanner::setNameFilters(const QStringList &nameFilters)
{
mNameFilters = nameFilters;
}

void FolderScanner::scanFolder(const QString &folder)
{
auto entry = std::make_unique<FolderEntry>(mFolder);
scan(*entry);
return entry;
QSet<QString> visitedFolders;
auto entry = std::make_unique<FolderEntry>(folder);
scan(*entry, visitedFolders);

emit scanFinished(entry.release());
}

void FolderScanner::scan(FolderEntry &folder)
void FolderScanner::scan(FolderEntry &folder, QSet<QString> &visitedFolders) const
{
const auto list = QDir(folder.filePath).entryInfoList(mNameFilters,
QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot,
QDir::Name | QDir::LocaleAware | QDir::DirsFirst);
if (QThread::currentThread()->isInterruptionRequested())
return;

constexpr QDir::SortFlags sortFlags { QDir::Name | QDir::LocaleAware | QDir::DirsFirst };
constexpr QDir::Filters filters { QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot };
const auto list = QDir(folder.filePath).entryInfoList(mNameFilters, filters, sortFlags);

for (const auto &fileInfo : list) {
auto entry = std::make_unique<FolderEntry>(fileInfo.filePath(), &folder);
Expand All @@ -276,9 +362,9 @@ void FolderScanner::scan(FolderEntry &folder)
const QString canonicalPath = fileInfo.canonicalFilePath();

// prevent potential endless symlink loop
if (!mVisitedFolders.contains(canonicalPath)) {
mVisitedFolders.insert(canonicalPath);
scan(*entry);
if (!visitedFolders.contains(canonicalPath)) {
visitedFolders.insert(canonicalPath);
scan(*entry, visitedFolders);
}

// Leave out empty directories
Expand All @@ -291,3 +377,5 @@ void FolderScanner::scan(FolderEntry &folder)
}

} // namespace Tiled

#include "projectmodel.moc"
Loading

0 comments on commit e7ab8b3

Please sign in to comment.