From 1b51dade3a86efb0002cfad3c81e7d287ec9de96 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Thu, 21 Aug 2025 18:30:41 +0200 Subject: [PATCH] Release section item when the corresponding view item removed The section item has not been removed or added to the section cache in all cases when the corresponding delegate item has been moved out of the visible area, either during flick or scroll. This happens in a case where the delegate item has been requested to be removed (as it goes out of the visible area) from the list view, but it's not released from the delegate model due to its caching mechanism. When an item is outside the visible area, releaseItem() is triggered, intended to free the item and its sections. The problem arises when releaseItem() calls QQmlInstanceModel::release(), which caches the item in the delegate model, but does not free the section. This prevents the section item from being released properly. This patch releases the section item whenever removeItem is triggered from the list view, which happens when the delegate item is moved out of the visible area. Fixes: QTBUG-137172 Pick-to: 6.8 6.5 Change-Id: Ib7e78309e076e76750b03f3238a7501563a3962a Reviewed-by: Oliver Eftevaag (cherry picked from commit 5a664f08367095b1af9198e3678663f4e7c1094a) Reviewed-by: Qt Cherry-pick Bot (cherry picked from commit d0287165a7a3d4ecb316663918ee8808c3023d4f) --- src/quick/items/qquicklistview.cpp | 4 ++ .../data/removeSectionsOnNonVisibleItems.qml | 67 +++++++++++++++++++ .../qquicklistview/tst_qquicklistview.cpp | 47 +++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 tests/auto/quick/qquicklistview/data/removeSectionsOnNonVisibleItems.qml diff --git a/src/quick/items/qquicklistview.cpp b/src/quick/items/qquicklistview.cpp index 69fdeafd09..6b29ac1f1d 100644 --- a/src/quick/items/qquicklistview.cpp +++ b/src/quick/items/qquicklistview.cpp @@ -806,6 +806,10 @@ void QQuickListViewPrivate::removeItem(FxViewItem *item) #endif { qCDebug(lcItemViewDelegateLifecycle) << "\treleasing stationary item" << item->index << (QObject *)(item->item); + if (auto *att = static_cast(item->attached)) { + releaseSectionItem(att->m_sectionItem); + att->m_sectionItem = nullptr; + } releaseItem(item, reusableFlag); } } diff --git a/tests/auto/quick/qquicklistview/data/removeSectionsOnNonVisibleItems.qml b/tests/auto/quick/qquicklistview/data/removeSectionsOnNonVisibleItems.qml new file mode 100644 index 0000000000..671dba3391 --- /dev/null +++ b/tests/auto/quick/qquicklistview/data/removeSectionsOnNonVisibleItems.qml @@ -0,0 +1,67 @@ +import QtQuick + +ListView { + id: listView + + ListModel { + id: listModel + Component.onCompleted: reloadModel() + } + + property list sectionItems + property int sectionType: 0 + property int sectionCount: 0 + + function reloadModel() { + ++listView.sectionType + listModel.clear() + for (let i = 0; i < 50; ++i) { + listModel.append({sectionStr: listView.sectionType + ":" + listView.sectionCount++}) + } + listView.sectionCount = 0 + } + + width: 640 + height: 480 + spacing: 24 + boundsBehavior: Flickable.DragOverBounds + displayMarginBeginning: 100 + displayMarginEnd: 100 + contentWidth: listView.width + + model: listModel + + section.property: "sectionStr" + section.criteria: ViewSection.FullString + section.labelPositioning: ViewSection.InlineLabels | ViewSection.CurrentLabelAtStart + section.delegate: Item { + id: item + property string sectionData: section + width: listView.width + height: 30 + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: textDate.width + 20 + height: 16 + radius: 8 + color: "#E0E1D8" + Text { + id: textDate + anchors.centerIn: parent + font.pixelSize: 10 + text: item.sectionData + } + } + Component.onCompleted: listView.sectionItems.push(item) + Component.onDestruction: { + let newSectionItems = [] + for (let index = 0; index < listView.sectionItems.length; index++) { + if (listView.sectionItems[index] !== null && listView.sectionItems[index] != item) + newSectionItems.push(listView.sectionItems[index]) + } + listView.sectionItems = newSectionItems + } + } + + delegate: Item { width: listView.width; height: 10 + index } +} diff --git a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp index 8eb6007184..1fb61a6a2e 100644 --- a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp +++ b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp @@ -122,6 +122,7 @@ private slots: void sectionPropertyChange(); void sectionDelegateChange(); void sectionsItemInsertion(); + void removeSectionsOnNonvisibleItems(); void cacheBuffer(); void positionViewAtBeginningEnd(); void positionViewAtIndex(); @@ -2783,6 +2784,52 @@ void tst_QQuickListView::sectionsSnap() QCOMPARE(listview->contentY(), qreal(-50)); } +void tst_QQuickListView::removeSectionsOnNonvisibleItems() +{ + QScopedPointer window(createView()); + window->setSource(testFileUrl("removeSectionsOnNonVisibleItems.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + auto verifySectionData = [](QObject *object, int sectionId) { + auto sectionList = object->property("sectionItems").value>(); + const int length = sectionList.count(§ionList); + for (int index = 0; index < length; index++) { + QQuickItem *currentItem = sectionList.at(§ionList, index); + QString sectionData = currentItem->property("sectionData").value(); + QStringList sectionDataList = sectionData.split(":"); + QVERIFY(sectionDataList.at(0).toInt() == sectionId); + } + }; + + auto *listView = qobject_cast(window->rootObject()); + QTRY_VERIFY(listView != nullptr); + QVERIFY(listView->contentItem()); + + auto device = QPointingDevice::primaryPointingDevice(); + const int stopFlickCount = 15; + int flickIndex = 0; + // The issue is more apparent when the list view used in this test case + // been flicked to the contentY: 965. + const float contentYThreadhold = 965.; + do { + QQuickTest::pointerFlick(device, window.data(), 0, QPoint(100, 100), QPoint(100, 50), 125); + QTRY_VERIFY(listView->isMovingVertically()); + QVERIFY(listView->contentY() != qreal(0)); + if (listView->contentY() >= contentYThreadhold) { + listView->cancelFlick(); + break; + } + } while (++flickIndex <= stopFlickCount); + + int verifySectionId = 0; + verifySectionData(listView, ++verifySectionId); + // Refresh the section item with the new model data + QMetaObject::invokeMethod(listView, "reloadModel"); + QVERIFY(QQuickTest::qWaitForPolish(listView)); + verifySectionData(listView, ++verifySectionId); +} + void tst_QQuickListView::currentIndex_delayedItemCreation() { QFETCH(bool, setCurrentToZero);