diff options
author | Mitch Curtis <[email protected]> | 2025-03-07 14:34:32 +0800 |
---|---|---|
committer | Mitch Curtis <[email protected]> | 2025-07-04 13:02:04 +0800 |
commit | fa8bfd6ed5ec4150cd2b05cd17b199097193fb1b (patch) | |
tree | c7245941420d0ea7b3471658cc54e185ff982930 /src | |
parent | 2e1337d87a60b8d1a30f5296bf3f614ec0bca383 (diff) |
Container's removeItem function removes and deletes items immediately.
This has caused issues in the past, like QTBUG-46798. QTBUG-133256 is
similar to that, except it also involves transitions.
This patch fixes the crash by listening for the destruction of items
created within an ObjectModel and removing them so that views don't
have dangling pointers to them.
Add removeAndDestroyObjectModelItem to tst_qquicklistview2, which is
similar to the test added for 1e3924d8f585dd9099eb74ffbc17950c693d14da
but adds more thorough checks and transitions.
Fixes: QTBUG-133256
Task-number: QTBUG-46798
Pick-to: 6.5 6.8 6.9 6.10
Change-Id: I9777713edfc6f82a4e9edefe2fd6c9831f03c261
Reviewed-by: Shawn Rutledge <[email protected]>
Reviewed-by: Richard Moe Gustavsen <[email protected]>
Diffstat (limited to 'src')
-rw-r--r-- | src/quick/items/qquickitemview.cpp | 95 | ||||
-rw-r--r-- | src/quick/items/qquickitemview_p_p.h | 1 |
2 files changed, 80 insertions, 16 deletions
diff --git a/src/quick/items/qquickitemview.cpp b/src/quick/items/qquickitemview.cpp index 653d4f4b15..d5b870ad26 100644 --- a/src/quick/items/qquickitemview.cpp +++ b/src/quick/items/qquickitemview.cpp @@ -1643,6 +1643,8 @@ QQuickItemViewPrivate::QQuickItemViewPrivate() bufferPause.setDuration(16); } +static const QQuickItemPrivate::ChangeTypes itemChangeListenerTypes = QQuickItemPrivate::Destroyed; + QQuickItemViewPrivate::~QQuickItemViewPrivate() { #if QT_CONFIG(quick_viewtransitions) @@ -2517,6 +2519,12 @@ FxViewItem *QQuickItemViewPrivate::createItem(int modelIndex, QQmlIncubator::Inc inRequest = false; return nullptr; } else { + // Container removes and instantly deletes items created within ObjectModels. + // We need to account for this to avoid having references to deleted items. + // itemDestroyed is called as a result of adding this listener. + if (qobject_cast<QQmlObjectModel *>(model)) + QQuickItemPrivate::get(item)->updateOrAddItemChangeListener(this, itemChangeListenerTypes); + item->setParentItem(q->contentItem()); if (requestedIndex == modelIndex) requestedIndex = -1; @@ -2563,6 +2571,7 @@ void QQuickItemView::initItem(int, QObject *object) } } +// This is called when the model (if it's a QQmlInstanceModel) emits destroyingItem. void QQuickItemView::destroyingItem(QObject *object) { Q_D(QQuickItemView); @@ -2570,6 +2579,7 @@ void QQuickItemView::destroyingItem(QObject *object) if (item) { item->setParentItem(nullptr); d->unrequestedItems.remove(item); + QQuickItemPrivate::get(item)->removeItemChangeListener(d, itemChangeListenerTypes); } } @@ -2599,29 +2609,82 @@ bool QQuickItemViewPrivate::releaseItem(FxViewItem *item, QQmlInstanceModel::Reu item->trackGeometry(false); QQmlInstanceModel::ReleaseFlags flags = {}; - if (model && item->item) { - flags = model->release(item->item, reusableFlag); - if (!flags) { - // item was not destroyed, and we no longer reference it. - if (item->item->parentItem() == contentItem) { - // Only cull the item if its parent item is still our contentItem. - // One case where this can happen is moving an item out of one ObjectModel and into another. - QQuickItemPrivate::get(item->item)->setCulled(true); + if (QPointer<QQuickItem> quickItem = item->item) { + if (model) { + flags = model->release(quickItem, reusableFlag); + if (!flags) { + // item was not destroyed, and we no longer reference it. + if (quickItem->parentItem() == contentItem) { + // Only cull the item if its parent item is still our contentItem. + // One case where this can happen is moving an item out of one ObjectModel and into another. + QQuickItemPrivate::get(quickItem)->setCulled(true); + } + // If deleteLater was called, the item isn't long for this world and so we shouldn't store references to it. + // This can happen when a Repeater is used to populate items in SwipeView's ListView contentItem. + if (!isClearing && !QObjectPrivate::get(quickItem)->deleteLaterCalled) + unrequestedItems.insert(quickItem, model->indexOf(quickItem, q)); + } else if (flags & QQmlInstanceModel::Destroyed) { + quickItem->setParentItem(nullptr); + } else if (flags & QQmlInstanceModel::Pooled) { + item->setVisible(false); } - // If deleteLater was called, the item isn't long for this world and so we shouldn't store references to it. - // This can happen when a Repeater is used to populate items in SwipeView's ListView contentItem. - if (!isClearing && !QObjectPrivate::get(item->item)->deleteLaterCalled) - unrequestedItems.insert(item->item, model->indexOf(item->item, q)); - } else if (flags & QQmlInstanceModel::Destroyed) { - item->item->setParentItem(nullptr); - } else if (flags & QQmlInstanceModel::Pooled) { - item->setVisible(false); } + + QQuickItemPrivate::get(quickItem)->removeItemChangeListener(this, itemChangeListenerTypes); + delete item->transitionableItem; + item->transitionableItem = nullptr; } + delete item; return flags != QQmlInstanceModel::Referenced; } +/*! + \internal + + Called when an item created in an ObjectModel is deleted rather than + removing it via the model. + + Similar in what it does to destroyRemoved except that it intentionally + doesn't account for delayRemove. +*/ +void QQuickItemViewPrivate::itemDestroyed(QQuickItem *item) +{ + // We can't check model->indexOf(item, q_func()) here, because the item + // may not exist there, so we instead check visibleItems. + FxViewItem *visibleFxItem = nullptr; + const int indexOfItem = -1; + for (auto *fxItem : std::as_const(visibleItems)) { + if (fxItem->item == item) { + visibleFxItem = fxItem; + break; + } + } + + // Make sure that we don't try to clean up the same FxViewItem twice, + // as apparently there can be two FxViewItems for the same QQuickItem. + if (currentItem && visibleFxItem) + Q_ASSERT(currentItem != visibleFxItem); + + if (visibleFxItem) { + qCDebug(lcItemViewDelegateLifecycle) << "removing deleted item" + << item << visibleFxItem << "at index" << indexOfItem << "without running transitions"; + // We need to remove it from visibleItems manually, as we don't want to call + // removeNonVisibleItems since it won't remove items with transitions. + const bool removedVisibleFxItem = visibleItems.removeOne(visibleFxItem); + Q_ASSERT(removedVisibleFxItem); + releaseItem(visibleFxItem, QQmlDelegateModel::NotReusable); + } + + if (currentItem && currentItem->item == item) { + releaseItem(currentItem, QQmlDelegateModel::NotReusable); + currentItem = nullptr; + } + + // Update the positioning of the items. + forceLayoutPolish(); +} + QQuickItem *QQuickItemViewPrivate::createHighlightItem() { QQuickItem *item = nullptr; diff --git a/src/quick/items/qquickitemview_p_p.h b/src/quick/items/qquickitemview_p_p.h index 77326aeddf..daeb7e0fdb 100644 --- a/src/quick/items/qquickitemview_p_p.h +++ b/src/quick/items/qquickitemview_p_p.h @@ -151,6 +151,7 @@ public: return releaseItem(oldCurrentItem, reusableFlag); } virtual bool releaseItem(FxViewItem *item, QQmlInstanceModel::ReusableFlag reusableFlag); + void itemDestroyed(QQuickItem *item) override; QQuickItem *createHighlightItem(); QQuickItem *createComponentItem(QQmlComponent *component, qreal zValue, bool createDefault = false) const; |