diff options
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, ¤tResult->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" |