// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "workspaceproject.h" #include "buildconfiguration.h" #include "buildinfo.h" #include "buildsteplist.h" #include "buildsystem.h" #include "processstep.h" #include "projectexplorer.h" #include "projectexplorerconstants.h" #include "projectexplorertr.h" #include "projectmanager.h" #include "projecttree.h" #include "runconfiguration.h" #include "runconfigurationaspects.h" #include "runcontrol.h" #include "target.h" #include "treescanner.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Utils; using namespace Core; namespace ProjectExplorer { Q_LOGGING_CATEGORY(wsbs, "qtc.projectexplorer.workspace.buildsystem", QtWarningMsg); Q_LOGGING_CATEGORY(wsp, "qtc.projectexplorer.workspace.project", QtWarningMsg); const QLatin1StringView FOLDER_MIMETYPE{"inode/directory"}; const QLatin1StringView WORKSPACE_MIMETYPE{"text/x-workspace-project"}; const char WORKSPACE_PROJECT_ID[] = "ProjectExplorer.WorkspaceProject"; const char WORKSPACE_PROJECT_RUNCONFIG_ID[] = "WorkspaceProject.RunConfiguration:"; const QLatin1StringView PROJECT_NAME_KEY{"project.name"}; const QLatin1StringView FILES_EXCLUDE_KEY{"files.exclude"}; const char EXCLUDE_ACTION_ID[] = "ProjectExplorer.ExcludeFromWorkspace"; const char RESCAN_ACTION_ID[] = "ProjectExplorer.RescanWorkspace"; const Result projectDefinition(const FilePath &path) { if (auto fileContents = path.fileContents()) return QJsonDocument::fromJson(*fileContents).object(); return {}; } static QFlags workspaceDirFilter = QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden; class WorkspaceBuildSystem final : public BuildSystem { public: WorkspaceBuildSystem(BuildConfiguration *bc); ~WorkspaceBuildSystem(); static QString name() { return "Workspace"; } void reparse(bool force); void triggerParsing() final; bool addFiles(Node *context, const FilePaths &filePaths, FilePaths *notAdded) final; RemovedFilesFromProject removeFiles(Node *context, const FilePaths &filePaths, FilePaths *notRemoved) final; bool deleteFiles(Node *context, const FilePaths &filePaths) final; bool renameFiles(Node *context, const FilePairs &filesToRename, FilePaths *notRenamed) final; bool supportsAction(Node *context, ProjectAction action, const Node *node) const final; void handleDirectoryChanged(const FilePath &directory); void scan(const FilePath &path); void scanNext(); private: bool isFiltered(const FilePath &path, QList versionControls) const; QList m_filters; std::unique_ptr m_watcher; ParseGuard m_parseGuard; FilePaths m_scanQueue; TreeScanner m_scanner; }; static FolderNode *findAvailableParent(ProjectNode *root, const FilePath &path) { FolderNode *result = nullptr; FolderNode *child = root; do { result = child; child = result->findChildFolderNode( [&path](FolderNode *fn) { return path.isChildOf(fn->path()) || path == fn->path(); }); } while (child); return result; } WorkspaceBuildSystem::WorkspaceBuildSystem(BuildConfiguration *bc) : BuildSystem(bc) { connect(&m_scanner, &TreeScanner::finished, this, [this] { QTC_ASSERT(!m_scanQueue.isEmpty(), return); const FilePath scannedDir = m_scanQueue.takeFirst(); TreeScanner::Result result = m_scanner.release(); auto addNodes = [this, &result](FolderNode *parent) { QElapsedTimer timer; timer.start(); const QList versionControls = VcsManager::versionControls(); for (auto node : result.takeFirstLevelNodes()) parent->addNode(std::unique_ptr(node)); qCDebug(wsbs) << "Added nodes in" << timer.elapsed() << "ms"; FilePaths toWatch; auto collectWatchFolders = [this, &toWatch, &versionControls](FolderNode *fn) { if (!isFiltered(fn->path(), versionControls)) toWatch << fn->path(); }; collectWatchFolders(parent); parent->forEachNode({}, collectWatchFolders); qCDebug(wsbs) << "Added and collected nodes in" << timer.elapsed() << "ms" << toWatch.size() << "dirs"; m_watcher->addDirectories(toWatch, FileSystemWatcher::WatchAllChanges); qCDebug(wsbs) << "Added and and collected and watched nodes in" << timer.elapsed() << "ms"; }; if (scannedDir == projectDirectory()) { qCDebug(wsbs) << "Finished scanning new root" << scannedDir; auto root = std::make_unique(scannedDir); root->setDisplayName(project()->displayName()); m_watcher.reset(new FileSystemWatcher); connect( m_watcher.get(), &FileSystemWatcher::directoryChanged, this, [this](const FilePath &path) { handleDirectoryChanged(path); }); addNodes(root.get()); setRootProjectNode(std::move(root)); } else { qCDebug(wsbs) << "Finished scanning subdir" << scannedDir; FolderNode *parent = findAvailableParent(project()->rootProjectNode(), scannedDir); const FilePath relativePath = scannedDir.relativeChildPath(parent->filePath()); const QString firstRelativeFolder = relativePath.path().left(relativePath.path().indexOf('/')); const FilePath nodePath = parent->filePath() / firstRelativeFolder; auto newNode = std::make_unique(nodePath); newNode->setDisplayName(firstRelativeFolder); addNodes(newNode.get()); parent->replaceSubtree(nullptr, std::move(newNode)); } scanNext(); }); m_scanner.setDirFilter(workspaceDirFilter); m_scanner.setFilter([&](const MimeType &, const FilePath &filePath) { return Utils::anyOf(m_filters, [filePath](const QRegularExpression &filter) { return filter.match(filePath.path()).hasMatch(); }); }); connect(project(), &Project::projectFileIsDirty, this, &BuildSystem::requestDelayedParse); requestDelayedParse(); } WorkspaceBuildSystem::~WorkspaceBuildSystem() { // Trigger any pending parsingFinished signals before destroying any other build system part: m_parseGuard = {}; } void WorkspaceBuildSystem::reparse(bool force) { const QList oldFilters = m_filters; m_filters.clear(); FilePath projectPath = project()->projectDirectory(); const QJsonObject json = projectDefinition(project()->projectFilePath()).value_or(QJsonObject()); const QJsonValue projectNameValue = json.value(PROJECT_NAME_KEY); if (projectNameValue.isString()) project()->setDisplayName(projectNameValue.toString()); const QJsonArray excludesJson = json.value(FILES_EXCLUDE_KEY).toArray(); for (const QJsonValue &excludeJson : excludesJson) { if (excludeJson.isString()) { FilePath absolute = projectPath.pathAppended(excludeJson.toString()); m_filters << QRegularExpression( Utils::wildcardToRegularExpression(absolute.path()), QRegularExpression::CaseInsensitiveOption); } } QList targetInfos; const QJsonArray targets = json.value("targets").toArray(); int i = 0; for (const QJsonValue &target : targets) { i++; QTC_ASSERT(target.isObject(), continue); const QJsonObject targetObject = target.toObject(); QJsonArray args = targetObject["arguments"].toArray(); QStringList arguments = Utils::transform(args, [](const QJsonValue &arg) { return arg.toString(); }); FilePath workingDirectory = FilePath::fromUserInput( targetObject["workingDirectory"].toString()); const QString name = targetObject["name"].toString(); const FilePath executable = FilePath::fromUserInput( targetObject["executable"].toString()); if (name.isEmpty() || executable.isEmpty()) continue; BuildTargetInfo bti; bti.buildKey = name + QString::number(i); bti.displayName = name; bti.displayNameUniquifier = QString(" (%1)").arg(i); bti.targetFilePath = executable; bti.projectFilePath = projectPath; bti.workingDirectory = workingDirectory; bti.additionalData = QVariantMap{{"arguments", arguments}}; targetInfos << bti; } setApplicationTargets(targetInfos); if (force || oldFilters != m_filters) scan(project()->projectDirectory()); else emitBuildSystemUpdated(); } void WorkspaceBuildSystem::triggerParsing() { reparse(false); } bool WorkspaceBuildSystem::addFiles(Node *, const FilePaths &, FilePaths *) { // nothing to do here since the changes will be picked up by the file system watcher return true; } RemovedFilesFromProject WorkspaceBuildSystem::removeFiles(Node *, const FilePaths &, FilePaths *) { // nothing to do here since the changes will be picked up by the file system watcher return RemovedFilesFromProject::Ok; } bool WorkspaceBuildSystem::deleteFiles(Node *, const FilePaths &) { // nothing to do here since the changes will be picked up by the file system watcher return true; } bool WorkspaceBuildSystem::renameFiles(Node *, const FilePairs &, FilePaths *) { // nothing to do here since the changes will be picked up by the file system watcher return true; } bool WorkspaceBuildSystem::supportsAction(Node *, ProjectAction action, const Node *) const { return action == ProjectAction::Rename || action == ProjectAction::AddNewFile || action == ProjectAction::EraseFile; } void WorkspaceBuildSystem::handleDirectoryChanged(const FilePath &directory) { ProjectNode *root = project()->rootProjectNode(); QTC_ASSERT(root, return); auto node = root->findNode( [&directory](Node *node) { if (node->asFolderNode()) { qCDebug(wsbs) << "comparing" << node->filePath() << directory; return node->filePath() == directory; } return false; }); if (!directory.exists()) { m_watcher->removeDirectory(directory); if (node) node->parentFolderNode()->replaceSubtree(node, nullptr); } else if (node) { FolderNode *fn = node->asFolderNode(); QTC_ASSERT(fn, return); const FilePaths entries = directory.dirEntries(workspaceDirFilter); const QList &versionControls = VcsManager::versionControls(); for (auto entry : entries) { if (isFiltered(entry, versionControls)) continue; if (entry.isDir()) { if (!fn->folderNode(entry)) scan(entry); } else if (!fn->fileNode(entry)) { fn->replaceSubtree( nullptr, std::make_unique(entry, Node::fileTypeForFileName(entry))); } } QList toRemove; auto filter = [&entries, &toRemove](Node *n) { if (!entries.contains(n->filePath())) toRemove << n; }; fn->forEachFileNode(filter); fn->forEachFolderNode(filter); for (auto n : std::as_const(toRemove)) fn->replaceSubtree(n, nullptr); } else { scan(directory); } } void WorkspaceBuildSystem::scan(const FilePath &path) { m_scanQueue << path; scanNext(); } void WorkspaceBuildSystem::scanNext() { if (m_scanQueue.isEmpty()) { qCDebug(wsbs) << "Scan done."; m_parseGuard.markAsSuccess(); m_parseGuard = {}; emitBuildSystemUpdated(); } else { if (!m_parseGuard.guardsProject()) m_parseGuard = guardParsingRun(); if (m_scanner.isFinished()) { const FilePath next = m_scanQueue.first(); qCDebug(wsbs) << "Start scanning" << next; m_scanner.asyncScanForFiles(next); } } } bool WorkspaceBuildSystem::isFiltered(const FilePath &path, QList versionControls) const { const bool explicitlyExcluded = Utils::anyOf( m_filters, [path](const QRegularExpression &filter) { return filter.match(path.path()).hasMatch(); }); if (explicitlyExcluded) return true; return Utils::anyOf(versionControls, [path](const IVersionControl *vc) { return vc->isVcsFileOrDirectory(path); }); } class WorkspaceRunConfiguration : public RunConfiguration { public: WorkspaceRunConfiguration(BuildConfiguration *bc, Id id) : RunConfiguration(bc, id) { environment.setSupportForBuildEnvironment(bc); hint.setText(Tr::tr("Clone the configuration to change it. Or, make the changes in " "the .qtcreator/project.json file.")); const BuildTargetInfo bti = buildTargetInfo(); executable.setLabelText(Tr::tr("Executable:")); executable.setExecutable(bti.targetFilePath); executable.setSettingsKey("Workspace.RunConfiguration.Executable"); executable.setEnvironment(environment.environment()); connect(&environment, &EnvironmentAspect::environmentChanged, this, [this] { executable.setEnvironment(environment.environment()); }); auto argumentsAsString = [this]() { return CommandLine{ "", buildTargetInfo().additionalData.toMap()["arguments"].toStringList()} .arguments(); }; arguments.setLabelText(Tr::tr("Arguments:")); arguments.setArguments(argumentsAsString()); arguments.setSettingsKey("Workspace.RunConfiguration.Arguments"); workingDirectory.setLabelText(Tr::tr("Working directory:")); if (!bti.workingDirectory.isEmpty()) workingDirectory.setDefaultWorkingDirectory(bti.workingDirectory); workingDirectory.setSettingsKey("Workspace.RunConfiguration.WorkingDirectory"); workingDirectory.setEnvironment(&environment); setUpdater([this, argumentsAsString] { if (enabled.value()) // skip the update for cloned run configurations return; const BuildTargetInfo bti = buildTargetInfo(); executable.setExecutable(bti.targetFilePath); arguments.setArguments(argumentsAsString()); if (!bti.workingDirectory.isEmpty()) workingDirectory.setDefaultWorkingDirectory(bti.workingDirectory); }); auto enabledUpdater = [this] { setEnabled(enabled.value()); }; connect(&enabled, &BaseAspect::changed, this, enabledUpdater); connect(this, &AspectContainer::fromMapFinished, this, enabledUpdater); enabledUpdater(); enabled.setSettingsKey("Workspace.RunConfiguration.Enabled"); } RunConfiguration *clone(BuildConfiguration *bc) override { RunConfiguration *result = RunConfiguration::clone(bc); dynamic_cast(result)->enabled.setValue(true); return result; } TextDisplay hint{this}; EnvironmentAspect environment{this}; ExecutableAspect executable{this}; ArgumentsAspect arguments{this}; WorkingDirectoryAspect workingDirectory{this}; BoolAspect enabled{this}; }; class WorkspaceProjectRunConfigurationFactory : public RunConfigurationFactory { public: WorkspaceProjectRunConfigurationFactory() { registerRunConfiguration( Id(WORKSPACE_PROJECT_RUNCONFIG_ID)); addSupportedProjectType(WORKSPACE_PROJECT_ID); } }; class WorkspaceBuildConfiguration : public BuildConfiguration { Q_OBJECT public: WorkspaceBuildConfiguration(Target *target, Id id) : BuildConfiguration(target, id) { setInitializer([this](const BuildInfo &info) { const QVariantMap extraInfos = info.extraInfo.toMap(); if (extraInfos.empty()) return; BuildStepList *steps = buildSteps(); for (const QVariant &buildStep : extraInfos["steps"].toList()) { const QVariantMap bs = buildStep.toMap(); auto step = new Internal::ProcessStep(steps, Constants::CUSTOM_PROCESS_STEP); step->setCommand(FilePath::fromUserInput(bs["executable"].toString())); step->setArguments(bs["arguments"].toStringList()); FilePath wd = FilePath::fromUserInput(bs["workingDirectory"].toString()); if (wd.isEmpty()) wd = "%{ActiveProject:BuildConfig:Path}"; step->setWorkingDirectory(wd, project()->projectDirectory()); steps->appendStep(step); } initializeExtraInfo(extraInfos); }); setBuildDirectoryHistoryCompleter("Workspace.BuildDir.History"); setConfigWidgetDisplayName(Tr::tr("Workspace Manager")); } void initializeExtraInfo(const QVariantMap &extraInfos) { resetExtraInfo(); if (extraInfos["forSetup"].toBool()) { originalExtraInfo = extraInfos; setEnabled(false); buildInfoResetConnection = connect( this, &BaseAspect::changed, this, &WorkspaceBuildConfiguration::resetExtraInfo); for (BuildStep *step : buildSteps()->steps()) step->setEnabled(false); } } void fromMap(const Utils::Store &map) override { BuildConfiguration::fromMap(map); initializeExtraInfo(mapFromStore(storeFromVariant(map.value("extraInfo")))); } void toMap(Utils::Store &map) const override { BuildConfiguration::toMap(map); if (originalExtraInfo) map.insert("extraInfo", *originalExtraInfo); } BuildConfiguration *clone(Target *target) const override { auto clone = BuildConfiguration::clone(target); if (auto bc = qobject_cast(clone); QTC_GUARD(bc)) bc->resetExtraInfo(); return clone; } void resetExtraInfo() { originalExtraInfo.reset(); disconnect(buildInfoResetConnection); setEnabled(true); for (auto step : buildSteps()->steps()) step->setEnabled(true); } std::optional originalExtraInfo; QMetaObject::Connection buildInfoResetConnection; }; class WorkspaceBuildConfigurationFactory : public BuildConfigurationFactory { public: static WorkspaceBuildConfigurationFactory *m_instance; WorkspaceBuildConfigurationFactory() { QTC_CHECK(m_instance == nullptr); m_instance = this; registerBuildConfiguration ("WorkspaceProject.BuildConfiguration"); setSupportedProjectType(WORKSPACE_PROJECT_ID); setSupportedProjectMimeTypeName(WORKSPACE_MIMETYPE); setBuildGenerator([this](const Kit *, const FilePath &projectPath, bool forSetup) { QList result = parseBuildConfigurations(projectPath, forSetup); if (!forSetup || result.isEmpty()) { BuildInfo info; info.buildSystemName = WorkspaceBuildSystem::name(); info.factory = this; info.typeName = msgBuildConfigurationBuild(); info.projectDirectory = projectPath.parentDir().parentDir(); info.buildDirectory = info.projectDirectory.pathAppended("build"); info.displayName = msgBuildConfigurationDefault(); result << info; } return result; }); } static QList parseBuildConfigurations(const FilePath &projectPath, bool forSetup = false) { const QJsonObject json = projectDefinition(projectPath).value_or(QJsonObject()); const QJsonArray buildConfigs = json.value("build.configuration").toArray(); QList buildInfos; for (const QJsonValue &buildConfig : buildConfigs) { QTC_ASSERT(buildConfig.isObject(), continue); BuildInfo buildInfo; const QJsonObject buildConfigObject = buildConfig.toObject(); buildInfo.displayName = buildConfigObject["name"].toString(); if (buildInfo.displayName.isEmpty()) continue; buildInfo.typeName = buildInfo.displayName; buildInfo.factory = m_instance; buildInfo.buildDirectory = FilePath::fromUserInput( buildConfigObject["buildDirectory"].toString()); if (buildInfo.buildDirectory.isRelativePath()) { buildInfo.buildDirectory = projectPath.parentDir().parentDir().resolvePath( buildInfo.buildDirectory); } QVariantList buildSteps; for (const QJsonValue &step : buildConfigObject["steps"].toArray()) { if (step.isObject() && step.toObject().contains("executable")) buildSteps.append(step.toObject().toVariantMap()); } if (buildSteps.isEmpty()) continue; QVariantMap extraInfo = buildConfigObject.toVariantMap(); extraInfo["forSetup"] = forSetup; buildInfo.extraInfo = extraInfo; buildInfos.append(buildInfo); } return buildInfos; } }; WorkspaceBuildConfigurationFactory *WorkspaceBuildConfigurationFactory::m_instance = nullptr; WorkspaceProject::WorkspaceProject(const FilePath &file, const QJsonObject &defaultConfiguration) : Project(FOLDER_MIMETYPE, file.isDir() ? file / ".qtcreator" / "project.json" : file) { QTC_CHECK(projectFilePath().absolutePath().ensureWritableDir()); if (!projectFilePath().exists() && QTC_GUARD(projectFilePath().ensureExistingFile())) { QJsonObject projectJson = defaultConfiguration; projectJson.insert("$schema", "https://download.qt.io/official_releases/qtcreator/latest/installer_source/jsonschemas/project.json"); QJsonArray excludes = projectJson.value(FILES_EXCLUDE_KEY).toArray(); excludes.append(".qtcreator/project.json.user"); projectJson.insert(FILES_EXCLUDE_KEY, excludes); projectFilePath().writeFileContents(QJsonDocument(projectJson).toJson()); } setType(WORKSPACE_PROJECT_ID); setDisplayName(projectDirectory().fileName()); setBuildSystemCreator(); connect(this, &Project::projectFileIsDirty, this, &WorkspaceProject::updateBuildConfigurations); } FilePath WorkspaceProject::projectDirectory() const { return Project::projectDirectory().parentDir(); } Project::RestoreResult WorkspaceProject::fromMap(const Store &map, QString *errorMessage) { if (const RestoreResult res = Project::fromMap(map, errorMessage); res != RestoreResult::Ok) return res; if (!activeTarget()) { addTargetForDefaultKit(); } else { // For projects created with Qt Creator < 17. for (Target *const t : targets()) { if (t->buildConfigurations().isEmpty()) t->updateDefaultBuildConfigurations(); QTC_CHECK(!t->buildConfigurations().isEmpty()); } } return RestoreResult::Ok; } void WorkspaceProject::updateBuildConfigurations() { qCDebug(wsp) << "Updating build configurations for" << displayName(); const QList buildInfos = WorkspaceBuildConfigurationFactory::parseBuildConfigurations(projectFilePath(), true); for (Target *target : targets()) { qCDebug(wsp) << "Updating build configurations for target" << target->displayName(); QList toAdd = buildInfos; QString removedActiveBuildConfiguration; for (BuildConfiguration *bc : target->buildConfigurations()) { auto *wbc = qobject_cast(bc); if (!wbc) continue; if (std::optional extraInfo = wbc->originalExtraInfo) { // remove the buildConfiguration if it is unchanged from the project file auto equalExtraInfo = [&extraInfo](const BuildInfo &info) { return info.extraInfo == *extraInfo; }; if (toAdd.removeIf(equalExtraInfo) == 0) { qCDebug(wsp) << " Removing build configuration" << wbc->displayName(); if (target->activeBuildConfiguration() == wbc) removedActiveBuildConfiguration = wbc->displayName(); target->removeBuildConfiguration(bc); } } } for (const BuildInfo &buildInfo : toAdd) { if (BuildConfiguration *bc = buildInfo.factory->create(target, buildInfo)) { qCDebug(wsp) << " Adding build configuration" << bc->displayName(); target->addBuildConfiguration(bc); if (!removedActiveBuildConfiguration.isEmpty() && (bc->displayName() == removedActiveBuildConfiguration || toAdd.size() == 1)) { target->setActiveBuildConfiguration(bc, SetActive::NoCascade); } } } } } void WorkspaceProject::saveProjectDefinition(const QJsonObject &json) { Utils::FileSaver saver(projectFilePath()); saver.write(QJsonDocument(json).toJson()); saver.finalize(); } void WorkspaceProject::excludePath(const FilePath &path) { QTC_ASSERT(projectFilePath().exists(), return); if (Result json = projectDefinition(projectFilePath())) { QJsonArray excludes = (*json)[FILES_EXCLUDE_KEY].toArray(); const QString relative = path.relativePathFromDir(projectDirectory()); if (excludes.contains(relative)) return; excludes << relative; json->insert(FILES_EXCLUDE_KEY, excludes); saveProjectDefinition(*json); } } void WorkspaceProject::excludeNode(Node *node) { node->setEnabled(false); if (auto fileNode = node->asFileNode()) { excludePath(fileNode->path()); } else if (auto folderNode = node->asFolderNode()) { folderNode->forEachNode([](Node *node) { node->setEnabled(false); }); excludePath(folderNode->path()); } } void setupWorkspaceProject(QObject *guard) { ProjectManager::registerProjectType(FOLDER_MIMETYPE); ProjectManager::registerProjectType(WORKSPACE_MIMETYPE); QAction *excludeAction = nullptr; ActionBuilder(guard, EXCLUDE_ACTION_ID) .setContext(WORKSPACE_PROJECT_ID) .setText(Tr::tr("Exclude from Project")) .addToContainer(Constants::M_FOLDERCONTEXT, Constants::G_FOLDER_OTHER) .addToContainer(Constants::M_FILECONTEXT, Constants::G_FILE_OTHER) .bindContextAction(&excludeAction) .setCommandAttribute(Command::CA_Hide) .addOnTriggered(guard, [] { Node *node = ProjectTree::currentNode(); QTC_ASSERT(node, return); const auto project = qobject_cast(node->getProject()); QTC_ASSERT(project, return); project->excludeNode(node); }); QAction *rescanAction = nullptr; ActionBuilder(guard, RESCAN_ACTION_ID) .setContext(WORKSPACE_PROJECT_ID) .setText(Tr::tr("Rescan Workspace")) .addToContainer(Constants::M_PROJECTCONTEXT, Constants::G_PROJECT_REBUILD) .bindContextAction(&rescanAction) .setCommandAttribute(Command::CA_Hide) .addOnTriggered(guard, [] { Node *node = ProjectTree::currentNode(); QTC_ASSERT(node, return); const auto project = qobject_cast(node->getProject()); QTC_ASSERT(project, return); if (auto buildSystem = dynamic_cast(project->activeBuildSystem())) buildSystem->reparse(true); }); QObject::connect( ProjectTree::instance(), &ProjectTree::aboutToShowContextMenu, ProjectExplorerPlugin::instance(), [excludeAction, rescanAction](Node *node) { const bool visible = node && qobject_cast(node->getProject()); excludeAction->setVisible(visible); rescanAction->setVisible(visible); if (visible) { excludeAction->setEnabled(node->isEnabled()); bool enableRescan = false; if (BuildSystem *buildSystem = activeBuildSystem(node->getProject())) enableRescan = !buildSystem->isParsing(); rescanAction->setEnabled(enableRescan); } }); static WorkspaceProjectRunConfigurationFactory theRunConfigurationFactory; static ProcessRunnerFactory theRunWorkerFactory{{WORKSPACE_PROJECT_RUNCONFIG_ID}}; static WorkspaceBuildConfigurationFactory theBuildConfigurationFactory; } } // namespace ProjectExplorer #include "workspaceproject.moc"