// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "qmlbuildsystem.h" #include "qmlprojectconstants.h" #include "mcubuildstep.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "projectitem/qmlprojectitem.h" #include "projectnode/qmlprojectnodes.h" #include "utils/algorithm.h" #include "utils/qtcassert.h" #include "texteditor/textdocument.h" using namespace ProjectExplorer; namespace QmlProjectManager { namespace { Q_LOGGING_CATEGORY(infoLogger, "QmlProjectManager.QmlBuildSystem", QtInfoMsg) } QmlBuildSystem::QmlBuildSystem(Target *target) : BuildSystem(target) { // refresh first - project information is used e.g. to decide the default RC's refresh(RefreshOptions::Project); updateDeploymentData(); registerMenuButtons(); connect(target->project(), &Project::activeTargetChanged, [this](Target *target) { refresh(RefreshOptions::NoFileRefresh); if (qtForMCUs()) MCUBuildStepFactory::attachToTarget(target); }); connect(target->project(), &Project::projectFileIsDirty, [this]() { refresh(RefreshOptions::Project); }); // FIXME: Check. Probably bogus after the BuildSystem move. // // addedTarget calls updateEnabled on the runconfigurations // // which needs to happen after refresh // foreach (Target *t, targets()) // addedTarget(t); } void QmlBuildSystem::updateDeploymentData() { if (!m_projectItem) return; if (DeviceTypeKitAspect::deviceTypeId(kit()) == ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE) { return; } ProjectExplorer::DeploymentData deploymentData; for (const auto &file : m_projectItem->files()) { deploymentData.addFile(file, targetFile(file).parentDir().toString()); } setDeploymentData(deploymentData); } void QmlBuildSystem::registerMenuButtons() { Core::ActionContainer *menu = Core::ActionManager::actionContainer(Core::Constants::M_FILE); // QML Project file update button // This button saves the current configuration into the .qmlproject file auto action = new QAction("Update QmlProject File", this); Core::Command *cmd = Core::ActionManager::registerAction(action, "QmlProject.ProjectManager"); menu->addAction(cmd, Core::Constants::G_FILE_SAVE); QObject::connect(action, &QAction::triggered, this, &QmlBuildSystem::updateProjectFile); } bool QmlBuildSystem::updateProjectFile() { qDebug() << "debug#1-mainfilepath" << mainFilePath(); QFile file(mainFilePath().fileName().append("project-test")); if (file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { qCritical() << "Cannot open Qml Project file for editing!"; return false; } QTextStream ts(&file); // License ts << "/* " "File generated by Qt Creator" "Copyright (C) 2016 The Qt Company Ltd." "SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH " "Qt-GPL-exception-1.0" "*/" << Qt::endl << Qt::endl; // Components ts << "import QmlProject 1.1" << Qt::endl << Qt::endl; return true; } void QmlBuildSystem::triggerParsing() { refresh(RefreshOptions::Project); } Utils::FilePath QmlBuildSystem::canonicalProjectDir() const { return BuildSystem::target() ->project() ->projectFilePath() .canonicalPath() .normalizedPathName() .parentDir(); } void QmlBuildSystem::refresh(RefreshOptions options) { ParseGuard guard = guardParsingRun(); switch (options) { case RefreshOptions::NoFileRefresh: break; case RefreshOptions::Project: initProjectItem(); [[fallthrough]]; case RefreshOptions::Files: parseProjectFiles(); } auto modelManager = QmlJS::ModelManagerInterface::instance(); if (!modelManager) return; QmlJS::ModelManagerInterface::ProjectInfo projectInfo = modelManager->defaultProjectInfoForProject(project(), project()->files(Project::HiddenRccFolders)); const QStringList searchPaths = makeAbsolute(canonicalProjectDir(), customImportPaths()); for (const QString &searchPath : searchPaths) projectInfo.importPaths.maybeInsert(Utils::FilePath::fromString(searchPath), QmlJS::Dialect::Qml); modelManager->updateProjectInfo(projectInfo, project()); guard.markAsSuccess(); emit projectChanged(); } void QmlBuildSystem::initProjectItem() { m_projectItem.reset(new QmlProjectItem{projectFilePath()}); connect(m_projectItem.get(), &QmlProjectItem::qmlFilesChanged, this, &QmlBuildSystem::refreshFiles); } void QmlBuildSystem::parseProjectFiles() { if (auto modelManager = QmlJS::ModelManagerInterface::instance()) { modelManager->updateSourceFiles(m_projectItem->files(), true); } Utils::FilePath mainFilePath{Utils::FilePath::fromString(m_projectItem->mainFile())}; if (!mainFilePath.isEmpty()) { mainFilePath = canonicalProjectDir().resolvePath(m_projectItem->mainFile()); Utils::FileReader reader; QString errorMessage; if (!reader.fetch(mainFilePath, &errorMessage)) { Core::MessageManager::writeFlashing( tr("Warning while loading project file %1.").arg(projectFilePath().toUserOutput())); Core::MessageManager::writeSilently(errorMessage); } } generateProjectTree(); } void QmlBuildSystem::generateProjectTree() { auto newRoot = std::make_unique(project()); for (const auto &file : m_projectItem->files()) { const FileType fileType = (file == projectFilePath()) ? FileType::Project : FileNode::fileTypeForFileName(file); newRoot->addNestedNode(std::make_unique(file, fileType)); } newRoot->addNestedNode(std::make_unique(projectFilePath(), FileType::Project)); setRootProjectNode(std::move(newRoot)); updateDeploymentData(); } bool QmlBuildSystem::setFileSettingInProjectFile(const QString &setting, const Utils::FilePath &mainFilePath, const QString &oldFile) { // make sure to change it also in the qmlproject file const Utils::FilePath qmlProjectFilePath = project()->projectFilePath(); Core::FileChangeBlocker fileChangeBlocker(qmlProjectFilePath); const QList editors = Core::DocumentModel::editorsForFilePath( qmlProjectFilePath); TextEditor::TextDocument *document = nullptr; if (!editors.isEmpty()) { document = qobject_cast(editors.first()->document()); if (document && document->isModified()) if (!Core::DocumentManager::saveDocument(document)) return false; } QString fileContent; QString error; Utils::TextFileFormat textFileFormat; const QTextCodec *codec = QTextCodec::codecForName("UTF-8"); // qml files are defined to be utf-8 Utils::TextFileFormat::ReadResult readResult = Utils::TextFileFormat::readFile(qmlProjectFilePath, codec, &fileContent, &textFileFormat, &error); if (readResult != Utils::TextFileFormat::ReadSuccess) { qWarning() << "Failed to read file" << qmlProjectFilePath << ":" << error; } const QString settingQmlCode = setting + ":"; const Utils::FilePath projectDir = project()->projectFilePath().parentDir(); const QString relativePath = mainFilePath.relativeChildPath(projectDir).path(); if (fileContent.indexOf(settingQmlCode) < 0) { QString addedText = QString("\n %1 \"%2\"\n").arg(settingQmlCode).arg(relativePath); auto index = fileContent.lastIndexOf("}"); fileContent.insert(index, addedText); } else { QString originalFileName = oldFile; originalFileName.replace(".", "\\."); const QRegularExpression expression( QString("%1\\s*\"(%2)\"").arg(settingQmlCode).arg(originalFileName)); const QRegularExpressionMatch match = expression.match(fileContent); fileContent.replace(match.capturedStart(1), match.capturedLength(1), relativePath); } if (!textFileFormat.writeFile(qmlProjectFilePath, fileContent, &error)) qWarning() << "Failed to write file" << qmlProjectFilePath << ":" << error; refresh(RefreshOptions::Project); return true; } bool QmlBuildSystem::blockFilesUpdate() const { return m_blockFilesUpdate; } void QmlBuildSystem::setBlockFilesUpdate(bool newBlockFilesUpdate) { m_blockFilesUpdate = newBlockFilesUpdate; } Utils::FilePath QmlBuildSystem::mainFilePath() const { return projectDirectory().pathAppended(mainFile()); } Utils::FilePath QmlBuildSystem::mainUiFilePath() const { return m_projectItem->mainUiFilePath(); } bool QmlBuildSystem::setMainFileInProjectFile(const Utils::FilePath &newMainFilePath) { return setFileSettingInProjectFile("mainFile", newMainFilePath, mainFile()); } bool QmlBuildSystem::setMainUiFileInProjectFile(const Utils::FilePath &newMainUiFilePath) { return setMainUiFileInMainFile(newMainUiFilePath) && setFileSettingInProjectFile("mainUiFile", newMainUiFilePath, m_projectItem->mainUiFile()); } bool QmlBuildSystem::setMainUiFileInMainFile(const Utils::FilePath &newMainUiFilePath) { Core::FileChangeBlocker fileChangeBlocker(mainFilePath()); const QList editors = Core::DocumentModel::editorsForFilePath(mainFilePath()); TextEditor::TextDocument *document = nullptr; if (!editors.isEmpty()) { document = qobject_cast(editors.first()->document()); if (document && document->isModified()) if (!Core::DocumentManager::saveDocument(document)) return false; } QString fileContent; QString error; Utils::TextFileFormat textFileFormat; const QTextCodec *codec = QTextCodec::codecForName("UTF-8"); // qml files are defined to be utf-8 if (Utils::TextFileFormat::readFile(mainFilePath(), codec, &fileContent, &textFileFormat, &error) != Utils::TextFileFormat::ReadSuccess) { qWarning() << "Failed to read file" << mainFilePath() << ":" << error; } const QString currentMain = QString("%1 {").arg(mainUiFilePath().baseName()); const QString newMain = QString("%1 {").arg(newMainUiFilePath.baseName()); if (fileContent.contains(currentMain)) fileContent.replace(currentMain, newMain); if (!textFileFormat.writeFile(mainFilePath(), fileContent, &error)) qWarning() << "Failed to write file" << mainFilePath() << ":" << error; return true; } Utils::FilePath QmlBuildSystem::targetDirectory() const { if (DeviceTypeKitAspect::deviceTypeId(kit()) == ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE) return canonicalProjectDir(); return m_projectItem ? Utils::FilePath::fromString(m_projectItem->targetDirectory()) : Utils::FilePath(); } Utils::FilePath QmlBuildSystem::targetFile(const Utils::FilePath &sourceFile) const { const QDir sourceDir(m_projectItem ? m_projectItem->sourceDirectory().path() : canonicalProjectDir().toString()); const QDir targetDir(targetDirectory().toString()); const QString relative = sourceDir.relativeFilePath(sourceFile.toString()); return Utils::FilePath::fromString(QDir::cleanPath(targetDir.absoluteFilePath(relative))); } void QmlBuildSystem::setSupportedLanguages(QStringList languages) { m_projectItem->setSupportedLanguages(languages); } void QmlBuildSystem::setPrimaryLanguage(QString language) { m_projectItem->setPrimaryLanguage(language); } QStringList QmlBuildSystem::makeAbsolute(const Utils::FilePath &path, const QStringList &relativePaths) { if (path.isEmpty()) return relativePaths; const QDir baseDir(path.toString()); return Utils::transform(relativePaths, [&baseDir](const QString &path) { return QDir::cleanPath(baseDir.absoluteFilePath(path)); }); } void QmlBuildSystem::refreshFiles(const QSet & /*added*/, const QSet &removed) { if (m_blockFilesUpdate) { qCDebug(infoLogger) << "Auto files refresh blocked."; return; } refresh(RefreshOptions::Files); if (!removed.isEmpty()) { if (auto modelManager = QmlJS::ModelManagerInterface::instance()) { modelManager->removeFiles( Utils::transform>(removed, [](const QString &s) { return Utils::FilePath::fromString(s); })); } } updateDeploymentData(); } QVariant QmlBuildSystem::additionalData(Utils::Id id) const { if (id == Constants::customFileSelectorsData) return customFileSelectors(); if (id == Constants::supportedLanguagesData) return supportedLanguages(); if (id == Constants::primaryLanguageData) return primaryLanguage(); if (id == Constants::customForceFreeTypeData) return forceFreeType(); if (id == Constants::customQtForMCUs) return qtForMCUs(); if (id == Constants::customQt6Project) return qt6Project(); if (id == Constants::mainFilePath) return mainFilePath().toString(); if (id == Constants::customImportPaths) return customImportPaths(); if (id == Constants::canonicalProjectDir) return canonicalProjectDir().toString(); return {}; } bool QmlBuildSystem::supportsAction(Node *context, ProjectAction action, const Node *node) const { if (dynamic_cast(context)) { if (action == AddNewFile || action == EraseFile) return true; QTC_ASSERT(node, return false); if (action == Rename && node->asFileNode()) { const FileNode *fileNode = node->asFileNode(); QTC_ASSERT(fileNode, return false); return fileNode->fileType() != FileType::Project; } return false; } return BuildSystem::supportsAction(context, action, node); } bool QmlBuildSystem::addFiles(Node *context, const Utils::FilePaths &filePaths, Utils::FilePaths *) { if (!dynamic_cast(context)) return false; Utils::FilePaths toAdd; for (const Utils::FilePath &filePath : filePaths) { if (!m_projectItem->matchesFile(filePath.toString())) toAdd << filePaths; } return toAdd.isEmpty(); } bool QmlBuildSystem::deleteFiles(Node *context, const Utils::FilePaths &filePaths) { if (dynamic_cast(context)) return true; return BuildSystem::deleteFiles(context, filePaths); } bool QmlBuildSystem::renameFile(Node *context, const Utils::FilePath &oldFilePath, const Utils::FilePath &newFilePath) { if (dynamic_cast(context)) { if (oldFilePath.endsWith(mainFile())) return setMainFileInProjectFile(newFilePath); if (oldFilePath.endsWith(m_projectItem->mainUiFile())) return setMainUiFileInProjectFile(newFilePath); return true; } return BuildSystem::renameFile(context, oldFilePath, newFilePath); } QString QmlBuildSystem::mainFile() const { return m_projectItem->mainFile(); } bool QmlBuildSystem::qtForMCUs() const { return m_projectItem->isQt4McuProject(); } bool QmlBuildSystem::qt6Project() const { return m_projectItem->versionQt() == "6"; } Utils::EnvironmentItems QmlBuildSystem::environment() const { return m_projectItem->environment(); } QStringList QmlBuildSystem::customImportPaths() const { return m_projectItem->importPaths(); } QStringList QmlBuildSystem::customFileSelectors() const { return m_projectItem->fileSelectors(); } bool QmlBuildSystem::multilanguageSupport() const { return m_projectItem->multilanguageSupport(); } QStringList QmlBuildSystem::supportedLanguages() const { return m_projectItem->supportedLanguages(); } QString QmlBuildSystem::primaryLanguage() const { return m_projectItem->primaryLanguage(); } bool QmlBuildSystem::forceFreeType() const { return m_projectItem->forceFreeType(); } bool QmlBuildSystem::widgetApp() const { return m_projectItem->widgetApp(); } QStringList QmlBuildSystem::shaderToolArgs() const { return m_projectItem->shaderToolArgs(); } QStringList QmlBuildSystem::shaderToolFiles() const { return m_projectItem->shaderToolFiles(); } QStringList QmlBuildSystem::importPaths() const { return m_projectItem->importPaths(); } Utils::FilePaths QmlBuildSystem::files() const { return m_projectItem->files(); } QString QmlBuildSystem::versionQt() const { return m_projectItem->versionQt(); } QString QmlBuildSystem::versionQtQuick() const { return m_projectItem->versionQtQuick(); } QString QmlBuildSystem::versionDesignStudio() const { return m_projectItem->versionDesignStudio(); } } // namespace QmlProjectManager