From 77d7106b3a889079cc8338d8d6474830799e8f3d Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Mon, 15 Apr 2024 15:05:53 +0200 Subject: [PATCH] Update recent projects asynchronously Avoid blocking Qt Creator whenever it gets active or projects change, if some kind of network mapped path is in the list of recent projects. We check for existence of recent projects, and that can take a long time in that case, especially if the target is not connected. We already avoid checking on explicit IDevices, but that doesn't help for other kinds of mounted directories. Maintain an "exists" state for each item and update that asynchronously, informing all interested parties of changes with the recentProjectsChanged signal. The state is saved in the settings, meaning that projects that were not found previously start out hidden, under the assumption that the existence check is fast for them if they become available again. Fixes: QTCREATORBUG-30681 Change-Id: Ic39a88b6b5128c3ae4582a6c66fc16be4b297e56 Reviewed-by: Christian Kandeler Reviewed-by: --- src/libs/utils/algorithm.h | 8 ++ .../projectexplorer/projectexplorer.cpp | 112 +++++++++++++----- src/plugins/projectexplorer/projectexplorer.h | 9 +- .../projectexplorer/projectwelcomepage.cpp | 6 +- .../studiowelcome/studiowelcomeplugin.cpp | 16 +-- 5 files changed, 107 insertions(+), 44 deletions(-) diff --git a/src/libs/utils/algorithm.h b/src/libs/utils/algorithm.h index d1c3f37bff4..849316c9a93 100644 --- a/src/libs/utils/algorithm.h +++ b/src/libs/utils/algorithm.h @@ -987,6 +987,14 @@ C filtered(const C &container, R (S::*predicate)() const) return out; } +template +Q_REQUIRED_RESULT C filtered(const C &container, R S::*predicate) +{ + C out; + std::copy_if(std::begin(container), std::end(container), inserter(out), std::mem_fn(predicate)); + return out; +} + ////////////////// // filteredCast ///////////////// diff --git a/src/plugins/projectexplorer/projectexplorer.cpp b/src/plugins/projectexplorer/projectexplorer.cpp index a409de59cb2..fced3700d0f 100644 --- a/src/plugins/projectexplorer/projectexplorer.cpp +++ b/src/plugins/projectexplorer/projectexplorer.cpp @@ -128,6 +128,7 @@ #include #include +#include #include #include #include @@ -262,6 +263,7 @@ const char PROJECT_OPEN_LOCATIONS_CONTEXT_MENU[] = "Project.P.OpenLocation.CtxM const char RECENTPROJECTS_FILE_NAMES_KEY[] = "ProjectExplorer/RecentProjects/FileNames"; const char RECENTPROJECTS_DISPLAY_NAMES_KEY[] = "ProjectExplorer/RecentProjects/DisplayNames"; +const char RECENTPROJECTS_EXISTENCE_KEY[] = "ProjectExplorer/RecentProjects/Existence"; const char CUSTOM_PARSER_COUNT_KEY[] = "ProjectExplorer/Settings/CustomParserCount"; const char CUSTOM_PARSER_PREFIX_KEY[] = "ProjectExplorer/Settings/CustomParser"; @@ -503,6 +505,7 @@ public: void setStartupProject(Project *project); bool closeAllFilesInProject(const Project *project); + void checkRecentProjectsAsync(); void updateRecentProjectMenu(); void clearRecentProjects(); void openRecentProject(const FilePath &filePath); @@ -613,6 +616,8 @@ public: QHash> m_projectCreators; RecentProjectsEntries m_recentProjects; // pair of filename, displayname + QFuture m_recentProjectsFuture; + QThreadPool m_recentProjectsPool; static const int m_maxRecentProjects = 25; FilePath m_lastOpenDirectory; @@ -772,6 +777,24 @@ ProjectExplorerPlugin *ProjectExplorerPlugin::instance() return m_instance; } +static void restoreRecentProjects(QtcSettings *s) +{ + const QStringList filePaths = s->value(Constants::RECENTPROJECTS_FILE_NAMES_KEY).toStringList(); + const QStringList displayNames + = s->value(Constants::RECENTPROJECTS_DISPLAY_NAMES_KEY).toStringList(); + // filename -> bool: + const QHash existence + = s->value(Constants::RECENTPROJECTS_EXISTENCE_KEY).toHash(); + if (QTC_GUARD(filePaths.size() == displayNames.size())) { + for (int i = 0; i < filePaths.size(); ++i) { + const bool exists = existence.value(filePaths.at(i), true).toBool(); + dd->m_recentProjects.append( + {FilePath::fromUserInput(filePaths.at(i)), displayNames.at(i), exists}); + } + } + dd->checkRecentProjectsAsync(); +} + bool ProjectExplorerPlugin::initialize(const QStringList &arguments, QString *error) { Q_UNUSED(error) @@ -1139,8 +1162,11 @@ bool ProjectExplorerPlugin::initialize(const QStringList &arguments, QString *er mrecent->menu()->setTitle(Tr::tr("Recent P&rojects")); mrecent->setOnAllDisabledBehavior(ActionContainer::Show); mfile->addMenu(mrecent, Core::Constants::G_FILE_OPEN); - connect(mfile->menu(), &QMenu::aboutToShow, - dd, &ProjectExplorerPluginPrivate::updateRecentProjectMenu); + connect( + m_instance, + &ProjectExplorerPlugin::recentProjectsChanged, + dd, + &ProjectExplorerPluginPrivate::updateRecentProjectMenu); // unload action dd->m_unloadAction = new Action(Tr::tr("Close Project"), Tr::tr("Close Pro&ject \"%1\""), @@ -1603,18 +1629,12 @@ bool ProjectExplorerPlugin::initialize(const QStringList &arguments, QString *er dd, &ProjectExplorerPluginPrivate::savePersistentSettings); connect(qApp, &QApplication::applicationStateChanged, this, [](Qt::ApplicationState state) { if (!PluginManager::isShuttingDown() && state == Qt::ApplicationActive) - dd->updateWelcomePage(); + dd->checkRecentProjectsAsync(); }); QtcSettings *s = ICore::settings(); - const QStringList fileNames = s->value(Constants::RECENTPROJECTS_FILE_NAMES_KEY).toStringList(); - const QStringList displayNames = s->value(Constants::RECENTPROJECTS_DISPLAY_NAMES_KEY) - .toStringList(); - if (fileNames.size() == displayNames.size()) { - for (int i = 0; i < fileNames.size(); ++i) { - dd->m_recentProjects.append({FilePath::fromUserInput(fileNames.at(i)), displayNames.at(i)}); - } - } + + restoreRecentProjects(s); const int customParserCount = s->value(Constants::CUSTOM_PARSER_COUNT_KEY).toInt(); for (int i = 0; i < customParserCount; ++i) { @@ -2115,6 +2135,35 @@ bool ProjectExplorerPluginPrivate::closeAllFilesInProject(const Project *project return EditorManager::closeDocuments(openFiles); } +void ProjectExplorerPluginPrivate::checkRecentProjectsAsync() +{ + m_recentProjectsFuture.cancel(); + + m_recentProjectsFuture + = QtConcurrent::mapped(&m_recentProjectsPool, m_recentProjects, [](RecentProjectsEntry p) { + // check if project is available, but avoid querying devices + p.exists = p.filePath.needsDevice() || p.filePath.isFile(); + return p; + }); + PluginManager::futureSynchronizer()->addFuture(m_recentProjectsFuture); + + onResultReady(m_recentProjectsFuture, this, [this](const RecentProjectsEntry &p) { + auto it = std::find_if( + m_recentProjects.begin(), m_recentProjects.end(), [&p](const RecentProjectsEntry &e) { + return p.filePath == e.filePath; + }); + // nothing to do if it no longer is in the recent projects, or if the state already was + // correct + if (it == m_recentProjects.end()) + return; + if (it->exists == p.exists) + return; + + *it = p; + emit m_instance->recentProjectsChanged(); + }); +} + void ProjectExplorerPluginPrivate::savePersistentSettings() { if (PluginManager::isShuttingDown()) @@ -2128,17 +2177,21 @@ void ProjectExplorerPluginPrivate::savePersistentSettings() QtcSettings *s = ICore::settings(); s->remove("ProjectExplorer/RecentProjects/Files"); - QStringList fileNames; + QStringList filePaths; QStringList displayNames; + QHash existence; RecentProjectsEntries::const_iterator it, end; end = dd->m_recentProjects.constEnd(); for (it = dd->m_recentProjects.constBegin(); it != end; ++it) { - fileNames << (*it).first.toUserOutput(); - displayNames << (*it).second; + const QString filePath = it->filePath.toUserOutput(); + filePaths << filePath; + displayNames << it->displayName; + existence.insert(filePath, it->exists); } - s->setValueWithDefault(Constants::RECENTPROJECTS_FILE_NAMES_KEY, fileNames); + s->setValueWithDefault(Constants::RECENTPROJECTS_FILE_NAMES_KEY, filePaths); s->setValueWithDefault(Constants::RECENTPROJECTS_DISPLAY_NAMES_KEY, displayNames); + s->setValueWithDefault(Constants::RECENTPROJECTS_EXISTENCE_KEY, existence); buildPropertiesSettings().writeSettings(); // FIXME: Should not be needed. @@ -2451,10 +2504,7 @@ void ProjectExplorerPluginPrivate::buildQueueFinished(bool success) RecentProjectsEntries ProjectExplorerPluginPrivate::recentProjects() const { - return Utils::filtered(dd->m_recentProjects, [](const RecentProjectsEntry &p) { - // check if project is available, but avoid querying devices - return p.first.needsDevice() || p.first.isFile(); - }); + return filtered(m_recentProjects, &RecentProjectsEntry::exists); } void ProjectExplorerPluginPrivate::updateActions() @@ -3018,16 +3068,13 @@ void ProjectExplorerPluginPrivate::addToRecentProjects(const FilePath &filePath, if (filePath.isEmpty()) return; - RecentProjectsEntries::iterator it; - for (it = m_recentProjects.begin(); it != m_recentProjects.end();) - if ((*it).first == filePath) - it = m_recentProjects.erase(it); - else - ++it; - - if (m_recentProjects.count() > m_maxRecentProjects) + Utils::erase(m_recentProjects, [filePath](const RecentProjectsEntry &e) { + return e.filePath == filePath; + }); + if (m_recentProjects.size() >= m_maxRecentProjects) m_recentProjects.removeLast(); - m_recentProjects.push_front({filePath, displayName}); + m_recentProjects.push_front({filePath, displayName, true}); + checkRecentProjectsAsync(); m_lastOpenDirectory = filePath.absolutePath(); emit m_instance->recentProjectsChanged(); } @@ -3054,7 +3101,7 @@ void ProjectExplorerPluginPrivate::updateRecentProjectMenu() const RecentProjectsEntries projects = recentProjects(); //projects (ignore sessions, they used to be in this list) for (const RecentProjectsEntry &item : projects) { - const FilePath &filePath = item.first; + const FilePath &filePath = item.filePath; if (filePath.endsWith(QLatin1String(".qws"))) continue; @@ -3079,13 +3126,12 @@ void ProjectExplorerPluginPrivate::updateRecentProjectMenu() connect(action, &QAction::triggered, this, &ProjectExplorerPluginPrivate::clearRecentProjects); } - emit m_instance->recentProjectsChanged(); } void ProjectExplorerPluginPrivate::clearRecentProjects() { m_recentProjects.clear(); - updateWelcomePage(); + emit m_instance->recentProjectsChanged(); } void ProjectExplorerPluginPrivate::openRecentProject(const FilePath &filePath) @@ -3101,8 +3147,10 @@ void ProjectExplorerPluginPrivate::removeFromRecentProjects(const FilePath &file { QTC_ASSERT(!filePath.isEmpty(), return); QTC_CHECK(Utils::eraseOne(m_recentProjects, [filePath](const RecentProjectsEntry &entry) { - return entry.first == filePath; + return entry.filePath == filePath; })); + checkRecentProjectsAsync(); + emit m_instance->recentProjectsChanged(); } void ProjectExplorerPluginPrivate::invalidateProject(Project *project) diff --git a/src/plugins/projectexplorer/projectexplorer.h b/src/plugins/projectexplorer/projectexplorer.h index 291734901a2..8f17af13df9 100644 --- a/src/plugins/projectexplorer/projectexplorer.h +++ b/src/plugins/projectexplorer/projectexplorer.h @@ -37,7 +37,14 @@ class AppOutputSettings; class MiniProjectTargetSelector; } -using RecentProjectsEntry = QPair; +class RecentProjectsEntry +{ +public: + Utils::FilePath filePath; + QString displayName; + bool exists = true; +}; + using RecentProjectsEntries = QList; class PROJECTEXPLORER_EXPORT OpenProjectResult diff --git a/src/plugins/projectexplorer/projectwelcomepage.cpp b/src/plugins/projectexplorer/projectwelcomepage.cpp index 29742256443..9364c6106f3 100644 --- a/src/plugins/projectexplorer/projectwelcomepage.cpp +++ b/src/plugins/projectexplorer/projectwelcomepage.cpp @@ -106,12 +106,12 @@ QVariant ProjectModel::data(const QModelIndex &index, int role) const RecentProjectsEntry data = m_projects.at(index.row()); switch (role) { case Qt::DisplayRole: - return data.second; + return data.displayName; case Qt::ToolTipRole: case FilePathRole: - return data.first.toVariant(); + return data.filePath.toVariant(); case PrettyFilePathRole: - return data.first.withTildeHomePath(); // FIXME: FilePath::displayName() ? + return data.filePath.withTildeHomePath(); // FIXME: FilePath::displayName() ? case ShortcutRole: { const Id projectBase = PROJECT_BASE_ID; if (Command *cmd = ActionManager::command(projectBase.withSuffix(index.row() + 1))) diff --git a/src/plugins/studiowelcome/studiowelcomeplugin.cpp b/src/plugins/studiowelcome/studiowelcomeplugin.cpp index ac6a17de874..9e93384cfae 100644 --- a/src/plugins/studiowelcome/studiowelcomeplugin.cpp +++ b/src/plugins/studiowelcome/studiowelcomeplugin.cpp @@ -455,20 +455,20 @@ QVariant ProjectModel::data(const QModelIndex &index, int role) const ProjectExplorer::ProjectExplorerPlugin::recentProjects().at(index.row()); switch (role) { case Qt::DisplayRole: - return data.second; + return data.displayName; break; case FilePathRole: - return data.first.toVariant(); + return data.filePath.toVariant(); case PrettyFilePathRole: - return data.first.absolutePath().withTildeHomePath(); + return data.filePath.absolutePath().withTildeHomePath(); case PreviewUrl: - return QVariant(QStringLiteral("image://project_preview/") + - QmlProjectManager::ProjectFileContentTools::appQmlFile( - data.first)); + return QVariant( + QStringLiteral("image://project_preview/") + + QmlProjectManager::ProjectFileContentTools::appQmlFile(data.filePath)); case TagData: - return tags(data.first); + return tags(data.filePath); case Description: - return description(data.first); + return description(data.filePath); default: return QVariant(); }