diff --git a/src/plugins/qtsupport/exampleslistmodel.cpp b/src/plugins/qtsupport/exampleslistmodel.cpp index d8593cb1fd8..18aa6cc3811 100644 --- a/src/plugins/qtsupport/exampleslistmodel.cpp +++ b/src/plugins/qtsupport/exampleslistmodel.cpp @@ -299,35 +299,53 @@ ExamplesViewController::ExamplesViewController(ExampleSetModel *exampleSetModel, updateExamples(); } -static bool isValidExampleOrDemo(ExampleItem *item) +static std::function isValidExampleOrDemo( + const QSet &instructionalsModules) { - QTC_ASSERT(item, return false); - if (item->type == Tutorial) - return true; - static QString invalidPrefix = QLatin1String("qthelp:////"); /* means that the qthelp url + return [instructionalsModules](ExampleItem *item) -> bool { + QTC_ASSERT(item, return false); + if (item->type == Tutorial) + return true; + static QString invalidPrefix = QLatin1String("qthelp:////"); /* means that the qthelp url doesn't have any namespace */ - QString reason; - bool ok = true; - if (!item->hasSourceCode || !item->projectPath.exists()) { - ok = false; - reason = QString::fromLatin1("projectPath \"%1\" empty or does not exist") - .arg(item->projectPath.toUserOutput()); - } else if (item->imageUrl.startsWith(invalidPrefix) || !QUrl(item->imageUrl).isValid()) { - ok = false; - reason = QString::fromLatin1("imageUrl \"%1\" not valid").arg(item->imageUrl); - } else if (!item->docUrl.isEmpty() - && (item->docUrl.startsWith(invalidPrefix) || !QUrl(item->docUrl).isValid())) { - ok = false; - reason = QString::fromLatin1("docUrl \"%1\" non-empty but not valid").arg(item->docUrl); - } - if (!ok) { - item->tags.append(QLatin1String("broken")); - qCDebug(log) << QString::fromLatin1("ERROR: Item \"%1\" broken: %2").arg(item->name, reason); - } - if (item->description.isEmpty()) - qCDebug(log) << QString::fromLatin1("WARNING: Item \"%1\" has no description") - .arg(item->name); - return ok || debugExamples(); + QString reason; + bool ok = true; + if (!item->hasSourceCode || !item->projectPath.exists()) { + ok = false; + reason = QString::fromLatin1("projectPath \"%1\" empty or does not exist") + .arg(item->projectPath.toUserOutput()); + } else if (item->imageUrl.startsWith(invalidPrefix) || !QUrl(item->imageUrl).isValid()) { + ok = false; + reason = QString::fromLatin1("imageUrl \"%1\" not valid").arg(item->imageUrl); + } else if (!item->docUrl.isEmpty() + && (item->docUrl.startsWith(invalidPrefix) || !QUrl(item->docUrl).isValid())) { + ok = false; + reason = QString::fromLatin1("docUrl \"%1\" non-empty but not valid").arg(item->docUrl); + } + if (!ok) { + item->tags.append(QLatin1String("broken")); + qCDebug(log) << QString::fromLatin1("ERROR: Item \"%1\" broken: %2") + .arg(item->name, reason); + } + if (item->description.isEmpty()) + qCDebug(log) << QString::fromLatin1("WARNING: Item \"%1\" has no description") + .arg(item->name); + // a single docdependencies entry is a string of items concatenated with ',' + // the collected meta data can be a list of this + for (const QString &entry : item->metaData.value("docdependencies")) { + const QStringList deps = entry.split(','); + for (const QString &dep : deps) { + if (!instructionalsModules.contains(dep)) { + item->tags.append("unresolvedDependency"); + qCDebug(log) << QLatin1String("INFO: Item \"%1\" requires \"%2\"") + .arg(item->name, dep); + ok = false; + break; + } + } + } + return ok || debugExamples(); + }; } // ordered list of "known" categories @@ -365,6 +383,7 @@ void ExamplesViewController::updateExamples() &demosInstallPath, &qtVersion, m_isExamples); + QSet instructionalsModules; QStringList categoryOrder; QList items; for (const QString &exampleSource : sources) { @@ -382,10 +401,12 @@ void ExamplesViewController::updateExamples() << result.error(); continue; } - items += filtered(result->items, isValidExampleOrDemo); + instructionalsModules.insert(result->instructionalsModule); + items += result->items; if (categoryOrder.isEmpty()) categoryOrder = result->categoryOrder; } + items = filtered(items, isValidExampleOrDemo(instructionalsModules)); if (m_isExamples) { if (m_exampleSetModel->selectedQtSupports(Android::Constants::ANDROID_DEVICE_TYPE)) { diff --git a/src/plugins/qtsupport/examplesparser.cpp b/src/plugins/qtsupport/examplesparser.cpp index 293c19ecb8f..678a3a3139d 100644 --- a/src/plugins/qtsupport/examplesparser.cpp +++ b/src/plugins/qtsupport/examplesparser.cpp @@ -283,20 +283,21 @@ expected_str parseExamples(const QByteArray &manifestData, const bool examples) { const FilePath path = manifestPath.parentDir(); - QStringList categoryOrder; - QList items; + ParsedExamples result; QXmlStreamReader reader(manifestData); while (!reader.atEnd()) { switch (reader.readNext()) { case QXmlStreamReader::StartElement: - if (categoryOrder.isEmpty() && reader.name() == QLatin1String("categories")) - categoryOrder = parseCategories(&reader); + if (reader.name() == QLatin1String("instructionals")) + result.instructionalsModule = reader.attributes().value("module").toString(); + else if (result.categoryOrder.isEmpty() && reader.name() == QLatin1String("categories")) + result.categoryOrder = parseCategories(&reader); else if (examples && reader.name() == QLatin1String("examples")) - items += parseExamples(&reader, path, examplesInstallPath); + result.items += parseExamples(&reader, path, examplesInstallPath); else if (examples && reader.name() == QLatin1String("demos")) - items += parseDemos(&reader, path, demosInstallPath); + result.items += parseDemos(&reader, path, demosInstallPath); else if (!examples && reader.name() == QLatin1String("tutorials")) - items += parseTutorials(&reader, path); + result.items += parseTutorials(&reader, path); break; default: // nothing break; @@ -304,14 +305,14 @@ expected_str parseExamples(const QByteArray &manifestData, } if (reader.hasError()) { - qDeleteAll(items); + qDeleteAll(result.items); return make_unexpected(QString("Could not parse file \"%1\" as XML document: %2:%3: %4") .arg(manifestPath.toUserOutput()) .arg(reader.lineNumber()) .arg(reader.columnNumber()) .arg(reader.errorString())); } - return {{items, categoryOrder}}; + return result; } static bool sortByHighlightedAndName(ExampleItem *first, ExampleItem *second) diff --git a/src/plugins/qtsupport/examplesparser.h b/src/plugins/qtsupport/examplesparser.h index aee539ad62b..645cf08739e 100644 --- a/src/plugins/qtsupport/examplesparser.h +++ b/src/plugins/qtsupport/examplesparser.h @@ -34,6 +34,7 @@ public: class QTSUPPORT_TEST_EXPORT ParsedExamples { public: + QString instructionalsModule; QList items; QStringList categoryOrder; }; diff --git a/tests/auto/examples/tst_examples.cpp b/tests/auto/examples/tst_examples.cpp index 5d30c8710b5..81077499d85 100644 --- a/tests/auto/examples/tst_examples.cpp +++ b/tests/auto/examples/tst_examples.cpp @@ -71,6 +71,7 @@ static ExampleItem fetchItem() void tst_Examples::parsing_data() { QTest::addColumn("data"); + QTest::addColumn("instructionalsModule"); QTest::addColumn("isExamples"); QTest::addColumn("name"); QTest::addColumn("description"); @@ -94,7 +95,7 @@ void tst_Examples::parsing_data() QTest::addRow("example") << QByteArray(R"raw( - + Embedded - )raw") << /*isExamples=*/true - << "Analog Clock" + )raw") << "QtGui" + << /*isExamples=*/true << "Analog Clock" << "The Analog Clock example shows how to draw the contents of a custom widget." << "qthelp://org.qt-project.qtwidgets.660/qtwidgets/images/analogclock-example.png" << QStringList{"ios", "widgets"} @@ -140,17 +141,18 @@ void tst_Examples::parsing_data() QTest::addRow("no category, highlighted") << QByteArray(R"raw( - + - )raw") << /*isExamples=*/true - << "No Category, highlighted" << QString() << QString() << QStringList() - << FilePath("examples") << QString() << FilePaths() << FilePath() << FilePaths() << Example - << /*hasSourceCode=*/false << false << /*isHighlighted=*/true << "" + )raw") << "QtQuick3D" + << /*isExamples=*/true << "No Category, highlighted" << QString() << QString() + << QStringList() << FilePath("examples") << QString() << FilePaths() << FilePath() + << FilePaths() << Example << /*hasSourceCode=*/false << false << /*isHighlighted=*/true + << "" << "" << QStringList() << MetaData() << QStringList{"Featured"} << QStringList(); QTest::addRow("tutorial with category") @@ -172,8 +174,8 @@ void tst_Examples::parsing_data() -)raw") << /*isExamples=*/false - << "A tutorial" +)raw") << "Qt" + << /*isExamples=*/false << "A tutorial" << "A dummy tutorial." << ":qtsupport/images/icons/tutorialicon.png" << QStringList{"qt creator", "build", "compile", "help"} << FilePath() @@ -187,6 +189,7 @@ void tst_Examples::parsing_data() void tst_Examples::parsing() { QFETCH(QByteArray, data); + QFETCH(QString, instructionalsModule); QFETCH(bool, isExamples); QFETCH(QStringList, categories); QFETCH(QStringList, categoryOrder); @@ -198,6 +201,7 @@ void tst_Examples::parsing() FilePath("demos"), isExamples); QVERIFY(result); + QCOMPARE(result->instructionalsModule, instructionalsModule); QCOMPARE(result->categoryOrder, categoryOrder); QCOMPARE(result->items.size(), 1); const ExampleItem item = *result->items.at(0);