forked from qt-creator/qt-creator
Examples: Read and handle "documentation dependencies"
Some examples are defined in a Qt module (and therefore examples- manifest), but have dependencies on other modules. The example-manifests contain a `module` attribute for their `instructionals` tag, and examples can contain "docdependencies" items in their meta data, which are comma-separated lists of these module names. Read the `module` attributes (the meta data is already generically read) and check if the current set of examples-manifest contains all required ones for an example. Hide it, if not. Task-number: QTBUG-120759 Change-Id: Ib8ff80cf8a13965e57b5c4317e5d121c4127278b Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: Kai Köhne <kai.koehne@qt.io>
This commit is contained in:
@@ -299,35 +299,53 @@ ExamplesViewController::ExamplesViewController(ExampleSetModel *exampleSetModel,
|
||||
updateExamples();
|
||||
}
|
||||
|
||||
static bool isValidExampleOrDemo(ExampleItem *item)
|
||||
static std::function<bool(ExampleItem *)> isValidExampleOrDemo(
|
||||
const QSet<QString> &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<QString> instructionalsModules;
|
||||
QStringList categoryOrder;
|
||||
QList<ExampleItem *> 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)) {
|
||||
|
@@ -283,20 +283,21 @@ expected_str<ParsedExamples> parseExamples(const QByteArray &manifestData,
|
||||
const bool examples)
|
||||
{
|
||||
const FilePath path = manifestPath.parentDir();
|
||||
QStringList categoryOrder;
|
||||
QList<ExampleItem *> 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<ParsedExamples> 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)
|
||||
|
@@ -34,6 +34,7 @@ public:
|
||||
class QTSUPPORT_TEST_EXPORT ParsedExamples
|
||||
{
|
||||
public:
|
||||
QString instructionalsModule;
|
||||
QList<ExampleItem *> items;
|
||||
QStringList categoryOrder;
|
||||
};
|
||||
|
@@ -71,6 +71,7 @@ static ExampleItem fetchItem()
|
||||
void tst_Examples::parsing_data()
|
||||
{
|
||||
QTest::addColumn<QByteArray>("data");
|
||||
QTest::addColumn<QString>("instructionalsModule");
|
||||
QTest::addColumn<bool>("isExamples");
|
||||
QTest::addColumn<QString>("name");
|
||||
QTest::addColumn<QString>("description");
|
||||
@@ -94,7 +95,7 @@ void tst_Examples::parsing_data()
|
||||
|
||||
QTest::addRow("example")
|
||||
<< QByteArray(R"raw(
|
||||
<instructionals module="Qt">
|
||||
<instructionals module="QtGui">
|
||||
<examples>
|
||||
<example docUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/qtwidgets-widgets-analogclock-example.html"
|
||||
imageUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/images/analogclock-example.png"
|
||||
@@ -119,8 +120,8 @@ void tst_Examples::parsing_data()
|
||||
<category>Embedded</category>
|
||||
</categories>
|
||||
</instructionals>
|
||||
)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(
|
||||
<instructionals module="Qt">
|
||||
<instructionals module="QtQuick3D">
|
||||
<examples>
|
||||
<example name="No Category, highlighted"
|
||||
isHighlighted="true">
|
||||
</example>
|
||||
</examples>
|
||||
</instructionals>
|
||||
)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()
|
||||
</tutorial>
|
||||
</tutorials>
|
||||
</instructionals>
|
||||
)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);
|
||||
|
Reference in New Issue
Block a user