From ce52c245aaccf3183ef4759882e25b51b539195a Mon Sep 17 00:00:00 2001 From: Ulf Hermann Date: Mon, 1 Mar 2021 10:11:18 +0100 Subject: Add a "prefer" directive to qmldir The argument is the path from which further files from the module should be loaded. This allows us to load potentially pre-compiled files from a resource contained in a plugin. The qmldir file has to be stored outside the plugin in order for the QML engine to find it, but the QML files can now be stored inside the plugin. [ChangeLog][QtQml] You can now specify that QML files in a QML module should be loaded from a different place than the directory where the qmldir file is found. To do this, add a "prefer" directive to the qmldir file. The most useful application of this is adding the QML files as resources to a plugin, and using qmlcachegen to pre-compile them. You can then add their path inside the resource file system as "prefer" path in order to make the engine load the pre-compiled files. The plain files should still be installed next to the qmldir file so that they remain visible to tooling. Change-Id: Ib281b39230621b3762432095a47fb499412cbaa6 Reviewed-by: Fabian Kosmale --- src/qml/doc/src/qmllanguageref/modules/qmldir.qdoc | 16 ++++++ src/qml/qml/qqmlimport.cpp | 14 +++++- src/qml/qml/qqmltypeloaderqmldircontent.cpp | 5 ++ src/qml/qml/qqmltypeloaderqmldircontent_p.h | 1 + src/qml/qmldirparser/qqmldirparser.cpp | 27 ++++++++++ src/qml/qmldirparser/qqmldirparser_p.h | 2 + .../qml/qqmldirparser/data/prefer-no-slash/qmldir | 5 ++ tests/auto/qml/qqmldirparser/data/prefer/qmldir | 5 ++ .../auto/qml/qqmldirparser/data/two-prefer/qmldir | 6 +++ tests/auto/qml/qqmldirparser/tst_qqmldirparser.cpp | 57 ++++++++++++++++++++++ tests/auto/qml/qqmlimport/CMakeLists.txt | 7 +++ tests/auto/qml/qqmlimport/Preferred.qml | 5 ++ .../qqmlimport/data/ModuleWithPrefer/Preferred.qml | 5 ++ .../qml/qqmlimport/data/ModuleWithPrefer/qmldir | 3 ++ tests/auto/qml/qqmlimport/data/prefer.qml | 3 ++ tests/auto/qml/qqmlimport/tst_qqmlimport.cpp | 12 +++++ 16 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 tests/auto/qml/qqmldirparser/data/prefer-no-slash/qmldir create mode 100644 tests/auto/qml/qqmldirparser/data/prefer/qmldir create mode 100644 tests/auto/qml/qqmldirparser/data/two-prefer/qmldir create mode 100644 tests/auto/qml/qqmlimport/Preferred.qml create mode 100644 tests/auto/qml/qqmlimport/data/ModuleWithPrefer/Preferred.qml create mode 100644 tests/auto/qml/qqmlimport/data/ModuleWithPrefer/qmldir create mode 100644 tests/auto/qml/qqmlimport/data/prefer.qml diff --git a/src/qml/doc/src/qmllanguageref/modules/qmldir.qdoc b/src/qml/doc/src/qmllanguageref/modules/qmldir.qdoc index 8f6aa0ae7c..95e8a179de 100644 --- a/src/qml/doc/src/qmllanguageref/modules/qmldir.qdoc +++ b/src/qml/doc/src/qmllanguageref/modules/qmldir.qdoc @@ -296,6 +296,22 @@ designersupported The items of an unsupported plugin are not painted in the Qt Quick Designer, but they are still available as empty boxes and the properties can be edited. + \row + \li + \code +prefer + \endcode + + \li This property directs the QML engine to load any further files for this + module from , rather than the current directory. This can be used + to load files compiled with qmlcachegen. + + For example, you can add a module's QML files as resources to a resource + path \c{:/my/path/MyModule/}. Then, add \c{prefer :/my/path/MyModule} to + the qmldir file in order to use the files in the resource system, rather + than the ones in the file system. If you then use qmlcachegen for those, + the pre-compiled files will be available to any clients of the module. + \endtable Each command in a \c qmldir file must be on a separate line. diff --git a/src/qml/qml/qqmlimport.cpp b/src/qml/qml/qqmlimport.cpp index 535b6713ba..0016d4bcc4 100644 --- a/src/qml/qml/qqmlimport.cpp +++ b/src/qml/qml/qqmlimport.cpp @@ -654,8 +654,18 @@ bool QQmlImportInstance::setQmldirContent(const QString &resolvedUrl, const QQmlTypeLoaderQmldirContent &qmldir, QQmlImportNamespace *nameSpace, QList *errors) { - Q_ASSERT(resolvedUrl.endsWith(Slash)); - url = resolvedUrl; + + const QString preferredPath = qmldir.preferredPath(); + if (preferredPath.isEmpty()) { + Q_ASSERT(resolvedUrl.endsWith(Slash)); + url = resolvedUrl; + } else { + Q_ASSERT(preferredPath.endsWith(Slash)); + if (preferredPath.startsWith(u':')) + url = QStringLiteral("qrc") + preferredPath; + else + url = QUrl::fromLocalFile(preferredPath).toString(); + } qmlDirComponents = qmldir.components(); diff --git a/src/qml/qml/qqmltypeloaderqmldircontent.cpp b/src/qml/qml/qqmltypeloaderqmldircontent.cpp index 0eddf796d7..ca9f5fc80d 100644 --- a/src/qml/qml/qqmltypeloaderqmldircontent.cpp +++ b/src/qml/qml/qqmltypeloaderqmldircontent.cpp @@ -115,6 +115,11 @@ QString QQmlTypeLoaderQmldirContent::qmldirLocation() const return m_location; } +QString QQmlTypeLoaderQmldirContent::preferredPath() const +{ + return m_parser.preferredPath(); +} + bool QQmlTypeLoaderQmldirContent::designerSupported() const { return m_parser.designerSupported(); diff --git a/src/qml/qml/qqmltypeloaderqmldircontent_p.h b/src/qml/qml/qqmltypeloaderqmldircontent_p.h index f9b68f3410..11f98b5c63 100644 --- a/src/qml/qml/qqmltypeloaderqmldircontent_p.h +++ b/src/qml/qml/qqmltypeloaderqmldircontent_p.h @@ -81,6 +81,7 @@ public: QQmlDirImports imports() const; QString qmldirLocation() const; + QString preferredPath() const; bool designerSupported() const; diff --git a/src/qml/qmldirparser/qqmldirparser.cpp b/src/qml/qmldirparser/qqmldirparser.cpp index 403c04b1e1..1dbf35f1d3 100644 --- a/src/qml/qmldirparser/qqmldirparser.cpp +++ b/src/qml/qmldirparser/qqmldirparser.cpp @@ -299,6 +299,28 @@ bool QQmlDirParser::parse(const QString &source) || sections[0] == QLatin1String("depends")) { if (!readImport(sections, sectionCount, Import::Default)) continue; + } else if (sections[0] == QLatin1String("prefer")) { + if (sectionCount < 2) { + reportError(lineNumber, 0, + QStringLiteral("prefer directive requires one argument, " + "but %1 were provided").arg(sectionCount - 1)); + continue; + } + + if (!_preferredPath.isEmpty()) { + reportError(lineNumber, 0, QStringLiteral( + "only one prefer directive may be defined in a qmldir file")); + continue; + } + + if (!sections[1].endsWith(u'/')) { + // Yes. People should realize it's a directory. + reportError(lineNumber, 0, QStringLiteral( + "the preferred directory has to end with a '/'")); + continue; + } + + _preferredPath = sections[1]; } else if (sectionCount == 2) { // No version specified (should only be used for relative qmldir files) const Component entry(sections[0], sections[1], QTypeRevision()); @@ -416,6 +438,11 @@ QStringList QQmlDirParser::classNames() const return _classNames; } +QString QQmlDirParser::preferredPath() const +{ + return _preferredPath; +} + QDebug &operator<< (QDebug &debug, const QQmlDirParser::Component &component) { const QString output = QStringLiteral("{%1 %2.%3}") diff --git a/src/qml/qmldirparser/qqmldirparser_p.h b/src/qml/qmldirparser/qqmldirparser_p.h index d71dbfe9dc..eabaceae91 100644 --- a/src/qml/qmldirparser/qqmldirparser_p.h +++ b/src/qml/qmldirparser/qqmldirparser_p.h @@ -160,6 +160,7 @@ public: QStringList typeInfos() const; QStringList classNames() const; + QString preferredPath() const; private: bool maybeAddComponent(const QString &typeName, const QString &fileName, const QString &version, QHash &hash, int lineNumber = -1, bool multi = true); @@ -168,6 +169,7 @@ private: private: QList _errors; QString _typeNamespace; + QString _preferredPath; QMultiHash _components; QList _dependencies; QList _imports; diff --git a/tests/auto/qml/qqmldirparser/data/prefer-no-slash/qmldir b/tests/auto/qml/qqmldirparser/data/prefer-no-slash/qmldir new file mode 100644 index 0000000000..3ad24eed20 --- /dev/null +++ b/tests/auto/qml/qqmldirparser/data/prefer-no-slash/qmldir @@ -0,0 +1,5 @@ +module FooBar +classname FooBarInstance +prefer :/foo/bar +plugin doesnotexist +Foo 1.0 Foo.qml diff --git a/tests/auto/qml/qqmldirparser/data/prefer/qmldir b/tests/auto/qml/qqmldirparser/data/prefer/qmldir new file mode 100644 index 0000000000..06cb227c43 --- /dev/null +++ b/tests/auto/qml/qqmldirparser/data/prefer/qmldir @@ -0,0 +1,5 @@ +module FooBar +classname FooBarInstance +prefer :/foo/bar/ +plugin doesnotexist +Foo 1.0 Foo.qml diff --git a/tests/auto/qml/qqmldirparser/data/two-prefer/qmldir b/tests/auto/qml/qqmldirparser/data/two-prefer/qmldir new file mode 100644 index 0000000000..08a5ca6834 --- /dev/null +++ b/tests/auto/qml/qqmldirparser/data/two-prefer/qmldir @@ -0,0 +1,6 @@ +module FooBar +classname FooBarInstance +prefer :/foo/bar/ +prefer :/wrong/wrong/ +plugin doesnotexist +Foo 1.0 Foo.qml diff --git a/tests/auto/qml/qqmldirparser/tst_qqmldirparser.cpp b/tests/auto/qml/qqmldirparser/tst_qqmldirparser.cpp index d53de4fd52..8f0c58653b 100644 --- a/tests/auto/qml/qqmldirparser/tst_qqmldirparser.cpp +++ b/tests/auto/qml/qqmldirparser/tst_qqmldirparser.cpp @@ -150,6 +150,7 @@ namespace { void tst_qqmldirparser::parse_data() { QTest::addColumn("file"); + QTest::addColumn("preferredPath"); QTest::addColumn("errors"); QTest::addColumn("plugins"); QTest::addColumn("classnames"); @@ -160,6 +161,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("empty") << "empty/qmldir" + << QString() << QStringList() << QStringList() << QStringList() @@ -170,6 +172,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("no-content") << "no-content/qmldir" + << QString() << QStringList() << QStringList() << QStringList() @@ -180,6 +183,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("one-section") << "one-section/qmldir" + << QString() << (QStringList() << "qmldir:1: a component declaration requires two or three arguments, but 1 were provided") << QStringList() << QStringList() @@ -190,6 +194,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("four-sections") << "four-sections/qmldir" + << QString() << (QStringList() << "qmldir:1: a component declaration requires two or three arguments, but 4 were provided") << QStringList() << QStringList() @@ -200,6 +205,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("incomplete-module") << "incomplete-module/qmldir" + << QString() << (QStringList() << "qmldir:1: module identifier directive requires one argument, but 0 were provided") << QStringList() << QStringList() @@ -210,6 +216,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("excessive-module") << "excessive-module/qmldir" + << QString() << (QStringList() << "qmldir:1: module identifier directive requires one argument, but 2 were provided") << QStringList() << QStringList() @@ -220,6 +227,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("repeated-module") << "repeated-module/qmldir" + << QString() << (QStringList() << "qmldir:2: only one module identifier directive may be defined in a qmldir file") << QStringList() << QStringList() @@ -230,6 +238,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("non-first-module") << "non-first-module/qmldir" + << QString() << (QStringList() << "qmldir:2: module identifier directive must be the first directive in a qmldir file") << (QStringList() << "foo|") << QStringList() @@ -240,6 +249,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("incomplete-plugin") << "incomplete-plugin/qmldir" + << QString() << (QStringList() << "qmldir:1: plugin directive requires one or two arguments, but 0 were provided") << QStringList() << QStringList() @@ -250,6 +260,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("excessive-plugin") << "excessive-plugin/qmldir" + << QString() << (QStringList() << "qmldir:1: plugin directive requires one or two arguments, but 3 were provided") << QStringList() << QStringList() @@ -260,6 +271,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("name-plugin") << "name-plugin/qmldir" + << QString() << QStringList() << (QStringList() << "foo|") << QStringList() @@ -270,6 +282,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("name-path-plugin") << "name-path-plugin/qmldir" + << QString() << QStringList() << (QStringList() << "foo|bar") << QStringList() @@ -280,6 +293,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("unversioned-component") << "unversioned-component/qmldir" + << QString() << QStringList() << QStringList() << QStringList() @@ -290,6 +304,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("invalid-versioned-component") << "invalid-versioned-component/qmldir" + << QString() << (QStringList() << "qmldir:1: invalid version 100, expected .") << QStringList() << QStringList() @@ -300,6 +315,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("versioned-component") << "versioned-component/qmldir" + << QString() << QStringList() << QStringList() << QStringList() @@ -310,6 +326,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("invalid-versioned-script") << "invalid-versioned-script/qmldir" + << QString() << (QStringList() << "qmldir:1: invalid version 100, expected .") << QStringList() << QStringList() @@ -320,6 +337,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("versioned-script") << "versioned-script/qmldir" + << QString() << QStringList() << QStringList() << QStringList() @@ -330,6 +348,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("multiple") << "multiple/qmldir" + << QString() << QStringList() << (QStringList() << "PluginA|plugina.so") << QStringList() @@ -342,6 +361,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("designersupported-yes") << "designersupported-yes/qmldir" + << QString() << QStringList() << (QStringList() << "foo|") << QStringList() @@ -352,6 +372,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("designersupported-no") << "designersupported-no/qmldir" + << QString() << QStringList() << (QStringList() << "foo|") << QStringList() @@ -362,6 +383,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("invalid-versioned-dependency") << "invalid-versioned-dependency/qmldir" + << QString() << (QStringList() << "qmldir:1: invalid version 100, expected .") << QStringList() << QStringList() @@ -372,6 +394,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("dependency") << "dependency/qmldir" + << QString() << QStringList() << (QStringList() << "foo|") << QStringList() @@ -382,6 +405,7 @@ void tst_qqmldirparser::parse_data() QTest::newRow("classname") << "classname/qmldir" + << QString() << QStringList() << (QStringList() << "qtquick2plugin|") << (QStringList() << "QtQuick2Plugin") @@ -389,6 +413,39 @@ void tst_qqmldirparser::parse_data() << QStringList() << QStringList() << true; + + QTest::newRow("prefer") + << "prefer/qmldir" + << ":/foo/bar/" + << QStringList() + << QStringList({"doesnotexist|"}) + << QStringList({"FooBarInstance"}) + << QStringList({"Foo|Foo.qml|1|0|false"}) + << QStringList() + << QStringList() + << false; + + QTest::newRow("two-prefer") + << "two-prefer/qmldir" + << ":/foo/bar/" + << QStringList({"qmldir:4: only one prefer directive may be defined in a qmldir file"}) + << QStringList({"doesnotexist|"}) + << QStringList({"FooBarInstance"}) + << QStringList({"Foo|Foo.qml|1|0|false"}) + << QStringList() + << QStringList() + << false; + + QTest::newRow("prefer-no-slash") + << "prefer-no-slash/qmldir" + << QString() + << QStringList({"qmldir:3: the preferred directory has to end with a '/'"}) + << QStringList({"doesnotexist|"}) + << QStringList({"FooBarInstance"}) + << QStringList({"Foo|Foo.qml|1|0|false"}) + << QStringList() + << QStringList() + << false; } void tst_qqmldirparser::parse() diff --git a/tests/auto/qml/qqmlimport/CMakeLists.txt b/tests/auto/qml/qqmlimport/CMakeLists.txt index dba34c1a0b..3d4f01da2c 100644 --- a/tests/auto/qml/qqmlimport/CMakeLists.txt +++ b/tests/auto/qml/qqmlimport/CMakeLists.txt @@ -36,6 +36,13 @@ qt_internal_add_test(tst_qqmlimport TESTDATA ${test_data} ) +qt_internal_add_resource(tst_qqmlimport "preferred" + PREFIX + "/qqmlimport/ModuleWithPrefer/" + FILES + "Preferred.qml" +) + ## Scopes: ##################################################################### diff --git a/tests/auto/qml/qqmlimport/Preferred.qml b/tests/auto/qml/qqmlimport/Preferred.qml new file mode 100644 index 0000000000..8fda58451e --- /dev/null +++ b/tests/auto/qml/qqmlimport/Preferred.qml @@ -0,0 +1,5 @@ +import QtQml + +QtObject { + objectName: "right" +} diff --git a/tests/auto/qml/qqmlimport/data/ModuleWithPrefer/Preferred.qml b/tests/auto/qml/qqmlimport/data/ModuleWithPrefer/Preferred.qml new file mode 100644 index 0000000000..a4af255b56 --- /dev/null +++ b/tests/auto/qml/qqmlimport/data/ModuleWithPrefer/Preferred.qml @@ -0,0 +1,5 @@ +import QtQml + +QtObject { + objectName: "wrong" +} diff --git a/tests/auto/qml/qqmlimport/data/ModuleWithPrefer/qmldir b/tests/auto/qml/qqmlimport/data/ModuleWithPrefer/qmldir new file mode 100644 index 0000000000..99f2a28cd1 --- /dev/null +++ b/tests/auto/qml/qqmlimport/data/ModuleWithPrefer/qmldir @@ -0,0 +1,3 @@ +module ModuleWithPrefer +prefer :/qqmlimport/ModuleWithPrefer/ +Preferred 1.0 Preferred.qml diff --git a/tests/auto/qml/qqmlimport/data/prefer.qml b/tests/auto/qml/qqmlimport/data/prefer.qml new file mode 100644 index 0000000000..7a6764e08f --- /dev/null +++ b/tests/auto/qml/qqmlimport/data/prefer.qml @@ -0,0 +1,3 @@ +import ModuleWithPrefer + +Preferred {} diff --git a/tests/auto/qml/qqmlimport/tst_qqmlimport.cpp b/tests/auto/qml/qqmlimport/tst_qqmlimport.cpp index df3d2ba625..4f2b4f6b13 100644 --- a/tests/auto/qml/qqmlimport/tst_qqmlimport.cpp +++ b/tests/auto/qml/qqmlimport/tst_qqmlimport.cpp @@ -55,6 +55,7 @@ private slots: void importDependenciesPrecedence(); void cleanup(); void envResourceImportPath(); + void preferResourcePath(); }; void tst_QQmlImport::cleanup() @@ -89,6 +90,17 @@ void tst_QQmlImport::envResourceImportPath() QVERIFY((importPaths.contains(path.startsWith(u':') ? QLatin1String("qrc") + path : path))); } +void tst_QQmlImport::preferResourcePath() +{ + QQmlEngine engine; + engine.addImportPath(dataDirectory()); + + QQmlComponent component(&engine, testFileUrl("prefer.qml")); + QVERIFY2(component.isReady(), component.errorString().toUtf8()); + QScopedPointer o(component.create()); + QCOMPARE(o->objectName(), "right"); +} + void tst_QQmlImport::testDesignerSupported() { QQuickView *window = new QQuickView(); -- cgit v1.2.3