Examples: Support manifest-defined category order

Reads a separate sorted list of categories from the manifest files.
The first of these lists that is found in the manifest files is used.
For example the Qt documentation defines the list in the manifest file
for qtdoc.

Change-Id: I57c2779862a5ebfc27707b53d43d4ed9e7e8c5f9
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Eike Ziller
2023-07-21 09:21:20 +02:00
parent 5af88f5702
commit 08bbe885b4
5 changed files with 92 additions and 29 deletions

View File

@@ -355,13 +355,14 @@ void ExamplesViewController::updateExamples()
&qtVersion); &qtVersion);
m_view->clear(); m_view->clear();
QStringList categoryOrder;
QList<ExampleItem *> items; QList<ExampleItem *> items;
for (const QString &exampleSource : sources) { for (const QString &exampleSource : sources) {
const auto manifest = FilePath::fromUserInput(exampleSource); const auto manifest = FilePath::fromUserInput(exampleSource);
qCDebug(log) << QString::fromLatin1("Reading file \"%1\"...") qCDebug(log) << QString::fromLatin1("Reading file \"%1\"...")
.arg(manifest.absoluteFilePath().toUserOutput()); .arg(manifest.absoluteFilePath().toUserOutput());
const expected_str<QList<ExampleItem *>> result const expected_str<ParsedExamples> result
= parseExamples(manifest, = parseExamples(manifest,
FilePath::fromUserInput(examplesInstallPath), FilePath::fromUserInput(examplesInstallPath),
FilePath::fromUserInput(demosInstallPath), FilePath::fromUserInput(demosInstallPath),
@@ -371,7 +372,9 @@ void ExamplesViewController::updateExamples()
<< result.error(); << result.error();
continue; continue;
} }
items += filtered(*result, isValidExampleOrDemo); items += filtered(result->items, isValidExampleOrDemo);
if (categoryOrder.isEmpty())
categoryOrder = result->categoryOrder;
} }
if (m_isExamples) { if (m_isExamples) {
@@ -386,7 +389,8 @@ void ExamplesViewController::updateExamples()
} }
const bool sortIntoCategories = !m_isExamples || qtVersion >= *minQtVersionForCategories; const bool sortIntoCategories = !m_isExamples || qtVersion >= *minQtVersionForCategories;
const QStringList order = m_isExamples ? *defaultOrder : QStringList(); const QStringList order = categoryOrder.isEmpty() && m_isExamples ? *defaultOrder
: categoryOrder;
const QList<std::pair<Section, QList<ExampleItem *>>> sections const QList<std::pair<Section, QList<ExampleItem *>>> sections
= getCategories(items, sortIntoCategories, order, m_isExamples); = getCategories(items, sortIntoCategories, order, m_isExamples);
for (int i = 0; i < sections.size(); ++i) { for (int i = 0; i < sections.size(); ++i) {

View File

@@ -75,6 +75,26 @@ static QHash<QString, QStringList> parseMeta(QXmlStreamReader *reader)
return result; return result;
} }
static QStringList parseCategories(QXmlStreamReader *reader)
{
QStringList categoryOrder;
while (!reader->atEnd()) {
switch (reader->readNext()) {
case QXmlStreamReader::StartElement:
if (reader->name() == QLatin1String("category"))
categoryOrder.append(reader->readElementText());
break;
case QXmlStreamReader::EndElement:
if (reader->name() == QLatin1String("categories"))
return categoryOrder;
break;
default:
break;
}
}
return categoryOrder;
}
static QList<ExampleItem *> parseExamples(QXmlStreamReader *reader, static QList<ExampleItem *> parseExamples(QXmlStreamReader *reader,
const FilePath &projectsOffset, const FilePath &projectsOffset,
const FilePath &examplesInstallPath) const FilePath &examplesInstallPath)
@@ -257,10 +277,10 @@ static QList<ExampleItem *> parseTutorials(QXmlStreamReader *reader, const FileP
return result; return result;
} }
expected_str<QList<ExampleItem *>> parseExamples(const FilePath &manifest, expected_str<ParsedExamples> parseExamples(const FilePath &manifest,
const FilePath &examplesInstallPath, const FilePath &examplesInstallPath,
const FilePath &demosInstallPath, const FilePath &demosInstallPath,
const bool examples) const bool examples)
{ {
const expected_str<QByteArray> contents = manifest.fileContents(); const expected_str<QByteArray> contents = manifest.fileContents();
if (!contents) if (!contents)
@@ -269,19 +289,22 @@ expected_str<QList<ExampleItem *>> parseExamples(const FilePath &manifest,
return parseExamples(*contents, manifest, examplesInstallPath, demosInstallPath, examples); return parseExamples(*contents, manifest, examplesInstallPath, demosInstallPath, examples);
} }
expected_str<QList<ExampleItem *>> parseExamples(const QByteArray &manifestData, expected_str<ParsedExamples> parseExamples(const QByteArray &manifestData,
const Utils::FilePath &manifestPath, const Utils::FilePath &manifestPath,
const FilePath &examplesInstallPath, const FilePath &examplesInstallPath,
const FilePath &demosInstallPath, const FilePath &demosInstallPath,
const bool examples) const bool examples)
{ {
const FilePath path = manifestPath.parentDir(); const FilePath path = manifestPath.parentDir();
QStringList categoryOrder;
QList<ExampleItem *> items; QList<ExampleItem *> items;
QXmlStreamReader reader(manifestData); QXmlStreamReader reader(manifestData);
while (!reader.atEnd()) { while (!reader.atEnd()) {
switch (reader.readNext()) { switch (reader.readNext()) {
case QXmlStreamReader::StartElement: case QXmlStreamReader::StartElement:
if (examples && reader.name() == QLatin1String("examples")) if (categoryOrder.isEmpty() && reader.name() == QLatin1String("categories"))
categoryOrder = parseCategories(&reader);
else if (examples && reader.name() == QLatin1String("examples"))
items += parseExamples(&reader, path, examplesInstallPath); items += parseExamples(&reader, path, examplesInstallPath);
else if (examples && reader.name() == QLatin1String("demos")) else if (examples && reader.name() == QLatin1String("demos"))
items += parseDemos(&reader, path, demosInstallPath); items += parseDemos(&reader, path, demosInstallPath);
@@ -301,7 +324,7 @@ expected_str<QList<ExampleItem *>> parseExamples(const QByteArray &manifestData,
.arg(reader.columnNumber()) .arg(reader.columnNumber())
.arg(reader.errorString())); .arg(reader.errorString()));
} }
return items; return {{items, categoryOrder}};
} }
static bool sortByHighlightedAndName(ExampleItem *first, ExampleItem *second) static bool sortByHighlightedAndName(ExampleItem *first, ExampleItem *second)
@@ -355,7 +378,7 @@ QList<std::pair<Core::Section, QList<ExampleItem *>>> getCategories(const QList<
} else { } else {
// All original items have been copied into a category or other, delete. // All original items have been copied into a category or other, delete.
qDeleteAll(items); qDeleteAll(items);
static const int defaultOrderSize = defaultOrder.size(); const int defaultOrderSize = defaultOrder.size();
int index = 0; int index = 0;
const auto end = categoryMap.constKeyValueEnd(); const auto end = categoryMap.constKeyValueEnd();
for (auto it = categoryMap.constKeyValueBegin(); it != end; ++it) { for (auto it = categoryMap.constKeyValueBegin(); it != end; ++it) {

View File

@@ -31,13 +31,20 @@ public:
QHash<QString, QStringList> metaData; QHash<QString, QStringList> metaData;
}; };
QTSUPPORT_TEST_EXPORT Utils::expected_str<QList<ExampleItem *>> parseExamples( class QTSUPPORT_TEST_EXPORT ParsedExamples
{
public:
QList<ExampleItem *> items;
QStringList categoryOrder;
};
QTSUPPORT_TEST_EXPORT Utils::expected_str<ParsedExamples> parseExamples(
const Utils::FilePath &manifest, const Utils::FilePath &manifest,
const Utils::FilePath &examplesInstallPath, const Utils::FilePath &examplesInstallPath,
const Utils::FilePath &demosInstallPath, const Utils::FilePath &demosInstallPath,
bool examples); bool examples);
QTSUPPORT_TEST_EXPORT Utils::expected_str<QList<ExampleItem *>> parseExamples( QTSUPPORT_TEST_EXPORT Utils::expected_str<ParsedExamples> parseExamples(
const QByteArray &manifestData, const QByteArray &manifestData,
const Utils::FilePath &manifestPath, const Utils::FilePath &manifestPath,
const Utils::FilePath &examplesInstallPath, const Utils::FilePath &examplesInstallPath,

View File

@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<instructionals module="Qt"> <instructionals module="Qt">
<categories>
<category>Help</category>
<category>Learning</category>
<category>Online</category>
<category>Talk</category>
</categories>
<tutorials> <tutorials>
<tutorial imageUrl=":qtsupport/images/icons/tutorialicon.png" difficulty="" docUrl="qthelp://org.qt-project.qtcreator/doc/creator-build-example-application.html" projectPath="" name="Building and Running an Example"> <tutorial imageUrl=":qtsupport/images/icons/tutorialicon.png" difficulty="" docUrl="qthelp://org.qt-project.qtcreator/doc/creator-build-example-application.html" projectPath="" name="Building and Running an Example">
<description><![CDATA[Testing that your installation is successful by opening an existing example application project.]]></description> <description><![CDATA[Testing that your installation is successful by opening an existing example application project.]]></description>

View File

@@ -90,9 +90,11 @@ void tst_Examples::parsing_data()
QTest::addColumn<QStringList>("platforms"); QTest::addColumn<QStringList>("platforms");
QTest::addColumn<MetaData>("metaData"); QTest::addColumn<MetaData>("metaData");
QTest::addColumn<QStringList>("categories"); QTest::addColumn<QStringList>("categories");
QTest::addColumn<QStringList>("categoryOrder");
QTest::addRow("example") QTest::addRow("example")
<< QByteArray(R"raw( << QByteArray(R"raw(
<instructionals module="Qt">
<examples> <examples>
<example docUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/qtwidgets-widgets-analogclock-example.html" <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" imageUrl="qthelp://org.qt-project.qtwidgets.660/qtwidgets/images/analogclock-example.png"
@@ -110,6 +112,13 @@ void tst_Examples::parsing_data()
</meta> </meta>
</example> </example>
</examples> </examples>
<categories>
<category>Application Examples</category>
<category>Desktop</category>
<category>Mobile</category>
<category>Embedded</category>
</categories>
</instructionals>
)raw") << /*isExamples=*/true )raw") << /*isExamples=*/true
<< "Analog Clock" << "Analog Clock"
<< "The Analog Clock example shows how to draw the contents of a custom widget." << "The Analog Clock example shows how to draw the contents of a custom widget."
@@ -126,23 +135,33 @@ void tst_Examples::parsing_data()
<< FilePaths() << Example << true << false << false << "" << FilePaths() << Example << true << false << false << ""
<< "" << QStringList() << "" << QStringList()
<< MetaData({{"category", {"Graphics", "Graphics", "Foobar"}}, {"tags", {"widgets"}}}) << MetaData({{"category", {"Graphics", "Graphics", "Foobar"}}, {"tags", {"widgets"}}})
<< QStringList{"Foobar", "Graphics"}; << QStringList{"Foobar", "Graphics"}
<< QStringList{"Application Examples", "Desktop", "Mobile", "Embedded"};
QTest::addRow("no category, highlighted") QTest::addRow("no category, highlighted")
<< QByteArray(R"raw( << QByteArray(R"raw(
<instructionals module="Qt">
<examples> <examples>
<example name="No Category, highlighted" <example name="No Category, highlighted"
isHighlighted="true"> isHighlighted="true">
</example> </example>
</examples> </examples>
</instructionals>
)raw") << /*isExamples=*/true )raw") << /*isExamples=*/true
<< "No Category, highlighted" << QString() << QString() << QStringList() << "No Category, highlighted" << QString() << QString() << QStringList()
<< FilePath("examples") << QString() << FilePaths() << FilePath() << FilePaths() << Example << FilePath("examples") << QString() << FilePaths() << FilePath() << FilePaths() << Example
<< /*hasSourceCode=*/false << false << /*isHighlighted=*/true << "" << /*hasSourceCode=*/false << false << /*isHighlighted=*/true << ""
<< "" << QStringList() << MetaData() << QStringList{"Featured"}; << "" << QStringList() << MetaData() << QStringList{"Featured"} << QStringList();
QTest::addRow("tutorial with category") QTest::addRow("tutorial with category")
<< QByteArray(R"raw( << QByteArray(R"raw(
<instructionals module="Qt">
<categories>
<category>Help</category>
<category>Learning</category>
<category>Online</category>
<category>Talk</category>
</categories>
<tutorials> <tutorials>
<tutorial imageUrl=":qtsupport/images/icons/tutorialicon.png" difficulty="" docUrl="qthelp://org.qt-project.qtcreator/doc/dummytutorial.html" projectPath="" name="A tutorial"> <tutorial imageUrl=":qtsupport/images/icons/tutorialicon.png" difficulty="" docUrl="qthelp://org.qt-project.qtcreator/doc/dummytutorial.html" projectPath="" name="A tutorial">
<description><![CDATA[A dummy tutorial.]]></description> <description><![CDATA[A dummy tutorial.]]></description>
@@ -152,6 +171,7 @@ void tst_Examples::parsing_data()
</meta> </meta>
</tutorial> </tutorial>
</tutorials> </tutorials>
</instructionals>
)raw") << /*isExamples=*/false )raw") << /*isExamples=*/false
<< "A tutorial" << "A tutorial"
<< "A dummy tutorial." << "A dummy tutorial."
@@ -160,7 +180,8 @@ void tst_Examples::parsing_data()
<< "qthelp://org.qt-project.qtcreator/doc/dummytutorial.html" << FilePaths() << FilePath() << "qthelp://org.qt-project.qtcreator/doc/dummytutorial.html" << FilePaths() << FilePath()
<< FilePaths() << Tutorial << /*hasSourceCode=*/false << /*isVideo=*/false << FilePaths() << Tutorial << /*hasSourceCode=*/false << /*isVideo=*/false
<< /*isHighlighted=*/false << QString() << QString() << QStringList() << /*isHighlighted=*/false << QString() << QString() << QStringList()
<< MetaData({{"category", {"Help"}}}) << QStringList("Help"); << MetaData({{"category", {"Help"}}}) << QStringList("Help")
<< QStringList{"Help", "Learning", "Online", "Talk"};
} }
void tst_Examples::parsing() void tst_Examples::parsing()
@@ -168,16 +189,18 @@ void tst_Examples::parsing()
QFETCH(QByteArray, data); QFETCH(QByteArray, data);
QFETCH(bool, isExamples); QFETCH(bool, isExamples);
QFETCH(QStringList, categories); QFETCH(QStringList, categories);
QFETCH(QStringList, categoryOrder);
const ExampleItem expected = fetchItem(); const ExampleItem expected = fetchItem();
const expected_str<QList<ExampleItem *>> result const expected_str<ParsedExamples> result = parseExamples(data,
= parseExamples(data, FilePath(
FilePath("manifest/examples-manifest.xml"), "manifest/examples-manifest.xml"),
FilePath("examples"), FilePath("examples"),
FilePath("demos"), FilePath("demos"),
isExamples); isExamples);
QVERIFY(result); QVERIFY(result);
QCOMPARE(result->size(), 1); QCOMPARE(result->categoryOrder, categoryOrder);
const ExampleItem item = *result->at(0); QCOMPARE(result->items.size(), 1);
const ExampleItem item = *result->items.at(0);
QCOMPARE(item.name, expected.name); QCOMPARE(item.name, expected.name);
QCOMPARE(item.description, expected.description); QCOMPARE(item.description, expected.description);
QCOMPARE(item.imageUrl, expected.imageUrl); QCOMPARE(item.imageUrl, expected.imageUrl);
@@ -197,7 +220,7 @@ void tst_Examples::parsing()
QCOMPARE(item.metaData, expected.metaData); QCOMPARE(item.metaData, expected.metaData);
const QList<std::pair<Section, QList<ExampleItem *>>> resultCategories const QList<std::pair<Section, QList<ExampleItem *>>> resultCategories
= getCategories(*result, true, {}, true); = getCategories(result->items, true, {}, true);
QCOMPARE(resultCategories.size(), categories.size()); QCOMPARE(resultCategories.size(), categories.size());
for (int i = 0; i < resultCategories.size(); ++i) { for (int i = 0; i < resultCategories.size(); ++i) {
QCOMPARE(resultCategories.at(i).first.name, categories.at(i)); QCOMPARE(resultCategories.at(i).first.name, categories.at(i));