From f8eee22dcdd9b6b530c0e5c346e16552352ec03b Mon Sep 17 00:00:00 2001 From: Bea Lam Date: Tue, 23 Feb 2010 15:40:22 +1000 Subject: Add XmlRole::isKey property for incremental data changes when reload() is called. Task-number: QT-2831 --- src/declarative/util/qmlxmllistmodel.cpp | 175 +++++++++++--- .../qmlxmllistmodel/tst_qmlxmllistmodel.cpp | 254 +++++++++++++++++++++ 2 files changed, 400 insertions(+), 29 deletions(-) diff --git a/src/declarative/util/qmlxmllistmodel.cpp b/src/declarative/util/qmlxmllistmodel.cpp index df2102a..5de4d6f 100644 --- a/src/declarative/util/qmlxmllistmodel.cpp +++ b/src/declarative/util/qmlxmllistmodel.cpp @@ -63,6 +63,8 @@ QT_BEGIN_NAMESPACE QML_DEFINE_TYPE(Qt,4,6,XmlRole,QmlXmlListModelRole) QML_DEFINE_TYPE(Qt,4,6,XmlListModel,QmlXmlListModel) +typedef QPair QmlXmlListRange; + /*! \qmlclass XmlRole QmlXmlListModelRole \brief The XmlRole element allows you to specify a role for an XmlListModel. @@ -94,14 +96,26 @@ QML_DEFINE_TYPE(Qt,4,6,XmlListModel,QmlXmlListModel) \endqml */ +/*! + \qmlproperty bool XmlRole::isKey + Defines whether this is a key role. + + Key roles are used to to determine whether a set of values should + be updated or added to the XML list model when XmlListModel::reload() + is called. + + \sa XmlListModel +*/ + class Q_DECLARATIVE_EXPORT QmlXmlListModelRole : public QObject { Q_OBJECT Q_PROPERTY(QString name READ name WRITE setName) Q_PROPERTY(QString query READ query WRITE setQuery) + Q_PROPERTY(bool isKey READ isKey WRITE setIsKey) public: - QmlXmlListModelRole() {} + QmlXmlListModelRole() : m_isKey(false) {} ~QmlXmlListModelRole() {} QString name() const { return m_name; } @@ -117,6 +131,9 @@ public: m_query = query; } + bool isKey() const { return m_isKey; } + void setIsKey(bool b) { m_isKey = b; } + bool isValid() { return !m_name.isEmpty() && !m_query.isEmpty(); } @@ -124,6 +141,7 @@ public: private: QString m_name; QString m_query; + bool m_isKey; }; QT_END_NAMESPACE QML_DECLARE_TYPE(QmlXmlListModelRole) @@ -166,7 +184,6 @@ public: int doQuery(QString query, QString namespaces, QByteArray data, QmlXmlRoleList *roleObjects) { QMutexLocker locker(&m_mutex); - m_modelData.clear(); m_size = 0; m_data = data; m_query = QLatin1String("doc($src)") + query; @@ -188,6 +205,16 @@ public: return m_modelData; } + QList insertedItemRanges() { + QMutexLocker locker(&m_mutex); + return m_insertedItemRanges; + } + + QList removedItemRanges() { + QMutexLocker locker(&m_mutex); + return m_removedItemRanges; + } + Q_SIGNALS: void queryCompleted(int queryId, int size); @@ -197,13 +224,12 @@ protected: m_mutex.lock(); int queryId = m_queryId; doQueryJob(); - if (m_size > 0) - doSubQueryJob(); + doSubQueryJob(); m_data.clear(); // no longer needed m_mutex.unlock(); m_mutex.lock(); - if (!m_abort && m_size > 0) + if (!m_abort) emit queryCompleted(queryId, m_size); if (!m_restart) m_condition.wait(&m_mutex); @@ -216,6 +242,8 @@ protected: private: void doQueryJob(); void doSubQueryJob(); + void getValuesOfKeyRoles(QStringList *values, QXmlQuery *query) const; + void addIndexToRangeList(QList *ranges, int index) const; private: QMutex m_mutex; @@ -231,6 +259,9 @@ private: int m_queryId; const QmlXmlRoleList *m_roleObjects; QList > m_modelData; + QStringList m_keysValues; + QList m_insertedItemRanges; + QList m_removedItemRanges; }; void QmlXmlQuery::doQueryJob() @@ -275,6 +306,40 @@ void QmlXmlQuery::doQueryJob() m_size = count; } +void QmlXmlQuery::getValuesOfKeyRoles(QStringList *values, QXmlQuery *query) const +{ + QStringList keysQueries; + for (int i=0; icount(); i++) { + if (m_roleObjects->at(i)->isKey()) + keysQueries << m_roleObjects->at(i)->query(); + } + QString keysQuery; + if (keysQueries.count() == 1) + keysQuery = m_prefix + keysQueries[0]; + else if (keysQueries.count() > 1) + keysQuery = m_prefix + QLatin1String("concat(") + keysQueries.join(QLatin1String(",")) + QLatin1String(")"); + + 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 QmlXmlQuery::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 QmlXmlQuery::doSubQueryJob() { m_modelData.clear(); @@ -285,6 +350,35 @@ void QmlXmlQuery::doSubQueryJob() QXmlQuery subquery; subquery.bindVariable(QLatin1String("inputDocument"), &b); + QStringList keysValues; + getValuesOfKeyRoles(&keysValues, &subquery); + + // See if any values of key roles have been inserted or removed. + m_insertedItemRanges.clear(); + m_removedItemRanges.clear(); + if (m_keysValues.isEmpty()) { + m_insertedItemRanges << qMakePair(0, m_size); + } else { + if (keysValues != m_keysValues) { + QStringList temp; + for (int i=0; isize(); ++i) { QmlXmlListModelRole *role = m_roleObjects->at(i); @@ -296,13 +390,13 @@ void QmlXmlQuery::doSubQueryJob() continue; } subquery.setQuery(m_prefix + QLatin1String("(let $v := ") + role->query() + QLatin1String(" return if ($v) then ") + role->query() + QLatin1String(" else \"\")")); - QXmlResultItems output3; - subquery.evaluateTo(&output3); - QXmlItem item(output3.next()); + QXmlResultItems resultItems; + subquery.evaluateTo(&resultItems); + QXmlItem item(resultItems.next()); QList resultList; while (!item.isNull()) { resultList << item.toAtomicValue(); //### we used to trim strings - item = output3.next(); + item = resultItems.next(); } //### should warn here if things have gone wrong. while (resultList.count() < m_size) @@ -408,25 +502,40 @@ void QmlXmlRoleList::insert(int i, QmlXmlListModelRole *role) /*! \qmlclass XmlListModel QmlXmlListModel - \brief The XmlListModel element allows you to specify a model using XPath expressions. + \brief The XmlListModel element is used to specify a model using XPath expressions. - XmlListModel allows you to construct a model from XML data that can then be used as a data source - for the view classes (ListView, PathView, GridView) and any other classes that interact with model - data (like Repeater). + XmlListModel is used to create a model from XML data that can be used as a data source + for the view classes (such as ListView, PathView, GridView) and other classes that interact with model + data (such as Repeater). - The following is an example of a model containing news from a Yahoo RSS feed: + Here is an example of a model containing news from a Yahoo RSS feed: \qml XmlListModel { id: feedModel source: "http://rss.news.yahoo.com/rss/oceania" query: "/rss/channel/item" XmlRole { name: "title"; query: "title/string()" } - XmlRole { name: "link"; query: "link/string()" } + XmlRole { name: "pubDate"; query: "pubDate/string()" } XmlRole { name: "description"; query: "description/string()" } } \endqml - \note The model is currently static, so the above is really just a snapshot of an RSS feed. To force a - reload of the entire model, you can call the reload function. + + You can also define certain roles as "keys" so that the model only adds data + that contains new values for these keys when reload() is called. + + For example, if the roles above were defined like this: + + \qml + XmlRole { name: "title"; query: "title/string()"; isKey: true } + XmlRole { name: "pubDate"; query: "pubDate/string()"; isKey: true } + \endqml + + Then when reload() is called, the model will only add new items with a + "title" and "pubDate" value combination that is not already present in + the model. + + This is useful to provide incremental updates and avoid repainting an + entire model in a view. */ QmlXmlListModel::QmlXmlListModel(QObject *parent) @@ -632,8 +741,13 @@ void QmlXmlListModel::componentComplete() /*! \qmlmethod XmlListModel::reload() - Reloads the model. All the existing model data will be removed, and the model - will be rebuilt from scratch. + 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. */ void QmlXmlListModel::reload() { @@ -645,12 +759,8 @@ void QmlXmlListModel::reload() d->qmlXmlQuery.abort(); d->queryId = -1; - //clear existing data - int count = d->size; - d->size = 0; - d->data.clear(); - if (count > 0) - emit itemsRemoved(0, count); + if (d->size < 0) + d->size = 0; if (d->src.isEmpty() && d->xml.isEmpty()) return; @@ -717,12 +827,19 @@ void QmlXmlListModel::queryCompleted(int id, int size) Q_D(QmlXmlListModel); if (id != d->queryId) return; + bool sizeChanged = size != d->size; d->size = size; - if (size > 0) { - d->data = d->qmlXmlQuery.modelData(); - emit itemsInserted(0, d->size); + d->data = d->qmlXmlQuery.modelData(); + + QList removed = d->qmlXmlQuery.removedItemRanges(); + for (int i=0; i inserted = d->qmlXmlQuery.insertedItemRanges(); + for (int i=0; i +#include +#include #ifdef QTEST_XMLPATTERNS #include @@ -46,6 +48,12 @@ #include #include "../../../shared/util.h" +typedef QPair QmlXmlListRange; +typedef QList QmlXmlModelData; + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QmlXmlModelData) + class tst_qmlxmllistmodel : public QObject { @@ -61,8 +69,47 @@ private slots: void roles(); void roleErrors(); void uniqueRoleNames(); + void useKeys(); + void useKeys_data(); + void noKeysValueChanges(); + void keysChanged(); private: + QString makeItemXmlAndData(const QString &data, QmlXmlModelData *modelData = 0) const + { + if (modelData) + modelData->clear(); + QString xml; + + if (!data.isEmpty()) { + QStringList items = data.split(";"); + foreach(const QString &item, items) { + QVariantList variants; + xml += QLatin1String(""); + QStringList fields = item.split(","); + foreach(const QString &field, fields) { + QStringList values = field.split("="); + Q_ASSERT(values.count() == 2); + 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(""); + } + QmlEngine engine; }; @@ -194,6 +241,213 @@ void tst_qmlxmllistmodel::uniqueRoleNames() delete listModel; } +void tst_qmlxmllistmodel::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(QmlXmlModelData, newData); + QFETCH(QList, insertRanges); + QFETCH(QList, removeRanges); + + QmlComponent component(&engine, QUrl::fromLocalFile(SRCDIR "/data/roleKeys.qml")); + QmlXmlListModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + + model->setXml(oldXml); + QTRY_COMPARE(model->count(), oldCount); + + QSignalSpy spyInsert(model, SIGNAL(itemsInserted(int,int))); + QSignalSpy spyRemove(model, SIGNAL(itemsRemoved(int,int))); + QSignalSpy spyCount(model, SIGNAL(countChanged())); + + model->setXml(newXml); + + if (oldCount != newData.count()) { + QTRY_COMPARE(model->count(), newData.count()); + QCOMPARE(spyCount.count(), 1); + } else { + QTRY_VERIFY(spyInsert.count() > 0 || spyRemove.count() > 0); + QCOMPARE(spyCount.count(), 0); + } + + QList roles = model->roles(); + for (int i=0; icount(); i++) { + for (int j=0; jdata(i, roles[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"); + + QmlXmlModelData 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") + << 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)); +} + +void tst_qmlxmllistmodel::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. + + QmlComponent component(&engine, QUrl::fromLocalFile(SRCDIR "/data/roleKeys.qml")); + QmlXmlListModel *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->setXml(xml); + QTRY_COMPARE(model->count(), 2); + + QSignalSpy spyInsert(model, SIGNAL(itemsInserted(int,int))); + QSignalSpy spyRemove(model, SIGNAL(itemsRemoved(int,int))); + QSignalSpy spyCount(model, SIGNAL(countChanged())); + + xml = makeItemXmlAndData("name=A,age=25,sport=AussieRules;name=B,age=35,sport=Athletics"); + model->setXml(xml); + + // wait for the new xml data to be set, and verify no signals were emitted + for (int i=0; i<50; i++) { + QTest::qWait(100); + if (model->data(0, model->roles()[2]).toString() != QLatin1String("AussieRules")) + break; + } + QCOMPARE(model->data(0, model->roles()[2]).toString(), QLatin1String("AussieRules")); + + QVERIFY(spyInsert.count() == 0); + QVERIFY(spyRemove.count() == 0); + QVERIFY(spyCount.count() == 0); + + QCOMPARE(model->count(), 2); +} + +void tst_qmlxmllistmodel::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 behaviour as + // if no keys are set). + + QmlComponent component(&engine, QUrl::fromLocalFile(SRCDIR "/data/roleKeys.qml")); + QmlXmlListModel *model = qobject_cast(component.create()); + QVERIFY(model != 0); + + QString xml = makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics"); + model->setXml(xml); + QTRY_COMPARE(model->count(), 2); + + QSignalSpy spyInsert(model, SIGNAL(itemsInserted(int,int))); + QSignalSpy spyRemove(model, SIGNAL(itemsRemoved(int,int))); + QSignalSpy spyCount(model, SIGNAL(countChanged())); + + QVERIFY(QMetaObject::invokeMethod(model, "disableNameKey")); + model->setXml(xml); + + QTRY_VERIFY(spyInsert.count() > 0 && spyRemove.count() > 0); + + QCOMPARE(spyInsert.count(), 1); + QCOMPARE(spyInsert[0][0].toInt(), 0); + QCOMPARE(spyInsert[0][1].toInt(), 2); + + QCOMPARE(spyRemove.count(), 1); + QCOMPARE(spyRemove[0][0].toInt(), 0); + QCOMPARE(spyRemove[0][1].toInt(), 2); + + QCOMPARE(spyCount.count(), 0); +} + QTEST_MAIN(tst_qmlxmllistmodel) #include "tst_qmlxmllistmodel.moc" -- cgit v0.12