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.10 6.9 6.8 6.5
Change-Id: Ib7e78309e076e76750b03f3238a7501563a3962a
Reviewed-by: Oliver Eftevaag <oliver.eftevaag@qt.io>
This commit is contained in:
Santhosh Kumar 2025-08-21 18:30:41 +02:00
parent 24838eb716
commit 5a664f0836
3 changed files with 118 additions and 0 deletions

View File

@ -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<QQuickListViewAttached*>(item->attached)) {
releaseSectionItem(att->m_sectionItem);
att->m_sectionItem = nullptr;
}
releaseItem(item, reusableFlag);
}
}

View File

@ -0,0 +1,67 @@
import QtQuick
ListView {
id: listView
ListModel {
id: listModel
Component.onCompleted: reloadModel()
}
property list<Item> 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 }
}

View File

@ -123,6 +123,7 @@ private slots:
void sectionPropertyChange();
void sectionDelegateChange();
void sectionsItemInsertion();
void removeSectionsOnNonvisibleItems();
void cacheBuffer();
void positionViewAtBeginningEnd();
void positionViewAtIndex();
@ -2784,6 +2785,52 @@ void tst_QQuickListView::sectionsSnap()
QCOMPARE(listview->contentY(), qreal(-50));
}
void tst_QQuickListView::removeSectionsOnNonvisibleItems()
{
QScopedPointer<QQuickView> 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<QQmlListProperty<QQuickItem>>();
const int length = sectionList.count(&sectionList);
for (int index = 0; index < length; index++) {
QQuickItem *currentItem = sectionList.at(&sectionList, index);
QString sectionData = currentItem->property("sectionData").value<QString>();
QStringList sectionDataList = sectionData.split(":");
QVERIFY(sectionDataList.at(0).toInt() == sectionId);
}
};
auto *listView = qobject_cast<QQuickListView*>(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);