aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/imports/CMakeLists.txt3
-rw-r--r--src/imports/xmllistmodel/CMakeLists.txt15
-rw-r--r--src/imports/xmllistmodel/plugin.cpp59
-rw-r--r--src/qml/configure.cmake7
-rw-r--r--src/qmlxmllistmodel/CMakeLists.txt29
-rw-r--r--src/qmlxmllistmodel/doc/qtqmlxmllistmodel.qdocconf36
-rw-r--r--src/qmlxmllistmodel/qqmlxmllistmodel.cpp992
-rw-r--r--src/qmlxmllistmodel/qqmlxmllistmodel_p.h250
-rw-r--r--src/qmlxmllistmodel/qtqmlxmllistmodelglobal_p.h74
-rw-r--r--sync.profile3
-rw-r--r--tests/auto/qml/CMakeLists.txt3
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/CMakeLists.txt27
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/attributes.qml11
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/attributes.xml14
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/elementErrors.qml9
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/empty.xml0
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/malformedAttribute.xml28
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/malformedData.qml10
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/malformedTagNestedLevel.xml28
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/malformedTagTopLevel.xml28
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/model.qml11
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/model.xml54
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/model2.xml14
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/nestedElements.qml11
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/nestedElements.xml76
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/propertychanges.qml10
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/proxyCrash.qml8
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/recipes.qml10
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/recipes.xml90
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/roleCrash.qml10
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/threading.qml8
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/data/unique.qml8
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp710
34 files changed, 2648 insertions, 1 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index db06625dfd..135f116450 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -17,6 +17,9 @@ if(TARGET Qt::Sql)
add_subdirectory(qmllocalstorage)
endif()
+if (QT_FEATURE_qml_xmllistmodel)
+ add_subdirectory(qmlxmllistmodel)
+endif()
if(TARGET Qt::Gui AND QT_FEATURE_qml_animation)
add_subdirectory(quick)
diff --git a/src/imports/CMakeLists.txt b/src/imports/CMakeLists.txt
index 79a6a86016..3db83444b7 100644
--- a/src/imports/CMakeLists.txt
+++ b/src/imports/CMakeLists.txt
@@ -5,6 +5,9 @@ add_subdirectory(qtqml)
add_subdirectory(models)
add_subdirectory(labsmodels)
add_subdirectory(tooling)
+if (QT_FEATURE_qml_xmllistmodel)
+ add_subdirectory(xmllistmodel)
+endif()
if(QT_FEATURE_qml_itemmodel)
add_subdirectory(folderlistmodel)
endif()
diff --git a/src/imports/xmllistmodel/CMakeLists.txt b/src/imports/xmllistmodel/CMakeLists.txt
new file mode 100644
index 0000000000..387b588e84
--- /dev/null
+++ b/src/imports/xmllistmodel/CMakeLists.txt
@@ -0,0 +1,15 @@
+qt_internal_add_qml_module(qmlxmllistmodelplugin
+ URI "QtQml.XmlListModel"
+ VERSION "${CMAKE_PROJECT_VERSION}"
+ CLASSNAME QtQmlXmlListModelPlugin
+ SKIP_TYPE_REGISTRATION
+ DEPENDENCIES
+ QtQml
+ PLUGIN_OPTIONAL
+ SOURCES
+ plugin.cpp
+ PUBLIC_LIBRARIES
+ Qt::Core
+ Qt::Qml
+ Qt::QmlXmlListModelPrivate
+)
diff --git a/src/imports/xmllistmodel/plugin.cpp b/src/imports/xmllistmodel/plugin.cpp
new file mode 100644
index 0000000000..d4ddcd317d
--- /dev/null
+++ b/src/imports/xmllistmodel/plugin.cpp
@@ -0,0 +1,59 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Quick Layouts module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtQml/qqmlextensionplugin.h>
+#include <QtQmlXmlListModel/private/qtqmlxmllistmodelglobal_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QtQmlXmlListModelPlugin : public QQmlEngineExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid)
+public:
+ QtQmlXmlListModelPlugin(QObject *parent = nullptr) : QQmlEngineExtensionPlugin(parent)
+ {
+ volatile auto registration = &qml_register_types_QtQml_XmlListModel;
+ Q_UNUSED(registration)
+ }
+};
+
+QT_END_NAMESPACE
+
+#include "plugin.moc"
diff --git a/src/qml/configure.cmake b/src/qml/configure.cmake
index 21acf9efa7..7f4a07b049 100644
--- a/src/qml/configure.cmake
+++ b/src/qml/configure.cmake
@@ -188,6 +188,13 @@ qt_feature("qml-itemmodel" PRIVATE
CONDITION QT_FEATURE_itemmodel
)
+qt_feature("qml-xmllistmodel" PRIVATE
+ SECTION "QML"
+ LABEL "QML XmlListModel"
+ PURPOSE "Enable XmlListModel in QML"
+ CONDITION QT_FEATURE_future
+)
+
# special case begin
qt_qml_find_python(__qt_qml_python_path __qt_qml_python_found)
# special case end
diff --git a/src/qmlxmllistmodel/CMakeLists.txt b/src/qmlxmllistmodel/CMakeLists.txt
new file mode 100644
index 0000000000..0e1f4b2f93
--- /dev/null
+++ b/src/qmlxmllistmodel/CMakeLists.txt
@@ -0,0 +1,29 @@
+qt_internal_add_module(QmlXmlListModel
+ GENERATE_METATYPES
+ SOURCES
+ qqmlxmllistmodel_p.h qqmlxmllistmodel.cpp
+ qtqmlxmllistmodelglobal_p.h
+ DEFINES
+ QT_BUILD_QMLXMLLISTMODEL_LIB
+ LIBRARIES
+ Qt::CorePrivate
+ PUBLIC_LIBRARIES
+ Qt::Core
+ Qt::Qml
+ PRIVATE_MODULE_INTERFACE
+ Qt::CorePrivate
+)
+
+qt_internal_add_docs(QmlXmlListModel
+ doc/qtqmlxmllistmodel.qdocconf
+)
+
+set_target_properties(QmlXmlListModel PROPERTIES
+ QT_QML_MODULE_INSTALL_QMLTYPES TRUE
+ QT_QML_MODULE_VERSION ${CMAKE_PROJECT_VERSION}
+ QT_QML_MODULE_URI QtQml.XmlListModel
+ QT_QMLTYPES_FILENAME plugins.qmltypes
+ QT_QML_MODULE_INSTALL_DIR "${INSTALL_QMLDIR}/QtQml/XmlListModel"
+)
+
+qt6_qml_type_registration(QmlXmlListModel)
diff --git a/src/qmlxmllistmodel/doc/qtqmlxmllistmodel.qdocconf b/src/qmlxmllistmodel/doc/qtqmlxmllistmodel.qdocconf
new file mode 100644
index 0000000000..f1ba27eaa9
--- /dev/null
+++ b/src/qmlxmllistmodel/doc/qtqmlxmllistmodel.qdocconf
@@ -0,0 +1,36 @@
+include($QT_INSTALL_DOCS/global/qt-module-defaults.qdocconf)
+include($QT_INSTALL_DOCS/config/exampleurl-qtdeclarative.qdocconf)
+
+project = QtQmlXmlListModel
+description = Qt QML XmlListModel Reference Documentation
+version = $QT_VERSION
+moduleheader = QtQmlXmlListModel
+qhp.projects = QtQmlXmlListModel
+
+qhp.QtQmlXmlListModel.file = qtqmlxmllistmodel.qhp
+qhp.QtQmlXmlListModel.namespace = org.qt-project.qtqmlxmllistmodel.$QT_VERSION_TAG
+qhp.QtQmlXmlListModel.virtualFolder = qtqmlxmllistmodel
+qhp.QtQmlXmlListModel.indexRoot =
+
+qhp.QtQmlXmlListModel.filterAttributes = qtqmlxmllistmodel $QT_VERSION qtrefdoc
+qhp.QtQmlXmlListModel.customFilters.Qt.name = QtQmlXmlListModel $QT_VERSION
+qhp.QtQmlXmlListModel.customFilters.Qt.filterAttributes = qtqmlxmllistmodel $QT_VERSION
+
+qhp.QtQmlXmlListModel.title = QML Types
+qhp.QtQmlXmlListModel.indexTitle = Qt XmlListModel QML Types
+qhp.QtQmlXmlListModel.selectors = qmlclass
+qhp.QtQmlXmlListModel.sortPages = true
+
+tagfile = qtqmlxmllistmodel.tags
+
+depends += qtcore qtqml qtdoc
+
+{headerdirs,sourcedirs} += \
+ ..
+
+exampledirs += .. \
+ snippets
+
+imagedirs += images
+
+navigation.qmltypespage = "Qt XmlListModel QML Types"
diff --git a/src/qmlxmllistmodel/qqmlxmllistmodel.cpp b/src/qmlxmllistmodel/qqmlxmllistmodel.cpp
new file mode 100644
index 0000000000..5907937bcf
--- /dev/null
+++ b/src/qmlxmllistmodel/qqmlxmllistmodel.cpp
@@ -0,0 +1,992 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qqmlxmllistmodel_p.h"
+
+#include <QQmlFile>
+#include <QFile>
+#include <QCoreApplication>
+
+Q_DECLARE_METATYPE(QQmlXmlListModelQueryResult)
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmlmodule QtQml.XmlListModel
+ \title Qt XmlListModel QML Types
+ \keyword Qt XmlListModel QML Types
+ \ingroup qmlmodules
+ \brief Provides QML types for creating models from XML data
+
+ This QML module contains types for creating models from XML data.
+
+ To use the types in this module, import the module with the following line:
+
+ \qml
+ import QtQml.XmlListModel
+ \endqml
+*/
+
+/*!
+ \qmltype XmlListModelRole
+ \inqmlmodule QtQml.XmlListModel
+ \brief For specifying a role to an \l XmlListModel.
+
+ \sa {All QML Types}{Qt QML}
+*/
+
+/*!
+ \qmlproperty string QtQml.XmlListModel::XmlListModelRole::name
+
+ The name for the role. This name is used to access the model data for this
+ role.
+
+ For example, the following model has a role named "title", which can be
+ accessed from the view's delegate:
+
+ \qml
+ XmlListModel {
+ id: xmlModel
+ source: "file.xml"
+ query: "/documents/document"
+ XmlListModelRole { name: "title"; elementName: "title" }
+ }
+ \endqml
+
+ \qml
+ ListView {
+ model: xmlModel
+ delegate: Text { text: title }
+ }
+ \endqml
+*/
+QString QQmlXmlListModelRole::name() const
+{
+ return m_name;
+}
+
+void QQmlXmlListModelRole::setName(const QString &name)
+{
+ if (name == m_name)
+ return;
+ m_name = name;
+ Q_EMIT nameChanged();
+}
+
+/*!
+ \qmlproperty string QtQml.XmlListModel::XmlListModelRole::elementName
+
+ The name of the xml element, or a path to the xml element, that will be
+ used to read the data. The element must actually contain text.
+
+ Optionally the \l attributeName property can be specified to extract
+ the data.
+
+//! [basic-example]
+ For example, the following model has a role named "title", which reads the
+ data from the xml element \c {<title>}. It also has another role named
+ "timestamp", which uses the same xml element \c {<title>}, but reads its
+ "created" attribute to extract the actual value.
+
+ \qml
+ XmlListModel {
+ id: xmlModel
+ source: "file.xml"
+ query: "/documents/document"
+ XmlListModelRole { name: "title"; elementName: "title" }
+ XmlListModelRole {
+ name: "timestamp"
+ elementName: "title"
+ attributeName: "created"
+ }
+ }
+
+ ListView {
+ anchors.fill: parent
+ model: xmlModel
+ delegate: Text { text: title + " created on " + timestamp }
+ }
+ \endqml
+//! [basic-example]
+
+//! [empty-elementName-example]
+ When the \l attributeName is specified, the \l elementName can be left
+ empty. In this case the attribute of the top level xml element of the query
+ will be read.
+
+ For example, if you have the following xml document:
+
+ \code
+ <documents>
+ <document title="Title1"/>
+ <document title="Title2"/>
+ </documents>
+ \endcode
+
+ To extract the document titles you need the following model:
+
+ \qml
+ XmlListModel {
+ id: xmlModel
+ source: "file.xml"
+ query: "/documents/document"
+ XmlListModelRole {
+ name: "title"
+ elementName: ""
+ attributeName: "title"
+ }
+ }
+ \endqml
+//! [empty-elementName-example]
+
+ The elementName property can actually contain a path to the nested xml
+ element. All the elements in the path must be joined with the \c {'/'}
+ character.
+
+ For example, if you have the following xml document:
+ \code
+ <documents>
+ <document>
+ <title>Title1</title>
+ <info>
+ <num_pages>10</num_pages>
+ </info>
+ </document>
+ <document>
+ <title>Title2</title>
+ <info>
+ <num_pages>20</num_pages>
+ </info>
+ </document>
+ </documents>
+ \endcode
+
+ You can extract the number of pages with the following role:
+
+ \qml
+ XmlListModel {
+ id: xmlModel
+ source: "file.xml"
+ query: "/documents/document"
+ // ...
+ XmlListModelRole {
+ name: "pages"
+ elementName: "info/num_pages"
+ }
+ }
+ \endqml
+
+ \note The path to the element must not start or end with \c {'/'}.
+
+ \sa attributeName
+*/
+QString QQmlXmlListModelRole::elementName() const
+{
+ return m_elementName;
+}
+
+void QQmlXmlListModelRole::setElementName(const QString &name)
+{
+ if (name.startsWith(QLatin1Char('/'))) {
+ qmlWarning(this) << tr("An xml element must not start with '/'");
+ return;
+ } else if (name.endsWith(QLatin1Char('/'))) {
+ qmlWarning(this) << tr("An xml element must not end with '/'");
+ return;
+ } else if (name.contains(QStringLiteral("//"))) {
+ qmlWarning(this) << tr("An xml element must not contain \"//\"");
+ return;
+ }
+
+ if (name == m_elementName)
+ return;
+ m_elementName = name;
+ Q_EMIT elementNameChanged();
+}
+
+/*!
+ \qmlproperty string QtQml.XmlListModel::XmlListModelRole::attributeName
+
+ The attribute of the xml element that will be used to read the data.
+ The xml element is specified by \l elementName property.
+
+ \include qqmlxmllistmodel.cpp basic-example
+
+ \include qqmlxmllistmodel.cpp empty-elementName-example
+
+ If you do not need to parse any attributes for the specified xml element,
+ simply leave this property blank.
+
+ \sa elementName
+*/
+QString QQmlXmlListModelRole::attributeName() const
+{
+ return m_attributeName;
+}
+
+void QQmlXmlListModelRole::setAttributeName(const QString &attributeName)
+{
+ if (m_attributeName == attributeName)
+ return;
+ m_attributeName = attributeName;
+ Q_EMIT attributeNameChanged();
+}
+
+bool QQmlXmlListModelRole::isValid() const
+{
+ return !m_name.isEmpty();
+}
+
+/*!
+ \qmltype XmlListModel
+ \inqmlmodule QtQml.XmlListModel
+ \brief For specifying a read-only model using XML data.
+
+ To use this element, you will need to import the module with the following line:
+ \code
+ import QtQml.XmlListModel
+ \endcode
+
+ XmlListModel is used to create a read-only model from XML data. It can be
+ used as a data source for view elements (such as ListView, PathView,
+ GridView) and other elements that interact with model data (such as
+ Repeater).
+
+ \note This model \b {does not} support the XPath queries. It supports simple
+ slash-separated paths and, optionally, one attribute for each element.
+
+ For example, if there is an XML document at https://www.qt.io/blog/rss.xml
+ like this:
+
+ \code
+ <?xml version="1.0" encoding="UTF-8"?>
+ <rss version="2.0">
+ ...
+ <channel>
+ <item>
+ <title>Qt 6.0.2 Released</title>
+ <link>https://www.qt.io/blog/qt-6.0.2-released</link>
+ <pubDate>Wed, 03 Mar 2021 12:40:43 GMT</pubDate>
+ </item>
+ <item>
+ <title>Qt 6.1 Beta Released</title>
+ <link>https://www.qt.io/blog/qt-6.1-beta-released</link>
+ <pubDate>Tue, 02 Mar 2021 13:05:47 GMT</pubDate>
+ </item>
+ <item>
+ <title>Qt Creator 4.14.1 released</title>
+ <link>https://www.qt.io/blog/qt-creator-4.14.1-released</link>
+ <pubDate>Wed, 24 Feb 2021 13:53:21 GMT</pubDate>
+ </item>
+ </channel>
+ </rss>
+ \endcode
+
+ A XmlListModel could create a model from this data, like this:
+
+ \qml
+ import QtQml.XmlListModel
+
+ XmlListModel {
+ id: xmlModel
+ source: "https://www.qt.io/blog/rss.xml"
+ query: "/rss/channel/item"
+
+ XmlListModelRole { name: "title"; elementName: "title" }
+ XmlListModelRole { name: "pubDate"; elementName: "pubDate" }
+ XmlListModelRole { name: "link"; elementName: "link" }
+ }
+ \endqml
+
+ The \l {XmlListModel::query}{query} value of "/rss/channel/item" specifies
+ that the XmlListModel should generate a model item for each \c {<item>} in
+ the XML document.
+
+ The \l [QML] {XmlListModelRole} objects define the model item attributes.
+ Here, each model item will have \c title, \c pubDate and \c link attributes
+ that match the \c title, \c pubDate and \c link values of its corresponding
+ \c {<item>}.
+ (See \l [QML] {XmlListModelRole} documentation for more examples.)
+
+ The model could be used in a ListView, like this:
+
+ \qml
+ ListView {
+ width: 180; height: 300
+ model: xmlModel
+ delegate: Text { text: title + ": " + pubDate + "; link: " + link }
+ }
+ \endqml
+
+ The \l XmlListModel data is loaded asynchronously, and \l status
+ is set to \c XmlListModel.Ready when loading is complete.
+ Note this means when \l XmlListModel is used for a view, the view is not
+ populated until the model is loaded.
+*/
+
+QQmlXmlListModel::QQmlXmlListModel(QObject *parent) : QAbstractListModel(parent) { }
+
+QQmlXmlListModel::~QQmlXmlListModel()
+{
+ // Cancel all objects
+ for (auto &w : m_watchers.values())
+ w->cancel();
+ // Wait until all objects are finished
+ while (!m_watchers.isEmpty()) {
+ auto it = m_watchers.begin();
+ it.value()->waitForFinished();
+ // Explicitly delete the watcher here, because the connected lambda
+ // would not be called until processEvents() is called
+ delete it.value();
+ m_watchers.erase(it);
+ }
+}
+
+QModelIndex QQmlXmlListModel::index(int row, int column, const QModelIndex &parent) const
+{
+ return !parent.isValid() && column == 0 && row >= 0 && m_size ? createIndex(row, column)
+ : QModelIndex();
+}
+
+int QQmlXmlListModel::rowCount(const QModelIndex &parent) const
+{
+ return !parent.isValid() ? m_size : 0;
+}
+
+QVariant QQmlXmlListModel::data(const QModelIndex &index, int role) const
+{
+ const int roleIndex = m_roles.indexOf(role);
+ return (roleIndex == -1 || !index.isValid()) ? QVariant()
+ : m_data.value(index.row()).value(roleIndex);
+}
+
+QHash<int, QByteArray> QQmlXmlListModel::roleNames() const
+{
+ QHash<int, QByteArray> roleNames;
+ for (int i = 0; i < m_roles.count(); ++i)
+ roleNames.insert(m_roles.at(i), m_roleNames.at(i).toUtf8());
+ return roleNames;
+}
+
+/*!
+ \qmlproperty int QtQml.XmlListModel::XmlListModel::count
+ The number of data entries in the model.
+*/
+int QQmlXmlListModel::count() const
+{
+ return m_size;
+}
+
+/*!
+ \qmlproperty url QtQml.XmlListModel::XmlListModel::source
+ The location of the XML data source.
+*/
+QUrl QQmlXmlListModel::source() const
+{
+ return m_source;
+}
+
+void QQmlXmlListModel::setSource(const QUrl &src)
+{
+ if (m_source != src) {
+ m_source = src;
+ reload();
+ Q_EMIT sourceChanged();
+ }
+}
+
+/*!
+ \qmlproperty string QtQml.XmlListModel::XmlListModel::query
+ A string representing the base path for creating model items from this
+ model's \l [QML] {XmlListModelRole} objects. The query should start with
+ \c {'/'}.
+*/
+QString QQmlXmlListModel::query() const
+{
+ return m_query;
+}
+
+void QQmlXmlListModel::setQuery(const QString &query)
+{
+ if (!query.startsWith(QLatin1Char('/'))) {
+ qmlWarning(this) << QCoreApplication::translate(
+ "XmlListModelRoleList", "An XmlListModel query must start with '/'");
+ return;
+ }
+
+ if (m_query != query) {
+ m_query = query;
+ reload();
+ Q_EMIT queryChanged();
+ }
+}
+
+/*!
+ \qmlproperty list<XmlListModelRole> QtQml.XmlListModel::XmlListModel::roles
+
+ The roles to make available for this model.
+*/
+QQmlListProperty<QQmlXmlListModelRole> QQmlXmlListModel::roleObjects()
+{
+ QQmlListProperty<QQmlXmlListModelRole> list(this, &m_roleObjects);
+ list.append = &QQmlXmlListModel::appendRole;
+ list.clear = &QQmlXmlListModel::clearRole;
+ return list;
+}
+
+void QQmlXmlListModel::appendRole(QQmlXmlListModelRole *role)
+{
+ if (role) {
+ int i = m_roleObjects.count();
+ m_roleObjects.append(role);
+ if (m_roleNames.contains(role->name())) {
+ qmlWarning(role)
+ << QQmlXmlListModel::tr(
+ "\"%1\" duplicates a previous role name and will be disabled.")
+ .arg(role->name());
+ return;
+ }
+ m_roles.insert(i, m_highestRole);
+ m_roleNames.insert(i, role->name());
+ ++m_highestRole;
+ }
+}
+
+void QQmlXmlListModel::clearRole()
+{
+ m_roles.clear();
+ m_roleNames.clear();
+ m_roleObjects.clear();
+}
+
+void QQmlXmlListModel::appendRole(QQmlListProperty<QQmlXmlListModelRole> *list,
+ QQmlXmlListModelRole *role)
+{
+ auto object = qobject_cast<QQmlXmlListModel *>(list->object);
+ if (object) // role is checked inside appendRole
+ object->appendRole(role);
+}
+
+void QQmlXmlListModel::clearRole(QQmlListProperty<QQmlXmlListModelRole> *list)
+{
+ auto object = qobject_cast<QQmlXmlListModel *>(list->object);
+ if (object)
+ object->clearRole();
+}
+
+void QQmlXmlListModel::tryExecuteQuery(const QByteArray &data)
+{
+ auto job = createJob(data);
+ m_queryId = job.queryId;
+ QQmlXmlListModelQueryRunnable *runnable = new QQmlXmlListModelQueryRunnable(std::move(job));
+ if (runnable) {
+ auto future = runnable->future();
+ auto *watcher = new ResultFutureWatcher();
+ // No need to connect to canceled signal, because it just notifies that
+ // QFuture::cancel() was called. We will get the finished() signal in
+ // both cases.
+ connect(watcher, &ResultFutureWatcher::finished, this, [id = m_queryId, this]() {
+ auto *watcher = static_cast<ResultFutureWatcher *>(sender());
+ if (watcher) {
+ if (!watcher->isCanceled()) {
+ QQmlXmlListModelQueryResult result = watcher->result();
+ // handle errors
+ for (const auto &errorInfo : result.errors)
+ queryError(errorInfo.first, errorInfo.second);
+ // fill results
+ queryCompleted(result);
+ }
+ // remove from watchers
+ m_watchers.remove(id);
+ watcher->deleteLater();
+ }
+ });
+ m_watchers[m_queryId] = watcher;
+ watcher->setFuture(future);
+ QThreadPool::globalInstance()->start(runnable);
+ } else {
+ m_errorString = tr("Failed to create an instance of QRunnable query object");
+ m_status = QQmlXmlListModel::Error;
+ m_queryId = -1;
+ Q_EMIT statusChanged(m_status);
+ }
+}
+
+QQmlXmlListModelQueryJob QQmlXmlListModel::createJob(const QByteArray &data)
+{
+ QQmlXmlListModelQueryJob job;
+ job.queryId = nextQueryId();
+ job.data = data;
+ job.query = m_query;
+
+ for (int i = 0; i < m_roleObjects.count(); i++) {
+ if (!m_roleObjects.at(i)->isValid()) {
+ job.roleNames << QString();
+ job.elementNames << QString();
+ job.elementAttributes << QString();
+ continue;
+ }
+ job.roleNames << m_roleObjects.at(i)->name();
+ job.elementNames << m_roleObjects.at(i)->elementName();
+ job.elementAttributes << m_roleObjects.at(i)->attributeName();
+ job.roleQueryErrorId << static_cast<void *>(m_roleObjects.at(i));
+ }
+
+ return job;
+}
+
+int QQmlXmlListModel::nextQueryId()
+{
+ m_nextQueryIdGenerator++;
+ if (m_nextQueryIdGenerator <= 0)
+ m_nextQueryIdGenerator = 1;
+ return m_nextQueryIdGenerator;
+}
+
+/*!
+ \qmlproperty enumeration QtQml.XmlListModel::XmlListModel::status
+ Specifies the model loading status, which can be one of the following:
+
+ \list
+ \li XmlListModel.Null - No XML data has been set for this model.
+ \li XmlListModel.Ready - The XML data has been loaded into the model.
+ \li XmlListModel.Loading - The model is in the process of reading and
+ loading XML data.
+ \li XmlListModel.Error - An error occurred while the model was loading. See
+ \l errorString() for details about the error.
+ \endlist
+
+ \sa progress
+*/
+QQmlXmlListModel::Status QQmlXmlListModel::status() const
+{
+ return m_status;
+}
+
+/*!
+ \qmlproperty real QtQml.XmlListModel::XmlListModel::progress
+
+ This indicates the current progress of the downloading of the XML data
+ source. This value ranges from 0.0 (no data downloaded) to
+ 1.0 (all data downloaded). If the XML data is not from a remote source,
+ the progress becomes 1.0 as soon as the data is read.
+
+ Note that when the progress is 1.0, the XML data has been downloaded, but
+ it is yet to be loaded into the model at this point. Use the status
+ property to find out when the XML data has been read and loaded into
+ the model.
+
+ \sa status, source
+*/
+qreal QQmlXmlListModel::progress() const
+{
+ return m_progress;
+}
+
+/*!
+ \qmlmethod QtQuick.XmlListModel::XmlListModel::errorString()
+
+ Returns a string description of the last error that occurred
+ if \l status is \l {XmlListModel}.Error.
+*/
+QString QQmlXmlListModel::errorString() const
+{
+ return m_errorString;
+}
+
+void QQmlXmlListModel::classBegin()
+{
+ m_isComponentComplete = false;
+}
+
+void QQmlXmlListModel::componentComplete()
+{
+ m_isComponentComplete = true;
+ reload();
+}
+
+/*!
+ \qmlmethod QtQml.XmlListModel::XmlListModel::reload()
+
+ Reloads the model.
+*/
+void QQmlXmlListModel::reload()
+{
+ if (!m_isComponentComplete)
+ return;
+
+ if (m_queryId > 0 && m_watchers.contains(m_queryId))
+ m_watchers[m_queryId]->cancel();
+
+ m_queryId = -1;
+
+ if (m_size < 0)
+ m_size = 0;
+
+#if QT_CONFIG(qml_network)
+ if (m_reply) {
+ m_reply->abort();
+ deleteReply();
+ }
+#endif
+
+ const QQmlContext *context = qmlContext(this);
+ const auto resolvedSource = context ? context->resolvedUrl(m_source) : m_source;
+
+ if (resolvedSource.isEmpty()) {
+ m_queryId = 0;
+ notifyQueryStarted(false);
+ QTimer::singleShot(0, this, &QQmlXmlListModel::dataCleared);
+ } else if (QQmlFile::isLocalFile(resolvedSource)) {
+ QFile file(QQmlFile::urlToLocalFileOrQrc(resolvedSource));
+ const bool opened = file.open(QIODevice::ReadOnly);
+ if (!opened)
+ qWarning("Failed to open file %s: %s", qPrintable(file.fileName()),
+ qPrintable(file.errorString()));
+ QByteArray data = opened ? file.readAll() : QByteArray();
+ notifyQueryStarted(false);
+ if (data.isEmpty()) {
+ m_queryId = 0;
+ QTimer::singleShot(0, this, &QQmlXmlListModel::dataCleared);
+ } else {
+ tryExecuteQuery(data);
+ }
+ } else {
+#if QT_CONFIG(qml_network)
+ notifyQueryStarted(true);
+ QNetworkRequest req(resolvedSource);
+ req.setRawHeader("Accept", "application/xml,*/*");
+ m_reply = qmlContext(this)->engine()->networkAccessManager()->get(req);
+
+ QObject::connect(m_reply, &QNetworkReply::finished, this,
+ &QQmlXmlListModel::requestFinished);
+ QObject::connect(m_reply, &QNetworkReply::downloadProgress, this,
+ &QQmlXmlListModel::requestProgress);
+#else
+ m_queryId = 0;
+ notifyQueryStarted(false);
+ QTimer::singleShot(0, this, &QQmlXmlListModel::dataCleared);
+#endif
+ }
+}
+
+#define XMLLISTMODEL_MAX_REDIRECT 16
+
+#if QT_CONFIG(qml_network)
+void QQmlXmlListModel::requestFinished()
+{
+ m_redirectCount++;
+ if (m_redirectCount < XMLLISTMODEL_MAX_REDIRECT) {
+ QVariant redirect = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
+ if (redirect.isValid()) {
+ QUrl url = m_reply->url().resolved(redirect.toUrl());
+ deleteReply();
+ setSource(url);
+ return;
+ }
+ }
+ m_redirectCount = 0;
+
+ if (m_reply->error() != QNetworkReply::NoError) {
+ m_errorString = m_reply->errorString();
+ deleteReply();
+
+ if (m_size > 0) {
+ beginRemoveRows(QModelIndex(), 0, m_size - 1);
+ m_data.clear();
+ m_size = 0;
+ endRemoveRows();
+ Q_EMIT countChanged();
+ }
+
+ m_status = Error;
+ m_queryId = -1;
+ Q_EMIT statusChanged(m_status);
+ } else {
+ QByteArray data = m_reply->readAll();
+ if (data.isEmpty()) {
+ m_queryId = 0;
+ QTimer::singleShot(0, this, &QQmlXmlListModel::dataCleared);
+ } else {
+ tryExecuteQuery(data);
+ }
+ deleteReply();
+
+ m_progress = 1.0;
+ Q_EMIT progressChanged(m_progress);
+ }
+}
+
+void QQmlXmlListModel::deleteReply()
+{
+ if (m_reply) {
+ QObject::disconnect(m_reply, 0, this, 0);
+ m_reply->deleteLater();
+ m_reply = nullptr;
+ }
+}
+#endif
+
+void QQmlXmlListModel::requestProgress(qint64 received, qint64 total)
+{
+ if (m_status == Loading && total > 0) {
+ m_progress = qreal(received) / total;
+ Q_EMIT progressChanged(m_progress);
+ }
+}
+
+void QQmlXmlListModel::dataCleared()
+{
+ QQmlXmlListModelQueryResult r;
+ r.queryId = 0;
+ queryCompleted(r);
+}
+
+void QQmlXmlListModel::queryError(void *object, const QString &error)
+{
+ for (int i = 0; i < m_roleObjects.count(); i++) {
+ if (m_roleObjects.at(i) == static_cast<QQmlXmlListModelRole *>(object)) {
+ qmlWarning(m_roleObjects.at(i))
+ << QQmlXmlListModel::tr("Query error: \"%1\"").arg(error);
+ return;
+ }
+ }
+ qmlWarning(this) << QQmlXmlListModel::tr("Query error: \"%1\"").arg(error);
+}
+
+void QQmlXmlListModel::queryCompleted(const QQmlXmlListModelQueryResult &result)
+{
+ if (result.queryId != m_queryId)
+ return;
+
+ int origCount = m_size;
+ bool sizeChanged = result.data.count() != m_size;
+
+ if (m_source.isEmpty())
+ m_status = Null;
+ else
+ m_status = Ready;
+ m_errorString.clear();
+ m_queryId = -1;
+
+ if (origCount > 0) {
+ beginRemoveRows(QModelIndex(), 0, origCount - 1);
+ endRemoveRows();
+ }
+ m_size = result.data.count();
+ m_data = result.data;
+
+ if (m_size > 0) {
+ beginInsertRows(QModelIndex(), 0, m_size - 1);
+ endInsertRows();
+ }
+
+ if (sizeChanged)
+ Q_EMIT countChanged();
+
+ Q_EMIT statusChanged(m_status);
+}
+
+void QQmlXmlListModel::notifyQueryStarted(bool remoteSource)
+{
+ m_progress = remoteSource ? 0.0 : 1.0;
+ m_status = QQmlXmlListModel::Loading;
+ m_errorString.clear();
+ Q_EMIT progressChanged(m_progress);
+ Q_EMIT statusChanged(m_status);
+}
+
+static qsizetype findIndexOfName(const QStringList &elementNames, const QStringView &name,
+ qsizetype startIndex = 0)
+{
+ for (auto idx = startIndex; idx < elementNames.size(); ++idx) {
+ if (elementNames[idx].startsWith(name))
+ return idx;
+ }
+ return -1;
+}
+
+QQmlXmlListModelQueryRunnable::QQmlXmlListModelQueryRunnable(QQmlXmlListModelQueryJob &&job)
+ : m_job(std::move(job))
+{
+ setAutoDelete(true);
+}
+
+void QQmlXmlListModelQueryRunnable::run()
+{
+ m_promise.start();
+ if (!m_promise.isCanceled()) {
+ QQmlXmlListModelQueryResult result;
+ result.queryId = m_job.queryId;
+ doQueryJob(&result);
+ m_promise.addResult(std::move(result));
+ }
+ m_promise.finish();
+}
+
+QFuture<QQmlXmlListModelQueryResult> QQmlXmlListModelQueryRunnable::future() const
+{
+ return m_promise.future();
+}
+
+void QQmlXmlListModelQueryRunnable::doQueryJob(QQmlXmlListModelQueryResult *currentResult)
+{
+ Q_ASSERT(m_job.queryId != -1);
+
+ QByteArray data(m_job.data);
+ QXmlStreamReader reader;
+ reader.addData(data);
+
+ QStringList items = m_job.query.split(QLatin1Char('/'), Qt::SkipEmptyParts);
+
+ while (!reader.atEnd() && !m_promise.isCanceled()) {
+ int i = 0;
+ while (i < items.count()) {
+ if (reader.readNextStartElement()) {
+ if (reader.name() == items.at(i)) {
+ if (i != items.count() - 1) {
+ i++;
+ continue;
+ } else {
+ processElement(currentResult, items.at(i), reader);
+ }
+ } else {
+ reader.skipCurrentElement();
+ }
+ }
+ if (reader.tokenType() == QXmlStreamReader::Invalid) {
+ reader.readNext();
+ break;
+ } else if (reader.hasError()) {
+ reader.raiseError();
+ break;
+ }
+ }
+ }
+}
+
+void QQmlXmlListModelQueryRunnable::processElement(QQmlXmlListModelQueryResult *currentResult,
+ const QString &element, QXmlStreamReader &reader)
+{
+ if (!reader.isStartElement() || reader.name() != element)
+ return;
+
+ const QStringList &elementNames = m_job.elementNames;
+ const QStringList &attributes = m_job.elementAttributes;
+ QFlatMap<int, QString> results;
+
+ // First of all check all the empty element names. They might have
+ // attributes to be read from the current element
+ if (!reader.attributes().isEmpty()) {
+ for (auto index = 0; index < elementNames.size(); ++index) {
+ if (elementNames.at(index).isEmpty() && !attributes.at(index).isEmpty()) {
+ const QString &attribute = attributes.at(index);
+ if (reader.attributes().hasAttribute(attribute))
+ results[index] = reader.attributes().value(attribute).toString();
+ }
+ }
+ }
+
+ // After that we recursively search for the elements, considering that we
+ // can have nested element names in our model, and that the same element
+ // can be used multiple types (with different attributes, for example)
+ readSubTree(QString(), reader, results, &currentResult->errors);
+
+ if (reader.hasError())
+ currentResult->errors.push_back(qMakePair(this, reader.errorString()));
+
+ currentResult->data << results;
+}
+
+void QQmlXmlListModelQueryRunnable::readSubTree(const QString &prefix, QXmlStreamReader &reader,
+ QFlatMap<int, QString> &results,
+ QList<QPair<void *, QString>> *errors)
+{
+ const QStringList &elementNames = m_job.elementNames;
+ const QStringList &attributes = m_job.elementAttributes;
+ while (reader.readNextStartElement()) {
+ const auto name = reader.name();
+ const QString fullName =
+ prefix.isEmpty() ? name.toString() : (prefix + QLatin1Char('/') + name.toString());
+ qsizetype index = name.isEmpty() ? -1 : findIndexOfName(elementNames, fullName);
+ if (index >= 0) {
+ // We can have multiple roles with the same element name, but
+ // different attributes, so we need to cache the attributes and
+ // element text.
+ const auto elementAttributes = reader.attributes();
+ // We can read text only when the element actually contains it,
+ // otherwise it will be an error. It can also be used to check that
+ // we've reached the bottom level.
+ QString elementText;
+ bool elementTextRead = false;
+ while (index >= 0) {
+ // if the path matches completely, not just starts with, we
+ // need to actually extract value
+ if (elementNames[index] == fullName) {
+ QString roleResult;
+ const QString &attribute = attributes.at(index);
+ if (!attribute.isEmpty()) {
+ if (elementAttributes.hasAttribute(attribute)) {
+ roleResult = elementAttributes.value(attributes.at(index)).toString();
+ } else {
+ errors->push_back(qMakePair(m_job.roleQueryErrorId.at(index),
+ QLatin1String("Attribute %1 not found")
+ .arg(attributes[index])));
+ }
+ } else if (!elementNames.at(index).isEmpty()) {
+ if (!elementTextRead) {
+ elementText = reader.readElementText();
+ elementTextRead = true;
+ }
+ roleResult = elementText;
+ }
+ results[index] = roleResult;
+ }
+ // search for the next role with the same element name
+ index = findIndexOfName(elementNames, fullName, index + 1);
+ }
+ if (!elementTextRead)
+ readSubTree(fullName, reader, results, errors);
+ } else {
+ reader.skipCurrentElement();
+ }
+ }
+}
+
+QT_END_NAMESPACE
diff --git a/src/qmlxmllistmodel/qqmlxmllistmodel_p.h b/src/qmlxmllistmodel/qqmlxmllistmodel_p.h
new file mode 100644
index 0000000000..ae372de55c
--- /dev/null
+++ b/src/qmlxmllistmodel/qqmlxmllistmodel_p.h
@@ -0,0 +1,250 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QQMLXMLLISTMODEL_H
+#define QQMLXMLLISTMODEL_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQml>
+#include <QAbstractItemModel>
+#include <QByteArray>
+#include <QtCore/private/qflatmap_p.h>
+#include <QHash>
+#include <QStringList>
+#include <QUrl>
+#include <QFuture>
+#if QT_CONFIG(qml_network)
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QXmlStreamReader>
+#endif
+
+#include "qtqmlxmllistmodelglobal_p.h"
+
+QT_BEGIN_NAMESPACE
+
+class QQmlContext;
+struct QQmlXmlListModelQueryJob
+{
+ int queryId;
+ QByteArray data;
+ QString query;
+ QStringList roleNames;
+ QStringList elementNames;
+ QStringList elementAttributes;
+ QList<void *> roleQueryErrorId;
+};
+struct QQmlXmlListModelQueryResult
+{
+ QML_ANONYMOUS
+ int queryId;
+ QList<QFlatMap<int, QString>> data;
+ QList<QPair<void *, QString>> errors;
+};
+
+class Q_QMLXMLLISTMODEL_PRIVATE_EXPORT QQmlXmlListModelRole : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
+ Q_PROPERTY(QString elementName READ elementName WRITE setElementName NOTIFY elementNameChanged)
+ Q_PROPERTY(QString attributeName READ attributeName WRITE setAttributeName NOTIFY
+ attributeNameChanged)
+ QML_NAMED_ELEMENT(XmlListModelRole)
+
+public:
+ QQmlXmlListModelRole() = default;
+ ~QQmlXmlListModelRole() = default;
+
+ QString name() const;
+ void setName(const QString &name);
+ QString elementName() const;
+ void setElementName(const QString &name);
+ QString attributeName() const;
+ void setAttributeName(const QString &attributeName);
+ bool isValid() const;
+
+signals:
+ void nameChanged();
+ void elementNameChanged();
+ void attributeNameChanged();
+
+private:
+ QString m_name;
+ QString m_elementName;
+ QString m_attributeName;
+};
+
+class QQmlXmlListModelQueryExecutor;
+
+class Q_QMLXMLLISTMODEL_PRIVATE_EXPORT QQmlXmlListModel : public QAbstractListModel,
+ public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+
+ Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+ Q_PROPERTY(qreal progress READ progress NOTIFY progressChanged)
+ Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
+ Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged)
+ Q_PROPERTY(QQmlListProperty<QQmlXmlListModelRole> roles READ roleObjects)
+ Q_PROPERTY(int count READ count NOTIFY countChanged)
+ QML_NAMED_ELEMENT(XmlListModel)
+ Q_CLASSINFO("DefaultProperty", "roles")
+
+public:
+ QQmlXmlListModel(QObject *parent = nullptr);
+ ~QQmlXmlListModel();
+
+ QModelIndex index(int row, int column, const QModelIndex &parent) const override;
+ int rowCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ int count() const;
+
+ QUrl source() const;
+ void setSource(const QUrl &);
+
+ QString query() const;
+ void setQuery(const QString &);
+
+ QQmlListProperty<QQmlXmlListModelRole> roleObjects();
+
+ void appendRole(QQmlXmlListModelRole *);
+ void clearRole();
+
+ enum Status { Null, Ready, Loading, Error };
+ Q_ENUM(Status)
+ Status status() const;
+ qreal progress() const;
+
+ Q_INVOKABLE QString errorString() const;
+
+ void classBegin() override;
+ void componentComplete() override;
+
+Q_SIGNALS:
+ void statusChanged(QQmlXmlListModel::Status);
+ void progressChanged(qreal progress);
+ void countChanged();
+ void sourceChanged();
+ void queryChanged();
+
+public Q_SLOTS:
+ void reload();
+
+private Q_SLOTS:
+#if QT_CONFIG(qml_network)
+ void requestFinished();
+#endif
+ void requestProgress(qint64, qint64);
+ void dataCleared();
+ void queryCompleted(const QQmlXmlListModelQueryResult &);
+ void queryError(void *object, const QString &error);
+
+private:
+ Q_DISABLE_COPY(QQmlXmlListModel)
+
+ void notifyQueryStarted(bool remoteSource);
+
+ static void appendRole(QQmlListProperty<QQmlXmlListModelRole> *, QQmlXmlListModelRole *);
+ static void clearRole(QQmlListProperty<QQmlXmlListModelRole> *);
+
+ void tryExecuteQuery(const QByteArray &data);
+
+ QQmlXmlListModelQueryJob createJob(const QByteArray &data);
+ int nextQueryId();
+
+#if QT_CONFIG(qml_network)
+ void deleteReply();
+
+ QNetworkReply *m_reply = nullptr;
+#endif
+
+ int m_size = 0;
+ QUrl m_source;
+ QString m_query;
+ QStringList m_roleNames;
+ QList<int> m_roles;
+ QList<QQmlXmlListModelRole *> m_roleObjects;
+ QList<QFlatMap<int, QString>> m_data;
+ bool m_isComponentComplete = true;
+ Status m_status = QQmlXmlListModel::Null;
+ QString m_errorString;
+ qreal m_progress = 0;
+ int m_queryId = -1;
+ int m_nextQueryIdGenerator = -1;
+ int m_redirectCount = 0;
+ int m_highestRole = Qt::UserRole;
+ using ResultFutureWatcher = QFutureWatcher<QQmlXmlListModelQueryResult>;
+ QFlatMap<int, ResultFutureWatcher *> m_watchers;
+};
+
+class QQmlXmlListModelQueryRunnable : public QRunnable
+{
+public:
+ explicit QQmlXmlListModelQueryRunnable(QQmlXmlListModelQueryJob &&job);
+ void run() override;
+
+ QFuture<QQmlXmlListModelQueryResult> future() const;
+
+private:
+ void doQueryJob(QQmlXmlListModelQueryResult *currentResult);
+ void processElement(QQmlXmlListModelQueryResult *currentResult, const QString &element,
+ QXmlStreamReader &reader);
+ void readSubTree(const QString &prefix, QXmlStreamReader &reader,
+ QFlatMap<int, QString> &results, QList<QPair<void *, QString>> *errors);
+
+ QQmlXmlListModelQueryJob m_job;
+ QPromise<QQmlXmlListModelQueryResult> m_promise;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQMLXMLLISTMODEL_H
diff --git a/src/qmlxmllistmodel/qtqmlxmllistmodelglobal_p.h b/src/qmlxmllistmodel/qtqmlxmllistmodelglobal_p.h
new file mode 100644
index 0000000000..068732cc05
--- /dev/null
+++ b/src/qmlxmllistmodel/qtqmlxmllistmodelglobal_p.h
@@ -0,0 +1,74 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtQml module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QTQMLXMLLISTMODELGLOBAL_P_H
+#define QTQMLXMLLISTMODELGLOBAL_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtCore/qglobal.h>
+
+QT_BEGIN_NAMESPACE
+
+#if !defined(QT_STATIC)
+# if defined(QT_BUILD_QMLXMLLISTMODEL_LIB)
+# define Q_QMLXMLLISTMODEL_EXPORT Q_DECL_EXPORT
+# else
+# define Q_QMLXMLLISTMODEL_EXPORT Q_DECL_IMPORT
+# endif
+#else
+# define Q_QMLXMLLISTMODEL_EXPORT
+#endif
+
+#define Q_QMLXMLLISTMODEL_PRIVATE_EXPORT Q_QMLXMLLISTMODEL_EXPORT
+
+QT_END_NAMESPACE
+
+void Q_QMLXMLLISTMODEL_PRIVATE_EXPORT qml_register_types_QtQml_XmlListModel();
+
+#endif // QTQMLXMLLISTMODELGLOBAL_P_H
diff --git a/sync.profile b/sync.profile
index ce4809c12e..af3c268ef9 100644
--- a/sync.profile
+++ b/sync.profile
@@ -18,7 +18,8 @@
"QtLabsAnimation" => "$basedir/src/labs/animation",
"QtLabsWavefrontMesh" => "$basedir/src/labs/wavefrontmesh",
"QtLabsQmlModels" => "$basedir/src/labs/models",
- "QtLabsSharedImage" => "$basedir/src/labs/sharedimage"
+ "QtLabsSharedImage" => "$basedir/src/labs/sharedimage",
+ "QtQmlXmlListModel" => "$basedir/src/qmlxmllistmodel"
);
%inject_headers = (
"$basedir/src/qml" => [ "^qqmljsgrammar_p.h", "^qqmljsparser_p.h", "^qml_compile_hash_p.h" ],
diff --git a/tests/auto/qml/CMakeLists.txt b/tests/auto/qml/CMakeLists.txt
index 58520d92a0..c0b26c09b4 100644
--- a/tests/auto/qml/CMakeLists.txt
+++ b/tests/auto/qml/CMakeLists.txt
@@ -99,6 +99,9 @@ if(QT_FEATURE_private_tests)
add_subdirectory(bindingdependencyapi)
add_subdirectory(v4misc)
add_subdirectory(qqmldelegatemodel) # special case
+ if (QT_FEATURE_qml_xmllistmodel)
+ add_subdirectory(qqmlxmllistmodel)
+ endif()
endif()
if(NOT CMAKE_CROSSCOMPILING)
add_subdirectory(qmltyperegistrar)
diff --git a/tests/auto/qml/qqmlxmllistmodel/CMakeLists.txt b/tests/auto/qml/qqmlxmllistmodel/CMakeLists.txt
new file mode 100644
index 0000000000..3748661ce2
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/CMakeLists.txt
@@ -0,0 +1,27 @@
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_qqmlxmllistmodel
+ SOURCES
+ ../../shared/util.cpp ../../shared/util.h
+ tst_qqmlxmllistmodel.cpp
+ INCLUDE_DIRECTORIES
+ ../../shared
+ LIBRARIES
+ Qt::Core
+ Qt::Qml
+ Qt::QmlXmlListModelPrivate
+ TESTDATA ${test_data}
+)
+
+qt_internal_extend_target(tst_qqmlxmllistmodel CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=\\\":/data\\\"
+)
+
+qt_internal_extend_target(tst_qqmlxmllistmodel CONDITION NOT ANDROID AND NOT IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\"
+)
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/attributes.qml b/tests/auto/qml/qqmlxmllistmodel/data/attributes.qml
new file mode 100644
index 0000000000..df44c7862b
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/attributes.qml
@@ -0,0 +1,11 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ source: "attributes.xml"
+ query: "/Pets/Pet"
+
+ XmlListModelRole { name: "name"; elementName: ""; attributeName: "name" }
+ XmlListModelRole { name: "type"; elementName: "info"; attributeName: "type" }
+ XmlListModelRole { name: "age"; elementName: "info"; attributeName: "age" }
+ XmlListModelRole { name: "size"; elementName: "info"; attributeName: "size" }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/attributes.xml b/tests/auto/qml/qqmlxmllistmodel/data/attributes.xml
new file mode 100644
index 0000000000..a10b77c7dc
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/attributes.xml
@@ -0,0 +1,14 @@
+<Pets>
+ <Pet name="Polly">
+ <info type="Parrot" age="12" size="Small"/>
+ </Pet>
+ <Pet name="Penny">
+ <info type="Turtle" age="4" size="Small"/>
+ </Pet>
+ <Pet name="Spot">
+ <info type="Dog" age="9" size="Medium"/>
+ </Pet>
+ <Pet name="Tiny">
+ <info type="Elephant" age="15" size="Large"/>
+ </Pet>
+</Pets>
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/elementErrors.qml b/tests/auto/qml/qqmlxmllistmodel/data/elementErrors.qml
new file mode 100644
index 0000000000..559aab1ef4
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/elementErrors.qml
@@ -0,0 +1,9 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ source: "model.xml"
+ query: "/Pets/Pet"
+ XmlListModelRole { name: "name"; elementName: "/name" } // starts with '/'
+ XmlListModelRole { name: "age"; elementName: "age/" } // ends with '/'
+ XmlListModelRole { name: "type"; elementName: "some//element" } // contains "//"
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/empty.xml b/tests/auto/qml/qqmlxmllistmodel/data/empty.xml
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/empty.xml
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/malformedAttribute.xml b/tests/auto/qml/qqmlxmllistmodel/data/malformedAttribute.xml
new file mode 100644
index 0000000000..6e4c0e3637
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/malformedAttribute.xml
@@ -0,0 +1,28 @@
+<Pets>
+ <Pet>
+ <name>Polly</name>
+ <completely>
+ <useless>
+ <hierarchy/>
+ </useless>
+ </completely>
+ <some>
+ <other>
+ <type>incorrect type tag</type>
+ <another>
+ <nested>
+ <tag/>
+ </nested>
+ </another>
+ <useful>
+ <tags>
+ <type>Parrot</type>
+ <size>incorrect size tag</size>
+ </tags>
+ </useful>
+ <age 1value="12"/>
+ </other>
+ <size>Small</size>
+ </some>
+ </Pet>
+</Pets> \ No newline at end of file
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/malformedData.qml b/tests/auto/qml/qqmlxmllistmodel/data/malformedData.qml
new file mode 100644
index 0000000000..092eaa0cdf
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/malformedData.qml
@@ -0,0 +1,10 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ query: "/Pets/Pet"
+
+ XmlListModelRole { name: "name"; elementName: "name" }
+ XmlListModelRole { name: "type"; elementName: "some/other/useful/tags/type" }
+ XmlListModelRole { name: "age"; elementName: "some/other/age"; attributeName: "value" }
+ XmlListModelRole { name: "size"; elementName: "some/size" }
+} \ No newline at end of file
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/malformedTagNestedLevel.xml b/tests/auto/qml/qqmlxmllistmodel/data/malformedTagNestedLevel.xml
new file mode 100644
index 0000000000..d0332e2924
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/malformedTagNestedLevel.xml
@@ -0,0 +1,28 @@
+<Pets>
+ <Pet>
+ <name>Polly</name>
+ <completely>
+ <useless>
+ <hierarchy/>
+ </useless>
+ </completely>
+ <some>
+ <other>
+ <type>incorrect type tag</type>
+ <another>
+ <nested>
+ <tag/>
+ </nested>
+ </another>
+ <useful>
+ <tags>
+ <type>Parrot
+ <size>incorrect size tag</size>
+ </tags>
+ </useful>
+ <age value="12"/>
+ </other>
+ <size>Small</size>
+ </some>
+ </Pet>
+</Pets> \ No newline at end of file
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/malformedTagTopLevel.xml b/tests/auto/qml/qqmlxmllistmodel/data/malformedTagTopLevel.xml
new file mode 100644
index 0000000000..b499db4b51
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/malformedTagTopLevel.xml
@@ -0,0 +1,28 @@
+<Pets>
+ <Pet>
+ <name>Polly</na>
+ <completely>
+ <useless>
+ <hierarchy/>
+ </useless>
+ </completely>
+ <some>
+ <other>
+ <type>incorrect type tag</type>
+ <another>
+ <nested>
+ <tag/>
+ </nested>
+ </another>
+ <useful>
+ <tags>
+ <type>Parrot</type>
+ <size>incorrect size tag</size>
+ </tags>
+ </useful>
+ <age value="12"/>
+ </other>
+ <size>Small</size>
+ </some>
+ </Pet>
+</Pets> \ No newline at end of file
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/model.qml b/tests/auto/qml/qqmlxmllistmodel/data/model.qml
new file mode 100644
index 0000000000..6634725d4c
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/model.qml
@@ -0,0 +1,11 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ source: "model.xml"
+ query: "/Pets/Pet"
+
+ XmlListModelRole { name: "name"; elementName: "name" }
+ XmlListModelRole { name: "type"; elementName: "type" }
+ XmlListModelRole { name: "age"; elementName: "age" }
+ XmlListModelRole { name: "size"; elementName: "size" }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/model.xml b/tests/auto/qml/qqmlxmllistmodel/data/model.xml
new file mode 100644
index 0000000000..40cd6d0432
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/model.xml
@@ -0,0 +1,54 @@
+<Pets>
+ <Pet>
+ <name>Polly</name>
+ <type>Parrot</type>
+ <age>12</age>
+ <size>Small</size>
+ </Pet>
+ <Pet>
+ <name>Penny</name>
+ <type>Turtle</type>
+ <age>4</age>
+ <size>Small</size>
+ </Pet>
+ <Pet>
+ <name>Warren</name>
+ <type>Rabbit</type>
+ <age>2</age>
+ <size>Small</size>
+ </Pet>
+ <Pet>
+ <name>Spot</name>
+ <type>Dog</type>
+ <age>9</age>
+ <size>Medium</size>
+ </Pet>
+ <Pet>
+ <name>Whiskers</name>
+ <type>Cat</type>
+ <age>2</age>
+ <size>Medium</size>
+ </Pet>
+ <Pet>
+ <name>Joey</name>
+ <type>Kangaroo</type>
+ <age>1</age>
+ </Pet>
+ <Pet>
+ <name>Kimba</name>
+ <type>Bunny</type>
+ <age>65</age>
+ <size>Large</size>
+ </Pet>
+ <Pet>
+ <name>Rover</name>
+ <type>Dog</type>
+ <size>Large</size>
+ </Pet>
+ <Pet>
+ <name>Tiny</name>
+ <type>Elephant</type>
+ <age>15</age>
+ <size>Large</size>
+ </Pet>
+</Pets>
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/model2.xml b/tests/auto/qml/qqmlxmllistmodel/data/model2.xml
new file mode 100644
index 0000000000..dab2ec6dc0
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/model2.xml
@@ -0,0 +1,14 @@
+<Pets>
+ <Pet>
+ <name>Polly</name>
+ <type>Parrot</type>
+ <age>12</age>
+ <size>Small</size>
+ </Pet>
+ <Pet>
+ <name>Penny</name>
+ <type>Turtle</type>
+ <age>4</age>
+ <size>Small</size>
+ </Pet>
+</Pets>
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/nestedElements.qml b/tests/auto/qml/qqmlxmllistmodel/data/nestedElements.qml
new file mode 100644
index 0000000000..2106ac3f77
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/nestedElements.qml
@@ -0,0 +1,11 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ source: "nestedElements.xml"
+ query: "/Pets/Pet"
+
+ XmlListModelRole { name: "name"; elementName: "name" }
+ XmlListModelRole { name: "type"; elementName: "some/other/useful/tags/type" }
+ XmlListModelRole { name: "age"; elementName: "some/other/age"; attributeName: "value" }
+ XmlListModelRole { name: "size"; elementName: "some/size" }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/nestedElements.xml b/tests/auto/qml/qqmlxmllistmodel/data/nestedElements.xml
new file mode 100644
index 0000000000..8ea904920e
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/nestedElements.xml
@@ -0,0 +1,76 @@
+<Pets>
+ <Pet>
+ <name>Polly</name>
+ <completely>
+ <useless>
+ <hierarchy/>
+ </useless>
+ </completely>
+ <some>
+ <other>
+ <type>incorrect type tag</type>
+ <another>
+ <nested>
+ <tag/>
+ </nested>
+ </another>
+ <useful>
+ <tags>
+ <type>Parrot</type>
+ <size>incorrect size tag</size>
+ </tags>
+ </useful>
+ <age value="12"/>
+ </other>
+ <size>Small</size>
+ </some>
+ </Pet>
+ <Pet>
+ <name>Penny</name>
+ <some>
+ <other>
+ <type>incorrect type tag</type>
+ <useful>
+ <tags>
+ <type>Turtle</type>
+ <size>incorrect size tag</size>
+ </tags>
+ </useful>
+ <age value="4"/>
+ </other>
+ <size>Small</size>
+ </some>
+ </Pet>
+ <Pet>
+ <name>Spot</name>
+ <some>
+ <other>
+ <type>incorrect type tag</type>
+ <useful>
+ <tags>
+ <type>Dog</type>
+ <size>incorrect size tag</size>
+ </tags>
+ </useful>
+ <age value="9"/>
+ </other>
+ <size>Medium</size>
+ </some>
+ </Pet>
+ <Pet>
+ <name>Tiny</name>
+ <some>
+ <other>
+ <type>incorrect type tag</type>
+ <useful>
+ <tags>
+ <type>Elephant</type>
+ <size>incorrect size tag</size>
+ </tags>
+ </useful>
+ <age value="15"/>
+ </other>
+ <size>Large</size>
+ </some>
+ </Pet>
+</Pets>
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/propertychanges.qml b/tests/auto/qml/qqmlxmllistmodel/data/propertychanges.qml
new file mode 100644
index 0000000000..3b3e77d726
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/propertychanges.qml
@@ -0,0 +1,10 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ source: "model.xml"
+ query: "/Pets/Pet"
+ XmlListModelRole { objectName: "role"; name: "name"; elementName: "name" }
+ XmlListModelRole { name: "type"; elementName: "type" }
+ XmlListModelRole { name: "age"; elementName: "age" }
+ XmlListModelRole { name: "size"; elementName: "size" }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/proxyCrash.qml b/tests/auto/qml/qqmlxmllistmodel/data/proxyCrash.qml
new file mode 100644
index 0000000000..9233975b19
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/proxyCrash.qml
@@ -0,0 +1,8 @@
+import QtQml.XmlListModel
+import SortFilterProxyModel 1.0
+
+SortFilterProxyModel {
+ source: XmlListModel {
+ XmlListModelRole { }
+ }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/recipes.qml b/tests/auto/qml/qqmlxmllistmodel/data/recipes.qml
new file mode 100644
index 0000000000..18cd8c301a
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/recipes.qml
@@ -0,0 +1,10 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ source: "recipes.xml"
+ query: "/recipes/recipe"
+ XmlListModelRole { name: "title"; elementName: ""; attributeName: "title" }
+ XmlListModelRole { name: "picture"; elementName: "picture" }
+ XmlListModelRole { name: "ingredients"; elementName: "ingredients" }
+ XmlListModelRole { name: "preparation"; elementName: "method" }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/recipes.xml b/tests/auto/qml/qqmlxmllistmodel/data/recipes.xml
new file mode 100644
index 0000000000..d71de60710
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/recipes.xml
@@ -0,0 +1,90 @@
+<recipes>
+ <recipe title="Pancakes">
+ <picture>content/pics/pancakes.jpg</picture>
+ <ingredients><![CDATA[<html>
+ <ul>
+ <li> 1 cup (150g) self-raising flour
+ <li> 1 tbs caster sugar
+ <li> 3/4 cup (185ml) milk
+ <li> 1 egg
+ </ul>
+ </html>
+ ]]></ingredients>
+ <method><![CDATA[<html>
+ <ol>
+ <li> Sift flour and sugar together into a bowl. Add a pinch of salt.
+ <li> Beat milk and egg together, then add to dry ingredients. Beat until smooth.
+ <li> Pour mixture into a pan on medium heat and cook until bubbles appear on the surface.
+ <li> Turn over and cook other side until golden.
+ </ol>
+ </html>
+ ]]></method>
+ </recipe>
+ <recipe title="Fruit Salad">
+ <picture>content/pics/fruit-salad.jpg</picture>
+ <ingredients><![CDATA[* Seasonal Fruit]]></ingredients>
+ <method><![CDATA[* Chop fruit and place in a bowl.]]></method>
+ </recipe>
+ <recipe title="Vegetable Soup">
+ <picture>content/pics/vegetable-soup.jpg</picture>
+ <ingredients><![CDATA[<html>
+ <ul>
+ <li> 1 onion
+ <li> 1 turnip
+ <li> 1 potato
+ <li> 1 carrot
+ <li> 1 head of celery
+ <li> 1 1/2 litres of water
+ </ul>
+ </html>
+ ]]></ingredients>
+ <method><![CDATA[<html>
+ <ol>
+ <li> Chop vegetables.
+ <li> Boil in water until vegetables soften.
+ <li> Season with salt and pepper to taste.
+ </ol>
+ </html>
+ ]]></method>
+ </recipe>
+ <recipe title="Hamburger">
+ <picture>content/pics/hamburger.jpg</picture>
+ <ingredients><![CDATA[<html>
+ <ul>
+ <li> 500g minced beef
+ <li> Seasoning
+ <li> lettuce, tomato, onion, cheese
+ <li> 1 hamburger bun for each burger
+ </ul>
+ </html>
+ ]]></ingredients>
+ <method><![CDATA[<html>
+ <ol>
+ <li> Mix the beef, together with seasoning, in a food processor.
+ <li> Shape the beef into burgers.
+ <li> Grill the burgers for about 5 mins on each side (until cooked through)
+ <li> Serve each burger on a bun with ketchup, cheese, lettuce, tomato and onion.
+ </ol>
+ </html>
+ ]]></method>
+ </recipe>
+ <recipe title="Lemonade">
+ <picture>content/pics/lemonade.jpg</picture>
+ <ingredients><![CDATA[<html>
+ <ul>
+ <li> 1 cup Lemon Juice
+ <li> 1 cup Sugar
+ <li> 6 Cups of Water (2 cups warm water, 4 cups cold water)
+ </ul>
+ </html>
+ ]]></ingredients>
+ <method><![CDATA[<html>
+ <ol>
+ <li> Pour 2 cups of warm water into a pitcher and stir in sugar until it dissolves.
+ <li> Pour in lemon juice, stir again, and add 4 cups of cold water.
+ <li> Chill or serve over ice cubes.
+ </ol>
+ </html>
+ ]]></method>
+ </recipe>
+</recipes>
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/roleCrash.qml b/tests/auto/qml/qqmlxmllistmodel/data/roleCrash.qml
new file mode 100644
index 0000000000..85cb692f03
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/roleCrash.qml
@@ -0,0 +1,10 @@
+import QtQuick 2.0
+import QtQml.XmlListModel
+
+XmlListModel {
+ id: model
+ XmlListModelRole {}
+ Component.onCompleted: {
+ model.roles = 0
+ }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/threading.qml b/tests/auto/qml/qqmlxmllistmodel/data/threading.qml
new file mode 100644
index 0000000000..43672d01ba
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/threading.qml
@@ -0,0 +1,8 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ query: "/data/item"
+ XmlListModelRole { name: "name"; elementName: "name" }
+ XmlListModelRole { name: "age"; elementName: "age" }
+ XmlListModelRole { name: "sport"; elementName: "sport" }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/data/unique.qml b/tests/auto/qml/qqmlxmllistmodel/data/unique.qml
new file mode 100644
index 0000000000..1289c7de93
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/data/unique.qml
@@ -0,0 +1,8 @@
+import QtQml.XmlListModel
+
+XmlListModel {
+ source: "model.xml"
+ query: "/Pets/Pet"
+ XmlListModelRole { name: "name"; elementName: "name" }
+ XmlListModelRole { name: "name"; elementName: "type" }
+}
diff --git a/tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp b/tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp
new file mode 100644
index 0000000000..947b64a5cb
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp
@@ -0,0 +1,710 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+#include <QSignalSpy>
+#include <QMetaObject>
+#include <QSortFilterProxyModel>
+#include <QFile>
+#include <QTimer>
+#include <QTemporaryFile>
+#include <QNetworkRequest>
+#include <QNetworkAccessManager>
+#include <QtQmlXmlListModel/private/qqmlxmllistmodel_p.h>
+#include "../../shared/util.h"
+
+typedef QList<QVariantList> QQmlXmlModelData;
+
+Q_DECLARE_METATYPE(QQmlXmlModelData)
+Q_DECLARE_METATYPE(QQmlXmlListModel::Status)
+
+class tst_QQmlXmlListModel : public QQmlDataTest
+
+{
+ Q_OBJECT
+public:
+ tst_QQmlXmlListModel() { }
+
+private slots:
+ void initTestCase() override
+ {
+ QQmlDataTest::initTestCase();
+ qRegisterMetaType<QQmlXmlListModel::Status>();
+ }
+
+ void buildModel();
+ void cdata();
+ void attributes();
+ void roles();
+ void elementErrors();
+ void uniqueRoleNames();
+ void headers();
+ void source();
+ void source_data();
+ void data();
+ void reload();
+ void threading();
+ void threading_data();
+ void propertyChanges();
+ void nestedElements();
+ void malformedData();
+ void malformedData_data();
+
+ void roleCrash();
+ void proxyCrash();
+
+private:
+ QString errorString(QAbstractItemModel *model)
+ {
+ QString ret;
+ QMetaObject::invokeMethod(model, "errorString", Q_RETURN_ARG(QString, ret));
+ return ret;
+ }
+
+ QString makeItemXmlAndData(const QString &data, QQmlXmlModelData *modelData = 0) const
+ {
+ if (modelData)
+ modelData->clear();
+ QString xml;
+
+ if (!data.isEmpty()) {
+ const QStringList items = data.split(QLatin1Char(';'));
+ for (const QString &item : items) {
+ if (item.isEmpty())
+ continue;
+ QVariantList variants;
+ xml += QLatin1String("<item>");
+ const QStringList fields = item.split(QLatin1Char(','));
+ for (const QString &field : fields) {
+ QStringList values = field.split(QLatin1Char('='));
+ if (values.count() != 2) {
+ qWarning() << "makeItemXmlAndData: invalid field:" << field;
+ continue;
+ }
+ xml += QString("<%1>%2</%1>").arg(values[0], values[1]);
+ if (!modelData)
+ continue;
+ bool isNum = false;
+ int number = values[1].toInt(&isNum);
+ if (isNum)
+ variants << number;
+ else
+ variants << values[1];
+ }
+ xml += QLatin1String("</item>");
+ if (modelData)
+ modelData->append(variants);
+ }
+ }
+
+ QString decl = "<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>";
+ return decl + QLatin1String("<data>") + xml + QLatin1String("</data>");
+ }
+
+ QQmlEngine engine;
+};
+
+class ScopedFile
+{
+public:
+ ScopedFile(const QUrl &url, const QByteArray &data) : m_fileUrl(url)
+ {
+ m_file.setFileName(url.toLocalFile());
+ m_created = m_file.open(QIODevice::WriteOnly | QIODevice::Truncate);
+ if (m_created) {
+ const auto written = m_file.write(data);
+ m_created = written == data.size();
+ m_file.close();
+ }
+ }
+ ~ScopedFile() { QFile::remove(m_file.fileName()); }
+
+ bool isCreated() const { return m_created; }
+ QUrl fileUrl() const { return m_fileUrl; }
+
+private:
+ QFile m_file;
+ const QUrl m_fileUrl;
+ bool m_created = false;
+};
+
+class CustomNetworkAccessManagerFactory : public QObject, public QQmlNetworkAccessManagerFactory
+{
+ Q_OBJECT
+public:
+ QVariantMap lastSentHeaders;
+
+protected:
+ QNetworkAccessManager *create(QObject *parent) override;
+};
+
+class CustomNetworkAccessManager : public QNetworkAccessManager
+{
+ Q_OBJECT
+public:
+ CustomNetworkAccessManager(CustomNetworkAccessManagerFactory *factory, QObject *parent)
+ : QNetworkAccessManager(parent), m_factory(factory)
+ {
+ }
+
+protected:
+ QNetworkReply *createRequest(Operation op, const QNetworkRequest &req,
+ QIODevice *outgoingData = 0) override
+ {
+ if (m_factory) {
+ QVariantMap map;
+ const auto rawHeaderList = req.rawHeaderList();
+ for (const QString &header : rawHeaderList)
+ map[header] = req.rawHeader(header.toUtf8());
+ m_factory->lastSentHeaders = map;
+ }
+ return QNetworkAccessManager::createRequest(op, req, outgoingData);
+ }
+
+ QPointer<CustomNetworkAccessManagerFactory> m_factory;
+};
+
+QNetworkAccessManager *CustomNetworkAccessManagerFactory::create(QObject *parent)
+{
+ return new CustomNetworkAccessManager(this, parent);
+}
+
+void tst_QQmlXmlListModel::buildModel()
+{
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QModelIndex index = model->index(3, 0);
+ QCOMPARE(model->data(index, Qt::UserRole).toString(), QLatin1String("Spot"));
+ QCOMPARE(model->data(index, Qt::UserRole + 1).toString(), QLatin1String("Dog"));
+ QCOMPARE(model->data(index, Qt::UserRole + 2).toInt(), 9);
+ QCOMPARE(model->data(index, Qt::UserRole + 3).toString(), QLatin1String("Medium"));
+}
+
+void tst_QQmlXmlListModel::cdata()
+{
+ QQmlComponent component(&engine, testFileUrl("recipes.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 5);
+
+ QVERIFY(model->data(model->index(2, 0), Qt::UserRole + 2)
+ .toString()
+ .startsWith(QLatin1String("<html>")));
+}
+
+void tst_QQmlXmlListModel::attributes()
+{
+ QQmlComponent component(&engine, testFileUrl("attributes.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 4);
+
+ const QList<QVariantList> desiredResults = { QVariantList { "Polly", "Parrot", 12, "Small" },
+ QVariantList { "Penny", "Turtle", 4, "Small" },
+ QVariantList { "Spot", "Dog", 9, "Medium" },
+ QVariantList { "Tiny", "Elephant", 15, "Large" } };
+
+ QVERIFY(model->rowCount() == desiredResults.size());
+
+ for (qsizetype idx = 0; idx < model->rowCount(); ++idx) {
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole).toString(),
+ desiredResults.at(idx).at(0).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 1).toString(),
+ desiredResults.at(idx).at(1).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 2).toInt(),
+ desiredResults.at(idx).at(2).toInt());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 3).toString(),
+ desiredResults.at(idx).at(3).toString());
+ }
+}
+
+void tst_QQmlXmlListModel::roles()
+{
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QHash<int, QByteArray> roleNames = model->roleNames();
+ QCOMPARE(roleNames.count(), 4);
+ QVERIFY(roleNames.key("name", -1) >= 0);
+ QVERIFY(roleNames.key("type", -1) >= 0);
+ QVERIFY(roleNames.key("age", -1) >= 0);
+ QVERIFY(roleNames.key("size", -1) >= 0);
+
+ QSet<int> roles;
+ roles.insert(roleNames.key("name"));
+ roles.insert(roleNames.key("type"));
+ roles.insert(roleNames.key("age"));
+ roles.insert(roleNames.key("size"));
+ QCOMPARE(roles.count(), 4);
+}
+
+void tst_QQmlXmlListModel::elementErrors()
+{
+ QQmlComponent component(&engine, testFileUrl("elementErrors.qml"));
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("elementErrors.qml").toString()
+ + ":6:5: QML XmlListModelRole: An xml element must not start with '/'")
+ .toUtf8()
+ .constData());
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("elementErrors.qml").toString()
+ + ":7:5: QML XmlListModelRole: An xml element must not end with '/'")
+ .toUtf8()
+ .constData());
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("elementErrors.qml").toString()
+ + ":8:5: QML XmlListModelRole: An xml element must not contain \"//\"")
+ .toUtf8()
+ .constData());
+
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QModelIndex index = model->index(3, 0);
+ QCOMPARE(model->data(index, Qt::UserRole).toString(), QString());
+ QCOMPARE(model->data(index, Qt::UserRole + 1).toString(), QString());
+ QCOMPARE(model->data(index, Qt::UserRole + 2).toString(), QString());
+}
+
+void tst_QQmlXmlListModel::uniqueRoleNames()
+{
+ QQmlComponent component(&engine, testFileUrl("unique.qml"));
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("unique.qml").toString()
+ + ":7:5: QML XmlListModelRole: \"name\" duplicates a previous role name "
+ "and will be disabled.")
+ .toUtf8()
+ .constData());
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QHash<int, QByteArray> roleNames = model->roleNames();
+ QCOMPARE(roleNames.count(), 1);
+}
+
+void tst_QQmlXmlListModel::headers()
+{
+ // ensure the QNetworkAccessManagers created for this test are immediately deleted
+ QQmlEngine qmlEng;
+
+ CustomNetworkAccessManagerFactory factory;
+ qmlEng.setNetworkAccessManagerFactory(&factory);
+
+ QQmlComponent component(&qmlEng, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Ready);
+
+ // It doesn't do a network request for a local file
+ QCOMPARE(factory.lastSentHeaders.count(), 0);
+
+ model->setProperty("source", QUrl("http://localhost/filethatdoesnotexist.xml"));
+ QTRY_COMPARE_WITH_TIMEOUT(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Error, 10000);
+
+ QVariantMap expectedHeaders;
+ expectedHeaders["Accept"] = "application/xml,*/*";
+
+ QCOMPARE(factory.lastSentHeaders.count(), expectedHeaders.count());
+ for (auto it = expectedHeaders.cbegin(), end = expectedHeaders.cend(); it != end; ++it) {
+ QVERIFY(factory.lastSentHeaders.contains(it.key()));
+ QCOMPARE(factory.lastSentHeaders[it.key()].toString(), it.value().toString());
+ }
+}
+
+void tst_QQmlXmlListModel::source()
+{
+ QFETCH(QUrl, source);
+ QFETCH(int, count);
+ QFETCH(QQmlXmlListModel::Status, status);
+
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QSignalSpy spy(model.get(), SIGNAL(statusChanged(QQmlXmlListModel::Status)));
+
+ QVERIFY(errorString(model.get()).isEmpty());
+ QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Loading);
+ QTRY_COMPARE(spy.count(), 1);
+ spy.clear();
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Ready);
+ QVERIFY(errorString(model.get()).isEmpty());
+ QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
+ QCOMPARE(model->rowCount(), 9);
+
+ model->setProperty("source", source);
+ if (model->property("source").toString().isEmpty())
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Null);
+ QCOMPARE(model->property("progress").toDouble(), qreal(source.isLocalFile() ? 1.0 : 0.0));
+ QTRY_COMPARE(spy.count(), 1);
+ spy.clear();
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Loading);
+ QVERIFY(errorString(model.get()).isEmpty());
+
+ QEventLoop loop;
+ QTimer timer;
+ timer.setSingleShot(true);
+ connect(model.get(), SIGNAL(statusChanged(QQmlXmlListModel::Status)), &loop, SLOT(quit()));
+ connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
+ timer.start(20000);
+ loop.exec();
+
+ if (spy.count() == 0 && status != QQmlXmlListModel::Ready) {
+ qWarning("QQmlXmlListModel invalid source test timed out");
+ } else {
+ QCOMPARE(spy.count(), 1);
+ spy.clear();
+ }
+
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")), status);
+ QCOMPARE(model->rowCount(), count);
+
+ if (status == QQmlXmlListModel::Ready)
+ QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
+
+ QCOMPARE(errorString(model.get()).isEmpty(), status == QQmlXmlListModel::Ready);
+}
+
+void tst_QQmlXmlListModel::source_data()
+{
+ QTest::addColumn<QUrl>("source");
+ QTest::addColumn<int>("count");
+ QTest::addColumn<QQmlXmlListModel::Status>("status");
+
+ QTest::newRow("valid") << testFileUrl("model2.xml") << 2 << QQmlXmlListModel::Ready;
+ QTest::newRow("invalid") << QUrl("http://blah.blah/blah.xml") << 0 << QQmlXmlListModel::Error;
+
+ // empty file
+ QTemporaryFile *temp = new QTemporaryFile(this);
+ if (temp->open())
+ QTest::newRow("empty file")
+ << QUrl::fromLocalFile(temp->fileName()) << 0 << QQmlXmlListModel::Ready;
+ temp->close();
+}
+
+void tst_QQmlXmlListModel::data()
+{
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+
+ for (int i = 0; i < 9; i++) {
+ QModelIndex index = model->index(i, 0);
+ for (int j = 0; j < model->roleNames().count(); j++) {
+ QCOMPARE(model->data(index, j), QVariant());
+ }
+ }
+ QTRY_COMPARE(model->rowCount(), 9);
+}
+
+void tst_QQmlXmlListModel::reload()
+{
+ // If no keys are used, the model should be rebuilt from scratch when
+ // reload() is called.
+
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QSignalSpy spyInsert(model.get(), SIGNAL(rowsInserted(QModelIndex, int, int)));
+ QSignalSpy spyRemove(model.get(), SIGNAL(rowsRemoved(QModelIndex, int, int)));
+ QSignalSpy spyCount(model.get(), SIGNAL(countChanged()));
+ // reload multiple times to test the xml query aborting
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QCoreApplication::processEvents();
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QTRY_COMPARE(spyCount.count(), 0);
+ QTRY_COMPARE(spyInsert.count(), 1);
+ QTRY_COMPARE(spyRemove.count(), 1);
+
+ QCOMPARE(spyInsert[0][1].toInt(), 0);
+ QCOMPARE(spyInsert[0][2].toInt(), 8);
+
+ QCOMPARE(spyRemove[0][1].toInt(), 0);
+ QCOMPARE(spyRemove[0][2].toInt(), 8);
+}
+
+void tst_QQmlXmlListModel::threading()
+{
+ QFETCH(int, xmlDataCount);
+
+ QQmlComponent component(&engine, testFileUrl("threading.qml"));
+
+ QScopedPointer<QAbstractItemModel> m1(qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(m1 != nullptr);
+ QScopedPointer<QAbstractItemModel> m2(qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(m2 != nullptr);
+ QScopedPointer<QAbstractItemModel> m3(qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(m3 != nullptr);
+
+ for (int dataCount = 0; dataCount < xmlDataCount; ++dataCount) {
+ QString data1, data2, data3;
+ for (int i = 0; i < dataCount; ++i) {
+ data1 += "name=A" + QString::number(i) + ",age=1" + QString::number(i)
+ + ",sport=Football;";
+ data2 += "name=B" + QString::number(i) + ",age=2" + QString::number(i)
+ + ",sport=Athletics;";
+ data3 += "name=C" + QString::number(i) + ",age=3" + QString::number(i)
+ + ",sport=Curling;";
+ }
+
+ ScopedFile f1(testFileUrl("file1.xml"), makeItemXmlAndData(data1).toLatin1());
+ ScopedFile f2(testFileUrl("file2.xml"), makeItemXmlAndData(data2).toLatin1());
+ ScopedFile f3(testFileUrl("file3.xml"), makeItemXmlAndData(data3).toLatin1());
+ QVERIFY(f1.isCreated() && f2.isCreated() && f3.isCreated());
+
+ m1->setProperty("source", f1.fileUrl());
+ m2->setProperty("source", f2.fileUrl());
+ m3->setProperty("source", f3.fileUrl());
+ QCoreApplication::processEvents();
+
+ QTRY_VERIFY(m1->rowCount() == dataCount && m2->rowCount() == dataCount
+ && m3->rowCount() == dataCount);
+
+ for (int i = 0; i < dataCount; ++i) {
+ QModelIndex index = m1->index(i, 0);
+ QList<int> roles = m1->roleNames().keys();
+ std::sort(roles.begin(), roles.end());
+ QCOMPARE(m1->data(index, roles.at(0)).toString(),
+ QLatin1Char('A') + QString::number(i));
+ QCOMPARE(m1->data(index, roles.at(1)).toString(),
+ QLatin1Char('1') + QString::number(i));
+ QCOMPARE(m1->data(index, roles.at(2)).toString(), QString("Football"));
+
+ index = m2->index(i, 0);
+ roles = m2->roleNames().keys();
+ std::sort(roles.begin(), roles.end());
+ QCOMPARE(m2->data(index, roles.at(0)).toString(),
+ QLatin1Char('B') + QString::number(i));
+ QCOMPARE(m2->data(index, roles.at(1)).toString(),
+ QLatin1Char('2') + QString::number(i));
+ QCOMPARE(m2->data(index, roles.at(2)).toString(), QString("Athletics"));
+
+ index = m3->index(i, 0);
+ roles = m3->roleNames().keys();
+ std::sort(roles.begin(), roles.end());
+ QCOMPARE(m3->data(index, roles.at(0)).toString(),
+ QLatin1Char('C') + QString::number(i));
+ QCOMPARE(m3->data(index, roles.at(1)).toString(),
+ QLatin1Char('3') + QString::number(i));
+ QCOMPARE(m3->data(index, roles.at(2)).toString(), QString("Curling"));
+ }
+
+ // clear sources, so that we could reuse same file names later
+ m1->setProperty("source", QUrl());
+ m2->setProperty("source", QUrl());
+ m3->setProperty("source", QUrl());
+ }
+}
+
+void tst_QQmlXmlListModel::threading_data()
+{
+ QTest::addColumn<int>("xmlDataCount");
+
+ QTest::newRow("1") << 1;
+ QTest::newRow("2") << 2;
+ QTest::newRow("10") << 10;
+}
+
+void tst_QQmlXmlListModel::propertyChanges()
+{
+ QQmlComponent component(&engine, testFileUrl("propertychanges.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QObject *role = model->findChild<QObject *>("role");
+ QVERIFY(role);
+
+ QSignalSpy nameSpy(role, SIGNAL(nameChanged()));
+ QSignalSpy elementSpy(role, SIGNAL(elementNameChanged()));
+
+ role->setProperty("name", "size");
+ role->setProperty("elementName", "size");
+
+ QCOMPARE(role->property("name").toString(), QString("size"));
+ QCOMPARE(role->property("elementName").toString(), QString("size"));
+
+ QCOMPARE(nameSpy.count(), 1);
+ QCOMPARE(elementSpy.count(), 1);
+
+ role->setProperty("name", "size");
+ role->setProperty("elementName", "size");
+
+ QCOMPARE(nameSpy.count(), 1);
+ QCOMPARE(elementSpy.count(), 1);
+
+ QSignalSpy sourceSpy(model.get(), SIGNAL(sourceChanged()));
+ QSignalSpy modelQuerySpy(model.get(), SIGNAL(queryChanged()));
+
+ model->setProperty("source", QUrl("model2.xml"));
+ model->setProperty("query", "/Pets");
+
+ QCOMPARE(model->property("source").toUrl(), QUrl("model2.xml"));
+ QCOMPARE(model->property("query").toString(), QString("/Pets"));
+
+ QTRY_COMPARE(model->rowCount(), 1);
+
+ QCOMPARE(sourceSpy.count(), 1);
+ QCOMPARE(modelQuerySpy.count(), 1);
+
+ model->setProperty("source", QUrl("model2.xml"));
+ model->setProperty("query", "/Pets");
+
+ QCOMPARE(sourceSpy.count(), 1);
+ QCOMPARE(modelQuerySpy.count(), 1);
+
+ QTRY_COMPARE(model->rowCount(), 1);
+}
+
+void tst_QQmlXmlListModel::nestedElements()
+{
+ QQmlComponent component(&engine, testFileUrl("nestedElements.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 4);
+
+ const QList<QVariantList> desiredResults = { QVariantList { "Polly", "Parrot", 12, "Small" },
+ QVariantList { "Penny", "Turtle", 4, "Small" },
+ QVariantList { "Spot", "Dog", 9, "Medium" },
+ QVariantList { "Tiny", "Elephant", 15, "Large" } };
+
+ QVERIFY(model->rowCount() == desiredResults.size());
+
+ for (qsizetype idx = 0; idx < model->rowCount(); ++idx) {
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole).toString(),
+ desiredResults.at(idx).at(0).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 1).toString(),
+ desiredResults.at(idx).at(1).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 2).toInt(),
+ desiredResults.at(idx).at(2).toInt());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 3).toString(),
+ desiredResults.at(idx).at(3).toString());
+ }
+}
+
+void tst_QQmlXmlListModel::malformedData()
+{
+ QFETCH(QUrl, fileName);
+ QFETCH(QString, errorMessage);
+
+ // In this test we check that malformed xml document would not cause
+ // infinite loop while parsing, and that the errors will be reported.
+
+ QTest::ignoreMessage(
+ QtWarningMsg,
+ (testFileUrl("malformedData.qml").toString() + errorMessage).toUtf8().constData());
+
+ QQmlComponent component(&engine, testFileUrl("malformedData.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ model->setProperty("source", fileName);
+ QTRY_VERIFY(model->rowCount() != 0);
+}
+
+void tst_QQmlXmlListModel::malformedData_data()
+{
+ QTest::addColumn<QUrl>("fileName");
+ QTest::addColumn<QString>("errorMessage");
+
+ QTest::addRow("tag mismatch top level")
+ << testFileUrl("malformedTagTopLevel.xml")
+ << QStringLiteral(
+ ":3:1: QML XmlListModel: Query error: \"Opening and ending tag mismatch.\"");
+ QTest::addRow("missing tag nested level")
+ << testFileUrl("malformedTagNestedLevel.xml")
+ << QStringLiteral(":3:1: QML XmlListModel: Query error: \"Expected character data.\"");
+ QTest::addRow("invalid attribute name")
+ << testFileUrl("malformedAttribute.xml")
+ << QStringLiteral(":3:1: QML XmlListModel: Query error: \"Expected '>' or '/', but got "
+ "'[0-9]'.\"");
+}
+
+void tst_QQmlXmlListModel::roleCrash()
+{
+ // don't crash
+ QQmlComponent component(&engine, testFileUrl("roleCrash.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+}
+
+class SortFilterProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QObject *source READ source WRITE setSource)
+
+public:
+ SortFilterProxyModel(QObject *parent = 0) : QSortFilterProxyModel(parent) { sort(0); }
+ QObject *source() const { return sourceModel(); }
+ void setSource(QObject *source) { setSourceModel(qobject_cast<QAbstractItemModel *>(source)); }
+};
+
+void tst_QQmlXmlListModel::proxyCrash()
+{
+ qmlRegisterType<SortFilterProxyModel>("SortFilterProxyModel", 1, 0, "SortFilterProxyModel");
+
+ // don't crash
+ QQmlComponent component(&engine, testFileUrl("proxyCrash.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+}
+
+QTEST_MAIN(tst_QQmlXmlListModel)
+
+#include "tst_qqmlxmllistmodel.moc"