QmlProject: Allow setting main qml file and main ui.qml file

The .qmlproject file already has the "mainFile" setting which indicates
which qml file is run. This patch adds a main ui.qml file that indicates
which ui.qml file is opened in the design mode if the .qmlproject is opened.

The patch also adds two context menu actions that allow setting the main qml
and main.ui.qml files without having to edit the .qmlproject file by hand.

Changing the main ui.qml file also checks if the current ui.qml file is
used as in the main qml file and if it is, then we switch the component there.

For now the actions are only available in QDS.

Task-number: QDS-6882
Change-Id: I1c6e19c039036dc635161fa6e06173356dc509aa
Reviewed-by: Henning Gründl <henning.gruendl@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
This commit is contained in:
Thomas Hartmann
2022-05-10 14:23:51 +02:00
parent 5e31fae6d9
commit de7a7b6ac8
5 changed files with 274 additions and 49 deletions

View File

@@ -91,6 +91,10 @@ QmlProjectItem *QmlProjectFileFormat::parseProjectFile(const Utils::FilePath &fi
if (mainFileProperty.isValid())
projectItem->setMainFile(mainFileProperty.value.toString());
const auto mainUiFileProperty = rootNode->property(QLatin1String("mainUiFile"));
if (mainUiFileProperty.isValid())
projectItem->setMainUiFile(mainUiFileProperty.value.toString());
const auto importPathsProperty = rootNode->property(QLatin1String("importPaths"));
if (importPathsProperty.isValid()) {
QStringList list = importPathsProperty.value.toStringList();

View File

@@ -81,6 +81,9 @@ public:
QString mainFile() const { return m_mainFile; }
void setMainFile(const QString &mainFilePath) { m_mainFile = mainFilePath; }
QString mainUiFile() const { return m_mainUiFile; }
void setMainUiFile(const QString &mainUiFilePath) { m_mainUiFile = mainUiFilePath; }
bool widgetApp() const { return m_widgetApp; }
void setWidgetApp(bool widgetApp) { m_widgetApp = widgetApp; }
@@ -107,6 +110,7 @@ protected:
QStringList m_supportedLanguages;
QString m_primaryLanguage;
QString m_mainFile;
QString m_mainUiFile;
Utils::EnvironmentItems m_environment;
QVector<QmlProjectContentItem *> m_content; // content property
bool m_forceFreeType = false;

View File

@@ -135,21 +135,38 @@ QmlProject::QmlProject(const Utils::FilePath &fileName)
disconnect(m_openFileConnection);
if (target && success) {
Utils::FilePaths uiFiles = getUiQmlFilesForFolder(projectDirectory()
+ "/content");
if (uiFiles.isEmpty())
uiFiles = getUiQmlFilesForFolder(projectDirectory());
if (!uiFiles.isEmpty()) {
Utils::FilePath currentFile;
if (auto cd = Core::EditorManager::currentDocument())
currentFile = cd->filePath();
auto target = activeTarget();
if (!target)
return;
if (currentFile.isEmpty() || !isKnownFile(currentFile))
QTimer::singleShot(1000, [uiFiles]() {
Core::EditorManager::openEditor(uiFiles.first(),
auto qmlBuildSystem = qobject_cast<QmlProjectManager::QmlBuildSystem *>(
target->buildSystem());
const Utils::FilePath mainUiFile = qmlBuildSystem->mainUiFilePath();
if (mainUiFile.completeSuffix() == "qi.qml" && mainUiFile.exists()) {
QTimer::singleShot(1000, [mainUiFile]() {
Core::EditorManager::openEditor(mainUiFile,
Utils::Id());
});
});
} else {
Utils::FilePaths uiFiles = getUiQmlFilesForFolder(projectDirectory()
+ "/content");
if (uiFiles.isEmpty())
uiFiles = getUiQmlFilesForFolder(projectDirectory());
if (!uiFiles.isEmpty()) {
Utils::FilePath currentFile;
if (auto cd = Core::EditorManager::currentDocument())
currentFile = cd->filePath();
if (currentFile.isEmpty() || !isKnownFile(currentFile))
QTimer::singleShot(1000, [uiFiles]() {
Core::EditorManager::openEditor(uiFiles.first(),
Utils::Id());
});
}
}
}
});
@@ -253,6 +270,58 @@ void QmlBuildSystem::parseProject(RefreshOptions options)
}
}
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<Core::IEditor *> editors = Core::DocumentModel::editorsForFilePath(qmlProjectFilePath);
TextEditor::TextDocument *document = nullptr;
if (!editors.isEmpty()) {
document = qobject_cast<TextEditor::TextDocument*>(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(qmlProjectFilePath, codec, &fileContent, &textFileFormat, &error)
!= Utils::TextFileFormat::ReadSuccess) {
qWarning() << "Failed to read file" << qmlProjectFilePath << ":" << error;
}
const QString settingQmlCode = setting + ":";
QDir projectDir = project()->projectFilePath().toDir();
projectDir.cdUp();
const QString relativePath = projectDir.relativeFilePath(mainFilePath.toString());
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(Everything);
return true;
}
void QmlBuildSystem::refresh(RefreshOptions options)
{
ParseGuard guard = guardParsingRun();
@@ -283,11 +352,68 @@ QString QmlBuildSystem::mainFile() const
return QString();
}
QString QmlBuildSystem::mainUiFile() const
{
if (m_projectItem)
return m_projectItem->mainUiFile();
return QString();
}
Utils::FilePath QmlBuildSystem::mainFilePath() const
{
return projectDirectory().pathAppended(mainFile());
}
Utils::FilePath QmlBuildSystem::mainUiFilePath() const
{
return projectDirectory().pathAppended(mainUiFile());
}
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, mainUiFile());
}
bool QmlBuildSystem::setMainUiFileInMainFile(const Utils::FilePath &newMainUiFilePath)
{
Core::FileChangeBlocker fileChangeBlocker(mainFilePath());
const QList<Core::IEditor *> editors = Core::DocumentModel::editorsForFilePath(mainFilePath());
TextEditor::TextDocument *document = nullptr;
if (!editors.isEmpty()) {
document = qobject_cast<TextEditor::TextDocument*>(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;
}
bool QmlBuildSystem::qtForMCUs() const
{
if (m_projectItem)
@@ -663,43 +789,10 @@ bool QmlBuildSystem::deleteFiles(Node *context, const FilePaths &filePaths)
bool QmlBuildSystem::renameFile(Node * context, const FilePath &oldFilePath, const FilePath &newFilePath)
{
if (dynamic_cast<QmlProjectNode *>(context)) {
if (oldFilePath.endsWith(mainFile())) {
setMainFile(newFilePath.toString());
// make sure to change it also in the qmlproject file
const Utils::FilePath qmlProjectFilePath = project()->projectFilePath();
Core::FileChangeBlocker fileChangeBlocker(qmlProjectFilePath);
const QList<Core::IEditor *> editors = Core::DocumentModel::editorsForFilePath(qmlProjectFilePath);
TextEditor::TextDocument *document = nullptr;
if (!editors.isEmpty()) {
document = qobject_cast<TextEditor::TextDocument*>(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(qmlProjectFilePath, codec, &fileContent, &textFileFormat, &error)
!= Utils::TextFileFormat::ReadSuccess) {
qWarning() << "Failed to read file" << qmlProjectFilePath << ":" << error;
}
// find the mainFile and do the file name with brackets in a capture group and mask the . with \.
QString originalFileName = oldFilePath.fileName();
originalFileName.replace(".", "\\.");
const QRegularExpression expression(QString("mainFile:\\s*\"(%1)\"").arg(originalFileName));
const QRegularExpressionMatch match = expression.match(fileContent);
fileContent.replace(match.capturedStart(1), match.capturedLength(1), newFilePath.fileName());
if (!textFileFormat.writeFile(qmlProjectFilePath, fileContent, &error))
qWarning() << "Failed to write file" << qmlProjectFilePath << ":" << error;
refresh(Everything);
}
if (oldFilePath.endsWith(mainFile()))
return setMainFileInProjectFile(newFilePath);
if (oldFilePath.endsWith(mainUiFile()))
return setMainUiFileInProjectFile(newFilePath);
return true;
}

View File

@@ -77,7 +77,13 @@ public:
Utils::FilePath canonicalProjectDir() const;
QString mainFile() const;
QString mainUiFile() const;
Utils::FilePath mainFilePath() const;
Utils::FilePath mainUiFilePath() const;
bool setMainFileInProjectFile(const Utils::FilePath &newMainFilePath);
bool setMainUiFileInProjectFile(const Utils::FilePath &newMainUiFilePath);
bool setMainUiFileInMainFile(const Utils::FilePath &newMainUiFilePath);
bool qtForMCUs() const;
bool qt6Project() const;
@@ -116,6 +122,10 @@ public:
void parseProject(RefreshOptions options);
private:
bool setFileSettingInProjectFile(const QString &setting,
const Utils::FilePath &mainFilePath,
const QString &oldFile);
std::unique_ptr<QmlProjectItem> m_projectItem;
Utils::FilePath m_canonicalProjectDir;
bool m_blockFilesUpdate = false;

View File

@@ -28,14 +28,20 @@
#include "qmlprojectconstants.h"
#include "qmlprojectrunconfiguration.h"
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/fileiconprovider.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagebox.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
#include <projectexplorer/projecttree.h>
#include <projectexplorer/runcontrol.h>
#include <projectexplorer/session.h>
#include <projectexplorer/target.h>
#include <qmljs/qmljsmodelmanagerinterface.h>
@@ -47,6 +53,7 @@
#include <utils/infobar.h>
#include <utils/qtcprocess.h>
#include <QAction>
#include <QMessageBox>
#include <QPushButton>
#include <QTimer>
@@ -197,6 +204,23 @@ void QmlProjectPlugin::openInQDSWithProject(const Utils::FilePath &filePath)
}
}
static QmlBuildSystem *qmlBuildSystemforFileNode(const FileNode *fileNode)
{
if (!fileNode)
return nullptr;
if (QmlProject *qmlProject = qobject_cast<QmlProject*>(fileNode->getProject())) {
auto target = qmlProject->activeTarget();
if (!target)
return nullptr;
return qobject_cast<QmlProjectManager::QmlBuildSystem *>(target->buildSystem());
}
return nullptr;
}
bool QmlProjectPlugin::initialize(const QStringList &, QString *errorMessage)
{
Q_UNUSED(errorMessage)
@@ -206,6 +230,7 @@ bool QmlProjectPlugin::initialize(const QStringList &, QString *errorMessage)
if (!qmlDesignerEnabled()) {
connect(Core::EditorManager::instance(),
&Core::EditorManager::currentEditorChanged,
this,
[this](Core::IEditor *editor) {
QmlJS::ModelManagerInterface *modelManager
= QmlJS::ModelManagerInterface::instance();
@@ -258,6 +283,95 @@ bool QmlProjectPlugin::initialize(const QStringList &, QString *errorMessage)
ProjectManager::registerProjectType<QmlProject>(QmlJSTools::Constants::QMLPROJECT_MIMETYPE);
Core::FileIconProvider::registerIconOverlayForSuffix(":/qmlproject/images/qmlproject.png",
"qmlproject");
if (QmlProject::isQtDesignStudio()) {
Core::ActionContainer *menu = Core::ActionManager::actionContainer(
ProjectExplorer::Constants::M_FILECONTEXT);
QAction *mainfileAction = new QAction(tr("Set as main .qml file"), this);
mainfileAction->setEnabled(false);
connect(mainfileAction, &QAction::triggered, this, []() {
const Node *currentNode = ProjectTree::currentNode();
if (!currentNode || !currentNode->asFileNode()
|| currentNode->asFileNode()->fileType() != FileType::QML)
return;
const Utils::FilePath file = currentNode->filePath();
QmlBuildSystem *buildSystem = qmlBuildSystemforFileNode(currentNode->asFileNode());
if (buildSystem)
buildSystem->setMainFileInProjectFile(file);
});
menu->addAction(Core::ActionManager::registerAction(
mainfileAction,
"QmlProject.setMainFile",
Core::Context(ProjectExplorer::Constants::C_PROJECT_TREE)),
ProjectExplorer::Constants::G_FILE_OTHER);
mainfileAction->setVisible(false);
connect(ProjectTree::instance(),
&ProjectTree::currentNodeChanged,
mainfileAction,
[mainfileAction](Node *node) {
const FileNode *fileNode = node ? node->asFileNode() : nullptr;
const bool isVisible = fileNode && fileNode->fileType() == FileType::QML
&& fileNode->filePath().completeSuffix() == "qml";
mainfileAction->setVisible(isVisible);
if (!isVisible)
return;
QmlBuildSystem *buildSystem = qmlBuildSystemforFileNode(fileNode);
if (buildSystem)
mainfileAction->setEnabled(buildSystem->mainFilePath()
!= fileNode->filePath());
});
QAction *mainUifileAction = new QAction(tr("Set as main .ui.qml file"), this);
mainUifileAction->setEnabled(false);
connect(mainUifileAction, &QAction::triggered, this, []() {
const Node *currentNode = ProjectTree::currentNode();
if (!currentNode || !currentNode->asFileNode()
|| currentNode->asFileNode()->fileType() != FileType::QML)
return;
const Utils::FilePath file = currentNode->filePath();
QmlBuildSystem *buildSystem = qmlBuildSystemforFileNode(currentNode->asFileNode());
if (buildSystem)
buildSystem->setMainUiFileInProjectFile(file);
});
menu->addAction(Core::ActionManager::registerAction(
mainUifileAction,
"QmlProject.setMainUIFile",
Core::Context(ProjectExplorer::Constants::C_PROJECT_TREE)),
ProjectExplorer::Constants::G_FILE_OTHER);
mainUifileAction->setVisible(false);
connect(ProjectTree::instance(),
&ProjectTree::currentNodeChanged,
mainUifileAction,
[mainUifileAction](Node *node) {
const FileNode *fileNode = node ? node->asFileNode() : nullptr;
const bool isVisible = fileNode && fileNode->fileType() == FileType::QML
&& fileNode->filePath().completeSuffix() == "ui.qml";
mainUifileAction->setVisible(isVisible);
if (!isVisible)
return;
QmlBuildSystem *buildSystem = qmlBuildSystemforFileNode(fileNode);
if (buildSystem)
mainUifileAction->setEnabled(buildSystem->mainUiFilePath()
!= fileNode->filePath());
});
}
return true;
} // namespace Internal