forked from qt-creator/qt-creator
Support for QML module mapping
QUL uses module mapping for theming of QtQuick.Controls: during code-generation the compiler is pointed to the Controls implementation it should use. This is done by rewriting any import of QtQuick.Controls with the given module name. The CMake build scripts will write a file for each target to the directory "qml_module_mappings" in the build dir, and those files will contain the mappings used. Fixes: QTCREATORBUG-25356 Change-Id: I3f74897836dde7717b03bd6dffa46dcc0689ffdd Reviewed-by: Fawzi Mohamed <fawzi.mohamed@qt.io>
This commit is contained in:
committed by
Erik Verbruggen
parent
98dc428c69
commit
5ad724a3ac
@@ -221,19 +221,21 @@ bool Bind::visit(UiImport *ast)
|
|||||||
if (ast->version)
|
if (ast->version)
|
||||||
version = ComponentVersion(ast->version->majorVersion, ast->version->minorVersion);
|
version = ComponentVersion(ast->version->majorVersion, ast->version->minorVersion);
|
||||||
|
|
||||||
if (ast->importUri) {
|
if (auto importUri = ast->importUri) {
|
||||||
QVersionNumber qtVersion;
|
QVersionNumber qtVersion;
|
||||||
|
QString uri = toString(importUri);
|
||||||
if (ModelManagerInterface *model = ModelManagerInterface::instance()) {
|
if (ModelManagerInterface *model = ModelManagerInterface::instance()) {
|
||||||
ModelManagerInterface::ProjectInfo pInfo = model->projectInfoForPath(_doc->fileName());
|
ModelManagerInterface::ProjectInfo pInfo = model->projectInfoForPath(_doc->fileName());
|
||||||
qtVersion = QVersionNumber::fromString(pInfo.qtVersionString);
|
qtVersion = QVersionNumber::fromString(pInfo.qtVersionString);
|
||||||
|
uri = pInfo.moduleMappings.value(uri, uri);
|
||||||
}
|
}
|
||||||
if (!version.isValid() && (!qtVersion.isNull() && qtVersion.majorVersion() < 6)) {
|
if (!version.isValid() && (!qtVersion.isNull() && qtVersion.majorVersion() < 6)) {
|
||||||
_diagnosticMessages->append(
|
_diagnosticMessages->append(
|
||||||
errorMessage(ast, tr("package import requires a version number")));
|
errorMessage(ast, tr("package import requires a version number")));
|
||||||
}
|
}
|
||||||
const QString importId = ast->importId.toString();
|
const QString importId = ast->importId.toString();
|
||||||
ImportInfo import = ImportInfo::moduleImport(toString(ast->importUri), version,
|
|
||||||
importId, ast);
|
ImportInfo import = ImportInfo::moduleImport(uri, version, importId, ast);
|
||||||
if (_doc->language() == Dialect::Qml) {
|
if (_doc->language() == Dialect::Qml) {
|
||||||
const QString importStr = import.name() + importId;
|
const QString importStr = import.name() + importId;
|
||||||
if (ModelManagerInterface::instance()) {
|
if (ModelManagerInterface::instance()) {
|
||||||
|
@@ -615,6 +615,7 @@ ModelManagerInterface::ProjectInfo ModelManagerInterface::projectInfoForPath(
|
|||||||
res.applicationDirectories.append(pInfo.applicationDirectories);
|
res.applicationDirectories.append(pInfo.applicationDirectories);
|
||||||
for (const auto &importPath : pInfo.importPaths)
|
for (const auto &importPath : pInfo.importPaths)
|
||||||
res.importPaths.maybeInsert(importPath);
|
res.importPaths.maybeInsert(importPath);
|
||||||
|
res.moduleMappings.insert(pInfo.moduleMappings);
|
||||||
}
|
}
|
||||||
res.applicationDirectories = Utils::filteredUnique(res.applicationDirectories);
|
res.applicationDirectories = Utils::filteredUnique(res.applicationDirectories);
|
||||||
return res;
|
return res;
|
||||||
|
@@ -72,6 +72,7 @@ public:
|
|||||||
QStringList allResourceFiles;
|
QStringList allResourceFiles;
|
||||||
QHash<QString, QString> resourceFileContents;
|
QHash<QString, QString> resourceFileContents;
|
||||||
QStringList applicationDirectories;
|
QStringList applicationDirectories;
|
||||||
|
QHash<QString, QString> moduleMappings; // E.g.: QtQuick.Controls -> MyProject.MyControls
|
||||||
|
|
||||||
// whether trying to run qmldump makes sense
|
// whether trying to run qmldump makes sense
|
||||||
bool tryQmlDump = false;
|
bool tryQmlDump = false;
|
||||||
|
@@ -685,7 +685,22 @@ void CMakeBuildSystem::updateProjectData()
|
|||||||
const bool mergedHeaderPathsAndQmlImportPaths = kit()->value(
|
const bool mergedHeaderPathsAndQmlImportPaths = kit()->value(
|
||||||
QtSupport::KitHasMergedHeaderPathsWithQmlImportPaths::id(), false).toBool();
|
QtSupport::KitHasMergedHeaderPathsWithQmlImportPaths::id(), false).toBool();
|
||||||
QStringList extraHeaderPaths;
|
QStringList extraHeaderPaths;
|
||||||
|
QList<QByteArray> moduleMappings;
|
||||||
for (const RawProjectPart &rpp : qAsConst(rpps)) {
|
for (const RawProjectPart &rpp : qAsConst(rpps)) {
|
||||||
|
FilePath moduleMapFile = cmakeBuildConfiguration()->buildDirectory()
|
||||||
|
.pathAppended("/qml_module_mappings/" + rpp.buildSystemTarget);
|
||||||
|
if (moduleMapFile.exists()) {
|
||||||
|
QFile mmf(moduleMapFile.toString());
|
||||||
|
if (mmf.open(QFile::ReadOnly)) {
|
||||||
|
QByteArray content = mmf.readAll();
|
||||||
|
auto lines = content.split('\n');
|
||||||
|
for (const auto &line : lines) {
|
||||||
|
if (!line.isEmpty())
|
||||||
|
moduleMappings.append(line.simplified());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mergedHeaderPathsAndQmlImportPaths) {
|
if (mergedHeaderPathsAndQmlImportPaths) {
|
||||||
for (const auto &headerPath : rpp.headerPaths) {
|
for (const auto &headerPath : rpp.headerPaths) {
|
||||||
if (headerPath.type == HeaderPathType::User)
|
if (headerPath.type == HeaderPathType::User)
|
||||||
@@ -693,7 +708,7 @@ void CMakeBuildSystem::updateProjectData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateQmlJSCodeModel(extraHeaderPaths);
|
updateQmlJSCodeModel(extraHeaderPaths, moduleMappings);
|
||||||
}
|
}
|
||||||
emit cmakeBuildConfiguration()->buildTypeChanged();
|
emit cmakeBuildConfiguration()->buildTypeChanged();
|
||||||
|
|
||||||
@@ -1187,8 +1202,10 @@ QList<ProjectExplorer::ExtraCompiler *> CMakeBuildSystem::findExtraCompilers()
|
|||||||
return extraCompilers;
|
return extraCompilers;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths)
|
void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths,
|
||||||
|
const QList<QByteArray> &moduleMappings)
|
||||||
{
|
{
|
||||||
|
qDebug()<<"cmake: module mappings:"<<moduleMappings;
|
||||||
QmlJS::ModelManagerInterface *modelManager = QmlJS::ModelManagerInterface::instance();
|
QmlJS::ModelManagerInterface *modelManager = QmlJS::ModelManagerInterface::instance();
|
||||||
|
|
||||||
if (!modelManager)
|
if (!modelManager)
|
||||||
@@ -1214,6 +1231,24 @@ void CMakeBuildSystem::updateQmlJSCodeModel(const QStringList &extraHeaderPaths)
|
|||||||
projectInfo.importPaths.maybeInsert(FilePath::fromString(extraHeaderPath),
|
projectInfo.importPaths.maybeInsert(FilePath::fromString(extraHeaderPath),
|
||||||
QmlJS::Dialect::Qml);
|
QmlJS::Dialect::Qml);
|
||||||
|
|
||||||
|
for (const QByteArray &mm : moduleMappings) {
|
||||||
|
auto kvPair = mm.split('=');
|
||||||
|
if (kvPair.size() != 2)
|
||||||
|
continue;
|
||||||
|
QString from = QString::fromUtf8(kvPair.at(0).trimmed());
|
||||||
|
QString to = QString::fromUtf8(kvPair.at(1).trimmed());
|
||||||
|
if (!from.isEmpty() && !to.isEmpty() && from != to) {
|
||||||
|
// The QML code-model does not support sub-projects, so if there are multiple mappings for a single module,
|
||||||
|
// choose the shortest one.
|
||||||
|
if (projectInfo.moduleMappings.contains(from)) {
|
||||||
|
if (to.size() < projectInfo.moduleMappings.value(from).size())
|
||||||
|
projectInfo.moduleMappings.insert(from, to);
|
||||||
|
} else {
|
||||||
|
projectInfo.moduleMappings.insert(from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
project()->setProjectLanguage(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID,
|
project()->setProjectLanguage(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID,
|
||||||
!projectInfo.sourceFiles.isEmpty());
|
!projectInfo.sourceFiles.isEmpty());
|
||||||
modelManager->updateProjectInfo(projectInfo, p);
|
modelManager->updateProjectInfo(projectInfo, p);
|
||||||
|
@@ -140,7 +140,8 @@ private:
|
|||||||
void updateProjectData();
|
void updateProjectData();
|
||||||
void updateFallbackProjectData();
|
void updateFallbackProjectData();
|
||||||
QList<ProjectExplorer::ExtraCompiler *> findExtraCompilers();
|
QList<ProjectExplorer::ExtraCompiler *> findExtraCompilers();
|
||||||
void updateQmlJSCodeModel(const QStringList &extraHeaderPaths);
|
void updateQmlJSCodeModel(const QStringList &extraHeaderPaths,
|
||||||
|
const QList<QByteArray> &moduleMappings);
|
||||||
|
|
||||||
void handleParsingSucceeded();
|
void handleParsingSucceeded();
|
||||||
void handleParsingFailed(const QString &msg);
|
void handleParsingFailed(const QString &msg);
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
Item {
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
module MyControls
|
||||||
|
import QtQuick
|
||||||
|
Oblong 1.0 Oblong.qml
|
@@ -0,0 +1,4 @@
|
|||||||
|
import QtQuick 2.15
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
module QtQuick.Controls
|
||||||
|
import QtQuick
|
||||||
|
Button 1.0 Button.qml
|
@@ -0,0 +1,5 @@
|
|||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
|
||||||
|
Item {
|
||||||
|
}
|
@@ -61,18 +61,22 @@ private slots:
|
|||||||
void importTypes_data();
|
void importTypes_data();
|
||||||
void importTypes();
|
void importTypes();
|
||||||
|
|
||||||
|
void moduleMapping_data();
|
||||||
|
void moduleMapping();
|
||||||
|
|
||||||
void initTestCase();
|
void initTestCase();
|
||||||
private:
|
private:
|
||||||
QStringList m_basePaths;
|
QStringList m_basePaths;
|
||||||
};
|
};
|
||||||
|
|
||||||
void scanDir(const QString &dir)
|
void scanDirectory(const QString &dir)
|
||||||
{
|
{
|
||||||
QFutureInterface<void> result;
|
QFutureInterface<void> result;
|
||||||
PathsAndLanguages paths;
|
PathsAndLanguages paths;
|
||||||
paths.maybeInsert(Utils::FilePath::fromString(dir), Dialect::Qml);
|
paths.maybeInsert(Utils::FilePath::fromString(dir), Dialect::Qml);
|
||||||
ModelManagerInterface::importScan(result, ModelManagerInterface::workingCopy(), paths,
|
ModelManagerInterface::importScan(result, ModelManagerInterface::workingCopy(), paths,
|
||||||
ModelManagerInterface::instance(), false);
|
ModelManagerInterface::instance(), false);
|
||||||
|
QCoreApplication::processEvents();
|
||||||
ModelManagerInterface::instance()->test_joinAllThreads();
|
ModelManagerInterface::instance()->test_joinAllThreads();
|
||||||
ViewerContext vCtx;
|
ViewerContext vCtx;
|
||||||
vCtx.paths.append(dir);
|
vCtx.paths.append(dir);
|
||||||
@@ -269,6 +273,7 @@ void tst_ImportCheck::importTypes()
|
|||||||
modelManager->activateScan();
|
modelManager->activateScan();
|
||||||
|
|
||||||
modelManager->updateSourceFiles(QStringList(qmlFile), false);
|
modelManager->updateSourceFiles(QStringList(qmlFile), false);
|
||||||
|
QCoreApplication::processEvents();
|
||||||
modelManager->test_joinAllThreads();
|
modelManager->test_joinAllThreads();
|
||||||
|
|
||||||
Snapshot snapshot = modelManager->newestSnapshot();
|
Snapshot snapshot = modelManager->newestSnapshot();
|
||||||
@@ -283,6 +288,7 @@ void tst_ImportCheck::importTypes()
|
|||||||
return link();
|
return link();
|
||||||
};
|
};
|
||||||
getContext();
|
getContext();
|
||||||
|
QCoreApplication::processEvents();
|
||||||
modelManager->test_joinAllThreads();
|
modelManager->test_joinAllThreads();
|
||||||
snapshot = modelManager->newestSnapshot();
|
snapshot = modelManager->newestSnapshot();
|
||||||
doc = snapshot.document(qmlFile);
|
doc = snapshot.document(qmlFile);
|
||||||
@@ -299,6 +305,102 @@ void tst_ImportCheck::importTypes()
|
|||||||
QVERIFY(allFound);
|
QVERIFY(allFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef QHash<QString, QString> StrStrHash;
|
||||||
|
|
||||||
|
void tst_ImportCheck::moduleMapping_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QString>("qmlFile");
|
||||||
|
QTest::addColumn<QString>("importPath");
|
||||||
|
QTest::addColumn<StrStrHash>("moduleMappings");
|
||||||
|
QTest::addColumn<QStringList>("expectedTypes");
|
||||||
|
QTest::addColumn<bool>("expectedResult");
|
||||||
|
|
||||||
|
QTest::newRow("check for plain QtQuick/Controls")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping")
|
||||||
|
<< StrStrHash()
|
||||||
|
<< QStringList({ "Item", "Button" })
|
||||||
|
<< true;
|
||||||
|
QTest::newRow("check that MyControls is not imported")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping")
|
||||||
|
<< StrStrHash()
|
||||||
|
<< QStringList({ "Item", "Oblong" })
|
||||||
|
<< false;
|
||||||
|
QTest::newRow("check that QtQuick controls cannot be found with a mapping")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping")
|
||||||
|
<< StrStrHash({ std::make_pair(QStringLiteral("QtQuick.Controls"), QStringLiteral("MyControls")) })
|
||||||
|
<< QStringList({ "Item", "Button" })
|
||||||
|
<< false;
|
||||||
|
QTest::newRow("check that custom controls can be found with a mapping")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping/importQtQuick.qml")
|
||||||
|
<< QString(TESTSRCDIR "/moduleMapping")
|
||||||
|
<< StrStrHash({ std::make_pair(QStringLiteral("QtQuick.Controls"), QStringLiteral("MyControls")) })
|
||||||
|
<< QStringList({ "Item", "Oblong" }) // item is in QtQuick, and should still be found, as only
|
||||||
|
// the QtQuick.Controls are redirected
|
||||||
|
<< true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tst_ImportCheck::moduleMapping()
|
||||||
|
{
|
||||||
|
QFETCH(QString, qmlFile);
|
||||||
|
QFETCH(QString, importPath);
|
||||||
|
QFETCH(StrStrHash, moduleMappings);
|
||||||
|
QFETCH(QStringList, expectedTypes);
|
||||||
|
QFETCH(bool, expectedResult);
|
||||||
|
|
||||||
|
// full reset
|
||||||
|
delete ModelManagerInterface::instance();
|
||||||
|
MyModelManager *modelManager = new MyModelManager;
|
||||||
|
|
||||||
|
ModelManagerInterface::ProjectInfo defaultProject;
|
||||||
|
defaultProject.importPaths = PathsAndLanguages();
|
||||||
|
QString qtQuickImportPath = QString(TESTSRCDIR "/importTypes/imports-QtQuick-qmldir-import");
|
||||||
|
defaultProject.importPaths.maybeInsert(Utils::FilePath::fromString(qtQuickImportPath), Dialect::Qml);
|
||||||
|
defaultProject.importPaths.maybeInsert(Utils::FilePath::fromString(importPath), Dialect::Qml);
|
||||||
|
defaultProject.moduleMappings = moduleMappings;
|
||||||
|
modelManager->setDefaultProject(defaultProject, nullptr);
|
||||||
|
modelManager->activateScan();
|
||||||
|
|
||||||
|
scanDirectory(importPath);
|
||||||
|
scanDirectory(qtQuickImportPath);
|
||||||
|
|
||||||
|
modelManager->updateSourceFiles(QStringList(qmlFile), false);
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
modelManager->test_joinAllThreads();
|
||||||
|
|
||||||
|
Snapshot snapshot = modelManager->newestSnapshot();
|
||||||
|
Document::Ptr doc = snapshot.document(qmlFile);
|
||||||
|
QVERIFY(!doc.isNull());
|
||||||
|
|
||||||
|
// It's unfortunate, but nowadays linking can trigger async module loads,
|
||||||
|
// so do it once to start the process, then do it again for real once the
|
||||||
|
// dependencies are available.
|
||||||
|
const auto getContext = [&]() {
|
||||||
|
Link link(snapshot, modelManager->completeVContext(modelManager->projectVContext(doc->language(), doc),doc),
|
||||||
|
modelManager->builtins(doc));
|
||||||
|
return link();
|
||||||
|
};
|
||||||
|
getContext();
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
modelManager->test_joinAllThreads();
|
||||||
|
snapshot = modelManager->newestSnapshot();
|
||||||
|
doc = snapshot.document(qmlFile);
|
||||||
|
|
||||||
|
ContextPtr context = getContext();
|
||||||
|
|
||||||
|
bool allFound = true;
|
||||||
|
for (const auto &expected : expectedTypes) {
|
||||||
|
if (!context->lookupType(doc.data(), QStringList(expected))) {
|
||||||
|
allFound = false;
|
||||||
|
qWarning() << "Type '" << expected << "' not found";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(allFound == expectedResult);
|
||||||
|
delete ModelManagerInterface::instance();
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef MANUAL_IMPORT_SCANNER
|
#ifdef MANUAL_IMPORT_SCANNER
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(test_project)
|
||||||
|
|
||||||
|
add_executable(test_exe test.cc test.qml)
|
||||||
|
|
||||||
|
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/qml_module_mappings/test_exe" CONTENT "QtQuick.Controls=MyControls\n")
|
||||||
|
|
@@ -0,0 +1,5 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int myproperty
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
module MyControls
|
||||||
|
import QtQuick
|
||||||
|
Button 1.0 Button.qml
|
9
tests/manual/qml/testprojects/modulemapping/README.txt
Normal file
9
tests/manual/qml/testprojects/modulemapping/README.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
This is a test for the module mapping feature used by Qt for MCUs.
|
||||||
|
|
||||||
|
Please add this source directory to the QML_IMPORT_PATH! A Qt for MCUs kit will do this automatically, but other kits
|
||||||
|
won't.
|
||||||
|
|
||||||
|
You can check that it works by going to test.qml, and "myproperty" should not be underligned as error. Without mapping,
|
||||||
|
the use of Button would resolve to QtQuick.Control's Button, which doesn't have that property. With the mapping, it
|
||||||
|
redirects to MyControls's Button which does have the property. You can verify this by control/command-clicking on the
|
||||||
|
property. This should take you to MyControls/Button.qml.
|
1
tests/manual/qml/testprojects/modulemapping/test.cc
Normal file
1
tests/manual/qml/testprojects/modulemapping/test.cc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
int main() {}
|
8
tests/manual/qml/testprojects/modulemapping/test.qml
Normal file
8
tests/manual/qml/testprojects/modulemapping/test.qml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.12
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Button {
|
||||||
|
myproperty: 1
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user