From 8c6e24329ecd65f364654b1ca2b6a273f0826a8b Mon Sep 17 00:00:00 2001 From: Frederik Gladhorn Date: Sat, 16 Jun 2018 21:45:35 +0200 Subject: Move xmllistmodel to xmlpatterns Change-Id: Ida2a23ae93b8625638397ee7ae10f32b9dcd2043 Reviewed-by: Simon Hausmann --- src/imports/imports.pro | 3 + src/imports/xmllistmodel/plugin.cpp | 67 ++ src/imports/xmllistmodel/plugins.qmltypes | 59 + src/imports/xmllistmodel/qmldir | 5 + src/imports/xmllistmodel/qqmlxmllistmodel.cpp | 1238 ++++++++++++++++++++ src/imports/xmllistmodel/qqmlxmllistmodel_p.h | 219 ++++ src/imports/xmllistmodel/xmllistmodel.pro | 12 + src/src.pro | 6 + tests/auto/auto.pro | 3 + tests/auto/qquickxmllistmodel/data/empty.xml | 0 tests/auto/qquickxmllistmodel/data/get.qml | 61 + tests/auto/qquickxmllistmodel/data/groups.qml | 10 + tests/auto/qquickxmllistmodel/data/groups.xml | 18 + tests/auto/qquickxmllistmodel/data/model.qml | 11 + tests/auto/qquickxmllistmodel/data/model.xml | 54 + tests/auto/qquickxmllistmodel/data/model2.xml | 14 + .../qquickxmllistmodel/data/propertychanges.qml | 11 + tests/auto/qquickxmllistmodel/data/proxyCrash.qml | 9 + tests/auto/qquickxmllistmodel/data/recipes.qml | 11 + tests/auto/qquickxmllistmodel/data/recipes.xml | 90 ++ tests/auto/qquickxmllistmodel/data/roleCrash.qml | 8 + tests/auto/qquickxmllistmodel/data/roleErrors.qml | 11 + tests/auto/qquickxmllistmodel/data/roleKeys.qml | 13 + tests/auto/qquickxmllistmodel/data/testtypes.qml | 8 + tests/auto/qquickxmllistmodel/data/unique.qml | 9 + .../auto/qquickxmllistmodel/qquickxmllistmodel.pro | 16 + .../qquickxmllistmodel/tst_qquickxmllistmodel.cpp | 1011 ++++++++++++++++ 27 files changed, 2977 insertions(+) create mode 100644 src/imports/imports.pro create mode 100644 src/imports/xmllistmodel/plugin.cpp create mode 100644 src/imports/xmllistmodel/plugins.qmltypes create mode 100644 src/imports/xmllistmodel/qmldir create mode 100644 src/imports/xmllistmodel/qqmlxmllistmodel.cpp create mode 100644 src/imports/xmllistmodel/qqmlxmllistmodel_p.h create mode 100644 src/imports/xmllistmodel/xmllistmodel.pro create mode 100644 tests/auto/qquickxmllistmodel/data/empty.xml create mode 100644 tests/auto/qquickxmllistmodel/data/get.qml create mode 100644 tests/auto/qquickxmllistmodel/data/groups.qml create mode 100644 tests/auto/qquickxmllistmodel/data/groups.xml create mode 100644 tests/auto/qquickxmllistmodel/data/model.qml create mode 100644 tests/auto/qquickxmllistmodel/data/model.xml create mode 100644 tests/auto/qquickxmllistmodel/data/model2.xml create mode 100644 tests/auto/qquickxmllistmodel/data/propertychanges.qml create mode 100644 tests/auto/qquickxmllistmodel/data/proxyCrash.qml create mode 100644 tests/auto/qquickxmllistmodel/data/recipes.qml create mode 100644 tests/auto/qquickxmllistmodel/data/recipes.xml create mode 100644 tests/auto/qquickxmllistmodel/data/roleCrash.qml create mode 100644 tests/auto/qquickxmllistmodel/data/roleErrors.qml create mode 100644 tests/auto/qquickxmllistmodel/data/roleKeys.qml create mode 100644 tests/auto/qquickxmllistmodel/data/testtypes.qml create mode 100644 tests/auto/qquickxmllistmodel/data/unique.qml create mode 100644 tests/auto/qquickxmllistmodel/qquickxmllistmodel.pro create mode 100644 tests/auto/qquickxmllistmodel/tst_qquickxmllistmodel.cpp diff --git a/src/imports/imports.pro b/src/imports/imports.pro new file mode 100644 index 0000000..72046db --- /dev/null +++ b/src/imports/imports.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs + +qtHaveModule(qml): SUBDIRS += xmllistmodel diff --git a/src/imports/xmllistmodel/plugin.cpp b/src/imports/xmllistmodel/plugin.cpp new file mode 100644 index 0000000..c5356b8 --- /dev/null +++ b/src/imports/xmllistmodel/plugin.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 +#include + +#include "qqmlxmllistmodel_p.h" + +QT_BEGIN_NAMESPACE + +class QmlXmlListModelPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) + +public: + QmlXmlListModelPlugin(QObject *parent = 0) : QQmlExtensionPlugin(parent) { } + void registerTypes(const char *uri) override + { + Q_ASSERT(QLatin1String(uri) == QLatin1String("QtQuick.XmlListModel")); + qmlRegisterType(uri,2,0,"XmlListModel"); + qmlRegisterType(uri,2,0,"XmlRole"); + + // Auto-increment the import to stay in sync with ALL future QtQuick minor versions from 5.11 onward + qmlRegisterModule(uri, 2, QT_VERSION_MINOR); + } +}; + +QT_END_NAMESPACE + +#include "plugin.moc" diff --git a/src/imports/xmllistmodel/plugins.qmltypes b/src/imports/xmllistmodel/plugins.qmltypes new file mode 100644 index 0000000..cc675d5 --- /dev/null +++ b/src/imports/xmllistmodel/plugins.qmltypes @@ -0,0 +1,59 @@ +import QtQuick.tooling 1.2 + +// This file describes the plugin-supplied types contained in the library. +// It is used for QML tooling purposes only. +// +// This file was auto-generated by: +// 'qmlplugindump -nonrelocatable QtQuick.XmlListModel 2.0' + +Module { + dependencies: ["QtQuick 2.8"] + Component { + name: "QQuickXmlListModel" + defaultProperty: "roles" + prototype: "QAbstractListModel" + exports: ["QtQuick.XmlListModel/XmlListModel 2.0"] + exportMetaObjectRevisions: [0] + Enum { + name: "Status" + values: { + "Null": 0, + "Ready": 1, + "Loading": 2, + "Error": 3 + } + } + Property { name: "status"; type: "Status"; isReadonly: true } + Property { name: "progress"; type: "double"; isReadonly: true } + Property { name: "source"; type: "QUrl" } + Property { name: "xml"; type: "string" } + Property { name: "query"; type: "string" } + Property { name: "namespaceDeclarations"; type: "string" } + Property { name: "roles"; type: "QQuickXmlListModelRole"; isList: true; isReadonly: true } + Property { name: "count"; type: "int"; isReadonly: true } + Signal { + name: "statusChanged" + Parameter { type: "QQuickXmlListModel::Status" } + } + Signal { + name: "progressChanged" + Parameter { name: "progress"; type: "double" } + } + Method { name: "reload" } + Method { + name: "get" + type: "QQmlV4Handle" + Parameter { name: "index"; type: "int" } + } + Method { name: "errorString"; type: "string" } + } + Component { + name: "QQuickXmlListModelRole" + prototype: "QObject" + exports: ["QtQuick.XmlListModel/XmlRole 2.0"] + exportMetaObjectRevisions: [0] + Property { name: "name"; type: "string" } + Property { name: "query"; type: "string" } + Property { name: "isKey"; type: "bool" } + } +} diff --git a/src/imports/xmllistmodel/qmldir b/src/imports/xmllistmodel/qmldir new file mode 100644 index 0000000..1f17dbb --- /dev/null +++ b/src/imports/xmllistmodel/qmldir @@ -0,0 +1,5 @@ +module QtQuick.XmlListModel +plugin qmlxmllistmodelplugin +classname QmlXmlListModelPlugin +typeinfo plugins.qmltypes +designersupported diff --git a/src/imports/xmllistmodel/qqmlxmllistmodel.cpp b/src/imports/xmllistmodel/qqmlxmllistmodel.cpp new file mode 100644 index 0000000..dbfdd5c --- /dev/null +++ b/src/imports/xmllistmodel/qqmlxmllistmodel.cpp @@ -0,0 +1,1238 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#if QT_CONFIG(qml_network) +#include +#include +#endif +#include +#include + +#include + +Q_DECLARE_METATYPE(QQuickXmlQueryResult) + +QT_BEGIN_NAMESPACE + +using namespace QV4; + +typedef QPair QQuickXmlListRange; + +#define XMLLISTMODEL_CLEAR_ID 0 + +/*! + \qmlmodule QtQuick.XmlListModel 2.11 + \title Qt Quick 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: + + \code + import QtQuick.XmlListModel 2.11 + \endcode +*/ + +/*! + \qmltype XmlRole + \instantiates QQuickXmlListModelRole + \inqmlmodule QtQuick.XmlListModel + \brief For specifying a role to an XmlListModel + \ingroup qtquick-models + + \sa {Qt QML} +*/ + +/*! + \qmlproperty string QtQuick.XmlListModel::XmlRole::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 + // ... + XmlRole { + name: "title" + query: "title/string()" + } + } + \endqml + + \qml + ListView { + model: xmlModel + delegate: Text { text: title } + } + \endqml +*/ + +/*! + \qmlproperty string QtQuick.XmlListModel::XmlRole::query + The relative XPath expression query for this role. The query must be relative; it cannot start + with a '/'. + + For example, if there is an XML document like this: + + \quotefile qml/xmlrole.xml + Here are some valid XPath expressions for XmlRole queries on this document: + + \snippet qml/xmlrole.qml 0 + \dots 4 + \snippet qml/xmlrole.qml 1 + + Accessing the model data for the above roles from a delegate: + + \snippet qml/xmlrole.qml 2 + + See the \l{http://www.w3.org/TR/xpath20/}{W3C XPath 2.0 specification} for more information. +*/ + +/*! + \qmlproperty bool QtQuick.XmlListModel::XmlRole::isKey + Defines whether this is a key role. + Key roles are used to determine whether a set of values should + be updated or added to the XML list model when XmlListModel::reload() + is called. + + \sa XmlListModel +*/ + +struct XmlQueryJob +{ + int queryId; + QByteArray data; + QString query; + QString namespaces; + QStringList roleQueries; + QList roleQueryErrorId; // the ptr to send back if there is an error + QStringList keyRoleQueries; + QStringList keyRoleResultsCache; + QString prefix; +}; + + +class QQuickXmlQueryEngine; +class QQuickXmlQueryThreadObject : public QObject +{ + Q_OBJECT +public: + QQuickXmlQueryThreadObject(QQuickXmlQueryEngine *); + + void processJobs(); + bool event(QEvent *e) override; + +private: + QQuickXmlQueryEngine *m_queryEngine; +}; + + +class QQuickXmlQueryEngine : public QThread +{ + Q_OBJECT +public: + QQuickXmlQueryEngine(QQmlEngine *eng); + ~QQuickXmlQueryEngine(); + + int doQuery(QString query, QString namespaces, QByteArray data, QList* roleObjects, QStringList keyRoleResultsCache); + void abort(int id); + + void processJobs(); + + static QQuickXmlQueryEngine *instance(QQmlEngine *engine); + +signals: + void queryCompleted(const QQuickXmlQueryResult &); + void error(void*, const QString&); + +protected: + void run() override; + +private: + void processQuery(XmlQueryJob *job); + void doQueryJob(XmlQueryJob *job, QQuickXmlQueryResult *currentResult); + void doSubQueryJob(XmlQueryJob *job, QQuickXmlQueryResult *currentResult); + void getValuesOfKeyRoles(const XmlQueryJob& currentJob, QStringList *values, QXmlQuery *query) const; + void addIndexToRangeList(QList *ranges, int index) const; + + QMutex m_mutex; + QQuickXmlQueryThreadObject *m_threadObject; + QList m_jobs; + QSet m_cancelledJobs; + QAtomicInt m_queryIds; + + QQmlEngine *m_engine; + QObject *m_eventLoopQuitHack; + + static QHash queryEngines; + static QMutex queryEnginesMutex; +}; +QHash QQuickXmlQueryEngine::queryEngines; +QMutex QQuickXmlQueryEngine::queryEnginesMutex; + + +QQuickXmlQueryThreadObject::QQuickXmlQueryThreadObject(QQuickXmlQueryEngine *e) + : m_queryEngine(e) +{ +} + +void QQuickXmlQueryThreadObject::processJobs() +{ + QCoreApplication::postEvent(this, new QEvent(QEvent::User)); +} + +bool QQuickXmlQueryThreadObject::event(QEvent *e) +{ + if (e->type() == QEvent::User) { + m_queryEngine->processJobs(); + return true; + } else { + return QObject::event(e); + } +} + + + +QQuickXmlQueryEngine::QQuickXmlQueryEngine(QQmlEngine *eng) +: QThread(eng), m_threadObject(0), m_queryIds(XMLLISTMODEL_CLEAR_ID + 1), m_engine(eng), m_eventLoopQuitHack(0) +{ + qRegisterMetaType("QQuickXmlQueryResult"); + + m_eventLoopQuitHack = new QObject; + m_eventLoopQuitHack->moveToThread(this); + connect(m_eventLoopQuitHack, SIGNAL(destroyed(QObject*)), SLOT(quit()), Qt::DirectConnection); + start(QThread::IdlePriority); +} + +QQuickXmlQueryEngine::~QQuickXmlQueryEngine() +{ + queryEnginesMutex.lock(); + queryEngines.remove(m_engine); + queryEnginesMutex.unlock(); + + m_eventLoopQuitHack->deleteLater(); + wait(); +} + +int QQuickXmlQueryEngine::doQuery(QString query, QString namespaces, QByteArray data, QList* roleObjects, QStringList keyRoleResultsCache) { + { + QMutexLocker m1(&m_mutex); + m_queryIds.ref(); + if (m_queryIds.load() <= 0) + m_queryIds.store(1); + } + + XmlQueryJob job; + job.queryId = m_queryIds.load(); + job.data = data; + job.query = QLatin1String("doc($src)") + query; + job.namespaces = namespaces; + job.keyRoleResultsCache = keyRoleResultsCache; + + for (int i=0; icount(); i++) { + if (!roleObjects->at(i)->isValid()) { + job.roleQueries << QString(); + continue; + } + job.roleQueries << roleObjects->at(i)->query(); + job.roleQueryErrorId << static_cast(roleObjects->at(i)); + if (roleObjects->at(i)->isKey()) + job.keyRoleQueries << job.roleQueries.last(); + } + + { + QMutexLocker ml(&m_mutex); + m_jobs.append(job); + if (m_threadObject) + m_threadObject->processJobs(); + } + + return job.queryId; +} + +void QQuickXmlQueryEngine::abort(int id) +{ + QMutexLocker ml(&m_mutex); + if (id != -1) + m_cancelledJobs.insert(id); +} + +void QQuickXmlQueryEngine::run() +{ + m_mutex.lock(); + m_threadObject = new QQuickXmlQueryThreadObject(this); + m_mutex.unlock(); + + processJobs(); + exec(); + + delete m_threadObject; + m_threadObject = 0; +} + +void QQuickXmlQueryEngine::processJobs() +{ + QMutexLocker locker(&m_mutex); + + while (true) { + if (m_jobs.isEmpty()) + return; + + XmlQueryJob currentJob = m_jobs.takeLast(); + while (m_cancelledJobs.remove(currentJob.queryId)) { + if (m_jobs.isEmpty()) + return; + currentJob = m_jobs.takeLast(); + } + + locker.unlock(); + processQuery(¤tJob); + locker.relock(); + } +} + +QQuickXmlQueryEngine *QQuickXmlQueryEngine::instance(QQmlEngine *engine) +{ + queryEnginesMutex.lock(); + QQuickXmlQueryEngine *queryEng = queryEngines.value(engine); + if (!queryEng) { + queryEng = new QQuickXmlQueryEngine(engine); + queryEngines.insert(engine, queryEng); + } + queryEnginesMutex.unlock(); + + return queryEng; +} + +void QQuickXmlQueryEngine::processQuery(XmlQueryJob *job) +{ + QQuickXmlQueryResult result; + result.queryId = job->queryId; + doQueryJob(job, &result); + doSubQueryJob(job, &result); + + { + QMutexLocker ml(&m_mutex); + if (m_cancelledJobs.contains(job->queryId)) { + m_cancelledJobs.remove(job->queryId); + } else { + emit queryCompleted(result); + } + } +} + +void QQuickXmlQueryEngine::doQueryJob(XmlQueryJob *currentJob, QQuickXmlQueryResult *currentResult) +{ + Q_ASSERT(currentJob->queryId != -1); + + QString r; + QXmlQuery query; + QBuffer buffer(¤tJob->data); + buffer.open(QIODevice::ReadOnly); + query.bindVariable(QLatin1String("src"), &buffer); + query.setQuery(currentJob->namespaces + currentJob->query); + query.evaluateTo(&r); + + //always need a single root element + QByteArray xml = "\n" + r.toUtf8() + ""; + QBuffer b(&xml); + b.open(QIODevice::ReadOnly); + + QString namespaces = QLatin1String("declare namespace dummy=\"http://qtsotware.com/dummy\";\n") + currentJob->namespaces; + QString prefix = QLatin1String("doc($inputDocument)/dummy:items/*"); + + //figure out how many items we are dealing with + int count = -1; + { + QXmlResultItems result; + QXmlQuery countquery; + countquery.bindVariable(QLatin1String("inputDocument"), &b); + countquery.setQuery(namespaces + QLatin1String("count(") + prefix + QLatin1Char(')')); + countquery.evaluateTo(&result); + QXmlItem item(result.next()); + if (item.isAtomicValue()) + count = item.toAtomicValue().toInt(); + } + + currentJob->data = xml; + currentJob->prefix = namespaces + prefix + QLatin1Char('/'); + currentResult->size = (count > 0 ? count : 0); +} + +void QQuickXmlQueryEngine::getValuesOfKeyRoles(const XmlQueryJob& currentJob, QStringList *values, QXmlQuery *query) const +{ + const QStringList &keysQueries = currentJob.keyRoleQueries; + QString keysQuery; + if (keysQueries.count() == 1) + keysQuery = currentJob.prefix + keysQueries[0]; + else if (keysQueries.count() > 1) + keysQuery = currentJob.prefix + QLatin1String("concat(") + keysQueries.join(QLatin1Char(',')) + QLatin1Char(')'); + + if (!keysQuery.isEmpty()) { + query->setQuery(keysQuery); + QXmlResultItems resultItems; + query->evaluateTo(&resultItems); + QXmlItem item(resultItems.next()); + while (!item.isNull()) { + values->append(item.toAtomicValue().toString()); + item = resultItems.next(); + } + } +} + +void QQuickXmlQueryEngine::addIndexToRangeList(QList *ranges, int index) const { + if (ranges->isEmpty()) + ranges->append(qMakePair(index, 1)); + else if (ranges->last().first + ranges->last().second == index) + ranges->last().second += 1; + else + ranges->append(qMakePair(index, 1)); +} + +void QQuickXmlQueryEngine::doSubQueryJob(XmlQueryJob *currentJob, QQuickXmlQueryResult *currentResult) +{ + Q_ASSERT(currentJob->queryId != -1); + + QBuffer b(¤tJob->data); + b.open(QIODevice::ReadOnly); + + QXmlQuery subquery; + subquery.bindVariable(QLatin1String("inputDocument"), &b); + + QStringList keyRoleResults; + getValuesOfKeyRoles(*currentJob, &keyRoleResults, &subquery); + + // See if any values of key roles have been inserted or removed. + + if (currentJob->keyRoleResultsCache.isEmpty()) { + currentResult->inserted << qMakePair(0, currentResult->size); + } else { + if (keyRoleResults != currentJob->keyRoleResultsCache) { + QStringList temp; + for (int i=0; ikeyRoleResultsCache.count(); i++) { + if (!keyRoleResults.contains(currentJob->keyRoleResultsCache[i])) + addIndexToRangeList(¤tResult->removed, i); + else + temp << currentJob->keyRoleResultsCache[i]; + } + for (int i=0; iinserted, i); + } + } + } + } + currentResult->keyRoleResultsCache = keyRoleResults; + + // Get the new values for each role. + //### we might be able to condense even further (query for everything in one go) + const QStringList &queries = currentJob->roleQueries; + for (int i = 0; i < queries.size(); ++i) { + QList resultList; + if (!queries[i].isEmpty()) { + subquery.setQuery(currentJob->prefix + QLatin1String("(let $v := string(") + queries[i] + QLatin1String(") return if ($v) then ") + queries[i] + QLatin1String(" else \"\")")); + if (subquery.isValid()) { + QXmlResultItems resultItems; + subquery.evaluateTo(&resultItems); + QXmlItem item(resultItems.next()); + while (!item.isNull()) { + resultList << item.toAtomicValue(); //### we used to trim strings + item = resultItems.next(); + } + } else { + emit error(currentJob->roleQueryErrorId.at(i), queries[i]); + } + } + //### should warn here if things have gone wrong. + while (resultList.count() < currentResult->size) + resultList << QVariant(); + currentResult->data << resultList; + b.seek(0); + } + + //this method is much slower, but works better for incremental loading + /*for (int j = 0; j < m_size; ++j) { + QList resultList; + for (int i = 0; i < m_roleObjects->size(); ++i) { + QQuickXmlListModelRole *role = m_roleObjects->at(i); + subquery.setQuery(m_prefix.arg(j+1) + role->query()); + if (role->isStringList()) { + QStringList data; + subquery.evaluateTo(&data); + resultList << QVariant(data); + //qDebug() << data; + } else { + QString s; + subquery.evaluateTo(&s); + if (role->isCData()) { + //un-escape + s.replace(QLatin1String("<"), QLatin1String("<")); + s.replace(QLatin1String(">"), QLatin1String(">")); + s.replace(QLatin1String("&"), QLatin1String("&")); + } + resultList << s.trimmed(); + //qDebug() << s; + } + b.seek(0); + } + m_modelData << resultList; + }*/ +} + +class QQuickXmlListModelPrivate : public QAbstractItemModelPrivate +{ + Q_DECLARE_PUBLIC(QQuickXmlListModel) +public: + QQuickXmlListModelPrivate() + : isComponentComplete(true), size(0), highestRole(Qt::UserRole) +#if QT_CONFIG(qml_network) + , reply(0) +#endif + , status(QQuickXmlListModel::Null), progress(0.0) + , queryId(-1), roleObjects(), redirectCount(0) {} + + + void notifyQueryStarted(bool remoteSource) { + Q_Q(QQuickXmlListModel); + progress = remoteSource ? 0.0 : 1.0; + status = QQuickXmlListModel::Loading; + errorString.clear(); + emit q->progressChanged(progress); + emit q->statusChanged(status); + } + +#if QT_CONFIG(qml_network) + void deleteReply() { + Q_Q(QQuickXmlListModel); + if (reply) { + QObject::disconnect(reply, 0, q, 0); + reply->deleteLater(); + reply = 0; + } + } +#endif + + bool isComponentComplete; + QUrl src; + QString xml; + QString query; + QString namespaces; + int size; + QList roles; + QStringList roleNames; + int highestRole; + +#if QT_CONFIG(qml_network) + QNetworkReply *reply; +#endif + QQuickXmlListModel::Status status; + QString errorString; + qreal progress; + int queryId; + QStringList keyRoleResultsCache; + QList roleObjects; + + static void append_role(QQmlListProperty *list, QQuickXmlListModelRole *role); + static void clear_role(QQmlListProperty *list); + QList > data; + int redirectCount; +}; + + +void QQuickXmlListModelPrivate::append_role(QQmlListProperty *list, QQuickXmlListModelRole *role) +{ + QQuickXmlListModel *_this = qobject_cast(list->object); + if (_this && role) { + int i = _this->d_func()->roleObjects.count(); + _this->d_func()->roleObjects.append(role); + if (_this->d_func()->roleNames.contains(role->name())) { + qmlWarning(role) << QQuickXmlListModel::tr("\"%1\" duplicates a previous role name and will be disabled.").arg(role->name()); + return; + } + _this->d_func()->roles.insert(i, _this->d_func()->highestRole); + _this->d_func()->roleNames.insert(i, role->name()); + ++_this->d_func()->highestRole; + } +} + +//### clear needs to invalidate any cached data (in data table) as well +// (and the model should emit the appropriate signals) +void QQuickXmlListModelPrivate::clear_role(QQmlListProperty *list) +{ + QQuickXmlListModel *_this = static_cast(list->object); + _this->d_func()->roles.clear(); + _this->d_func()->roleNames.clear(); + _this->d_func()->roleObjects.clear(); +} + +/*! + \qmltype XmlListModel + \instantiates QQuickXmlListModel + \inqmlmodule QtQuick.XmlListModel + \brief For specifying a read-only model using XPath expressions + \ingroup qtquick-models + + + To use this element, you will need to import the module with the following line: + \code + import QtQuick.XmlListModel 2.0 + \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 \l Repeater). + + For example, if there is a XML document at http://www.mysite.com/feed.xml like this: + + \code + + + ... + + + A blog post + Sat, 07 Sep 2010 10:00:01 GMT + + + Another blog post + Sat, 07 Sep 2010 15:35:01 GMT + + + + \endcode + + A XmlListModel could create a model from this data, like this: + + \qml + import QtQuick 2.0 + import QtQuick.XmlListModel 2.0 + + XmlListModel { + id: xmlModel + source: "http://www.mysite.com/feed.xml" + query: "/rss/channel/item" + + XmlRole { name: "title"; query: "title/string()" } + XmlRole { name: "pubDate"; query: "pubDate/string()" } + } + \endqml + + The \l {XmlListModel::query}{query} value of "/rss/channel/item" specifies that the XmlListModel should generate + a model item for each \c in the XML document. + + The XmlRole objects define the + model item attributes. Here, each model item will have \c title and \c pubDate + attributes that match the \c title and \c pubDate values of its corresponding \c . + (See \l XmlRole::query for more examples of valid XPath expressions for XmlRole.) + + The model could be used in a ListView, like this: + + \qml + ListView { + width: 180; height: 300 + model: xmlModel + delegate: Text { text: title + ": " + pubDate } + } + \endqml + + \image qml-xmllistmodel-example.png + + The XmlListModel data is loaded asynchronously, and \l status + is set to \c XmlListModel.Ready when loading is complete. + Note this means when XmlListModel is used for a view, the view is not + populated until the model is loaded. + + + \section2 Using Key XML Roles + + You can define certain roles as "keys" so that when reload() is called, + the model will only add and refresh data that contains new values for + these keys. + + For example, if above role for "pubDate" was defined like this instead: + + \qml + XmlRole { name: "pubDate"; query: "pubDate/string()"; isKey: true } + \endqml + + Then when reload() is called, the model will only add and reload + items with a "pubDate" value that is not already + present in the model. + + This is useful when displaying the contents of XML documents that + are incrementally updated (such as RSS feeds) to avoid repainting the + entire contents of a model in a view. + + If multiple key roles are specified, the model only adds and reload items + with a combined value of all key roles that is not already present in + the model. + + \sa {Qt Quick Demo - RSS News} +*/ + +QQuickXmlListModel::QQuickXmlListModel(QObject *parent) + : QAbstractListModel(*(new QQuickXmlListModelPrivate), parent) +{ +} + +QQuickXmlListModel::~QQuickXmlListModel() +{ +} + +/*! + \qmlproperty list QtQuick.XmlListModel::XmlListModel::roles + + The roles to make available for this model. +*/ +QQmlListProperty QQuickXmlListModel::roleObjects() +{ + Q_D(QQuickXmlListModel); + QQmlListProperty list(this, d->roleObjects); + list.append = &QQuickXmlListModelPrivate::append_role; + list.clear = &QQuickXmlListModelPrivate::clear_role; + return list; +} + +QModelIndex QQuickXmlListModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_D(const QQuickXmlListModel); + return !parent.isValid() && column == 0 && row >= 0 && row < d->size + ? createIndex(row, column) + : QModelIndex(); +} + +int QQuickXmlListModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QQuickXmlListModel); + return !parent.isValid() ? d->size : 0; +} + +QVariant QQuickXmlListModel::data(const QModelIndex &index, int role) const +{ + Q_D(const QQuickXmlListModel); + const int roleIndex = d->roles.indexOf(role); + return (roleIndex == -1 || !index.isValid()) + ? QVariant() + : d->data.value(roleIndex).value(index.row()); +} + +QHash QQuickXmlListModel::roleNames() const +{ + Q_D(const QQuickXmlListModel); + QHash roleNames; + for (int i = 0; i < d->roles.count(); ++i) + roleNames.insert(d->roles.at(i), d->roleNames.at(i).toUtf8()); + return roleNames; +} + +/*! + \qmlproperty int QtQuick.XmlListModel::XmlListModel::count + The number of data entries in the model. +*/ +int QQuickXmlListModel::count() const +{ + Q_D(const QQuickXmlListModel); + return d->size; +} + +/*! + \qmlproperty url QtQuick.XmlListModel::XmlListModel::source + The location of the XML data source. + + If both \c source and \l xml are set, \l xml is used. +*/ +QUrl QQuickXmlListModel::source() const +{ + Q_D(const QQuickXmlListModel); + return d->src; +} + +void QQuickXmlListModel::setSource(const QUrl &src) +{ + Q_D(QQuickXmlListModel); + if (d->src != src) { + d->src = src; + if (d->xml.isEmpty()) // src is only used if d->xml is not set + reload(); + emit sourceChanged(); + } +} + +/*! + \qmlproperty string QtQuick.XmlListModel::XmlListModel::xml + This property holds the XML data for this model, if set. + + The text is assumed to be UTF-8 encoded. + + If both \l source and \c xml are set, \c xml is used. +*/ +QString QQuickXmlListModel::xml() const +{ + Q_D(const QQuickXmlListModel); + return d->xml; +} + +void QQuickXmlListModel::setXml(const QString &xml) +{ + Q_D(QQuickXmlListModel); + if (d->xml != xml) { + d->xml = xml; + reload(); + emit xmlChanged(); + } +} + +/*! + \qmlproperty string QtQuick.XmlListModel::XmlListModel::query + An absolute XPath query representing the base query for creating model items + from this model's XmlRole objects. The query should start with '/' or '//'. +*/ +QString QQuickXmlListModel::query() const +{ + Q_D(const QQuickXmlListModel); + return d->query; +} + +void QQuickXmlListModel::setQuery(const QString &query) +{ + Q_D(QQuickXmlListModel); + if (!query.startsWith(QLatin1Char('/'))) { + qmlWarning(this) << QCoreApplication::translate("QQuickXmlRoleList", "An XmlListModel query must start with '/' or \"//\""); + return; + } + + if (d->query != query) { + d->query = query; + reload(); + emit queryChanged(); + } +} + +/*! + \qmlproperty string QtQuick.XmlListModel::XmlListModel::namespaceDeclarations + The namespace declarations to be used in the XPath queries. + + The namespaces should be declared as in XQuery. For example, if a requested document + at http://mysite.com/feed.xml uses the namespace "http://www.w3.org/2005/Atom", + this can be declared as the default namespace: + + \qml + XmlListModel { + source: "http://mysite.com/feed.xml" + query: "/feed/entry" + namespaceDeclarations: "declare default element namespace 'http://www.w3.org/2005/Atom';" + + XmlRole { name: "title"; query: "title/string()" } + } + \endqml +*/ +QString QQuickXmlListModel::namespaceDeclarations() const +{ + Q_D(const QQuickXmlListModel); + return d->namespaces; +} + +void QQuickXmlListModel::setNamespaceDeclarations(const QString &declarations) +{ + Q_D(QQuickXmlListModel); + if (d->namespaces != declarations) { + d->namespaces = declarations; + reload(); + emit namespaceDeclarationsChanged(); + } +} + +/*! + \qmlmethod object QtQuick.XmlListModel::XmlListModel::get(int index) + + Returns the item at \a index in the model. + + For example, for a model like this: + + \qml + XmlListModel { + id: model + source: "http://mysite.com/feed.xml" + query: "/feed/entry" + XmlRole { name: "title"; query: "title/string()" } + } + \endqml + + This will access the \c title value for the first item in the model: + + \js + var title = model.get(0).title; + \endjs +*/ +QQmlV4Handle QQuickXmlListModel::get(int index) const +{ + // Must be called with a context and handle scope + Q_D(const QQuickXmlListModel); + + if (index < 0 || index >= count()) + return QQmlV4Handle(Encode::undefined()); + + QQmlEngine *engine = qmlContext(this)->engine(); + ExecutionEngine *v4engine = engine->handle(); + Scope scope(v4engine); + Scoped o(scope, v4engine->newObject()); + ScopedString name(scope); + ScopedValue value(scope); + for (int ii = 0; ii < d->roleObjects.count(); ++ii) { + name = v4engine->newIdentifier(d->roleObjects[ii]->name()); + value = v4engine->fromVariant(d->data.value(ii).value(index)); + o->insertMember(name.getPointer(), value); + } + + return QQmlV4Handle(o); +} + +/*! + \qmlproperty enumeration QtQuick.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 errorString() for details + about the error. + \endlist + + \sa progress + +*/ +QQuickXmlListModel::Status QQuickXmlListModel::status() const +{ + Q_D(const QQuickXmlListModel); + return d->status; +} + +/*! + \qmlproperty real QtQuick.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 QQuickXmlListModel::progress() const +{ + Q_D(const QQuickXmlListModel); + return d->progress; +} + +/*! + \qmlmethod QtQuick.XmlListModel::XmlListModel::errorString() + + Returns a string description of the last error that occurred + if \l status is XmlListModel::Error. +*/ +QString QQuickXmlListModel::errorString() const +{ + Q_D(const QQuickXmlListModel); + return d->errorString; +} + +void QQuickXmlListModel::classBegin() +{ + Q_D(QQuickXmlListModel); + d->isComponentComplete = false; + + QQuickXmlQueryEngine *queryEngine = QQuickXmlQueryEngine::instance(qmlEngine(this)); + connect(queryEngine, SIGNAL(queryCompleted(QQuickXmlQueryResult)), + SLOT(queryCompleted(QQuickXmlQueryResult))); + connect(queryEngine, SIGNAL(error(void*,QString)), + SLOT(queryError(void*,QString))); +} + +void QQuickXmlListModel::componentComplete() +{ + Q_D(QQuickXmlListModel); + d->isComponentComplete = true; + reload(); +} + +/*! + \qmlmethod QtQuick.XmlListModel::XmlListModel::reload() + + Reloads the model. + + If no key roles have been specified, all existing model + data is removed, and the model is rebuilt from scratch. + + Otherwise, items are only added if the model does not already + contain items with matching key role values. + + \sa {Using key XML roles}, XmlRole::isKey +*/ +void QQuickXmlListModel::reload() +{ + Q_D(QQuickXmlListModel); + + if (!d->isComponentComplete) + return; + + QQuickXmlQueryEngine::instance(qmlEngine(this))->abort(d->queryId); + d->queryId = -1; + + if (d->size < 0) + d->size = 0; + +#if QT_CONFIG(qml_network) + if (d->reply) { + d->reply->abort(); + d->deleteReply(); + } +#endif + + if (!d->xml.isEmpty()) { + d->queryId = QQuickXmlQueryEngine::instance(qmlEngine(this))->doQuery(d->query, d->namespaces, d->xml.toUtf8(), &d->roleObjects, d->keyRoleResultsCache); + d->notifyQueryStarted(false); + + } else if (d->src.isEmpty()) { + d->queryId = XMLLISTMODEL_CLEAR_ID; + d->notifyQueryStarted(false); + QTimer::singleShot(0, this, SLOT(dataCleared())); + + } else if (QQmlFile::isLocalFile(d->src)) { + QFile file(QQmlFile::urlToLocalFileOrQrc(d->src)); + QByteArray data = file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray(); + d->notifyQueryStarted(false); + if (data.isEmpty()) { + d->queryId = XMLLISTMODEL_CLEAR_ID; + QTimer::singleShot(0, this, SLOT(dataCleared())); + } else { + d->queryId = QQuickXmlQueryEngine::instance(qmlEngine(this))->doQuery( + d->query, d->namespaces, data, &d->roleObjects, d->keyRoleResultsCache); + } + } else { +#if QT_CONFIG(qml_network) + d->notifyQueryStarted(true); + QNetworkRequest req(d->src); + req.setRawHeader("Accept", "application/xml,*/*"); + d->reply = qmlContext(this)->engine()->networkAccessManager()->get(req); + QObject::connect(d->reply, SIGNAL(finished()), this, SLOT(requestFinished())); + QObject::connect(d->reply, SIGNAL(downloadProgress(qint64,qint64)), + this, SLOT(requestProgress(qint64,qint64))); +#else + d->queryId = XMLLISTMODEL_CLEAR_ID; + d->notifyQueryStarted(false); + QTimer::singleShot(0, this, SLOT(dataCleared())); +#endif + } +} + +#define XMLLISTMODEL_MAX_REDIRECT 16 + +#if QT_CONFIG(qml_network) +void QQuickXmlListModel::requestFinished() +{ + Q_D(QQuickXmlListModel); + + d->redirectCount++; + if (d->redirectCount < XMLLISTMODEL_MAX_REDIRECT) { + QVariant redirect = d->reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + if (redirect.isValid()) { + QUrl url = d->reply->url().resolved(redirect.toUrl()); + d->deleteReply(); + setSource(url); + return; + } + } + d->redirectCount = 0; + + if (d->reply->error() != QNetworkReply::NoError) { + d->errorString = d->reply->errorString(); + d->deleteReply(); + + if (d->size > 0) { + beginRemoveRows(QModelIndex(), 0, d->size - 1); + d->data.clear(); + d->size = 0; + endRemoveRows(); + emit countChanged(); + } + + d->status = Error; + d->queryId = -1; + emit statusChanged(d->status); + } else { + QByteArray data = d->reply->readAll(); + if (data.isEmpty()) { + d->queryId = XMLLISTMODEL_CLEAR_ID; + QTimer::singleShot(0, this, SLOT(dataCleared())); + } else { + d->queryId = QQuickXmlQueryEngine::instance(qmlEngine(this))->doQuery(d->query, d->namespaces, data, &d->roleObjects, d->keyRoleResultsCache); + } + d->deleteReply(); + + d->progress = 1.0; + emit progressChanged(d->progress); + } +} +#endif + +void QQuickXmlListModel::requestProgress(qint64 received, qint64 total) +{ + Q_D(QQuickXmlListModel); + if (d->status == Loading && total > 0) { + d->progress = qreal(received)/total; + emit progressChanged(d->progress); + } +} + +void QQuickXmlListModel::dataCleared() +{ + Q_D(QQuickXmlListModel); + QQuickXmlQueryResult r; + r.queryId = XMLLISTMODEL_CLEAR_ID; + r.size = 0; + r.removed << qMakePair(0, count()); + r.keyRoleResultsCache = d->keyRoleResultsCache; + queryCompleted(r); +} + +void QQuickXmlListModel::queryError(void* object, const QString& error) +{ + // Be extra careful, object may no longer exist, it's just an ID. + Q_D(QQuickXmlListModel); + for (int i=0; iroleObjects.count(); i++) { + if (d->roleObjects.at(i) == static_cast(object)) { + qmlWarning(d->roleObjects.at(i)) << QQuickXmlListModel::tr("invalid query: \"%1\"").arg(error); + return; + } + } + qmlWarning(this) << QQuickXmlListModel::tr("invalid query: \"%1\"").arg(error); +} + +void QQuickXmlListModel::queryCompleted(const QQuickXmlQueryResult &result) +{ + Q_D(QQuickXmlListModel); + if (result.queryId != d->queryId) + return; + + int origCount = d->size; + bool sizeChanged = result.size != d->size; + + d->keyRoleResultsCache = result.keyRoleResultsCache; + if (d->src.isEmpty() && d->xml.isEmpty()) + d->status = Null; + else + d->status = Ready; + d->errorString.clear(); + d->queryId = -1; + + bool hasKeys = false; + for (int i=0; iroleObjects.count(); i++) { + if (d->roleObjects[i]->isKey()) { + hasKeys = true; + break; + } + } + if (!hasKeys) { + if (origCount > 0) { + beginRemoveRows(QModelIndex(), 0, origCount - 1); + endRemoveRows(); + } + d->size = result.size; + d->data = result.data; + if (d->size > 0) { + beginInsertRows(QModelIndex(), 0, d->size - 1); + endInsertRows(); + } + } else { + for (int i=0; i 0) { + beginRemoveRows(QModelIndex(), index, index + count - 1); + endRemoveRows(); + } + } + d->size = result.size; + d->data = result.data; + for (int i=0; i 0) { + beginInsertRows(QModelIndex(), index, index + count - 1); + endInsertRows(); + } + } + } + if (sizeChanged) + emit countChanged(); + + emit statusChanged(d->status); +} + +QT_END_NAMESPACE + +#include diff --git a/src/imports/xmllistmodel/qqmlxmllistmodel_p.h b/src/imports/xmllistmodel/qqmlxmllistmodel_p.h new file mode 100644 index 0000000..65f1299 --- /dev/null +++ b/src/imports/xmllistmodel/qqmlxmllistmodel_p.h @@ -0,0 +1,219 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 QQUICKXMLLISTMODEL_H +#define QQUICKXMLLISTMODEL_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 +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + + +class QQmlContext; +class QQuickXmlListModelRole; +class QQuickXmlListModelPrivate; + +struct QQuickXmlQueryResult { + int queryId; + int size; + QList > data; + QList > inserted; + QList > removed; + QStringList keyRoleResultsCache; +}; + +class QQuickXmlListModel : 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 xml READ xml WRITE setXml NOTIFY xmlChanged) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(QString namespaceDeclarations READ namespaceDeclarations WRITE setNamespaceDeclarations NOTIFY namespaceDeclarationsChanged) + Q_PROPERTY(QQmlListProperty roles READ roleObjects) + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_CLASSINFO("DefaultProperty", "roles") + +public: + QQuickXmlListModel(QObject *parent = 0); + ~QQuickXmlListModel(); + + 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 roleNames() const override; + + int count() const; + + QQmlListProperty roleObjects(); + + QUrl source() const; + void setSource(const QUrl&); + + QString xml() const; + void setXml(const QString&); + + QString query() const; + void setQuery(const QString&); + + QString namespaceDeclarations() const; + void setNamespaceDeclarations(const QString&); + + Q_INVOKABLE QQmlV4Handle get(int index) const; + + 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(QQuickXmlListModel::Status); + void progressChanged(qreal progress); + void countChanged(); + void sourceChanged(); + void xmlChanged(); + void queryChanged(); + void namespaceDeclarationsChanged(); + +public Q_SLOTS: + // ### need to use/expose Expiry to guess when to call this? + // ### property to auto-call this on reasonable Expiry? + // ### LastModified/Age also useful to guess. + // ### Probably also applies to other network-requesting types. + void reload(); + +private Q_SLOTS: +#if QT_CONFIG(qml_network) + void requestFinished(); +#endif + void requestProgress(qint64,qint64); + void dataCleared(); + void queryCompleted(const QQuickXmlQueryResult &); + void queryError(void* object, const QString& error); + +private: + Q_DECLARE_PRIVATE(QQuickXmlListModel) + Q_DISABLE_COPY(QQuickXmlListModel) +}; + +class QQuickXmlListModelRole : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(bool isKey READ isKey WRITE setIsKey NOTIFY isKeyChanged) +public: + QQuickXmlListModelRole() : m_isKey(false) {} + ~QQuickXmlListModelRole() {} + + QString name() const { return m_name; } + void setName(const QString &name) { + if (name == m_name) + return; + m_name = name; + Q_EMIT nameChanged(); + } + + QString query() const { return m_query; } + void setQuery(const QString &query) + { + if (query.startsWith(QLatin1Char('/'))) { + qmlWarning(this) << tr("An XmlRole query must not start with '/'"); + return; + } + if (m_query == query) + return; + m_query = query; + Q_EMIT queryChanged(); + } + + bool isKey() const { return m_isKey; } + void setIsKey(bool b) { + if (m_isKey == b) + return; + m_isKey = b; + Q_EMIT isKeyChanged(); + } + + bool isValid() const { + return !m_name.isEmpty() && !m_query.isEmpty(); + } + +Q_SIGNALS: + void nameChanged(); + void queryChanged(); + void isKeyChanged(); + +private: + QString m_name; + QString m_query; + bool m_isKey; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickXmlListModel) +QML_DECLARE_TYPE(QQuickXmlListModelRole) + +#endif // QQUICKXMLLISTMODEL_H diff --git a/src/imports/xmllistmodel/xmllistmodel.pro b/src/imports/xmllistmodel/xmllistmodel.pro new file mode 100644 index 0000000..8ee9ea3 --- /dev/null +++ b/src/imports/xmllistmodel/xmllistmodel.pro @@ -0,0 +1,12 @@ +CXX_MODULE = qml +TARGET = qmlxmllistmodelplugin +TARGETPATH = QtQuick/XmlListModel +IMPORT_VERSION = 2.$$QT_MINOR_VERSION + +QT = xmlpatterns qml-private core-private +qtConfig(qml-network): QT += network + +SOURCES += qqmlxmllistmodel.cpp plugin.cpp +HEADERS += qqmlxmllistmodel_p.h + +load(qml_plugin) diff --git a/src/src.pro b/src/src.pro index cd32a1b..5083270 100644 --- a/src/src.pro +++ b/src/src.pro @@ -1,2 +1,8 @@ TEMPLATE = subdirs SUBDIRS += xmlpatterns + +qtHaveModule(qml){ + SUBDIRS += imports + imports.depends = xmlpatterns +} + diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index aba49b6..40f4c5c 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -53,4 +53,7 @@ xmlpatternsxqts.depends = xmlpatternssdk xmlpatternsxqts \ xmlpatternsxslts \ +qtHaveModule(quick): SUBIDIRS += qquickxmllistmodel + !cross_compile: SUBDIRS += host.pro + diff --git a/tests/auto/qquickxmllistmodel/data/empty.xml b/tests/auto/qquickxmllistmodel/data/empty.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/auto/qquickxmllistmodel/data/get.qml b/tests/auto/qquickxmllistmodel/data/get.qml new file mode 100644 index 0000000..509da71 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/get.qml @@ -0,0 +1,61 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + source: "model.xml" + query: "/Pets/Pet" + XmlRole { name: "name"; query: "name/string()" } + XmlRole { name: "type"; query: "type/string()" } + XmlRole { name: "age"; query: "age/number()" } + XmlRole { name: "size"; query: "size/string()" } + + id: root + + property bool preTest: false + property bool postTest: false + + function runPreTest() { + if (root.get(0) != undefined) + return; + + preTest = true; + } + + function runPostTest() { + if (root.get(-1) != undefined) + return; + + var row = root.get(0); + if (row.name != "Polly" || + row.type != "Parrot" || + row.age != 12 || + row.size != "Small") + return; + + row = root.get(1); + if (row.name != "Penny" || + row.type != "Turtle" || + row.age != 4 || + row.size != "Small") + return; + + row = root.get(7); + if (row.name != "Rover" || + row.type != "Dog" || + row.age != 0 || + row.size != "Large") + return; + + row = root.get(8); + if (row.name != "Tiny" || + row.type != "Elephant" || + row.age != 15 || + row.size != "Large") + return; + + if (root.get(9) != undefined) + return; + + postTest = true; + } +} diff --git a/tests/auto/qquickxmllistmodel/data/groups.qml b/tests/auto/qquickxmllistmodel/data/groups.qml new file mode 100644 index 0000000..c1b574a --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/groups.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + source: "groups.xml" + query: "//animal[@name='Garfield']/parent::group" + + XmlRole { name: "id"; query: "@id/string()" } + XmlRole { name: "name"; query: "@name/string()" } +} diff --git a/tests/auto/qquickxmllistmodel/data/groups.xml b/tests/auto/qquickxmllistmodel/data/groups.xml new file mode 100644 index 0000000..5de4d2e --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/groups.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/auto/qquickxmllistmodel/data/model.qml b/tests/auto/qquickxmllistmodel/data/model.qml new file mode 100644 index 0000000..2df3927 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/model.qml @@ -0,0 +1,11 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + source: "model.xml" + query: "/Pets/Pet" + XmlRole { name: "name"; query: "name/string()" } + XmlRole { name: "type"; query: "type/string()" } + XmlRole { name: "age"; query: "age/number()" } + XmlRole { name: "size"; query: "size/string()" } +} diff --git a/tests/auto/qquickxmllistmodel/data/model.xml b/tests/auto/qquickxmllistmodel/data/model.xml new file mode 100644 index 0000000..40cd6d0 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/model.xml @@ -0,0 +1,54 @@ + + + Polly + Parrot + 12 + Small + + + Penny + Turtle + 4 + Small + + + Warren + Rabbit + 2 + Small + + + Spot + Dog + 9 + Medium + + + Whiskers + Cat + 2 + Medium + + + Joey + Kangaroo + 1 + + + Kimba + Bunny + 65 + Large + + + Rover + Dog + Large + + + Tiny + Elephant + 15 + Large + + diff --git a/tests/auto/qquickxmllistmodel/data/model2.xml b/tests/auto/qquickxmllistmodel/data/model2.xml new file mode 100644 index 0000000..dab2ec6 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/model2.xml @@ -0,0 +1,14 @@ + + + Polly + Parrot + 12 + Small + + + Penny + Turtle + 4 + Small + + diff --git a/tests/auto/qquickxmllistmodel/data/propertychanges.qml b/tests/auto/qquickxmllistmodel/data/propertychanges.qml new file mode 100644 index 0000000..f8a97bf --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/propertychanges.qml @@ -0,0 +1,11 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + source: "model.xml" + query: "/Pets/Pet" + XmlRole { objectName: "role"; name: "name"; query: "name/string()" } + XmlRole { name: "type"; query: "type/string()" } + XmlRole { name: "age"; query: "age/number()" } + XmlRole { name: "size"; query: "size/string()" } +} diff --git a/tests/auto/qquickxmllistmodel/data/proxyCrash.qml b/tests/auto/qquickxmllistmodel/data/proxyCrash.qml new file mode 100644 index 0000000..c0c5a25 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/proxyCrash.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 +import SortFilterProxyModel 1.0 + +SortFilterProxyModel { + source: XmlListModel { + XmlRole { } + } +} diff --git a/tests/auto/qquickxmllistmodel/data/recipes.qml b/tests/auto/qquickxmllistmodel/data/recipes.qml new file mode 100644 index 0000000..dc609e9 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/recipes.qml @@ -0,0 +1,11 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + source: "recipes.xml" + query: "/recipes/recipe" + XmlRole { name: "title"; query: "@title/string()" } + XmlRole { name: "picture"; query: "picture/string()" } + XmlRole { name: "ingredients"; query: "ingredients/string()" } + XmlRole { name: "preparation"; query: "method/string()" } +} diff --git a/tests/auto/qquickxmllistmodel/data/recipes.xml b/tests/auto/qquickxmllistmodel/data/recipes.xml new file mode 100644 index 0000000..d71de60 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/recipes.xml @@ -0,0 +1,90 @@ + + + content/pics/pancakes.jpg + +
    +
  • 1 cup (150g) self-raising flour +
  • 1 tbs caster sugar +
  • 3/4 cup (185ml) milk +
  • 1 egg +
+ + ]]>
+ +
    +
  1. Sift flour and sugar together into a bowl. Add a pinch of salt. +
  2. Beat milk and egg together, then add to dry ingredients. Beat until smooth. +
  3. Pour mixture into a pan on medium heat and cook until bubbles appear on the surface. +
  4. Turn over and cook other side until golden. +
+ + ]]>
+
+ + content/pics/fruit-salad.jpg + + + + + content/pics/vegetable-soup.jpg + +
    +
  • 1 onion +
  • 1 turnip +
  • 1 potato +
  • 1 carrot +
  • 1 head of celery +
  • 1 1/2 litres of water +
+ + ]]>
+ +
    +
  1. Chop vegetables. +
  2. Boil in water until vegetables soften. +
  3. Season with salt and pepper to taste. +
+ + ]]>
+
+ + content/pics/hamburger.jpg + +
    +
  • 500g minced beef +
  • Seasoning +
  • lettuce, tomato, onion, cheese +
  • 1 hamburger bun for each burger +
+ + ]]>
+ +
    +
  1. Mix the beef, together with seasoning, in a food processor. +
  2. Shape the beef into burgers. +
  3. Grill the burgers for about 5 mins on each side (until cooked through) +
  4. Serve each burger on a bun with ketchup, cheese, lettuce, tomato and onion. +
+ + ]]>
+
+ + content/pics/lemonade.jpg + +
    +
  • 1 cup Lemon Juice +
  • 1 cup Sugar +
  • 6 Cups of Water (2 cups warm water, 4 cups cold water) +
+ + ]]>
+ +
    +
  1. Pour 2 cups of warm water into a pitcher and stir in sugar until it dissolves. +
  2. Pour in lemon juice, stir again, and add 4 cups of cold water. +
  3. Chill or serve over ice cubes. +
+ + ]]>
+
+
diff --git a/tests/auto/qquickxmllistmodel/data/roleCrash.qml b/tests/auto/qquickxmllistmodel/data/roleCrash.qml new file mode 100644 index 0000000..6a7059b --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/roleCrash.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + id: model + XmlRole {} + Component.onCompleted: model.roles = 0 +} diff --git a/tests/auto/qquickxmllistmodel/data/roleErrors.qml b/tests/auto/qquickxmllistmodel/data/roleErrors.qml new file mode 100644 index 0000000..91664b6 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/roleErrors.qml @@ -0,0 +1,11 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + source: "model.xml" + query: "/Pets/Pet" + XmlRole { name: "name"; query: "/name/string()" } //starts with '/' + XmlRole { name: "type"; query: "type" } //no type + XmlRole { name: "age"; query: "age/" } //ends with '/' + XmlRole { name: "size"; query: "size/number()" } //wrong type +} diff --git a/tests/auto/qquickxmllistmodel/data/roleKeys.qml b/tests/auto/qquickxmllistmodel/data/roleKeys.qml new file mode 100644 index 0000000..9f667d8 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/roleKeys.qml @@ -0,0 +1,13 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + query: "/data/item" + XmlRole { id: nameRole; name: "name"; query: "name/string()"; isKey: true } + XmlRole { name: "age"; query: "age/number()"; isKey: true } + XmlRole { name: "sport"; query: "sport/string()" } + + function disableNameKey() { + nameRole.isKey = false; + } +} diff --git a/tests/auto/qquickxmllistmodel/data/testtypes.qml b/tests/auto/qquickxmllistmodel/data/testtypes.qml new file mode 100644 index 0000000..5ec1ffa --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/testtypes.qml @@ -0,0 +1,8 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + query: "/data" + XmlRole { name: "stringValue"; query: "a-string/string()" } + XmlRole { name: "numberValue"; query: "a-number/number()" } +} diff --git a/tests/auto/qquickxmllistmodel/data/unique.qml b/tests/auto/qquickxmllistmodel/data/unique.qml new file mode 100644 index 0000000..322a2e4 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/data/unique.qml @@ -0,0 +1,9 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +XmlListModel { + source: "model.xml" + query: "/Pets/Pet" + XmlRole { name: "name"; query: "name/string()" } + XmlRole { name: "name"; query: "type/string()" } +} diff --git a/tests/auto/qquickxmllistmodel/qquickxmllistmodel.pro b/tests/auto/qquickxmllistmodel/qquickxmllistmodel.pro new file mode 100644 index 0000000..e2d873c --- /dev/null +++ b/tests/auto/qquickxmllistmodel/qquickxmllistmodel.pro @@ -0,0 +1,16 @@ +CONFIG += testcase +TARGET = tst_qquickxmllistmodel +macos:CONFIG -= app_bundle + +SOURCES += tst_qquickxmllistmodel.cpp \ + ../../../../src/imports/xmllistmodel/qqmlxmllistmodel.cpp +HEADERS += ../../../../src/imports/xmllistmodel/qqmlxmllistmodel_p.h + +include (../../shared/util.pri) + +TESTDATA = data/* + +QT += core-private gui-private qml-private network testlib xmlpatterns + +OTHER_FILES += \ + data/groups.qml diff --git a/tests/auto/qquickxmllistmodel/tst_qquickxmllistmodel.cpp b/tests/auto/qquickxmllistmodel/tst_qquickxmllistmodel.cpp new file mode 100644 index 0000000..b2e5703 --- /dev/null +++ b/tests/auto/qquickxmllistmodel/tst_qquickxmllistmodel.cpp @@ -0,0 +1,1011 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../shared/util.h" +#include + +#include +#include +#include "../../../../src/imports/xmllistmodel/qqmlxmllistmodel_p.h" + +#include + +typedef QPair QQuickXmlListRange; +typedef QList QQmlXmlModelData; + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QQmlXmlModelData) +Q_DECLARE_METATYPE(QQuickXmlListModel::Status) + +class tst_qquickxmllistmodel : public QQmlDataTest + +{ + Q_OBJECT +public: + tst_qquickxmllistmodel() {} + +private slots: + void initTestCase() { + QQmlDataTest::initTestCase(); + qRegisterMetaType(); + } + + void buildModel(); + void testTypes(); + void testTypes_data(); + void cdata(); + void attributes(); + void roles(); + void roleErrors(); + void uniqueRoleNames(); + void headers(); + void xml(); + void xml_data(); + void source(); + void source_data(); + void data(); + void get(); + void reload(); + void useKeys(); + void useKeys_data(); + void noKeysValueChanges(); + void keysChanged(); + void threading(); + void threading_data(); + void propertyChanges(); + void selectAncestor(); + + 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()) { + QStringList items = data.split(QLatin1Char(';')); + foreach (const QString &item, items) { + if (item.isEmpty()) + continue; + QVariantList variants; + xml += QLatin1String(""); + QStringList fields = item.split(QLatin1Char(',')); + foreach (const QString &field, fields) { + QStringList values = field.split(QLatin1Char('=')); + if (values.count() != 2) { + qWarning() << "makeItemXmlAndData: invalid field:" << field; + continue; + } + xml += QString("<%1>%2").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(""); + if (modelData) + modelData->append(variants); + } + } + + QString decl = ""; + return decl + QLatin1String("") + xml + QLatin1String(""); + } + + QQmlEngine engine; +}; + +class CustomNetworkAccessManagerFactory : public QObject, public QQmlNetworkAccessManagerFactory +{ + Q_OBJECT +public: + QVariantMap lastSentHeaders; + +protected: + QNetworkAccessManager *create(QObject *parent); +}; + +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) + { + if (m_factory) { + QVariantMap map; + foreach (const QString &header, req.rawHeaderList()) + map[header] = req.rawHeader(header.toUtf8()); + m_factory->lastSentHeaders = map; + } + return QNetworkAccessManager::createRequest(op, req, outgoingData); + } + + QPointer m_factory; +}; + +QNetworkAccessManager *CustomNetworkAccessManagerFactory::create(QObject *parent) +{ + return new CustomNetworkAccessManager(this, parent); +} + + +void tst_qquickxmllistmodel::buildModel() +{ + QQmlComponent component(&engine, testFileUrl("model.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + 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")); + + delete model; +} + +void tst_qquickxmllistmodel::testTypes() +{ + QFETCH(QString, xml); + QFETCH(QString, roleName); + QFETCH(QVariant, expectedValue); + + QQmlComponent component(&engine, testFileUrl("testtypes.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + model->setProperty("xml",xml.toUtf8()); + QMetaObject::invokeMethod(model, "reload"); + QTRY_COMPARE(model->rowCount(), 1); + + int role = model->roleNames().key(roleName.toUtf8(), -1); + QVERIFY(role >= 0); + + QModelIndex index = model->index(0, 0); + if (expectedValue.toString() == "nan") + QVERIFY(qIsNaN(model->data(index, role).toDouble())); + else + QCOMPARE(model->data(index, role), expectedValue); + + delete model; +} + +void tst_qquickxmllistmodel::testTypes_data() +{ + QTest::addColumn("xml"); + QTest::addColumn("roleName"); + QTest::addColumn("expectedValue"); + + QTest::newRow("missing string field") << "" + << "stringValue" << QVariant(""); + QTest::newRow("empty string") << "" + << "stringValue" << QVariant(""); + QTest::newRow("1-char string") << "5" + << "stringValue" << QVariant("5"); + QTest::newRow("string ok") << "abc def g" + << "stringValue" << QVariant("abc def g"); + + QTest::newRow("missing number field") << "" + << "numberValue" << QVariant(""); + double nan = qQNaN(); + QTest::newRow("empty number field") << "" + << "numberValue" << QVariant(nan); + QTest::newRow("number field with string") << "a string" + << "numberValue" << QVariant(nan); + QTest::newRow("-1") << "-1" + << "numberValue" << QVariant("-1"); + QTest::newRow("-1.5") << "-1.5" + << "numberValue" << QVariant("-1.5"); + QTest::newRow("0") << "0" + << "numberValue" << QVariant("0"); + QTest::newRow("+1") << "1" + << "numberValue" << QVariant("1"); + QTest::newRow("+1.5") << "1.5" + << "numberValue" << QVariant("1.5"); +} + +void tst_qquickxmllistmodel::cdata() +{ + QQmlComponent component(&engine, testFileUrl("recipes.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 5); + + QVERIFY(model->data(model->index(2, 0), Qt::UserRole+2).toString().startsWith(QLatin1String(""))); + + delete model; +} + +void tst_qquickxmllistmodel::attributes() +{ + QQmlComponent component(&engine, testFileUrl("recipes.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 5); + QCOMPARE(model->data(model->index(2, 0), Qt::UserRole).toString(), QLatin1String("Vegetable Soup")); + + delete model; +} + +void tst_qquickxmllistmodel::roles() +{ + QQmlComponent component(&engine, testFileUrl("model.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 9); + + QHash 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 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); + + delete model; +} + +void tst_qquickxmllistmodel::roleErrors() +{ + QQmlComponent component(&engine, testFileUrl("roleErrors.qml")); + QTest::ignoreMessage(QtWarningMsg, (testFileUrl("roleErrors.qml").toString() + ":7:5: QML XmlRole: An XmlRole query must not start with '/'").toUtf8().constData()); + QTest::ignoreMessage(QtWarningMsg, (testFileUrl("roleErrors.qml").toString() + ":10:5: QML XmlRole: invalid query: \"age/\"").toUtf8().constData()); + + //### make sure we receive all expected warning messages. + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 9); + + QModelIndex index = model->index(3, 0); + //### should any of these return valid values? + QCOMPARE(model->data(index, Qt::UserRole), QVariant()); + QCOMPARE(model->data(index, Qt::UserRole+1), QVariant()); + QCOMPARE(model->data(index, Qt::UserRole+2), QVariant()); + + QEXPECT_FAIL("", "QTBUG-10797", Continue); + QCOMPARE(model->data(index, Qt::UserRole+3), QVariant()); + + delete model; +} + +void tst_qquickxmllistmodel::uniqueRoleNames() +{ + QQmlComponent component(&engine, testFileUrl("unique.qml")); + QTest::ignoreMessage(QtWarningMsg, (testFileUrl("unique.qml").toString() + ":8:5: QML XmlRole: \"name\" duplicates a previous role name and will be disabled.").toUtf8().constData()); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 9); + + QHash roleNames = model->roleNames(); + QCOMPARE(roleNames.count(), 1); + + delete model; +} + + +void tst_qquickxmllistmodel::xml() +{ + QFETCH(QString, xml); + QFETCH(int, count); + + QQmlComponent component(&engine, testFileUrl("model.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + + QSignalSpy spy(model, SIGNAL(statusChanged(QQuickXmlListModel::Status))); + QVERIFY(errorString(model).isEmpty()); + QCOMPARE(model->property("progress").toDouble(), qreal(1.0)); + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Loading); + QTRY_COMPARE(spy.count(), 1); spy.clear(); + QTest::qWait(50); + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Ready); + QVERIFY(errorString(model).isEmpty()); + QCOMPARE(model->property("progress").toDouble(), qreal(1.0)); + QCOMPARE(model->rowCount(), 9); + + // if xml is empty (i.e. clearing) it won't have any effect if a source is set + if (xml.isEmpty()) + model->setProperty("source",QUrl()); + model->setProperty("xml",xml); + QCOMPARE(model->property("progress").toDouble(), qreal(1.0)); // immediately goes to 1.0 if using setXml() + QTRY_COMPARE(spy.count(), 1); spy.clear(); + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Loading); + QTRY_COMPARE(spy.count(), 1); spy.clear(); + if (xml.isEmpty()) + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Null); + else + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Ready); + QVERIFY(errorString(model).isEmpty()); + QCOMPARE(model->rowCount(), count); + + delete model; +} + +void tst_qquickxmllistmodel::xml_data() +{ + QTest::addColumn("xml"); + QTest::addColumn("count"); + + QTest::newRow("xml with no items") << "" << 0; + QTest::newRow("empty xml") << "" << 0; + QTest::newRow("one item") << "HobbesTiger7Large" << 1; +} + +void tst_qquickxmllistmodel::headers() +{ + // ensure the QNetworkAccessManagers created for this test are immediately deleted + QQmlEngine qmlEng; + + CustomNetworkAccessManagerFactory factory; + qmlEng.setNetworkAccessManagerFactory(&factory); + + QQmlComponent component(&qmlEng, testFileUrl("model.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::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(qvariant_cast(model->property("status")), + QQuickXmlListModel::Error); + + QVariantMap expectedHeaders; + expectedHeaders["Accept"] = "application/xml,*/*"; + + QCOMPARE(factory.lastSentHeaders.count(), expectedHeaders.count()); + foreach (const QString &header, expectedHeaders.keys()) { + QVERIFY(factory.lastSentHeaders.contains(header)); + QCOMPARE(factory.lastSentHeaders[header].toString(), expectedHeaders[header].toString()); + } + + delete model; +} + +void tst_qquickxmllistmodel::source() +{ + QFETCH(QUrl, source); + QFETCH(int, count); + QFETCH(QQuickXmlListModel::Status, status); + + QQmlComponent component(&engine, testFileUrl("model.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QSignalSpy spy(model, SIGNAL(statusChanged(QQuickXmlListModel::Status))); + + QVERIFY(errorString(model).isEmpty()); + QCOMPARE(model->property("progress").toDouble(), qreal(1.0)); + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Loading); + QTRY_COMPARE(spy.count(), 1); spy.clear(); + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Ready); + QVERIFY(errorString(model).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(model->property("status")), + QQuickXmlListModel::Null); + QCOMPARE(model->property("progress").toDouble(), qreal(source.isLocalFile() ? 1.0 : 0.0)); + QTRY_COMPARE(spy.count(), 1); spy.clear(); + QCOMPARE(qvariant_cast(model->property("status")), + QQuickXmlListModel::Loading); + QVERIFY(errorString(model).isEmpty()); + + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + connect(model, SIGNAL(statusChanged(QQuickXmlListModel::Status)), &loop, SLOT(quit())); + connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + timer.start(20000); + loop.exec(); + + if (spy.count() == 0 && status != QQuickXmlListModel::Ready) { + qWarning("QQuickXmlListModel invalid source test timed out"); + } else { + QCOMPARE(spy.count(), 1); spy.clear(); + } + + QCOMPARE(qvariant_cast(model->property("status")), status); + QCOMPARE(model->rowCount(), count); + + if (status == QQuickXmlListModel::Ready) + QCOMPARE(model->property("progress").toDouble(), qreal(1.0)); + + QCOMPARE(errorString(model).isEmpty(), status == QQuickXmlListModel::Ready); + + delete model; +} + +void tst_qquickxmllistmodel::source_data() +{ + QTest::addColumn("source"); + QTest::addColumn("count"); + QTest::addColumn("status"); + + QTest::newRow("valid") << testFileUrl("model2.xml") << 2 + << QQuickXmlListModel::Ready; + QTest::newRow("invalid") << QUrl("http://blah.blah/blah.xml") << 0 + << QQuickXmlListModel::Error; + + // empty file + QTemporaryFile *temp = new QTemporaryFile(this); + if (temp->open()) + QTest::newRow("empty file") << QUrl::fromLocalFile(temp->fileName()) << 0 + << QQuickXmlListModel::Ready; + temp->close(); +} + +void tst_qquickxmllistmodel::data() +{ + QQmlComponent component(&engine, testFileUrl("model.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + + for (int i=0; i<9; i++) { + QModelIndex index = model->index(i, 0); + for (int j=0; jroleNames().count(); j++) { + QCOMPARE(model->data(index, j), QVariant()); + } + } + QTRY_COMPARE(model->rowCount(), 9); + + delete model; +} + +void tst_qquickxmllistmodel::get() +{ + QQmlComponent component(&engine, testFileUrl("get.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + + QVERIFY(model != 0); + + QVERIFY(QMetaObject::invokeMethod(model, "runPreTest")); + QCOMPARE(model->property("preTest").toBool(), true); + + QTRY_COMPARE(model->rowCount(), 9); + + QVERIFY(QMetaObject::invokeMethod(model, "runPostTest")); + QCOMPARE(model->property("postTest").toBool(), true); + + delete model; +} + +void tst_qquickxmllistmodel::reload() +{ + // If no keys are used, the model should be rebuilt from scratch when + // reload() is called. + + QQmlComponent component(&engine, testFileUrl("model.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 9); + + QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int))); + QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int))); + QSignalSpy spyCount(model, SIGNAL(countChanged())); + //reload multiple times to test the xml query aborting + QMetaObject::invokeMethod(model, "reload"); + QMetaObject::invokeMethod(model, "reload"); + QCoreApplication::processEvents(); + QMetaObject::invokeMethod(model, "reload"); + QMetaObject::invokeMethod(model, "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); + + delete model; +} + +void tst_qquickxmllistmodel::useKeys() +{ + // If using incremental updates through keys, the model should only + // insert & remove some of the items, instead of throwing everything + // away and causing the view to repaint the whole view. + + QFETCH(QString, oldXml); + QFETCH(int, oldCount); + QFETCH(QString, newXml); + QFETCH(QQmlXmlModelData, newData); + QFETCH(QList, insertRanges); + QFETCH(QList, removeRanges); + + QQmlComponent component(&engine, testFileUrl("roleKeys.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + + model->setProperty("xml",oldXml); + QTRY_COMPARE(model->rowCount(), oldCount); + + QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int))); + QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int))); + QSignalSpy spyCount(model, SIGNAL(countChanged())); + + model->setProperty("xml",newXml); + + if (oldCount != newData.count()) { + QTRY_COMPARE(model->rowCount(), newData.count()); + QCOMPARE(spyCount.count(), 1); + } else { + QTRY_VERIFY(spyInsert.count() > 0 || spyRemove.count() > 0); + QCOMPARE(spyCount.count(), 0); + } + + QList roles = model->roleNames().keys(); + std::sort(roles.begin(), roles.end()); + for (int i=0; irowCount(); i++) { + QModelIndex index = model->index(i, 0); + for (int j=0; jdata(index, roles.at(j)), newData[i][j]); + } + + QCOMPARE(spyInsert.count(), insertRanges.count()); + for (int i=0; i("oldXml"); + QTest::addColumn("oldCount"); + QTest::addColumn("newXml"); + QTest::addColumn("newData"); + QTest::addColumn >("insertRanges"); + QTest::addColumn >("removeRanges"); + + QQmlXmlModelData modelData; + + QTest::newRow("append 1") + << makeItemXmlAndData("name=A,age=25,sport=Football") << 1 + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics", &modelData) + << modelData + << (QList() << qMakePair(1, 1)) + << QList(); + + QTest::newRow("append multiple") + << makeItemXmlAndData("name=A,age=25,sport=Football") << 1 + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling", &modelData) + << modelData + << (QList() << qMakePair(1, 2)) + << QList(); + + QTest::newRow("insert in different spots") + << makeItemXmlAndData("name=B,age=35,sport=Athletics") << 1 + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", &modelData) + << modelData + << (QList() << qMakePair(0, 1) << qMakePair(2,2)) + << QList(); + + QTest::newRow("insert in middle") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=D,age=55,sport=Golf") << 2 + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", &modelData) + << modelData + << (QList() << qMakePair(1, 2)) + << QList(); + + QTest::newRow("remove first") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics") << 2 + << makeItemXmlAndData("name=B,age=35,sport=Athletics", &modelData) + << modelData + << QList() + << (QList() << qMakePair(0, 1)); + + QTest::newRow("remove last") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics") << 2 + << makeItemXmlAndData("name=A,age=25,sport=Football", &modelData) + << modelData + << QList() + << (QList() << qMakePair(1, 1)); + + QTest::newRow("remove from multiple spots") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing") << 5 + << makeItemXmlAndData("name=A,age=25,sport=Football;name=C,age=45,sport=Curling", &modelData) + << modelData + << QList() + << (QList() << qMakePair(1, 1) << qMakePair(3,2)); + + QTest::newRow("remove all") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling") << 3 + << makeItemXmlAndData("", &modelData) + << modelData + << QList() + << (QList() << qMakePair(0, 3)); + + QTest::newRow("replace item") + << makeItemXmlAndData("name=A,age=25,sport=Football") << 1 + << makeItemXmlAndData("name=ZZZ,age=25,sport=Football", &modelData) + << modelData + << (QList() << qMakePair(0, 1)) + << (QList() << qMakePair(0, 1)); + + QTest::newRow("add and remove simultaneously, in different spots") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf") << 4 + << makeItemXmlAndData("name=B,age=35,sport=Athletics;name=E,age=65,sport=Fencing", &modelData) + << modelData + << (QList() << qMakePair(1, 1)) + << (QList() << qMakePair(0, 1) << qMakePair(2,2)); + + QTest::newRow("insert at start, remove at end i.e. rss feed") + << makeItemXmlAndData("name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing") << 3 + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling", &modelData) + << modelData + << (QList() << qMakePair(0, 2)) + << (QList() << qMakePair(1, 2)); + + QTest::newRow("remove at start, insert at end") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling") << 3 + << makeItemXmlAndData("name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing", &modelData) + << modelData + << (QList() << qMakePair(1, 2)) + << (QList() << qMakePair(0, 2)); + + QTest::newRow("all data has changed") + << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35") << 2 + << makeItemXmlAndData("name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", &modelData) + << modelData + << (QList() << qMakePair(0, 2)) + << (QList() << qMakePair(0, 2)); +} + +void tst_qquickxmllistmodel::noKeysValueChanges() +{ + // The 'key' roles are 'name' and 'age', as defined in roleKeys.qml. + // If a 'sport' value is changed, the model should not be reloaded, + // since 'sport' is not marked as a key. + + QQmlComponent component(&engine, testFileUrl("roleKeys.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + + QString xml; + + xml = makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics"); + model->setProperty("xml",xml); + QTRY_COMPARE(model->rowCount(), 2); + + model->setProperty("xml",""); + + QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int))); + QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int))); + QSignalSpy spyCount(model, SIGNAL(countChanged())); + + xml = makeItemXmlAndData("name=A,age=25,sport=AussieRules;name=B,age=35,sport=Athletics"); + model->setProperty("xml",xml); + + QList roles = model->roleNames().keys(); + std::sort(roles.begin(), roles.end()); + // wait for the new xml data to be set, and verify no signals were emitted + QTRY_VERIFY(model->data(model->index(0, 0), roles.at(2)).toString() != QLatin1String("Football")); + QCOMPARE(model->data(model->index(0, 0), roles.at(2)).toString(), QLatin1String("AussieRules")); + + QCOMPARE(spyInsert.count(), 0); + QCOMPARE(spyRemove.count(), 0); + QCOMPARE(spyCount.count(), 0); + + QCOMPARE(model->rowCount(), 2); + + delete model; +} + +void tst_qquickxmllistmodel::keysChanged() +{ + // If the key roles change, the next time the data is reloaded, it should + // delete all its data and build a clean model (i.e. same behavior as + // if no keys are set). + + QQmlComponent component(&engine, testFileUrl("roleKeys.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + + QString xml = makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics"); + model->setProperty("xml",xml); + QTRY_COMPARE(model->rowCount(), 2); + + model->setProperty("xml",""); + + QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int))); + QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int))); + QSignalSpy spyCount(model, SIGNAL(countChanged())); + + QVERIFY(QMetaObject::invokeMethod(model, "disableNameKey")); + model->setProperty("xml",xml); + + QTRY_VERIFY(spyInsert.count() > 0 && spyRemove.count() > 0); + + QCOMPARE(spyInsert.count(), 1); + QCOMPARE(spyInsert[0][1].toInt(), 0); + QCOMPARE(spyInsert[0][2].toInt(), 1); + + QCOMPARE(spyRemove.count(), 1); + QCOMPARE(spyRemove[0][1].toInt(), 0); + QCOMPARE(spyRemove[0][2].toInt(), 1); + + QCOMPARE(spyCount.count(), 0); + + delete model; +} + +void tst_qquickxmllistmodel::threading() +{ + QFETCH(int, xmlDataCount); + + QQmlComponent component(&engine, testFileUrl("roleKeys.qml")); + + QAbstractItemModel *m1 = qobject_cast(component.create()); + QVERIFY(m1 != 0); + QAbstractItemModel *m2 = qobject_cast(component.create()); + QVERIFY(m2 != 0); + QAbstractItemModel *m3 = qobject_cast(component.create()); + QVERIFY(m3 != 0); + + for (int dataCount=0; dataCountsetProperty("xml",makeItemXmlAndData(data1)); + m2->setProperty("xml",makeItemXmlAndData(data2)); + m3->setProperty("xml",makeItemXmlAndData(data3)); + QCoreApplication::processEvents(); + m2->setProperty("xml",makeItemXmlAndData(data2)); + m1->setProperty("xml",makeItemXmlAndData(data1)); + m2->setProperty("xml",makeItemXmlAndData(data2)); + QCoreApplication::processEvents(); + m3->setProperty("xml",makeItemXmlAndData(data3)); + QCoreApplication::processEvents(); + m2->setProperty("xml",makeItemXmlAndData(data2)); + m1->setProperty("xml",makeItemXmlAndData(data1)); + m2->setProperty("xml",makeItemXmlAndData(data2)); + m3->setProperty("xml",makeItemXmlAndData(data3)); + QCoreApplication::processEvents(); + m2->setProperty("xml",makeItemXmlAndData(data2)); + m3->setProperty("xml",makeItemXmlAndData(data3)); + m3->setProperty("xml",makeItemXmlAndData(data3)); + QCoreApplication::processEvents(); + + QTRY_VERIFY(m1->rowCount() == dataCount && m2->rowCount() == dataCount && m3->rowCount() == dataCount); + + for (int i=0; iindex(i, 0); + QList 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")); + } + } + + delete m1; + delete m2; + delete m3; +} + +void tst_qquickxmllistmodel::threading_data() +{ + QTest::addColumn("xmlDataCount"); + + QTest::newRow("1") << 1; + QTest::newRow("2") << 2; + QTest::newRow("10") << 10; +} + +void tst_qquickxmllistmodel::propertyChanges() +{ + QQmlComponent component(&engine, testFileUrl("propertychanges.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 9); + + QObject *role = model->findChild("role"); + QVERIFY(role); + + QSignalSpy nameSpy(role, SIGNAL(nameChanged())); + QSignalSpy querySpy(role, SIGNAL(queryChanged())); + QSignalSpy isKeySpy(role, SIGNAL(isKeyChanged())); + + role->setProperty("name","size"); + role->setProperty("query","size/string()"); + role->setProperty("isKey",true); + + QCOMPARE(role->property("name").toString(), QString("size")); + QCOMPARE(role->property("query").toString(), QString("size/string()")); + QVERIFY(role->property("isKey").toBool()); + + QCOMPARE(nameSpy.count(),1); + QCOMPARE(querySpy.count(),1); + QCOMPARE(isKeySpy.count(),1); + + role->setProperty("name","size"); + role->setProperty("query","size/string()"); + role->setProperty("isKey",true); + + QCOMPARE(nameSpy.count(),1); + QCOMPARE(querySpy.count(),1); + QCOMPARE(isKeySpy.count(),1); + + QSignalSpy sourceSpy(model, SIGNAL(sourceChanged())); + QSignalSpy xmlSpy(model, SIGNAL(xmlChanged())); + QSignalSpy modelQuerySpy(model, SIGNAL(queryChanged())); + QSignalSpy namespaceDeclarationsSpy(model, SIGNAL(namespaceDeclarationsChanged())); + + model->setProperty("source",QUrl("")); + model->setProperty("xml","PollyParrot12Small"); + model->setProperty("query","/Pets"); + model->setProperty("namespaceDeclarations","declare namespace media=\"http://search.yahoo.com/mrss/\";"); + + QCOMPARE(model->property("source").toUrl(), QUrl("")); + QCOMPARE(model->property("xml").toString(), QString("PollyParrot12Small")); + QCOMPARE(model->property("query").toString(), QString("/Pets")); + QCOMPARE(model->property("namespaceDeclarations").toString(), QString("declare namespace media=\"http://search.yahoo.com/mrss/\";")); + + QTRY_COMPARE(model->rowCount(), 1); + + QCOMPARE(sourceSpy.count(),1); + QCOMPARE(xmlSpy.count(),1); + QCOMPARE(modelQuerySpy.count(),1); + QCOMPARE(namespaceDeclarationsSpy.count(),1); + + model->setProperty("source",QUrl("")); + model->setProperty("xml","PollyParrot12Small"); + model->setProperty("query","/Pets"); + model->setProperty("namespaceDeclarations","declare namespace media=\"http://search.yahoo.com/mrss/\";"); + + QCOMPARE(sourceSpy.count(),1); + QCOMPARE(xmlSpy.count(),1); + QCOMPARE(modelQuerySpy.count(),1); + QCOMPARE(namespaceDeclarationsSpy.count(),1); + + QTRY_COMPARE(model->rowCount(), 1); + delete model; +} + +void tst_qquickxmllistmodel::selectAncestor() +{ + QQmlComponent component(&engine, testFileUrl("groups.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + QTRY_COMPARE(model->rowCount(), 1); + + QModelIndex index = model->index(0, 0); + QCOMPARE(model->data(index, Qt::UserRole).toInt(), 12); + QCOMPARE(model->data(index, Qt::UserRole+1).toString(), QLatin1String("cats")); +} + +void tst_qquickxmllistmodel::roleCrash() +{ + // don't crash + QQmlComponent component(&engine, testFileUrl("roleCrash.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + delete model; +} + +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(source)); } +}; + +void tst_qquickxmllistmodel::proxyCrash() +{ + qmlRegisterType("SortFilterProxyModel", 1, 0, "SortFilterProxyModel"); + + // don't crash + QQmlComponent component(&engine, testFileUrl("proxyCrash.qml")); + QAbstractItemModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + delete model; +} + +QTEST_MAIN(tst_qquickxmllistmodel) + +#include "tst_qquickxmllistmodel.moc" -- cgit v1.2.1