From ea1681cf2ac36450778552567201662ff1a39ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Arve=20S=C3=A6ther?= Date: Tue, 9 Mar 2021 10:13:57 +0100 Subject: DelegateModelGroup: Fix bug where item could be removed from the model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If an item was removed from the DelegateModelGroup before it was completed it caused subsequent items in the model to be missing in some cases. The reason was that while populating the ListView, it iterated with an index for each item to call createItem() on. However, createItem() might call onCompleted (which in the case of QTBUG-86708 removed the item from the DelegateModel), which caused the next index we called createItem() with to be wrong (it became one step ahead). We therefore add a helper class MutableModelIterator, which keeps track of if a index in the model got removed (and if the iterator index needs to be adjusted because of that).... Task-number: QTBUG-86708 Change-Id: I33537b43727aed4f2b9bdda794b011b6684c44b4 Reviewed-by: Richard Moe Gustavsen (cherry picked from commit 0ff9db566c48172c688bf9327fe6a781dc4a1c34) Reviewed-by: Jan Arve Sæther --- src/quick/items/qquicklistview.cpp | 102 +++++++++++++++++++-- .../qml/qqmldelegatemodel/data/removeFromGroup.qml | 45 +++++++++ .../qml/qqmldelegatemodel/qqmldelegatemodel.pro | 2 +- .../qqmldelegatemodel/tst_qqmldelegatemodel.cpp | 15 +++ 4 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 tests/auto/qml/qqmldelegatemodel/data/removeFromGroup.qml diff --git a/src/quick/items/qquicklistview.cpp b/src/quick/items/qquicklistview.cpp index 044f863ab1..bdb3b1227f 100644 --- a/src/quick/items/qquicklistview.cpp +++ b/src/quick/items/qquicklistview.cpp @@ -382,6 +382,83 @@ private: } }; +/*! \internal + \brief A helper class for iterating over a model that might change + + When populating the ListView from a model under normal + circumstances, we would iterate over the range of model indices + correspondning to the visual range, and basically call + createItem(index++) in order to create each item. + + This will also emit Component.onCompleted() for each item, which + might do some weird things... For instance, it might remove itself + from the model, and this might change model count and the indices + of the other subsequent entries in the model. + + This class takes such changes to the model into consideration while + iterating, and will adjust the iterator index and keep track of + whether the iterator has reached the end of the range. + + It keeps track of changes to the model by connecting to + QQmlInstanceModel::modelUpdated() from its constructor. + When destroyed, it will automatically disconnect. You can + explicitly disconnect earlier by calling \fn disconnect(). +*/ +class MutableModelIterator { +public: + MutableModelIterator(QQmlInstanceModel *model, int iBegin, int iEnd) + : removedAtIndex(false) + , backwards(iEnd < iBegin) + { + conn = QObject::connect(model, &QQmlInstanceModel::modelUpdated, + [&] (const QQmlChangeSet &changeSet, bool /*reset*/) + { + for (const QQmlChangeSet::Change &rem : changeSet.removes()) { + idxEnd -= rem.count; + if (rem.start() <= index) { + index -= rem.count; + if (index < rem.start() + rem.count) + removedAtIndex = true; // model index was removed + } + } + for (const QQmlChangeSet::Change &ins : changeSet.inserts()) { + idxEnd += ins.count; + if (ins.start() <= index) + index += ins.count; + } + } + ); + index = iBegin; + idxEnd = iEnd; + } + + bool hasNext() const { + return backwards ? index > idxEnd : index < idxEnd; + } + + void next() { index += (backwards ? -1 : +1); } + + ~MutableModelIterator() + { + disconnect(); + } + + void disconnect() + { + if (conn) { + QObject::disconnect(conn); + conn = QMetaObject::Connection(); // set to nullptr + } + } + int index = 0; + int idxEnd; + unsigned removedAtIndex : 1; + unsigned backwards : 1; +private: + QMetaObject::Connection conn; +}; + + //---------------------------------------------------------------------------- bool QQuickListViewPrivate::isContentFlowReversed() const @@ -3570,7 +3647,6 @@ bool QQuickListViewPrivate::applyInsertionChange(const QQmlChangeSet::Change &ch if (insertResult->visiblePos.isValid() && pos < insertResult->visiblePos) { // Insert items before the visible item. int insertionIdx = index; - int i = 0; qreal from = tempPos - displayMarginBeginning - buffer; if (insertionIdx < visibleIndex) { @@ -3579,15 +3655,18 @@ bool QQuickListViewPrivate::applyInsertionChange(const QQmlChangeSet::Change &ch insertResult->sizeChangesBeforeVisiblePos += count * (averageSize + spacing); } } else { - for (i = count-1; i >= 0 && pos >= from; --i) { + MutableModelIterator it(model, modelIndex + count - 1, modelIndex -1); + for (; it.hasNext() && pos >= from; it.next()) { // item is before first visible e.g. in cache buffer FxViewItem *item = nullptr; - if (change.isMove() && (item = currentChanges.removedItems.take(change.moveKey(modelIndex + i)))) - item->index = modelIndex + i; + if (change.isMove() && (item = currentChanges.removedItems.take(change.moveKey(it.index)))) + item->index = it.index; if (!item) - item = createItem(modelIndex + i, QQmlIncubator::Synchronous); + item = createItem(it.index, QQmlIncubator::Synchronous); if (!item) return false; + if (it.removedAtIndex) + continue; visibleAffected = true; visibleItems.insert(insertionIdx, item); @@ -3620,16 +3699,20 @@ bool QQuickListViewPrivate::applyInsertionChange(const QQmlChangeSet::Change &ch } } else { - for (int i = 0; i < count && pos <= lastVisiblePos; ++i) { + MutableModelIterator it(model, modelIndex, modelIndex + count); + for (; it.hasNext() && pos <= lastVisiblePos; it.next()) { visibleAffected = true; FxViewItem *item = nullptr; - if (change.isMove() && (item = currentChanges.removedItems.take(change.moveKey(modelIndex + i)))) - item->index = modelIndex + i; + if (change.isMove() && (item = currentChanges.removedItems.take(change.moveKey(it.index)))) + item->index = it.index; bool newItem = !item; + it.removedAtIndex = false; if (!item) - item = createItem(modelIndex + i, QQmlIncubator::Synchronous); + item = createItem(it.index, QQmlIncubator::Synchronous); if (!item) return false; + if (it.removedAtIndex) + continue; visibleItems.insert(index, item); if (index == 0) @@ -3650,6 +3733,7 @@ bool QQuickListViewPrivate::applyInsertionChange(const QQmlChangeSet::Change &ch pos += item->size() + spacing; ++index; } + it.disconnect(); if (0 < index && index < visibleItems.count()) { FxViewItem *prevItem = visibleItems.at(index - 1); diff --git a/tests/auto/qml/qqmldelegatemodel/data/removeFromGroup.qml b/tests/auto/qml/qqmldelegatemodel/data/removeFromGroup.qml new file mode 100644 index 0000000000..4ae1a8aacc --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/removeFromGroup.qml @@ -0,0 +1,45 @@ +import QtQuick 2.8 +import QtQml.Models 2.1 + +Item { + id: root + width: 200 + height: 200 + + DelegateModel { + id: visualModel + model: ListModel { + id: myLM + ListElement { + name: "Apple" + } + ListElement { + name: "Banana" + } + ListElement { + name: "Orange" + } + } + filterOnGroup: "selected" + groups: [ + DelegateModelGroup { + name: "selected" + includeByDefault: true + } + ] + delegate: Text { + Component.onCompleted: { + if (index === 1) { + DelegateModel.inSelected = false + } + } + text: index + ": " + model.name + } + } + + // Needs an actual ListView in order for the DelegateModel to instantiate all items + ListView { + model: visualModel + anchors.fill: parent + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/qqmldelegatemodel.pro b/tests/auto/qml/qqmldelegatemodel/qqmldelegatemodel.pro index 7fdd3ab5f1..fbd72f6a44 100644 --- a/tests/auto/qml/qqmldelegatemodel/qqmldelegatemodel.pro +++ b/tests/auto/qml/qqmldelegatemodel/qqmldelegatemodel.pro @@ -2,7 +2,7 @@ CONFIG += testcase TARGET = tst_qqmldelegatemodel macos:CONFIG -= app_bundle -QT += qml testlib core-private qml-private qmlmodels-private +QT += qml quick testlib core-private qml-private qmlmodels-private SOURCES += tst_qqmldelegatemodel.cpp diff --git a/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp b/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp index 87f42c0c8a..71550a50f3 100644 --- a/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp +++ b/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include "../../shared/util.h" @@ -42,6 +44,7 @@ public: private slots: void valueWithoutCallingObjectFirst_data(); void valueWithoutCallingObjectFirst(); + void filterOnGroup_removeWhenCompleted(); }; class AbstractItemModel : public QAbstractItemModel @@ -134,6 +137,18 @@ void tst_QQmlDelegateModel::valueWithoutCallingObjectFirst() QCOMPARE(model->variantValue(index, role), expectedValue); } +void tst_QQmlDelegateModel::filterOnGroup_removeWhenCompleted() +{ + QQuickView view(testFileUrl("removeFromGroup.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QQuickItem *root = view.rootObject(); + QVERIFY(root); + QQmlDelegateModel *model = root->findChild(); + QVERIFY(model); + QTest::qWaitFor([=]{ return model->count() == 2; } ); +} + QTEST_MAIN(tst_QQmlDelegateModel) #include "tst_qqmldelegatemodel.moc" -- cgit v1.2.3